12-大模型智能体开发工程师:Function Calling原理与实战
系列文章导航:AI系列文章导航目录-持续更新中
第12课:Function Calling原理与实战
📝 本文摘要:本文详解Function Calling机制——没有它Agent只能聊天,有了它Agent能调API、查数据库、发邮件。内容包括:无Function Calling时的变通方案(Prompt解析JSON/ReAct)、Function Calling的完整工作流(工具定义→模型决策→输出结构化→调用执行→结果返回)、OpenAI vs Anthropic工具调用格式对比、多函数并行调用、实战示例(天气查询+数据库+邮件发送),以及Function Calling局限性讨论。
Function Calling是Agent"动手"的基础。没有它,Agent只能聊天;有了它,Agent能调API、查数据库、发邮件——真正地"做事"。
一、Function Calling的本质
一句话理解:Function Calling让模型能够"动手做事"。没有它,模型只能聊天;有了它,模型能调API、查数据库、发邮件——真正地"做事"。
核心原理:模型不是自己执行函数,而是告诉你"我想调哪个函数、传什么参数",然后你的代码去执行。
整个流程类比: 你(开发者) → 给模型一份"工具清单"(告诉它有哪些工具可用) 用户 → 提问题 模型 → 判断: 这个问题需要用工具吗? ├─ 不需要 → 直接回答 └─ 需要 → 输出: "我要调get_weather,参数是{city:北京}" 你(开发者) → 执行get_weather("北京"),把结果返回给模型 模型 → 基于工具结果生成最终回答: "北京今天晴,28°C"1.1 没有Function Calling时怎么做
方式1: Prompt Hack "请输出以下格式来调用工具: TOOL_CALL: {\"name\": \"get_weather\", \"args\": {\"city\": \"北京\"}}" 问题: - 模型不一定按格式输出 - 格式可能被模型"创造性"修改 - 参数可能不符合预期类型 - 每次都要写很长的Prompt 方式2: 正则匹配 让模型输出自然语言,用正则提取意图 "我想查北京天气" → 正则匹配 → 调get_weather("北京") 问题: - 复杂意图很难用正则覆盖 - 参数提取不可靠1.2 Function Calling做了什么
核心思想: 把"工具调用"变成模型的原生能力 传统方式: 模型只输出文本 → 你从文本中提取意图 → 你调API 问题: 提取意图不可靠,格式不稳定 FC方式: 模型输出结构化的工具调用 → 你直接调API 优势: 100%结构化,参数类型正确,可靠性极高 本质: 模型被训练成"知道什么时候该调工具、怎么调" 这不是Prompt技巧,而是模型能力(通过专门训练获得) 类比: 传统方式 = 你跟一个不会用电脑的人说"帮我查下天气",他口头告诉你怎么查,你自己操作 FC方式 = 你跟一个会用电脑的人说"帮我查下天气",他直接打开天气网站查给你1.3 Function Calling的完整流程
这是Agent最核心的循环,必须彻底理解!
┌─ 你的代码 ──────────────────────────────────────────┐ │ │ │ 1. 定义工具 (tools参数) │ │ → 告诉模型: 你有哪些工具可用,每个工具做什么 │ │ 2. 发送: messages + tools → LLM API │ │ │ ├─ LLM处理 ─────────────────────────────────────────┤ │ │ │ 3. LLM判断: 需要调工具吗? │ │ ├── 不需要 → 直接生成文本回复 │ │ └── 需要 → 生成tool_calls (结构化的工具调用) │ │ { │ │ "name": "get_weather", │ │ "arguments": "{\"city\": \"北京\"}" │ │ } │ │ │ ├─ 你的代码 ──────────────────────────────────────────┤ │ │ │ 4. 解析tool_calls,执行对应函数 │ │ → 你的代码调用真实的get_weather()函数 │ │ 5. 把结果加入messages,再次调用LLM │ │ → 告诉模型: 工具返回了什么结果 │ │ │ ├─ LLM处理 ─────────────────────────────────────────┤ │ │ │ 6. LLM基于工具结果生成最终回复 │ │ → "北京今天天气晴,温度28°C,湿度45%" │ │ │ └──────────────────────────────────────────────────┘ 注意: 整个过程中,LLM从未直接执行任何函数! 它只是"说"它想调什么,你的代码负责实际执行。 这是安全性的关键——你可以在执行前做权限检查、参数验证等。二、Function Calling实战
2.1 定义工具
fromopenaiimportOpenAIimportjson client=OpenAI()# 工具定义(告诉模型有哪些工具可用)tools=[{"type":"function","function":{"name":"get_weather","description":"获取指定城市的天气信息","parameters":{"type":"object","properties":{"city":{"type":"string","description":"城市名称,如'北京'、'上海'"},"unit":{"type":"string","enum":["celsius","fahrenheit"],"description":"温度单位,默认摄氏度"}},"required":["city"],"additionalProperties":False}}},{"type":"function","function":{"name":"query_order","description":"查询订单信息","parameters":{"type":"object","properties":{"order_id":{"type":"string","description":"订单编号"}},"required":["order_id"],"additionalProperties":False}}}]2.2 完整的Function Calling循环
# 实际的工具实现defget_weather(city:str,unit:str="celsius")->dict:"""模拟天气API"""weather_data={"北京":{"temp":28,"condition":"晴","humidity":45},"上海":{"temp":32,"condition":"多云","humidity":78},"深圳":{"temp":35,"condition":"雷阵雨","humidity":85},}data=weather_data.get(city,{"temp":25,"condition":"未知","humidity":50})ifunit=="fahrenheit":data["temp"]=data["temp"]*9/5+32returndatadefquery_order(order_id:str)->dict:"""模拟订单查询API"""orders={"ORD001":{"status":"已发货","items":["手机壳","充电器"],"total":128.5},"ORD002":{"status":"待发货","items":["耳机"],"total":299.0},}returnorders.get(order_id,{"status":"未找到","items":[],"total":0})# 工具映射tool_map={"get_weather":get_weather,"query_order":query_order,}# 完整的Agent循环defagent_chat(user_message:str)->str:messages=[{"role":"system","content":"你是一个智能助手,可以查询天气和订单信息。"},{"role":"user","content":user_message}]max_rounds=5# 防止死循环for_inrange(max_rounds):response=client.chat.completions.create(model="gpt-4o-mini",messages=messages,tools=tools,tool_choice="auto"# auto: 模型自己决定是否调工具)msg=response.choices[0].message# 情况1: 模型直接回复(不需要调工具)ifmsg.contentandnotmsg.tool_calls:returnmsg.content# 情况2: 模型要调工具ifmsg.tool_calls:messages.append(msg)# 把模型的tool_call加入历史fortool_callinmsg.tool_calls:func_name=tool_call.function.name func_args=json.loads(tool_call.function.arguments)print(f" → 调用工具:{func_name}({func_args})")# 执行工具result=tool_map[func_name](**func_args)# 把工具结果加入messagesmessages.append({"role":"tool","tool_call_id":tool_call.id,"content":json.dumps(result,ensure_ascii=False)})return"抱歉,处理过程中遇到了问题。"# 测试print(agent_chat("北京今天天气怎么样?"))# → 调用工具: get_weather({'city': '北京'})# → "北京今天天气晴,温度28°C,湿度45%。"print(agent_chat("我的订单ORD001到哪了?"))# → 调用工具: query_order({'order_id': 'ORD001'})# → "您的订单ORD001已发货,包含手机壳和充电器,总金额128.5元。"print(agent_chat("你好"))# → 不调工具,直接回复问候2.3 tool_choice参数(控制模型是否/如何调用工具)
# "auto"(自动模式): 模型自己决定是否调工具(默认)tool_choice="auto"# "none"(禁用模式): 禁止调工具,强制纯文本回复tool_choice="none"# "required"(强制模式): 强制调工具,模型必须选择一个工具调用tool_choice="required"# 指定工具(指定模式): 强制调用某个特定工具tool_choice={"type":"function","function":{"name":"get_weather"}}三、Function Calling的底层原理
3.1 模型是怎么学会调工具的
训练阶段: 1. 收集大量"用户意图→工具调用"的配对数据 2. 用SFT(Supervised Fine-Tuning,监督微调)教模型: 看到什么意图时应该输出什么工具调用 3. 用RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习)优化: 工具调用正确的给奖励 推理阶段: 1. tools参数被转换为特殊Token序列,拼接到输入中 2. 模型看到这些特殊Token,"知道"有工具可用 3. 当判断需要调工具时,输出tool_calls格式的Token序列 4. 这些Token序列被API层解析为结构化的JSON3.2 并行工具调用
# 一个回复中可以包含多个tool_callsresponse=client.chat.completions.create(model="gpt-4o",messages=[{"role":"user","content":"北京和上海今天天气怎么样?"}],tools=tools)# 模型会并行输出两个tool_calls:# tool_calls[0]: get_weather({"city": "北京"})# tool_calls[1]: get_weather({"city": "上海"})# 你需要都执行,然后把两个结果都返回四、Function Calling的工程化要点
4.1 工具描述是关键
模型选择工具完全依赖description! ❌ 差的描述: "获取信息" → 模型不知道获取什么信息 ✅ 好的描述: "查询指定城市的当前天气信息,包括温度、天气状况和湿度" → 模型知道什么时候该调这个工具 参数描述同样重要: ❌ "城市" → 模型可能传"北京朝阳区" ✅ "城市名称,如北京、上海、广州" → 模型传正确的值4.2 错误处理
defagent_chat_robust(user_message:str)->str:messages=[{"role":"user","content":user_message}]for_inrange(5):try:response=client.chat.completions.create(model="gpt-4o-mini",messages=messages,tools=tools)exceptExceptionase:returnf"API调用失败:{e}"msg=response.choices[0].messageifnotmsg.tool_calls:returnmsg.contentor""messages.append(msg)fortool_callinmsg.tool_calls:try:func_name=tool_call.function.name func_args=json.loads(tool_call.function.arguments)iffunc_namenotintool_map:result={"error":f"未知工具:{func_name}"}else:result=tool_map[func_name](**func_args)exceptjson.JSONDecodeError:result={"error":"参数解析失败"}exceptTypeErrorase:result={"error":f"参数类型错误:{e}"}exceptExceptionase:result={"error":f"执行失败:{e}"}messages.append({"role":"tool","tool_call_id":tool_call.id,"content":json.dumps(result,ensure_ascii=False)})return"处理超时,请稍后重试。"4.3 工具设计原则
1. 单一职责: 每个工具只做一件事 ✅ query_order() + create_refund() ❌ order_operation(type="query_or_refund") 2. 清晰的输入输出: 参数类型明确,返回值结构化 3. 有边界: 工具应该有明确的成功/失败状态 4. 安全性: 危险操作需要确认(如删除、支付) 5. 幂等性: 同样的输入应该得到同样的结果五、不同模型的Function Calling对比
| 模型 | 并行调用 | 强制调用 | Structured Output | 可靠性 |
|---|---|---|---|---|
| GPT-4o | ✅ | ✅ | ✅ | ★★★★★ |
| GPT-4.1 | ✅ | ✅ | ✅ | ★★★★★ |
| Claude 3.5+ | ✅ | ✅ | ✅ | ★★★★★ |
| DeepSeek-V3 | ✅ | ❌ | 部分 | ★★★★☆ |
| Qwen2.5 | ✅ | 部分 | 部分 | ★★★★☆ |
| 本地模型 | 有限 | ❌ | ❌ | ★★★☆☆ |
📝 作业
作业1:实现一个带有3个工具的Agent
实现一个"个人助手Agent",支持以下工具:
search_web(query)- 搜索网络(模拟实现)calculate(expression)- 计算数学表达式translate(text, target_lang)- 翻译文本(模拟实现)
参考答案:
fromopenaiimportOpenAIimportjson client=OpenAI(base_url="http://localhost:11434/v1",api_key="ollama")# 工具实现defsearch_web(query:str)->str:mock_results={"Python":"Python是由Guido van Rossum创建的高级编程语言,最新版本3.12","AI":"2026年AI领域最热门的方向是Agent和推理模型",}forkey,valinmock_results.items():ifkey.lower()inquery.lower():returnvalreturnf"搜索'{query}'的结果:未找到相关信息"defcalculate(expression:str)->str:try:# 安全起见,只允许基本数学运算allowed=set("0123456789+-*/().% ")ifall(cinallowedforcinexpression):result=eval(expression)returnstr(result)return"不支持的表达式"except:return"计算错误"deftranslate(text:str,target_lang:str)->str:mock={"hello":"你好","world":"世界","你好":"Hello","世界":"World"}words=text.lower().split()result=" ".join(mock.get(w,w)forwinwords)returnf"[{target_lang}]{result}"tool_map={"search_web":search_web,"calculate":calculate,"translate":translate}tools=[{"type":"function","function":{"name":"search_web","description":"搜索互联网获取信息","parameters":{"type":"object","properties":{"query":{"type":"string","description":"搜索关键词"}},"required":["query"],"additionalProperties":False}}},{"type":"function","function":{"name":"calculate","description":"计算数学表达式,如'2+3*4'","parameters":{"type":"object","properties":{"expression":{"type":"string","description":"数学表达式"}},"required":["expression"],"additionalProperties":False}}},{"type":"function","function":{"name":"translate","description":"翻译文本到目标语言","parameters":{"type":"object","properties":{"text":{"type":"string","description":"要翻译的文本"},"target_lang":{"type":"string","description":"目标语言,如'en'或'zh'"}},"required":["text","target_lang"],"additionalProperties":False}}}]defagent_chat(user_message:str)->str:messages=[{"role":"system","content":"你是一个个人助手,可以搜索信息、计算和翻译。"},{"role":"user","content":user_message}]for_inrange(5):response=client.chat.completions.create(model="qwen2.5:7b",messages=messages,tools=tools,tool_choice="auto")msg=response.choices[0].messageifmsg.contentandnotmsg.tool_calls:returnmsg.contentifmsg.tool_calls:messages.append(msg)fortcinmsg.tool_calls:args=json.loads(tc.function.arguments)result=tool_map[tc.function.name](**args)print(f" →{tc.function.name}({args}) ={result}")messages.append({"role":"tool","tool_call_id":tc.id,"content":json.dumps({"result":result},ensure_ascii=False)})return"处理超时"# 测试print(agent_chat("帮我算一下(128+256)*3等于多少"))print(agent_chat("Python是谁创建的?"))下一篇文章见:AI系列文章导航目录-持续更新中
