导学阶段-Function Calling 协议
本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。
问模型北京今天天气怎么样,它不会真的去查。它做的事情是:判断这个问题该用 get_weather,把工具名和参数放进一个结构化字段返回给程序。程序执行工具拿到”北京 晴 25°C”,把结果写回消息列表,再调一次模型。整个过程在 messages 里留下四条记录。这套结构就是 Function Calling 协议。
工具说明书由 schema 拼成
每个工具上线前要写一份机器可读的说明,OpenAI 的格式是一个固定嵌套的字典。
1 | TOOLS = [{ |
模型从这里读出三件事:工具叫 get_weather,它的用途是查城市当前天气,调用时必须传一个字符串 city。
函数体的代码模型看不到,它只有这份 schema。所以 description 的写法直接决定成败:写成”获取数据”,模型不知道什么时候该用;写成”查询指定城市的当前天气”,遇到天气问题它才会想到这个工具,也才不会把它用在”查北京人口”上。这份描述是给模型的调用说明,不是给人看的注释。
schema 通过 tools 参数传进去,同时用 tool_choice 控制这一次允不允许调用。
1 | resp = client.chat.completions.create( |
| 取值 | 含义 |
|---|---|
"auto"(默认) |
模型自己决定调不调 |
"none" |
这次不允许调工具 |
"required" |
这次至少要调一个 |
{"type": "function", "function": {"name": "get_weather"}} |
这次必须调指定工具 |
日常场景用 auto 就够。required 适合流程里必须落地一次查询的步骤,none 适合要模型只做文字总结的那一步。
四条消息的往返
一次完整的工具调用,messages 里至少有四条消息,角色的种类也从三种变成四种。
1 | messages = [ |
system 消息不参与调用与结果的配对,它只交代可用工具和角色设定。tool 是新增的第四种角色,它只承载工具的执行结果,靠 tool_call_id 指回上一条 assistant 里的某个调用。arguments 是 JSON 字符串,不是字典,执行前要 json.loads。
把这四条交给模型再调一次,它会生成第五条 assistant 消息,那是一段自然语言:”北京今天晴,25°C,适合短袖加薄外套。”到这里一次工具调用才完整结束。
tool_calls 是列表,不是单个对象,因为模型可以一次要求并行调用。问”北京、上海、深圳三地天气”,它可能一次返回三个 get_weather 请求。
1 | async def run_tools(msg, messages): |
就算一开始只处理单个调用,写循环时也应当把 tool_calls 当列表遍历。后面要加并行,只需要替换执行那几行,消息结构不用改。
消息回填容易出错的地方
模型决定调工具时,assistant 的 content 常常是 None 或空字符串。真正的指令在 tool_calls 里。打印 content 看到 None 就以为请求失败,是常见的误判。
带 tool_calls 的那条 assistant 消息必须原样追加回 messages。只追加 tool 消息而漏掉它,模型下一轮看不到自己刚才决定要调什么,往往会把同一个调用再发一次,循环空转。
tool_call_id 必须和 tool_calls 里的 id 严格对应。一次返回多个调用时,结果与请求全靠这个 id 配对,对错了位置,模型会把字节数当成天气。
从正则解析到结构化字段
2022 年 ReAct 论文提出边想边调工具时,没有这种字段。做法是在 system 里规定模型按 Thought: ... / Action: get_weather(city="北京") 的格式输出,再用正则从文本里抠出工具名和参数。
| 模型的输出 | 解析为什么会坏 |
|---|---|
Action: get_weather(city="北京")\n这是因为... |
多出来的解释被贪婪匹配吞进参数 |
Action: search(query="hello, "world"") |
嵌套引号让参数切分错位 |
Action: get_weather(city="北 |
流式输出未收完就解析,参数残缺 |
每一条都要写专门的容错分支,还要应付模型把 Action 写成 action、Tool、Use 的各种变体。流式输出下 tool_calls 也是分片到达的,参数要按 index 累积到结束才是完整 JSON。2023 年 6 月之后,工具请求变成严格的 JSON 字段,解析不再靠猜格式,写 Agent 的主要工作量从”防御模型的措辞”转回到”设计工具和循环”。
对搭建 Agent 意味着什么
协议的稳定带来一个直接结果:messages 列表就是 Agent 的全部状态。模型不记得上一轮,程序每轮把完整列表传回去,因此上下文长度、消息顺序、字段完整性三样东西决定了 Agent 能不能连续工作。
参数是合法 JSON,不代表参数有意义。{"city": "北金"} 语法正确,工具却查不到。工具层仍要做校验,并且把失败信息转成字符串回填给模型,让它自己换参数重试。协议只保证格式,不保证语义。
下一节处理另一个重复劳动:每接入一个工具都要手写三十行 schema,这件事完全可以由函数签名自动生成。


