模型只会读字、写字,一样东西都碰不到。可 Claude Code 能读你的文件、跑测试、看日志——中间那座桥叫 Function Calling。
这一节是拓展阅读:你不会在 Claude Code 里手写 Function Calling 的代码。但看懂它,前面几节的零碎会拼成一张图:为什么工具定义一开局就占上下文,为什么每次执行都要过权限,为什么 MCP 装多了会拖累模型。
一、模型只会说,不会做
模型是文字进、文字出。训练完就冻结,不知道你磁盘上有什么,也执行不了任何东西。
你问它「杭州今天下雨吗」,它只能根据训练数据里的常识回答,或者干脆编。想让它看今天的代码、跑一次 npm test,它做不到。
最早的绕法是让人当搬运工:让它输出一段命令,你复制到终端,跑完把输出贴回去。几次还能忍,任务一长就崩:
- 每轮都要人盯着,半夜跑不了
- 它输出的格式时不时变一下,多一个空行、少一个反引号,粘贴就错
- 一次只能走一步,改五个文件要看十几次输出
问题不在模型不够聪明,在于它没有一个结构化的出口:能说清楚「我要干什么、参数是什么」,让程序去执行。
Function Calling(函数调用)就是修这个的。
你在请求里附上一份工具清单,告诉模型「这些事你能让我做」。模型想用某个工具时,不再输出一段自然语言,而是输出一条结构化的调用请求:工具名 + 参数。你的程序执行完,把结果按格式送回给模型,模型接着往下想。
一句话:模型负责提要求,执行永远在模型外面。
这一点值得较真。模型没有删你的文件,是你的程序收到它的请求后删的。分不清这条界线,后面权限怎么卡、提示注入为什么危险,都会想歪。
二、一次调用发了什么
拿一个查天气的工具举例,省略模型名和鉴权头。请求里除了消息,多了一段 tools:
{
"tools": [
{
"name": "get_weather",
"description": "查询指定城市的当前天气。用户问天气、气温、要不要带伞时使用。",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名,比如 杭州" }
},
"required": ["city"]
}
}
],
"messages": [{ "role": "user", "content": "杭州今天要带伞吗?" }]
}模型看完,回你两条内容:一句人话,一个 tool_use:
{
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "我查一下杭州的天气。" },
{
"type": "tool_use",
"id": "toolu_01ABC",
"name": "get_weather",
"input": { "city": "杭州" }
}
]
}到这儿模型就停了,它不会自己跑函数。
你的程序拿到这条请求,做四件事:校验参数(city 在不在、是不是字符串)→ 过权限 → 真的去调天气接口 → 把结果包成 tool_result 发回去。发的时候要把模型那条回复原样放进 messages,再追加结果:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01ABC",
"content": "杭州 18℃,多云,降水概率 10%"
}
]
}模型这才继续,回一句「不用带伞」。
四步走完,记住三件事:
- 工具清单是请求的一部分,不是模型的记忆。 每次都要重新带上;不在清单里的工具,模型不知道它存在
tool_use_id是配对用的。 一次回复里可能有好几个调用,结果回来时必须对得上号。每个tool_use都必须有一条tool_result,少一条,下一次请求直接报错- 模型只挑工具、填参数。 会不会执行、怎么执行、要不要问过你,全看你写的客户端
一次回复,可以同时要几个工具
模型经常一口气发好几个 tool_use,比如同时读三个文件。客户端会看情况分批:只读的并行跑,会改东西的一个一个来(Claude Code 里连续只读工具默认最多并行 10 个)。这也是为什么 Agent 有时候看起来「同时在做几件事」。
模型输出是流式的,tool_use 的 JSON 也是一段段吐出来的。客户端要等参数拼完整、校验通过,才敢执行。
三、在 Claude Code 里,一切都是 Function Calling
现在回头看 Claude Code 干的活:
| 你看到的动作 | 背后的工具 |
|---|---|
| 读文件 | Read |
| 改文件 | Edit、Write |
| 跑命令 | Bash |
| 搜内容 / 找文件 | Grep、Glob |
| 查网页 | WebFetch、WebSearch |
| 用 MCP 查文档 | mcp__context7__query-docs 这类 |
对模型来说没有区别:都是清单上的一项,都有名字、描述、参数 schema。内置的和 MCP 接进来的,走的是同一个出口。
前面 Loops 那一节的循环——思考 → 调工具 → 看结果 → 再思考——第二步就是 Function Calling。循环本身不在模型里,它是客户端(Claude Code)写的一段程序。
下次运行 /context,看工具定义那一栏:你看到的每一项,就是模型眼里的一个可调用工具,也是它开局就占掉一块上下文的原因。
前面几节和后面要讲的「为什么」,到这儿能串起来了:
| 书里讲过的 | 在 Function Calling 里的位置 |
|---|---|
| 上下文窗口里那 14.9k | 大头是系统提示词和工具定义,每个工具都有一份说明书 |
| 权限模式 | 卡在「模型要调工具」和「工具真正执行」之间 |
| CLAUDE.md | 一段文本,每次会话自动加载,改的是模型想做什么 |
| Skill | 也是一段文本,教它怎么用已有的工具,不新增工具 |
| MCP | 往工具清单里加几项,工具变了,机制没变 |
| 工具返回的结果 | 和工具定义一样进上下文,下一轮跟着消息一起发回去 |
所以「Agent 做错事」要分两种:想错了(提示词、描述、上下文的问题),和本不该让它做(权限和工具清单的问题)。第一种改文字,第二种改闸门。
四、它不是什么
Function Calling 经常被说成「Agent 的能力」,其实它只是中间那座桥。把它和几个容易混的东西分开:
- Function Calling ≠ Agent。 Agent = 模型 + 工具 + 循环 + 上下文管理 + 权限。Function Calling 只解决「模型怎么把要求说清楚」,循环、压缩、权限都得客户端自己写
- Function Calling ≠ 模型自己执行。 模型只输出意图,执行在你这边。所以「AI 自己把服务器删了」这种说法,省掉了中间那个真正动手的程序
- Function Calling ≠ 一定可靠。 它会挑错工具,也会编出不存在的工具名、把参数填错。所以要有参数校验、要有失败重试,也要能让它看到错误信息后自己改
- 工具越多 ≠ 越好。 每一项工具都占上下文,描述重叠时模型更容易挑错。这也是后面讲 MCP 时强调「按需接」的原因之一
为什么 2023 年才普及
不是没人想过。早些年也有让模型输出固定格式、再用正则去抠的做法,问题是模型不够稳:同一个提示,今天输出 JSON,明天输出一段解释加 JSON,解析器天天修。
等模型能稳定照着 schema 输出、也能判断「这个任务该不该调工具」了,这套东西才真正能用。2023 年 6 月,OpenAI 在 API 里加了 function calling;Anthropic 后来在 Messages API 里用 tools + tool_use 实现了同一件事;2024 年底 MCP 把这套思路往「工具怎么被发现和共享」推了一步。
叫法也乱:Function Calling、Tool Calling、Tool Use,说的是同一件事。早期强调「调用函数」,现在更常说「调用工具」,因为工具不一定是个函数,也可能是一次查询、一次浏览器操作。
你不用写,但你在影响它
在 Claude Code 里,你确实不用碰 tools 参数和 tool_result。但你每天都在间接改它:
- 写进 CLAUDE.md 的话、Skill 的 description,会影响它选哪个工具、什么时候选
- 权限规则决定哪些
tool_use能真的执行 - 接哪些 MCP,决定清单有多长、模型要在一堆相似工具里挑
懂这层机制,排查问题就有方向:它的工具选错了,先看描述和工具数量,别急着换模型;它做了不该做的事,先看权限,别只在 CLAUDE.md 里写「不要」。
小结
- 模型只会输出文字。Function Calling 给它一个结构化出口:说清要调哪个工具、参数是什么;执行永远在模型外面
- 一次调用要发两次请求:第一次带上工具清单、拿回
tool_use;第二次带上tool_result、拿回最终回答。工具清单每轮都要带,tool_use_id必须配对 - 它不是 Agent,只是一座桥。Claude Code 的每个动作都走这座桥,所以上下文、权限、MCP、Skill 的很多现象,都能在这里找到解释