
1. 项目概述从“笨办法”开始揭开Function Calling的神秘面纱最近和几个做后端和产品的朋友聊天发现一个挺有意思的现象大家谈起AI Agent都眉飞色舞觉得这是未来但一深聊到怎么让大模型去“执行任务”比如调用个API、查个数据库很多人就卡壳了。核心的障碍往往就落在“Function Calling”这个听起来有点技术黑话的词上。很多人看了官方文档一堆JSON Schema、工具定义感觉云里雾里不知道从何下手。这让我想起自己刚开始学编程的时候老师总说“不要怕笨办法一行行代码敲出来你就懂了”。所以今天咱们就抛开那些高大上的框架和概念用最“笨”的办法手把手写一个Function Calling的流程把它的里里外外扒个干净。Function Calling到底是什么你可以把它理解成大模型和你自己写的程序之间的一座“协议桥”。大模型比如GPT-4很聪明能理解你的自然语言指令但它自己不会写代码、不会调接口、不会查数据库。Function Calling就是一套机制让你先告诉大模型“嘿我这里有这么几个工具函数它们分别是干嘛的需要什么参数。” 然后当用户提出一个需求时大模型会判断“哦这个需求需要调用用户提供的那个工具并且需要的参数是这些。” 接着它不会自己去执行而是把这个“调用指令”返回给你的程序。最后由你的程序去真正执行这个函数并把结果返回给大模型由大模型组织成最终的回答告诉用户。整个过程大模型扮演的是“大脑”和“调度员”而实际干活的“手”和“脚”是你写的代码。这个项目适合谁呢如果你是对AI应用开发感兴趣的开发者无论是前端、后端还是全栈觉得大模型API除了聊天对话还能做更多事或者你是产品经理、创业者想搞清楚AI Agent到底是怎么运作的以便更好地设计产品逻辑亦或是你刚刚接触LLM应用开发被各种框架搞得头晕想回归本质理解最核心的交互模式。那么跟着这个“笨办法”项目走一遍你会获得远比调用一个现成SDK更扎实的理解。我们不会一上来就用LangChain或Semantic Kernel这类重型框架而是从最原始的HTTP请求和JSON处理开始让你看清每一个字节的流动。相信我踩过这些“坑”之后你再去看那些框架会觉得它们亲切无比因为你知道它们在帮你解决什么问题了。2. 核心原理拆解Function Calling如何让大模型“动手”要理解Function Calling我们不能只停留在“它是桥”这个比喻上得深入到通信协议和决策逻辑的层面。这就像你要和一位博学但行动不便的专家合作你需要一套明确的“工作指令单”。2.1 核心交互协议从自然语言到结构化指令大模型本身是一个生成文本的模型它怎么知道要生成一个“函数调用”而不是一段回答呢关键在于你发送给它的消息Message里除了传统的用户user和助手assistant角色多了一个“工具”tool的角色更具体地说是你在请求中声明了“工具”tools或“函数”functions旧版参数名。这个交互流程可以分解为以下步骤定义工具你在代码里准备好一个或多个函数比如get_current_weather(location: string, unit: celsius|fahrenheit)。然后你需要用JSON Schema一种描述JSON数据结构的规范来严格定义这个函数函数名、描述、参数列表每个参数的名字、类型、描述、是否必填等。这个描述的目的是为了让大模型能理解这个函数的用途和使用方法。发起对话你将用户的自然语言问题如“北京天气怎么样”和你定义好的工具列表一起通过API发送给大模型。模型决策大模型收到请求后会进行推理。它首先判断用户的意图是否需要调用你提供的工具。如果需要它会根据对工具描述的理解从用户的自然语言中提取出必要的、结构化的参数。例如从“北京天气怎么样”中提取出location: “北京”并可能根据工具描述或默认值补充unit: “celsius”。返回调用指令大模型不会执行函数而是会在回复中返回一个特殊的结构。在OpenAI的API中这个回复的finish_reason会是tool_calls并且在message中包含一个tool_calls数组里面是它想要调用的函数名称和提取好的参数一个严格的JSON对象。本地执行你的程序接收到这个响应后解析出函数名和参数然后在你本地的代码环境中找到对应的函数传入参数并执行它。比如调用本地的get_current_weather(“北京”, “celsius”)这个函数可能去调用一个真实的天气API。提交结果并获取最终回答你将函数执行的结果比如{“temperature”: 22, “condition”: “晴朗”}作为一个新的消息以tool的角色并附上对应的tool_call_id来自上一步再次发送给大模型。大模型会结合最初的对话历史和这个工具执行结果生成面向用户的、自然语言的最终回答例如“北京目前天气晴朗气温22摄氏度。”注意整个过程中大模型从未直接执行任何代码。它只做了两件事1. 判断是否需要调用工具2. 从自然语言中提取结构化参数。所有的执行风险都控制在你的本地代码中这是非常重要的安全边界。2.2 与AI Agent架构的关系从工具调用到智能体理解了基础的Function CallingAI Agent的概念就清晰多了。你可以把一个最简单的AI Agent看作是一个循环感知接收用户输入- 思考LLM决策可能涉及Function Calling- 行动执行被调用的函数- 观察获取行动结果- 再思考LLM整合结果并决定下一步…… 如此循环直到完成任务。Function Calling就是这个“思考”环节中让Agent能够采取“行动”的关键机制。没有Function CallingLLM只是一个聊天机器人有了它LLM就具备了操作外部世界你的代码、系统、互联网服务的能力从而进化成Agent。而最近热词里提到的Harness可以理解为包裹在这个核心“感知-思考-行动”循环之外的基础设施层。它不负责替代Agent的推理逻辑而是提供诸如工具的管理与路由、执行过程的持久化与回溯记忆、多步骤任务的规划与编排、与其他Agent的协作、安全性检查、监控报警等能力。Harness让Agent的开发从“手工作坊”走向“工业化生产”。3. 环境准备与最小化原型搭建理论说再多不如动手。我们避开所有框架用最原始的Python脚本来实现一个Function Calling流程。这能让你对每个环节都有绝对的控制力和清晰的认识。3.1 工具选型与依赖安装我们只需要两个核心库openai用于调用大模型APIpython-dotenv用于管理API密钥。坚决不用其他任何高级框架。pip install openai python-dotenv创建一个.env文件来存放你的OpenAI API密钥OPENAI_API_KEY你的sk-xxx密钥为什么这么选因为我们的目标是“理解原理”而不是“快速开发”。openai官方库提供了最直接、最底层的API访问方式没有额外的抽象。python-dotenv是管理环境变量的最佳实践避免将密钥硬编码在代码中。3.2 定义我们的第一个“工具”函数我们设计一个简单的工具查询城市信息。假设我们有一个本地的“数据库”实际上用一个字典模拟而不是真的去调用外部API这样更聚焦于流程。# mock_database.py # 模拟一个简单的城市信息数据库 city_database { “北京”: {“country”: “中国”, “population”: “2189万”, “famous_for”: “故宫、长城、烤鸭”}, “巴黎”: {“country”: “法国”, “population”: “1100万”, “famous_for”: “埃菲尔铁塔、卢浮宫、香水”}, “东京”: {“country”: “日本”, “population”: “1396万”, “famous_for”: “东京塔、寿司、动漫”}, “纽约”: {“country”: “美国”, “population”: “1880万”, “famous_for”: “自由女神像、华尔街、百老汇”}, } def get_city_info(city_name: str) - dict: “”” 根据城市名查询信息。 这是一个模拟的本地函数模拟了查询数据库或调用内部API的过程。 Args: city_name: 城市名称例如“北京”。 Returns: 一个包含城市信息的字典如果城市不存在则返回错误信息。 “”” city_name city_name.strip() info city_database.get(city_name) if info: return {“status”: “success”, “data”: info} else: return {“status”: “error”, “message”: f“未找到城市 {city_name} 的信息。”}这个函数就是我们的“手”。它接受一个字符串参数返回一个结构化的字典。注意我们设计了清晰的返回结构status,data/message这有利于后续处理。3.3 构建工具描述JSON Schema这是连接“大脑”LLM和“手”我们的函数的关键一步。我们需要用LLM能理解的格式告诉它这个工具怎么用。# 在 main.py 中定义工具列表 tools [ { “type”: “function” # 固定为“function” “function”: { “name”: “get_city_info” # 必须与本地函数名严格一致 “description”: “根据提供的城市名称查询该城市的基本信息包括所属国家、人口和著名景点或特色。” # 清晰描述直接影响LLM是否调用它 “parameters”: { “type”: “object” “properties”: { “city_name”: { “type”: “string” “description”: “城市的名称例如‘北京’、‘巴黎’。” } }, “required”: [“city_name”] # 声明必填参数 “additionalProperties”: False # 禁止传入未定义的参数增强安全性 }, }, } ]实操心得description是灵魂这个描述直接决定了LLM在什么情况下会调用这个函数。要写得具体、准确最好包含例子。比如“查询城市信息”就太模糊“查询城市的基本信息如国家、人口和特色”就好很多。name必须匹配这里的name和后面我们用来查找本地函数的字符串必须完全一致通常直接使用函数名。严格模式设置additionalProperties: False是个好习惯可以防止LLM“脑补”出一些不存在的参数传过来导致你的函数调用出错。4. 核心流程实现手搓一个Function Calling循环现在我们把“大脑”、“协议”和“手”组装起来完成一次完整的对话循环。4.1 初始化客户端与对话历史import os import json from openai import OpenAI from dotenv import load_dotenv from mock_database import get_city_info # 导入我们刚才写的工具函数 load_dotenv() client OpenAI(api_keyos.getenv(“OPENAI_API_KEY”)) # 初始化对话历史这是一个消息列表 conversation_history []4.2 发送请求并处理模型响应这是最核心的一步我们发送带有工具定义的请求并处理可能返回的工具调用。def chat_with_tools(user_input: str, history: list, tools_definition: list): “”” 与LLM对话并处理可能的工具调用。 “”” # 1. 将用户输入加入历史 history.append({“role”: “user” “content”: user_input}) # 2. 发起API调用关键是将tools参数传进去 try: response client.chat.completions.create( model“gpt-3.5-turbo-1106” # 或 gpt-4-turbo-preview确保模型支持tool calls messageshistory, toolstools_definition tool_choice“auto” # “auto”让模型自己决定是否调用工具 ) except Exception as e: print(f“API调用出错 {e}”) return history, None # 3. 获取助手的回复消息 assistant_message response.choices[0].message # 将助手的回复包含可能的tool_calls加入历史 history.append(assistant_message.to_dict()) # 注意这里要转换为字典 # 4. 检查回复中是否包含工具调用 tool_calls assistant_message.tool_calls if tool_calls: print(f“模型决定调用工具。调用次数 {len(tool_calls)}”) # 5. 处理每一个工具调用 for tool_call in tool_calls: # 提取工具调用信息 function_name tool_call.function.name function_args json.loads(tool_call.function.arguments) # 参数是JSON字符串需要解析 call_id tool_call.id print(f“正在调用函数 {function_name}”) print(f“函数参数 {function_args}”) # 6. 在本地执行对应的函数 # 这里是一个简单的映射实际项目可能需要更复杂的路由 available_functions { “get_city_info”: get_city_info } function_to_call available_functions.get(function_name) if function_to_call: # 执行本地函数 function_response function_to_call(**function_args) print(f“函数执行结果 {function_response}”) # 7. 将工具执行结果作为一条新消息追加到历史 history.append({ “role”: “tool” “content”: json.dumps(function_response, ensure_asciiFalse) # 结果需要是字符串 “tool_call_id”: call_id # 必须关联对应的调用ID }) else: # 如果找不到对应的函数返回错误信息 error_response {“status”: “error” “message”: f“函数 {function_name} 未在本地定义。”} history.append({ “role”: “tool” “content”: json.dumps(error_response, ensure_asciiFalse) “tool_call_id”: call_id }) else: # 如果没有工具调用直接打印模型的文本回复 print(f“AI回复无工具调用 {assistant_message.content}”) return history, assistant_message.content关键点解析tool_choice参数设置为“auto”是最常见的让模型自主决定。你也可以强制要求{“type”: “function” “function”: {“name”: “xxx”}}或禁止“none”调用某个工具。tool_calls列表模型可能一次决定调用多个工具所以需要遍历处理。参数解析tool_call.function.arguments是一个JSON格式的字符串必须用json.loads()解析成Python字典才能传给函数。tool角色消息这是将执行结果反馈给模型的关键。tool_call_id必须与触发该结果的调用ID一致这样模型才知道哪个工具调用有了结果。结果格式化工具执行结果content字段必须是字符串。通常我们将字典json.dumps成字符串。ensure_asciiFalse能确保中文正常显示。4.3 运行与迭代完成多轮对话我们写一个简单的循环来模拟对话def main(): history [] tools_def tools # 使用之前定义的工具列表 print(“开始对话输入‘退出’结束...”) while True: user_input input(“\n你 “) if user_input.lower() in [“退出” “exit” “quit”]: break history, final_answer chat_with_tools(user_input, history, tools_def) # 如果上一轮有工具调用我们需要再请求一次模型让它基于工具结果生成最终回答 # 检查历史中最后一条消息是否是tool角色 if history and history[-1][“role”] “tool”: print(“正在根据工具执行结果生成最终回答...”) # 再次调用chat_with_tools但这次用户输入为空让模型基于现有历史总结 history, final_answer chat_with_tools(“” history, tools_def) if final_answer: print(f“AI {final_answer}”) if __name__ “__main__”: main()这个循环的逻辑是用户输入问题。chat_with_tools函数处理如果模型调用了工具会把工具执行结果追加到历史。检查历史最后一条是不是工具结果。如果是说明模型还没给出最终答案我们需要再调用一次API。这次我们发送空的用户消息模型会看到完整的对话历史用户问题 - 模型决定调用工具 - 工具返回结果然后基于此生成面向用户的最终回答。打印最终回答。这就是一个最简化的、单次工具调用的AI Agent工作循环。5. 深入进阶处理复杂场景与常见陷阱通过上面的最小原型你已经掌握了骨架。但在实际项目中情况要复杂得多。下面我们深入几个关键场景。5.1 多工具选择与参数提取当你有多个工具时模型的决策逻辑会变得更复杂。工具描述 (description) 的清晰度和区分度至关重要。假设我们增加一个工具def get_population_rank(city_list: list) - dict: “””根据城市列表返回一个人口排名模拟。“”” # ... 模拟排序逻辑 return {“status”: “success” “rank”: [“东京” “纽约” “北京”]} tools.append({ “type”: “function” “function”: { “name”: “get_population_rank” “description”: “比较一组城市的人口规模并返回从多到少的排名列表。输入是一个城市名称的列表。” “parameters”: { “type”: “object” “properties”: { “city_list”: { “type”: “array” “items”: {“type”: “string”} “description”: “需要比较的城市名称列表例如[‘北京’ ‘东京’ ‘纽约’]。” } }, “required”: [“city_list”] }, }, })当用户提问“北京、东京和纽约哪个城市人口最多”时模型需要理解意图这是一个比较查询可能需要调用工具。选择工具对比get_city_info查询单个城市详情和get_population_rank比较列表。根据描述后者更匹配。提取参数从问题中识别出城市列表[“北京” “东京” “纽约”]并构造成JSON Schema要求的数组格式。注意事项工具描述要互斥避免两个工具的描述过于相似导致模型困惑。给每个工具一个独特、具体的职责描述。参数类型要准确比如city_list是数组 (array)就必须在Schema中明确定义items的类型。如果模型提取的参数类型不对你的代码在解析或调用时就会出错。处理模型“脑补”有时模型会为可选参数提供默认值或者提取出你未定义的参数如果你没设置additionalProperties: False。你的本地函数需要有健壮性比如使用**kwargs接收多余参数或进行严格的参数校验。5.2 多轮对话与状态管理在我们的最小原型中对话历史 (conversation_history) 就是状态。但在复杂Agent中状态管理是个大学问。场景用户问“北京人口多少”我们调用get_city_info返回了信息。用户接着问“它比东京人多吗”。这时模型需要记住上下文知道“它”指代“北京”。记住数据知道北京的人口数据已经在历史中来自工具执行结果。决定行动它可能需要再次调用get_city_info获取东京的数据或者直接利用历史中的数据进行推理比较。实操心得历史窗口限制大模型有上下文长度限制。长时间对话后需要做历史摘要或选择性遗忘只保留最重要的上下文。工具结果的有效性工具返回的数据是“事实”模型会倾向于相信并使用它。确保你的工具返回的数据是准确、可靠的。避免无限循环在复杂的规划型Agent中模型可能陷入“调用工具 - 分析结果 - 再次调用相似工具”的循环。需要设置最大迭代次数或超时机制。5.3 错误处理与边界情况一个健壮的Function Calling实现必须考虑各种失败情况。模型未调用工具但应该调用可能原因工具描述不清、用户意图太模糊、模型能力限制。处理策略可以在用户侧提示“您是否想查询XX信息”或者设计一个“澄清”流程让Agent主动询问缺失的参数。模型调用了错误的工具或参数提取错误可能原因工具描述有歧义、参数Schema定义不严谨如枚举值未列出。处理策略在本地函数调用前增加校验。如果参数不符合要求在工具返回结果中明确给出错误信息如{“status”: “error” “message”: “参数‘unit’必须为‘celsius’或‘fahrenheit’”}让模型有机会纠正。本地函数执行失败如网络超时、数据库错误处理策略本地函数应有完善的异常捕获并返回结构化的错误信息给模型。例如{“status”: “error” “message”: “查询天气服务暂时不可用”}。模型通常能理解这种错误并可能建议用户重试或转向其他问题。模型生成不符合JSON Schema的arguments处理策略在json.loads(tool_call.function.arguments)时用try...except包裹。如果解析失败可以构造一个错误消息作为工具结果返回让模型重新生成。一个增强版的本地函数调用处理段示例def execute_function_call(tool_call): function_name tool_call.function.name try: function_args json.loads(tool_call.function.arguments) except json.JSONDecodeError as e: return { “status”: “error” “message”: f“工具调用参数JSON解析失败 {e}。原始参数 {tool_call.function.arguments}” } available_functions {“get_city_info”: get_city_info} function_to_call available_functions.get(function_name) if not function_to_call: return {“status”: “error” “message”: f“未知工具函数 {function_name}”} # 参数校验示例检查city_name是否存在且为非空字符串 if function_name “get_city_info”: city_name function_args.get(“city_name”) if not city_name or not isinstance(city_name, str) or not city_name.strip(): return {“status”: “error” “message”: “参数‘city_name’必须为非空字符串。”} try: # 执行真正的函数 result function_to_call(**function_args) return result except Exception as e: # 记录本地日志但返回给模型的信息可以更友好 print(f“函数 {function_name} 执行内部错误 {e}”) return {“status”: “error” “message”: “工具执行时发生内部错误请稍后再试。”}6. 从“笨办法”到工程化框架与生态一览当你亲手实现过一遍上述流程后再去看现有的AI Agent开发框架和生态你就会明白它们存在的价值——它们帮你解决了大量重复、繁琐且容易出错的基础工作。6.1 主流开发框架对比框架/库语言核心特点适用场景LangChainPython/JS生态最丰富模块化设计提供大量现成的工具、链、记忆、Agent模板。学习曲线较陡抽象层次高。快速构建复杂的、多步骤的AI应用和Agent研究原型和中等规模生产。Semantic KernelC#/Python微软出品与.NET生态深度集成强调“规划器”和“插件”概念适合企业级应用。.NET技术栈团队构建集成在现有企业系统中的AI能力。LlamaIndexPython专注于RAG在数据索引和检索方面非常强大。其Agent能力常与LangChain结合使用。构建需要深度结合私有知识库的问答系统和Agent。AutoGenPython专注于多Agent对话可以轻松定义多个具有不同角色和能力的Agent让它们协作完成任务。研究多智能体协作、模拟复杂对话和决策流程。Spring AIJavaSpring生态的AI集成为Java开发者提供熟悉的编程模型如AiClientPromptTemplate。Java/Spring Boot技术栈的团队将AI能力集成到现有Java后端服务中。选择建议个人学习/快速原型从LangChain (Python)开始它的社区最活跃教程最多能让你最快看到效果。企业级/.NET环境Semantic Kernel是自然选择与Azure云服务、Teams等集成好。Java后台服务Spring AI让你能用写Spring MVC的方式写AI功能集成成本最低。深入研究多智能体AutoGen提供了最直观的多Agent编程模型。6.2 核心组件与基础设施Harness层当我们谈论AI Agent的“Harness”或“基础设施层”时我们指的是那些支撑Agent可靠运行的非核心推理部分工具管理与路由不仅仅是函数映射还包括工具的版本管理、权限控制哪些Agent能用哪些工具、负载均衡调用哪个后端API实例。记忆Memory分为短期记忆对话历史和长期记忆向量数据库存储的过往经验。如何高效地存储、检索、摘要历史信息是Agent拥有“持续人格”的关键。规划与反思Planning Reflection让Agent能拆解复杂任务“写一份报告” - “1. 搜集资料 2. 拟定大纲 3. 撰写内容”并在执行后评估结果“我写的这段代码能运行吗需要加错误处理吗”。这通常通过让LLM生成和评估一系列步骤来实现。监控与可观测性记录每一次工具调用、每一次模型响应的输入输出、耗时、Token消耗。这对于调试、成本控制和理解Agent行为至关重要。安全与护栏检查用户输入是否有害、工具调用参数是否合规、工具输出是否包含敏感信息。防止Agent做出危险或越权的操作。6.3 学习路线与项目推荐如果你已经理解了Function Calling的原理并想继续深入AI Agent开发我建议的学习路线是巩固基础熟练掌握至少一个大模型APIOpenAI Anthropic 国内平台等的Function Calling调用方式。理解不同的模型如GPT-4 Turbo vs GPT-3.5在工具调用能力上的差异。掌握一个框架深入学习LangChain。不要只停留在调用AgentExecutor.run()要理解其内部的ToolAgentChainMemory等核心概念。尝试用LangChain重写我们上面的“笨办法”项目体会框架带来的便利。实践核心模式RAG Agent结合LlamaIndex或LangChain的Retrieval模块做一个能回答你个人文档问题的Agent。自动化Agent做一个能自动处理邮件分类、总结会议纪要的Agent。多Agent系统用AutoGen模拟一个“程序员”Agent和一个“测试员”Agent协作写简单代码的场景。关注前沿项目去GitHub上关注一些高星项目看别人是怎么设计的。ai-agents/agent相关主题搜索这些关键词关注框架本身的演进。应用型项目例如AutoGPTBabyAGI虽然有些过时但思想经典以及各种CRM Agent、GitHub Bot Agent等看具体场景下的实现。最后一点个人体会Function Calling是AI应用从“玩具”走向“工具”的质变点。最开始用手搓的方式理解它虽然痛苦但这份痛苦会让你对后续框架的每一个便捷功能都心存感激并且当出现诡异bug时你才有能力深入到最底层去排查。AI Agent的开发目前还处在“手工艺”向“工程化”过渡的早期既有巨大的创新空间也充满了各种坑。从最笨的办法开始一步步搭建你的理解可能是应对这个快速变化领域最踏实的方式。当你下次再听到“AI Agent”时你脑子里浮现的不再是一个模糊的概念而是一条条清晰的JSON消息、一个个本地函数调用和一轮轮决策循环。