
会聊天,不等于会做事
你做了一个终端里的 AI 助手,能多轮对话,能解释代码,Markdown 渲染也很漂亮。
然后你对它说:
帮我看看这个项目的入口文件里有什么。
如果它没有工具,最可能的回答是:
我无法直接访问你的文件系统。你可以把文件内容贴给我,我再帮你分析。
这句话听起来礼貌,但从 Agent 的角度看,问题很明显:它只有嘴,没有手。
它可以分析你贴进来的代码,却不能自己找文件;它可以告诉你“应该运行测试”,却不能自己执行命令;它可以建议你修改某一行,却不能自己打开文件、精确替换、再验证结果。
这就是聊天机器人和 Agent 的分界线:
聊天机器人:用户给上下文,模型生成文本
Agent:模型决定下一步动作,应用执行工具,结果再回到模型
Function Calling,也常被叫作 Tool Use,就是让模型从“只会说”走向“可以请求做事”的第一层协议。
一句话定义
Function Calling 可以这样理解:
模型根据用户问题和工具描述,生成一个结构化工具调用请求;应用接收请求、执行真实工具,再把执行结果作为上下文返回给模型。
注意这里的主语。
模型不直接读文件,不直接查数据库,不直接执行 shell,也不直接调用你的业务接口。它只输出一段结构化内容,类似:
{
"name": "read_file",
"arguments": {
"path": "/workspace/src/main.ts",
"offset": 1,
"limit": 120
}
}
真正决定“能不能读、怎么读、读完返回什么”的,是你的应用。

先纠正三个误解
误解一:模型调用了函数
更准确的说法是:模型请求调用函数。
OpenAI 的 Function Calling 文档把函数定义为一种工具,它通过 JSON Schema 描述参数,模型把数据传给你的应用,由你的代码访问数据或执行动作。Anthropic 的 Tool Use 文档也把客户端工具说得很明确:Claude 返回 tool_use 内容块,你的代码执行操作,再发送 tool_result。
所以不要把 Function Calling 想成:
LLM -> function()
它更像:
LLM -> 结构化调用请求
应用 -> 权限/参数/执行/日志
应用 -> tool_result
LLM -> 基于结果继续回答或继续调用工具
这个边界非常重要。因为安全、权限、幂等、审计、超时、重试,都不应该交给模型“自觉遵守”。
误解二:有 JSON Schema 就够了
JSON Schema 能约束字段类型、必填项、枚举值和嵌套结构。它能告诉模型:path 是字符串,limit 是数字,mode 只能是 read 或 write。
但它不能天然知道:
- 这个路径是否在工作区内;
- 当前用户是否允许访问这个文件;
- 这个 shell 命令是否危险;
- 这个订单是不是当前用户的订单;
- 这个工具结果是否包含敏感字段。
所以 Schema 是入场券,不是安全系统。
真正的工具系统,还需要输入校验、策略引擎、风险分级、执行沙箱、审计日志和错误回填。
误解三:工具调用就是最终答案
tool_use 不是答案,它只是下一步动作。
一次真正的 Agent 任务经常是这样的:
用户:帮我看登录接口为什么 500
模型:先 grep LoginController
应用:返回匹配文件
模型:读取 Controller 和 Service
应用:返回文件内容
模型:发现空指针风险,修改代码
应用:执行 EditFile
模型:运行测试
应用:返回测试失败
模型:继续修
应用:返回测试通过
模型:总结修复点
这条链路里,模型每一步都在根据工具结果更新判断。Function Calling 只是把“下一步动作”结构化了,Agent Loop 才会让这些动作串成完整任务。
官方协议的共同骨架
不同平台的字段名不完全一样,但核心骨架很接近。
| 阶段 | 做什么 | 关键点 |
|---|---|---|
| 定义工具 | 把工具名、描述、输入 schema 提供给模型 | 描述决定模型什么时候用工具 |
| 模型请求 | 模型返回工具名和参数 | 这只是请求,不代表已执行 |
| 应用执行 | 本地代码调用文件、命令、API 或业务服务 | 权限、超时、幂等、审计都在这里 |
| 回填结果 | 把 tool_result 返回给模型 | 错误也要作为结构化反馈 |
| 继续循环 | 模型基于结果回答或继续调工具 | 这就是 Agent Loop 的基础 |
MCP Tools 规范也是类似思路:服务端暴露工具,工具有名字、描述和输入 schema;工具被设计成模型可根据上下文自动发现和调用,但应用侧仍应提供清晰的工具暴露提示、调用可见性和人工确认机制。
这说明一个共识正在形成:
Tool Use 的关键不是“模型变成执行器”,而是“模型和执行系统之间有了清晰契约”。
工具描述比很多人想得更重要
很多人写工具时,最重视 execute(),最随便写 description。
这很危险。
模型决定要不要用某个工具,主要看的就是工具名、描述和参数说明。Anthropic 的 Define tools 文档明确要求 description 说明工具做什么、什么时候用、行为如何。OpenAI 的文档也把 description 描述为告诉模型何时、如何使用函数的字段。
差描述通常长这样:
读取文件
好描述应该更像这样:
读取指定路径的文本文件内容,返回带行号的文本。
适用于需要查看文件完整内容或指定行范围的场景。
路径必须位于当前工作区内,不能读取二进制文件。
大文件请先用 grep 定位,再用 offset 和 limit 分段读取。
如果文件不存在、越权或是二进制文件,返回结构化错误。
这段描述告诉模型五件事:
- 这个工具做什么;
- 什么时候该用;
- 什么时候不要用;
- 参数有什么约束;
- 返回值和错误长什么样。
一个可维护的工具定义,至少应该包含这些信息:
| 字段 | 作用 | 示例 |
|---|---|---|
name | 工具唯一标识 | ReadFile |
description | 给模型看的使用说明 | 什么时候读文件、限制是什么 |
input_schema | 参数结构约束 | JSON Schema |
output_contract | 返回值约定 | 文本、结构化 JSON、错误码 |
category | 工具分类 | file、search、shell、api |
read_only | 是否只读 | Grep 是只读,WriteFile 不是 |
destructive | 是否高风险 | Bash、删除、退款重试 |
concurrency_safe | 是否可并发 | 多个只读搜索可以并发 |
timeout_ms | 执行上限 | Bash 默认 30 秒 |
validate() | 执行前校验 | 路径、参数、权限预检 |
不要把这些字段看成“工程洁癖”。它们会直接影响模型路由、运行时策略和后续审计。
一个工具运行时应该拆成几层
如果只是 Demo,你可以写一个 if tool_name == "read_file"。
但只要工具数量超过三五个,最好尽快拆成运行时架构。

