1. 为什么 Function Calling 是 Agent 的“手和脚”
最近大模型应用圈子里,讨论 Agent 的声音越来越密集。聊得最多的一个话题,就是 Function Calling(函数调用)。我自己在做了几个真实项目之后,越来越觉得,Function Calling 才是 Agent 从“聊天机器人”走向“能干活的助理”最关键的那道门槛。
先给还不熟悉的朋友打个比方。大模型本身就像一个非常聪明但手头没有工具的人。你问他“帮我算一下这个月开销”,他能给你一个笼统的方法论,但他翻不了你的账单、连不上你的支付平台。而 Function Calling 相当于给这个人配了一双手和一双脚,让他知道“什么时候该去拿账单”“拿到账单之后该怎么算”,甚至算完之后“该把结果记到哪里”。没有这双手脚,模型再聪明,也只能停留在“嘴上说说”的阶段。
从技术角度解释,Function Calling 是模型在生成回复时,输出一个结构化的调用请求,请求里包含函数名和参数,然后由我们自己的代码去真正执行这个函数,再把结果返回给模型继续生成。整个过程模型不需要真的“会”执行函数,它只需要学会“决定调哪个函数、传什么参数”。这恰恰是 Agent 架构里最核心的 Harness Engineering(外部工程框架设计)要做的事情。
这篇内容我打算从自己踩过的坑出发,把 Function Calling 从设计、实现、调试到生产落地讲透。无论你是刚入门大模型应用开发,还是已经在做 Agent 框架,应该都能从里面找到一些可以搬运的经验。
这里说的“手和脚”,不只是比喻。手负责“操作”——调用外部工具、读写数据、发起请求;脚负责“移动”——决定下一步该调用什么、调用顺序是什么、出错之后怎么绕路。把这两件事在工程上处理妥当,Agent 才算是从“能说”变为“能做”。
2. 理解 Function Calling 的底层逻辑:模型到底在做什么
很多人第一次接触 Function Calling,会误以为模型真的在执行代码。实际上模型做的事情非常纯粹:根据当前对话上下文,决定“此刻应该调用哪个工具”,并生成一段符合函数描述 JSON Schema 的参数。真正的执行发生在我们自己的服务里。
2.1 从“意图识别”到“结构化输出”
传统做法里,想让模型触发某个工具,常见方案是让模型输出一段特殊格式的文本,比如“调用天气查询,城市=北京”,然后我们用正则或者字符串匹配去解析。这种方式在 Demo 阶段够用,一旦函数变多、参数嵌套复杂,解析就容易出错,而且模型“自由发挥”的空间太大,经常给你来一句“我建议你查询北京的天气”,这种句子根本没法解析。
Function Calling 的本质,是把“意图识别”和“结构化输出”这两件事合并成了一件事。模型在预训练和指令微调阶段,已经学会了“当用户请求涉及某个能力时,输出一个符合工具描述的结构化调用”。我们不再需要自己写解析器,只需判断模型返回的响应里是否带有 tool_calls 字段,有则执行,无则直接回复。
这里有一个容易忽略的点:模型选择工具,依据的是我们传入的“函数描述”,而不是函数内部的实现细节。也就是说,你给模型看的是一份“菜单”,菜单上写着菜名、口味特点、适合场景,模型照着菜单点菜,而后厨怎么做菜,模型完全不需要关心。所以,函数描述写得清不清楚,直接决定了模型“点菜”准不准。
2.2 模型侧的训练机制与工具选择依据
从模型训练的角度看,目前主流大模型都经过了“工具使用”相关的指令微调。训练数据里包含了大量“用户问题—工具调用—工具结果—最终回复”的样本,模型由此学会了两个能力:
- 什么时候该调用工具,而不是硬答。
- 该调用哪个工具,以及如何从用户话术中抽取参数。
比如用户说“帮我查一下明天杭州到上海的高铁”,模型需要从这句话中抽出日期、出发地、目的地三个参数,并且知道该调用“查询列车”函数而不是“查询天气”函数。如果参数不完整,模型甚至会主动问用户要缺失信息,这个行为也是训练出来的。
理解了这一层,你就会明白:Function Calling 的效果上限,很大程度取决于函数的“语义清晰度”和“参数设计合理度”。模型不是神,它只能根据你给的描述来做决策。同样的底层模型,函数写得好不好,效果可以差出好几个档次。
2.3 与 RAG、普通提示词工程的边界与配合
在 Agent 架构里,Function Calling 经常和 RAG(检索增强生成)一起出现,但二者解决的问题不同。RAG 解决的是“模型不知道的知识”,通过检索外部文档给模型补充上下文;Function Calling 解决的是“模型做不到的操作”,通过调用工具改变现实世界或获取实时数据。
举个例子:用户问“我们公司上季度华东区的销售额是多少”。这个数据不在模型脑子里,也不在静态文档里,而在数据库中。正确做法是:先通过 RAG 确认“销售额数据存在数据仓库”,或者直接通过 Function Calling 调用“查询销售数据”函数,让后端去数据库执行 SQL,拿到结果再交给模型组织语言回答。
实际项目中,两者常常串联:先用 Function Calling 调用“知识库检索”函数,把检索结果作为上下文的一部分,再生成最终答案。这种“工具链”的模式,正是 Agent 能力强弱的分水岭。
3. 函数描述设计:从“能用”到“好用”的关键跨越
刚上手 Function Calling 的时候,我最大的感受是:写函数本身不难,难的是把函数描述写好。模型看不到你的代码注释,它只能看到你在 API 请求里传过去的那段 JSON 描述。这段描述,就是你给模型画的“地图”。
3.1 描述文本的三大原则:清晰、具体、可区分
第一个原则是“说人话”。不要写“该函数用于获取信息”,这种描述等于没说。要写清楚“该函数用于根据城市名称查询指定日期的天气情况,返回温度、降水概率、风力等级”。模型看到之后,才能准确判断什么场景该调用它。
第二个原则是“边界明确”。一个函数只做一件事,并且描述里写清楚“适合做什么、不适合做什么”。比如“创建订单”函数,就应该写明“仅在用户确认购买意向且商品库存充足时调用;若用户只是询问价格,不应调用”。这种负向说明非常管用,能显著减少模型误调用。
第三个原则是“相互区分”。如果你的系统里有“查询订单状态”和“查询物流信息”两个函数,描述里一定要把差异点写出来。否则模型很可能把两个函数搞混,明明用户想知道快递到哪了,它却去调了订单状态查询,返回一个“已发货”就完事了。
3.2 参数设计:类型、必填项与枚举约束
参数设计直接关系到模型生成参数的成功率。我见过很多菜鸟项目,把所有参数都设置成 string 类型,模型传出来的日期是“明天”、价格是“大约一百多”,后端一执行就报错。参数类型一定要严格,日期就用 date 格式,数字就用 number,能用枚举就别用自由文本。
特别是枚举约束,能大大降低模型的出错率。比如“查询天气”函数的城市参数,如果你不做约束,模型可能传出“杭州市”“杭州城市”“hangzhou”各种写法;如果你在 enum 里明确列出“杭州, 上海, 北京”,模型就会老老实实从中选一个。这其实是用 Schema 的约束力,弥补模型在“规范化”方面的不足。
必填项的设计也有讲究。一个常见误区是把所有参数都设为必填,导致模型在信息不足时强行编造一个值。正确做法是:用户可能没说全的参数,尽量设为可选,并在函数逻辑里做缺省处理;真正缺失且影响执行的关键参数,应该让模型返回“需要追问用户”的回复,而不是强行调用。这里可以在描述里专门写一句:“如果用户未提供出发地,应询问用户而不是调用函数。”
3.3 一个可直接参考的天气函数描述示例
下面给一个我自己常用的天气查询函数描述,你可以直接参考结构:
{ "type": "function", "function": { "name": "query_weather", "description": "查询指定城市在指定日期的天气情况,包含温度范围、降水概率、风力等级。仅在用户表达查询天气需求时调用;若用户询问穿衣建议,可调用本函数获取天气数据后结合常识回答。若未提供城市或日期,应先询问用户,不能自行假设。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "enum": ["北京", "上海", "广州", "深圳", "杭州"], "description": "城市名称,必须是枚举值之一" }, "date": { "type": "string", "format": "date", "description": "查询日期,格式为 YYYY-MM-DD,默认为今天" } }, "required": ["city"] } } }这个例子里有三个细节值得注意:
- description 里写了“若未提供城市或日期,应先询问用户”,这句能显著降低模型瞎猜的概率。
- city 参数用了 enum,把可选范围锁死。
- date 参数设为可选,并给了默认值逻辑,既方便用户,又减少模型编造日期的可能。
函数描述本质上是一份“给模型看的说明书”,说明书越精确,模型的表现越稳定。这个投资非常值得——它不会让你的代码更复杂,却能让你的 Agent 聪明一个档次。
4. Agent Harness Engineering:把“调用决策”变成“可靠流程”
如果你只是调用单个函数,那直接用 API 的 Function Calling 功能就够了。但一旦涉及多轮调用、多个工具、条件分支,你就需要一个外部的“调度框架”——这就是 Harness Engineering 要做的事情。Harness 这个词在英文里有“马具、绳索”的意思,放在 Agent 语境里,就是用来“驾驭”模型行为的那套工程结构。
4.1 什么是 Harness:连接模型与工具的调度层
Harness 的核心职责有三个:接收模型输出的 tool_calls、真正执行对应的函数、把执行结果返回给模型。听起来简单,但落地时你会发现,这里藏着一堆细节。
首先是“循环控制”。模型可能调用一次函数后,需要根据结果再调用另一个函数。比如 Agent 先调“查询用户订单”,发现订单状态是“退款中”,于是再调“查询退款进度”。这个过程需要我们在代码里写一个循环,不断把模型的新输出发回 API,直到模型不再请求工具调用为止。这个循环就是 Agent 执行力的基本形态。
其次是要处理“模型输出格式”的差异。不同厂商的 API 在 Function Calling 的返回结构上并不完全一致,有的叫 tool_calls,有的叫 function_call。Harness 的职责之一就是屏蔽这些差异,让你的业务代码面对统一的接口。这也是为什么很多人愿意用现成的 Agent 框架,而不是自己裸调 API。
4.2 多工具决策编排:顺序执行、条件分支与并行调用
多工具编排是 Harness 的核心难点。我自己的经验是,先画一张“Agent 的行动流程图”,把可能的分支都列出来,再决定是让模型自由决策,还是用代码写死规则。
“模型自由决策”适合场景变化多、用户输入不可预测的情况。例如一个客服 Agent,用户可能问订单、问物流、问发票,与其用规则去猜,不如把工具列表都抛给模型,让它根据用户问题自己选。但这种方式的代价是,模型偶尔会选错工具,你需要有兜底方案。
“代码写死规则”适合流程固定的场景。例如“先查询用户身份,再查询用户权限,最后执行操作”,这个顺序是确定的,不需要模型参与决策。此时直接在 Harness 里写一个编排逻辑,把模型当“参数抽取器”用反而更可靠。
还有一点容易被忽略:并行调用。如果用户同时问“北京和上海的天气”,模型可能会在一个响应里同时发起两个 query_weather 调用,参数分别是北京和上海。Harness 需要支持这种并行执行,并且把两个结果一起返回给模型。如果你把函数写成了“只能一次处理一个城市”,就会浪费模型天然具备的并行能力,而且响应速度会明显变慢。
4.3 状态管理:多轮交互中如何保持上下文
Agent 和普通对话最大的区别,在于它要完成一项“任务”,而任务往往需要多轮交互。用户先说“帮我订一张去杭州的票”,Agent 查了车次之后反问“你要几点的”,用户回答“上午的”。这个过程中,模型需要记住“出发地是上海”“目的地是杭州”“日期是明天”这些信息。这些信息从哪里来?从对话历史里来。
所以 Harness 必须管理好对话历史。每轮模型调用后,我们都要把“用户发言、模型文本回复、工具调用请求、工具执行结果”全部拼接到消息列表里,发给模型。这也是常见的 agent 死循环问题的根源——如果工具结果没有被正确追加,模型下一轮就“失忆”了,会重新问你一遍已经提供过的信息。
实际操作中,我会把工具结果包装成一条 role=tool 的消息,并确保 tool_call_id 与模型输出中的 id 完全对应。这个 id 匹配看上去是小事,但漏掉它整个对话就会报错,卡住排查半天的人不在少数。
5. 从零搭建一个可用的 Function Calling 服务
说完了理论和架构,接下来我们进入实战。我会带你从零搭建一个具备 Function Calling 能力的最小服务,包含环境准备、核心代码实现、以及一套完整的调用链路。用到的技术栈是 Python + OpenAI SDK 兼容接口,当前主流厂商的 API 大多支持 OpenAI 格式,这套代码可以直接平移。
5.1 环境准备与依赖安装
建议使用 Python 3.10 及以上版本,用一个干净虚拟环境。安装依赖:
pip install openai python-dotenv然后创建一个.env文件,写入你使用的模型 API 配置:
API_KEY=你的密钥 BASE_URL=https://你的服务地址 MODEL_NAME=你的模型名需要说明的是,这里刻意不指定具体厂商,因为 Function Calling 的标准接口已经相当统一。无论你用的是哪家服务,只要支持 OpenAI 兼容格式,代码结构基本一致。
5.2 最小可运行的 Function Calling 代码实现
我们来写一个“航班查询”的 Agent。先定义两个工具:查询航班信息和查询当地天气。模型根据用户问题决定调用哪个函数。
import os import json from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.getenv("API_KEY"), base_url=os.getenv("BASE_URL") ) MODEL = os.getenv("MODEL_NAME") def query_flight(departure: str, arrival: str, date: str): """模拟航班查询,实际项目可替换为真实的机票接口""" flight_data = { "上海-北京": {"航班号": "MU5101", "起飞": "08:00", "到达": "10:15", "价格": 1200}, "上海-杭州": {"航班号": "CA1701", "起飞": "09:30", "到达": "10:20", "价格": 800} } key = f"{departure}-{arrival}" if key in flight_data: return json.dumps(flight_data[key], ensure_ascii=False) return json.dumps({"error": "未找到该航线数据"}, ensure_ascii=False) def query_weather(city: str): """模拟天气查询""" weather_data = { "北京": {"天气": "晴", "温度": "22-30℃"}, "杭州": {"天气": "小雨", "温度": "24-28℃"} } if city in weather_data: return json.dumps(weather_data[city], ensure_ascii=False) return json.dumps({"error": "未找到该城市数据"}, ensure_ascii=False) TOOLS = [ { "type": "function", "function": { "name": "query_flight", "description": "查询两个城市之间的航班信息,包括航班号、起飞时间、到达时间、价格。仅在用户明确询问航班或机票时调用。", "parameters": { "type": "object", "properties": { "departure": {"type": "string", "description": "出发城市"}, "arrival": {"type": "string", "description": "到达城市"}, "date": {"type": "string", "description": "出行日期,格式 YYYY-MM-DD"} }, "required": ["departure", "arrival", "date"] } } }, { "type": "function", "function": { "name": "query_weather", "description": "查询指定城市的天气情况,返回天气现象和温度范围。", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } } } ]这里我把航班和天气做成了可区分且功能独立的两个函数。注意query_weather没有加入 enum,目的是让你感受一下有约束和没约束的差别。实际项目中,我强烈建议加上。
核心循环逻辑如下:
def run_agent(user_input: str): messages = [{"role": "user", "content": user_input}] while True: response = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto" ) assistant_message = response.choices[0].message if not assistant_message.tool_calls: print("最终回答:", assistant_message.content) break # 把模型的中间回复追加进消息列表 messages.append(assistant_message) # 逐个执行工具调用 for tool_call in assistant_message.tool_calls: fn_name = tool_call.function.name fn_args = json.loads(tool_call.function.arguments) if fn_name == "query_flight": result = query_flight(**fn_args) elif fn_name == "query_weather": result = query_weather(**fn_args) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result })测试一下:
if __name__ == "__main__": run_agent("帮我查一下明天从上海到北京的航班,另外北京天气怎么样?")模型会先同时调用query_flight和query_weather,拿到结果后生成一段综合回答。这个示例虽然小,但它已经包含了 Harness 最核心的循环逻辑:模型请求调用、后端执行、结果回传、模型再生成。
5.3 参数传递与结果回传的正确姿势
代码里最容易出错的地方,是tool_call_id。在追加工具结果时,必须确保tool_call_id和模型返回的tool_call.id完全一致,否则 API 会报错。这是 OpenAI 接口的硬性要求,也是很多新人第一次跑通后又被卡住的经典位置。
另一个容易踩的坑,是把工具结果直接作为普通文本塞进 user 消息。正确做法是用role=tool的角色消息。两种写法在简单场景下可能都能跑,但一旦进入多轮工具调用,前者会导致上下文混乱,模型分不清哪段是“历史对话”、哪段是“工具证据”。
还有一点关于参数解析:模型返回的arguments是一个 JSON 字符串,通常已经是合法的 JSON,但偶尔也会出现格式不规范的情况。稳妥做法是用json.loads包一层 try-except,解析失败时返回“参数格式错误”给模型,让它重新生成。这种防御性编程,在生产环境中非常有必要。
6. 多轮 Agent 工作流:从单次调用到全流程闭环
单次 Function Calling 只能算“工具调用”,真正让 Agent 产生质变的,是把多次调用组合成一个完整工作流。这一节我们聊聊如何把多个工具调用编排成有目标、有顺序、能容错的 Agent 流程。
6.1 任务拆解:把复杂请求拆成可执行的工具链
一个复杂的用户请求,往往需要多个工具的配合。例如“帮我订明天去杭州的高铁票,顺便看看那边适合住哪个区”。这个请求背后至少涉及三个能力:查询高铁班次、预订车票、查询杭州各区域酒店和景点分布。
如果你把三个能力封装成一个巨大的“一键搞定”函数,描述会非常臃肿,参数会非常复杂,模型反而容易出错。正确的做法是拆成多个小函数,让模型自己决定调用顺序。但在拆解之前,Harness 层要做好一件事:给模型提供足够清晰的工具列表,并且在每轮之间保留完整上下文。
实际测试中我发现,模型对这种多步任务的处理能力,很大程度上依赖于每一步返回结果的“可读性”。例如query_flight返回的是一段 JSON,模型能看懂;但如果你的函数返回的是大段原始日志,模型就会被噪音干扰,甚至遗漏关键信息。所以函数返回的内容,也应该“清洗好再给模型看”。
比如查完航班后返回:
{"航班号": "MU5101", "起飞": "08:00", "到达": "10:15", "价格": 1200}模型一看就知道接下来该怎么回答用户。而如果返回的是几十行机票数据的原始 dump,模型就得花力气“阅读理解”,出错的概率自然上升。
6.2 循环上限与死循环规避机制
多轮调用最怕的就是“死循环”。模型反复调用同一个函数,拿到结果又调用,永远等不到最终回复。原因通常是函数返回的错误信息不够明确,模型无法理解错误原因,于是反复重试同一操作。
我常用的规避机制有三个:
- 设置最大迭代次数,比如 10 轮,超过即终止并返回兜底文案。
- 给工具结果附带错误类型说明,比如“参数错误”“数据不存在”“系统异常”,让模型知道该换路而不是硬试。
- 在工具描述里写上“如果查询结果为空,直接告诉用户未找到,不要重复调用本函数”。
下面是一个带循环上限的实现片段:
MAX_ITERATIONS = 10 iterations = 0 while iterations < MAX_ITERATIONS: iterations += 1 response = client.chat.completions.create(...) ... if not tool_calls: break else: print("达到最大调用次数,强制终止")while...else的写法在 Python 中很巧妙:循环正常结束才执行 else,一旦 break 就跳过。用在这里非常合适,可以清楚区分“正常结束”和“被迫终止”两种路径。
6.3 错误容忍与用户友好降级
Agent 项目里有一句常说的话:异常不是会不会发生的问题,而是什么时候发生的问题。外部接口可能超时,数据库可能连不上,模型可能生成非法参数。Harness 的成熟度,恰恰体现在这些异常发生时的处理方式上。
我的策略是“分级降级”。首先,函数内部逻辑要能捕获所有预期内的异常,并把错误信息转换成模型能看懂的文本。其次,Harness 层要能感知“某个工具连续失败”,并主动切换策略。比如航班查询接口超时,可以先用缓存数据回答“暂未获取到最新航班,以下是昨日参考信息”,而不是让模型干等。
举个例子,我之前做过一个项目,天气接口偶尔会返回超时。早期版本里,函数超时后返回“程序异常”,模型看到这四个字完全不知道说什么,只能回复“抱歉,我无法回答”。后来改成超时后返回“天气服务暂时不可用,建议用户稍后再试”,模型就能很好地把这个信息组织成一句自然的安抚话术。这个细节看似不起眼,但对用户体验的提升非常明显。
7. 实测对比:带 Function Calling 与不带效果的差异
这一节,我把自己做过的一组对比实验数据放出来。虽然不同模型在不同任务上的表现会有差异,但整体趋势非常有参考价值。测试任务有三个:单工具信息查询、多工具顺序编排、复杂任务需要决策分支。
7.1 测试集设计与评测标准
我先设计了一批贴近真实用户的测试问题,比如:
- “帮我查一下上海明天会不会下雨”(单工具)
- “帮我订一张后天从北京去深圳的机票,然后再查一下深圳后天的天气”(双工具顺序)
- “我想从上海去杭州玩两天,帮我安排一下交通和住宿建议”(需要决策和多个工具配合)
评测标准分三档:完全成功(任务目标完成且信息正确)、部分成功(调用对了工具但结果不准确)、失败(工具调用错误或无法完成)。
7.2 数据结果与观察结论
先看不带 Function Calling 的对照组。我让模型直接回答,“能否查询明天上海天气”——模型给出的答案是“上海明天多云转阴,气温 24-29℃”。字面看像模像样,但这个数据完全是编的,没有任何信源。若用户拿这个信息安排户外活动,很可能被坑。
再看“帮我订一张后天从北京去深圳的机票”,不带 Function Calling 的模型顶多给你一句“你可以去航空公司官网查询”,或者直接编一个航班号。这种答案在 Demo 里还能看,但真要让用户去执行,基本是废的。
接入 Function Calling 之后,同样是这些问题,模型会先输出调用请求,后端执行完返回真实数据,再组织成完整回答。单工具查询的准确率从“不可用”直接提升到“可用”,多工具任务的成功率也能稳定在 80% 以上——剩余 20% 的失败,大多出在参数缺失或极端情况下,通过优化描述可以继续逼近。
有一个特别有意思的观察:接入 Function Calling 后,用户对“模型是否靠谱”的感知会明显提升。因为答案里的每一个数字都有数据来源,用户能感受到“这个助手是真的在帮我干活”。
7.3 成本与延迟的取舍
Function Calling 带来的代价也很直接:多轮调用意味着多次 API 请求,Token 消耗和响应延迟都会上升。一次复杂任务,可能产生 3 到 5 轮模型调用,累计 Token 是单轮对话的 3 倍以上。
成本优化这块我积累了三个技巧:
- 函数描述不要太啰嗦,控制每个函数的描述在 100 字以内,工具列表整体控制在合理范围。
- 对工具结果做截断,只返回模型必要的信息,避免把几十 KB 的原始数据塞进上下文。
- 对简单任务走“快速通道”,先让模型尝试不调用工具直接回答,只有在用户明确需要实时数据时才进入 Function Calling 流程。
延迟方面,并行调用是解决多工具场景延迟的利器。模型同时发出两个工具请求,后端并行执行,总耗时约等于最慢的那个函数,而不是串行之和。这一点在构建面向用户的实时对话系统时非常关键。
8. 生产环境避坑实录与经验总结
最后这一部分,我把自己在真实项目中踩过的坑和验证过的经验做个系统整理。这些都是文档里不会写、但实际开发中一定会遇到的细节。
8.1 函数描述面试官视角:容易被忽略的细节
我把函数描述比作“面试官的岗位说明书”。一份优秀的描述,不仅要说明“这个岗位做什么”,还要说明“什么情况下不要做”。很多项目里模型误调用工具,原因不是模型笨,而是描述里没写清楚边界。
举个真实案例。一个订单系统里,我设计了两个函数:“查询订单”和“催单”。用户说“我上周买的手机怎么还没发货”,模型应该调用“查询订单”看物流状态。但如果“催单”的描述写的是“用于处理用户询问订单发货进度的问题”,模型就很可能误调用“催单”,直接触发一个全自动投诉工单,把用户和客服都吓一跳。
解决方式是给“催单”加上一句强约束:“仅当用户明确表达不满并要求催促商家时,才可调用本函数;单纯询问进度应调用查询订单。”加上这句话之后,误调用率大幅下降。所以描述里的负向约束,真的不是可有可无的装饰。
8.2 线上问题排查的三板斧:日志、回放、灰度
Function Calling 服务上线后,最怕的就是“模型偶发行为异常”。今天能跑通的流程,明天可能换个用户输入就崩了。排查这类问题,我总结了一套三板斧。
第一板斧是完整日志。每轮对话、每次工具调用、每个 token 消耗,都要记录下来。尤其是模型返回的原始 tool_calls 结构,必须原样落盘。没有日志,出问题根本无从下手。
第二板斧是回放。把线上用户的实际输入抽取出来,在测试环境重新跑一遍,对比模型的行为差异。很多偶发问题,其实是用户输入里出现了测试集没覆盖到的表达方式,回放能帮你快速定位。
第三板斧是灰度。每次修改函数描述或新增工具,不要全量上线。先在 10% 流量上观察模型行为,对比错误率和用户反馈,确认没问题再放量。这一条救了我很多次——有些函数描述改动,你以为优化了,实际效果反而变差了。
8.3 对 Agent 后续演进的一些心得
做了这么久 Function Calling,我最大的心得是:模型的能力边界,很大程度上由你给它配的“工具”决定。同一个模型,配三个工具和配三十个工具,展示出的智能水平完全不同。工具就是模型的“感官”和“四肢”,它们越丰富、越可靠,模型就越像一个能独立完成任务的 Agent。
另一个心得是:不要迷信“全自动”。有些流程,与其让模型自由发挥,不如用代码写死一部分逻辑,只在关键决策点上让模型做判断。混合编排(Harness 规则 + 模型决策)往往比纯模型决策更稳。真正优秀的 Agent 架构,是“该模型做决定的让模型做,该代码保证的用代码保证”。
最后分享一个我经常用的小技巧:在函数描述里给每个工具加一个“使用场景示例”。比如“当用户问‘明天能穿短袖吗’时,可调用本函数获取天气后结合体感温度回答”。这种示例能显著提升模型对调用时机的判断准确率,成本几乎为零,效果却非常好。
如果你正准备做一个 Agent 项目,我的建议很简单:先把一个工具的 Function Calling 跑通,再逐步增加工具数量,最后再考虑复杂编排。不要一上来就设计一个十多个工具的大系统——工程上的复杂度会淹没模型本身的能力。吃透这一篇,你的 Agent 就已经比大多数“聊天机器人改个名字”的 Demo 领先一大截了。