前置课 · 第一部分 LLM · L7

Function Calling:模型其实从不执行任何函数

20 min · agent 技术栈里被误解最深的一环,逐字段走一遍完整往返

🎒 预备知识:完成上一课「L6」(零基础入口:第 0 课)

本课唯一结论:模型只做一件事——输出一段结构化 JSON 声明"我想调什么、参数是什么"。 执行永远发生在你的代码里。所谓 function calling 是"模型出主意、harness 动手"的协议。

一、完整往返逐段走读(本课主菜)

第 ① 步:你随请求发送工具定义。这段 schema 会进上下文(占 token!), 也是模型"知道有哪些工具"的唯一途径:

tools = [{
  "type": "function",
  "function": {
    "name": "read_file",                      # 调用名,模型输出时引用它
    "description": "读取本地文本文件内容。只支持 utf-8 文本,不支持二进制。",
                                              # ↑ 模型路由的唯一依据,何时用/何时不用都要写
    "parameters": {                           # JSON Schema:约束模型生成的参数
      "type": "object",
      "properties": {
        "path": {"type": "string", "description": "相对路径,如 notes.md"}
      },
      "required": ["path"]
    }
  }
}]

第 ② 步:模型不调用任何东西,只是回答里多了一段结构化声明:

message.tool_calls = [{
  "id": "call_abc123",                        # 这次调用的凭证,结果要凭它回填
  "function": {
    "name": "read_file",
    "arguments": "{\"path\": \"notes.md\"}"   # 注意:arguments 是字符串化的 JSON
  }                                            # ← 是模型生成的,可能有错!
}]

第 ③ 步:你的代码真的去读文件,结果作为 tool 消息回填。关键细节: assistant 的 tool_calls 消息本身也要先 append 进历史,否则协议不完整:

messages.append(message)                     # ② 的声明先入历史
content = open("notes.md").read()[:4000]     # 截断!工具结果太大是窗口杀手
messages.append({
  "role": "tool",
  "tool_call_id": "call_abc123",             # 凭 id 对应——模型靠它知道结果属于哪次调用
  "content": content,                        # 失败时:content = '{"error": "FileNotFoundError"}'
})                                           # ↑ 失败也是结果,同样回填(A3 铁律①)

第 ④ 步:带着完整历史再调模型→ 它基于结果继续:可能给最终回答(停止), 可能再要工具(下一轮循环)。这个往返放进循环,就是 agent(A1)。

二、三个实践要点(每个都对应一类真实事故)

① 工具定义占上下文,而且每轮都占。10 个工具 ≈ 1000–3000 token, 不管用不用都在窗口里。工具一多,就必须按需加载(MCP 的 tool-search、 Easel 的 skill frontmatter 路由都是为此,A2 展开)。

② arguments 是模型生成的 = 可能错。路径不存在、类型不对、编造参数都常见。 harness 必须 validate 后再执行(OpenClaw 源码里有专门的 validateToolArguments), 校验失败把错误回填给模型重试——不是抛给用户。

③ description 是给模型看的 UI。它决定模型"何时选这个工具"。 写清三件事:做什么、何时用、何时不用(负空间同样是路由信息)。 Easel 技能规范要求 description 写"与相邻技能的边界",就是把这个道理制度化(正课 L06)。

三、常见踩坑

坑 1:忘 append assistant 消息。只回填 tool 结果不回填 tool_calls 声明, 多数 API 直接 400(找不到 tool_call_id 的归属)。

坑 2:并行调用当串行处理。模型一次可能返回多个 tool_calls(比如同时读两个文件), 它们之间没有顺序保证,应并行执行、按 id 各自回填。

坑 3:工具结果不截断。读了个 50KB 的日志原文直接回填——两三轮窗口就爆了。 结果要截断/摘要/存盘后给指针(A5 蒸馏原则)。

四、检索练习

五、出口检验

模型输出 tool_calls 后,如果 harness 直接抛异常而不是回填错误结果,从"模型下一轮看到什么"推演: 会发生什么?为什么说这等于放弃了 agent 的核心能力?发到对话里我批改。