我会把最小工具运行时拆成六层。
1. Tool Registry
Registry 是工具注册中心。
它负责:
- 注册工具;
- 按名称查找工具;
- 按场景启用或禁用工具;
- 把内部工具定义转换成模型 API 需要的格式;
- 控制本轮对话暴露哪些工具。
不要每次请求都手写工具列表。工具列表应该由 Registry 统一生成。
2. Schema Validator
模型生成的参数不能直接执行。
先用 schema 做结构校验:
- 必填字段是否存在;
- 类型是否正确;
- 枚举值是否合法;
- 数字范围是否合理;
- 字符串长度是否超限。
结构校验失败,不应该让程序崩溃,而应该返回一个可给模型理解的错误结果,让模型补参数或修正参数。
3. Policy Engine
Schema 只能管结构,Policy 管边界。
例如:
- 文件路径是否越过工作区;
- 写文件是否需要用户确认;
- Bash 命令是否命中危险模式;
- 当前用户能否调用这个业务 API;
- 本轮是否允许暴露高风险工具。
Policy Engine 的结果最好也是结构化的:允许、拒绝、需要确认、需要降级。
4. Tool Executor
Executor 负责真正执行工具。
它要处理:
- 超时;
- 并发;
- 输出截断;
- 非零退出码;
- 资源清理;
- 执行异常;
- 结果归一化。
这里有一个容易踩的点:命令返回非零退出码,不一定是系统错误。
比如测试失败、编译失败、grep 没匹配到,都可能是对模型有价值的反馈。它们应该作为工具结果返回,而不是直接抛成 Agent 崩溃。
5. Result Adapter
工具内部结果不一定适合原样给模型。
Result Adapter 负责把执行结果转换成模型能消费的形式:
- 成功结果保留关键内容;
- 长输出做安全截断;
- 敏感字段脱敏;
- 错误信息给出可恢复建议;
- 状态未知时明确标记
UNKNOWN,避免模型瞎承诺。
6. Trace / Audit
只要 Agent 能调工具,就必须记录轨迹。
至少要记录:
- 哪次对话;
- 哪个模型;
- 哪个工具;
- 参数摘要;
- 策略判断;
- 执行耗时;
- 结果状态;
- 错误码;
- 最终回答是否引用了工具结果。
没有 Trace,Function Calling 就很难从 Demo 走到生产。
Coding Agent 最小六件套
如果目标是做一个能改代码的 Agent,第一版工具不需要很多,但要覆盖最基本的读、搜、写、改、跑。
ReadFile:要带行号和范围
ReadFile 看起来简单,其实有三个细节:
- 返回内容最好带行号,方便模型引用和后续 Edit;
- 支持
offset和limit,避免一次读爆上下文; - 检测二进制文件,不要把乱码塞给模型。
错误也要区分:文件不存在、路径越权、权限不足、二进制文件,不要统一返回 read failed。
Glob:找文件,不要扫垃圾目录
Glob 用来找文件名和项目结构。
它应该默认排除:
.git
node_modules
target
dist
vendor
__pycache__
.idea
结果要限制数量,并按最近修改或路径稳定排序。否则一个 **/* 就能把上下文打爆。
Grep:找内容,但要控制噪音
Grep 是 Coding Agent 的眼睛之一。
它要支持:
- 正则搜索;
- 文件类型过滤;
- 上下文行;
- 最大结果数;
- 二进制排除;
- 输出
path:line:content。
但 Grep 只知道字符串,不知道符号关系。复杂 Java/Spring 项目后续最好接 LSP,让 Agent 能查 definition、references 和 diagnostics。
WriteFile:覆盖写入要谨慎
WriteFile 适合创建新文件或完整覆盖小文件。
它至少要:
- 创建父目录;
- 明确是否覆盖已有文件;
- 返回写入字节数;
- 写前触发策略检查;
- 写后可选返回摘要。
不要让模型随手覆盖大文件,尤其不要在没有确认的情况下覆盖配置、迁移脚本和生产资源文件。
EditFile:要求唯一匹配
EditFile 是最省 token 的修改方式。
它的核心参数通常是:
{
"path": "/workspace/src/UserService.java",
"old_string": "return userRepository.findById(id);",
"new_string": "return userRepository.findByIdAndTenantId(id, tenantId);"
}
关键约束:old_string 必须唯一匹配。
如果出现 0 次,说明模型上下文过期或文件读错;如果出现多次,说明上下文不够,应该让模型提供更长片段,而不是帮它猜。
Bash:最强,也最危险
Bash 能编译、测试、安装依赖、查看环境、运行脚本。
也正因为它太强,默认应该最保守:
- 设置工作目录;
- 设置超时;
- 合并 stdout/stderr;
- 长输出截断;
- 危险命令拦截或确认;
- 默认不并发;
- 记录完整命令和退出码。
对模型来说,Bash 的输出经常是最有价值的反馈。测试失败不是坏事,隐藏失败才是坏事。
流式 tool_use 为什么更麻烦
普通文本流很好处理:来了就追加。
但工具调用流不是这样。
Anthropic 的 streaming 文档里,工具输入参数会以 input_json_delta 的形式分片到达,每个 delta 只是一段 partial_json。Fine-grained tool streaming 还提醒开发者:为了降低延迟,参数流可能绕过完整 JSON 缓冲,你需要处理 partial 或 invalid JSON 的边界。
也就是说,你不能收到一小段就立刻 JSON.parse()。
一个更稳的处理方式是:
content_block_start(tool_use)
记录 id 和 name
初始化 inputBuffer
content_block_delta(input_json_delta)
inputBuffer += partial_json
content_block_stop
parse inputBuffer
生成完整 ToolUse 事件
伪代码可以这样写:
type PendingToolUse = {
id: string;
name: string;
inputBuffer: string;
};
let pending: PendingToolUse | null = null;
function onContentBlockStart(block: any) {
if (block.type === "tool_use") {
pending = {
id: block.id,
name: block.name,
inputBuffer: ""
};
}
}
function onContentBlockDelta(delta: any) {
if (pending && delta.type === "input_json_delta") {
pending.inputBuffer += delta.partial_json;
}
}
function onContentBlockStop() {
if (!pending) return;
try {
const input = JSON.parse(pending.inputBuffer);
emitToolUse({
id: pending.id,
name: pending.name,
input
});
} catch (error) {
emitToolUseParseError({
id: pending.id,
name: pending.name,
raw: pending.inputBuffer,
message: String(error)
});
} finally {
pending = null;
}
}
这里最重要的是两个字:缓冲。
不要把流式 tool_use 当普通文本;也不要假设模型输出的每个参数片段都是可解析 JSON。
错误也是工具结果的一部分
工具系统里,一个成熟设计是:把可恢复错误返回给模型,而不是让 Agent Loop 崩掉。
例如 ReadFile 返回:
{
"is_error": true,
"code": "FILE_NOT_FOUND",
"message": "文件不存在:/workspace/config.yaml。可以先用 Glob 搜索 *.yaml。"
}
这比抛一个内部异常更有用。
模型看到这个结果,可能会继续调用 Glob 找配置文件;看到权限拒绝,它应该停止;看到参数缺失,它可以追问;看到测试失败,它可以根据报错继续修。
我建议至少把工具结果分成这几类:
| 状态 | 含义 | 模型应该怎么做 |
|---|---|---|
SUCCESS | 工具执行成功 | 基于结果继续 |
VALIDATION_ERROR | 参数结构不对 | 修正参数或追问 |
POLICY_DENIED | 权限或策略拒绝 | 停止绕路,向用户说明 |
EXECUTION_ERROR | 工具执行失败但状态明确 | 根据错误调整 |
TIMEOUT | 执行超时 | 不要假装成功 |
UNKNOWN | 写操作状态不确定 | 等待确认,不要重试高风险动作 |
如果只返回一段模糊的 failed,模型很容易瞎补。

一套最小落地清单
如果你正在给自己的 Agent 加工具系统,可以按这个顺序做:
- 先定义工具接口,不要只写函数。
- 每个工具都写清
name、description、input_schema、read_only、destructive、category。 - 工具描述里写明“什么时候用、什么时候不用、参数限制、返回格式、错误格式”。
- Registry 统一生成模型 API 需要的工具定义。
- 执行前先做 schema 校验,再做 policy 校验。
- 写操作和 Bash 默认需要更严格的确认或沙箱。
- 工具错误返回给模型,但要结构化,不要只给
failed。 - Bash 非零退出码通常是工具反馈,不要直接当程序崩溃。
- 流式 tool_use 要缓冲 JSON 片段,到 block stop 再解析。
- 记录 Trace,至少覆盖工具名、参数摘要、策略判断、结果状态和耗时。

面试或技术分享可以怎么讲
如果被问到“Function Calling 是怎么工作的”,不要只说“模型会调用函数”。
可以这样回答:
Function Calling 的本质是模型和应用之间的结构化动作协议。应用先把可用工具的名称、描述和 JSON Schema 发给模型;模型根据上下文输出工具调用请求,包括工具名和参数;应用拿到请求后做参数校验、权限判断和真实执行;再把 tool_result 返回给模型。模型不会直接执行函数,它只是提出调用意图。真正的执行边界、错误处理、审计和权限控制都在应用侧。
如果继续问“工具系统怎么设计”,可以补一句:
工具不应该只有 name 和 execute。生产级工具至少要有 description、input schema、只读/破坏性标记、分类、超时、并发安全、validate 和结构化 ToolResult。工具描述影响模型选工具,元信息影响运行时策略,ToolResult 影响 Agent Loop 的恢复能力。
这比“给模型挂几个 API”更接近真实工程。
总结
Function Calling 让 Agent 有了“动手”的入口,但它不是魔法。
模型仍然只是在生成内容,只是这次生成的是结构化调用请求。你的应用才是真正的执行者。
所以一个靠谱的工具系统,重点不是能不能把某个函数暴露给模型,而是能不能回答这些问题:
- 模型知道什么时候该用这个工具吗?
- 参数结构能被校验吗?
- 工具执行前有权限和风险判断吗?
- 错误能作为反馈回到模型吗?
- 流式 tool_use 能稳定解析吗?
- 工具调用轨迹能被审计和回放吗?
一句话记住:
Function Calling 的核心不是“让模型执行函数”,而是“让模型提交动作请求,让应用受控执行”。
参考资料
- OpenAI API Docs:Function Calling
https://developers.openai.com/api/docs/guides/function-calling - Anthropic Claude Docs:Tool use overview
https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview - Anthropic Claude Docs:Define tools
https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools - Anthropic Claude Docs:Fine-grained tool streaming
https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming - Anthropic Claude Docs:Streaming messages
https://platform.claude.com/docs/en/build-with-claude/streaming - Model Context Protocol Specification:Tools
https://modelcontextprotocol.io/specification/2025-06-18/server/tools - Spring AI Reference:Tool Calling
https://docs.spring.io/spring-ai/reference/api/tools.html - LangChain4j Docs:Tools Function Calling
https://docs.langchain4j.dev/tutorials/tools/ - JSON Schema:Object reference
https://json-schema.org/understanding-json-schema/reference/object

1354

被折叠的 条评论
为什么被折叠?



