《从零理解 AI Agent》系列 · 第 3 / 7 篇
上一篇讲完了心跳(Loop),这一篇讲手脚(工具)。没有工具的 Agent 只能纸上谈兵——它知道该怎么改代码,但它摸不到你的文件系统。我们来搞清楚四件事:模型是怎么"调用"函数的、为什么工具不能太多、MCP 和 Skills 是什么、权限怎么设计。
一、Function Calling:模型是怎么"调用"函数的?
你问 ChatGPT:"北京今天的天气怎么样?"它不会瞎编天气,而是返回这样一段东西:
{
"type": "tool_use",
"name": "get_weather",
"input": { "city": "北京" }
}
问题来了:模型怎么知道要调用 get_weather?它又怎么知道参数叫 city?
真相是:模型从来不会自己调用函数。整个 Function Calling 流程是这样的:
- 我们提前塞了一份"工具菜单":在调用 API 时,附上一组 JSON Schema,描述每个工具的名字、参数格式、用途
- 模型看着菜单做决策:根据用户的问题,判断要不要用工具、用哪个
- 模型生成一段符合 JSON Schema 的 JSON——这就是所谓的"函数调用"(其实只是生成了一段文本!)
- 我们的代码解析这段 JSON,真正去触发对应的函数
- 执行结果塞回 messages,再发给模型,让它生成最终回复
另一个问题:模型凭什么保证输出的 JSON 一定合法?
两个手段:
- 训练:模型见过海量合法 JSON,学会了格式
- 约束解码(Constrained Decoding):模型每算一个 token 时,把不合法的选项直接排除掉。就像考试时把错误选项划掉,只剩正确答案可以选
约束解码这项技术不仅用于工具调用,还支撑了 Structured Output(结构化输出)——让模型严格按你定义的 JSON 结构返回数据,比如:
{ "score": 80, "issue": ["变量命名不规范", "缺少错误处理"], "pass": true }
这在上下文压缩(生成摘要)、生成式 UI、信息提取场景中非常常用。
二、工具系统的第一原则:模型生成的输入是不可信的
这是工具系统设计的金科玉律。模型可能幻觉出不存在的参数——你让它读 src/utils.ts,它可能输出 src/helper/utils.ts(一个不存在的路径)。
所以,从"模型表达意图"到"工具真正执行"之间,需要一条完整的处理管线:
- 验证:检查模型输出的 JSON 类型是否合法(约束解码)
- 校准:业务逻辑层面的语义校准
- 拦截补全:模型说要调
read_file,参数是"src/index.ts"(相对路径)——需要拦截并转换为绝对路径 - 前置 Hook:防止模型生成的 JSON 中夹带恶意代码,在执行前先过一道检查
- 权限检查(三级):
- 规则匹配:比如"读文件类工具直接放行"——够快够便宜
- 分类器判断:用一个轻量 LLM 评估操作是否安全
- 交互式询问:规则和分类器都拿不准时,弹框让用户手动确认
- 真正执行:
tool.call()。注意:工具的执行结果可能非常大(比如读了一个几万行的日志),直接塞给模型会导致上下文爆炸。Claude Code 设了 50K 字符阈值:超阈值的结果存到磁盘,只给模型传"内容摘要 + 文件路径" - 后置 Hook:工具执行完后做收尾处理——过滤敏感信息、文件改动后自动 lint、记录审计日志
三、工具越多越好吗?——恰恰相反
一个反直觉的事实:工具函数超过 50 个,模型选对的准确率会跌破 50%。
为什么?三个原因:
- 注意力稀释:还记得上一篇的 QKV 机制吗?工具越多,每个工具描述在上下文中的注意力权重就越小
- 语义碰撞:不同工具的描述可能语义相近("查询用户"和"搜索用户"),模型分不清该调哪个
- 预算挤压:每个工具的 Schema 都要占 token,工具多了,留给用户消息和历史对话的空间就少了
解法:延迟加载(Deferred Tool Loading)——不一次性把所有工具摊给模型,而是把不常用的标记为"延迟加载",模型需要时再取。
3.1 Claude Code 的做法:ToolSearch 机制
模型看到的不是完整的工具 Schema,而是一份"菜单名称列表":
以下工具可用,但需要通过 ToolSearch 获取完整 Schema:
["read_file", "write_file", "delete_file", ...]
模型需要时通过 ToolSearch 查询,支持三种方式:
- 精确选择:直接指定工具名,如 "webSearch"
- 模糊匹配:输入"搜索"、"获取"等关键词,系统结合上下文推测要哪个工具
- 必选 + 排序:一次查询多个,按相关性排序返回
哪些工具会被延迟加载?两类:所有 MCP 接入的外部工具(用户自己装的,不可控),以及内置工具中标记了 shouldDefer: true 的。
顺便一提,延迟加载还有个隐藏福利:它是 KV Cache 的守护者。如果把所有工具 Schema 全量塞进上下文,每新增一个工具,整个 prompt 前缀都变了,KV Cache 全部失效重算。
3.2 其他 Agent 的思路
- OpenClaw(工具配置文件):把工具按场景分组——
minimal(最基础交互)、coding(代码相关)、messaging(通讯相关)、full(全部能力)。不在当前场景的工具全部延迟加载。 - Manus(小工具集哲学):用最少的工具实现最大的功能:
- 原子工具:读文件、写文件、发 HTTP 请求等最基础的操作
- CLI 工具:通过沙箱暴露
git、npm等命令行——一个 CLI 背后抵得上几十个专用工具 - 现场写脚本:复杂逻辑让模型现用 Python/Node.js 写临时脚本执行
四、MCP:工具世界的"USB 接口"
4.1 为什么需要 MCP?
Agent 的工具分两种:内置工具(Read、Bash 等)和用户自己装的外部工具。问题在于,各家 Agent 定义工具的 JSON Schema 标准都不一样——Cursor 有自己的插件格式,ChatGPT 有应用市场,Coze 有 plugin 系统。工具开发者要为每个平台各写一套适配。
MCP(Model Context Protocol) 就是为了统一这件事:一套基于 JSON-RPC 2.0 的标准协议。你可以把 MCP 理解成一个"转接头"或"USB 接口"——工具开发者只要按 MCP 标准实现一次,所有支持 MCP 的 Agent 都能直接用。
MCP Server 可以对外暴露三种东西:
- Tool:可被模型调用的工具
- Resource:可被模型读取的数据源(文件、数据库)
- Prompt:预设的提示词模板
4.2 MCP 的三个硬伤
- Token 占用:一个 MCP Server 暴露 15~20 个工具是常态,每个都有名称、参数、描述——还记得"工具越多准确率越低"吗?
- 安全风险:一个恶意 MCP Server 可以在返回内容里藏一段提示词(提示词注入攻击),诱导 Agent 干坏事
- 复杂度:每个 MCP Server 都要一个额外进程、一套配置、一条通信链路
所以接了一堆 MCP 之后,常见两个症状:Agent 变迟钝、回答质量下降;Agent 做一些你从没要求的额外动作。
4.3 Claude Code 怎么管 MCP?三个字:强监管
- 命名空间隔离:所有 MCP 工具强制改名
mcp_<serverName>_<toolName>,解决工具名冲突;内置工具拥有优先执行权 - 全部延迟加载:MCP 工具一律不默认进上下文
- 共享权限管线:MCP 工具没有任何特权,照样走内部那条"验证→校准→拦截→授权"的管线
五、Skills:与其教新协议,不如用旧能力
MCP 之外还有一条路:Skills。它的哲学是——与其教模型使用新的协议,不如让它使用它最擅长的能力:读文件。
一个 Skill 本质上就是一个文件夹:一个 Markdown 文件 + 几个脚本。模型会用 Read 工具读它,就等于"学会"了这项技能。
问题:100+ 个 Skill,全塞进上下文不可能。怎么办?三层渐进式加载:
-
Frontmatter 技能头(永远加载):每个 skill.md 开头有一段 YAML 元信息,只占几十 token:
--- name: weather-info description: 获取全球任意城市的实时天气信息和天气预报。 当用户询问天气、温度、降雨、风速等气象信息时,使用此技能。 ---这就像书的目录——模型永远知道"有哪些技能可翻"。
-
完整内容(按需加载):模型判断当前任务与某个 Skill 相关时,才加载该 Skill 的正文。
-
引用文件(再按需加载):Skill 文件夹里的
scripts/(可执行脚本)、references/(参考文档)从不主动加载,模型需要时再用 Read 读取。
Skills vs MCP:不是竞争,是分工
| MCP | Skills | |
|---|---|---|
| 路线 | 协议标准化 | 文件约定 |
| 优点 | 跨平台、通用 | 简单轻量,模型天然会用 |
| 缺点 | 协议本身有 Token 开销 | 没有标准化跨平台能力 |
| 定位 | 能力(能做什么) | 知识 + 能力(知道怎么做) |
一句话总结:Skills 负责"知道怎么做",MCP 负责"实现它"。它们是合作关系。
六、权限系统:给手脚戴上镣铐
Agent 能删文件、能跑命令,如果不加约束,等于把 root 密码交给一个刚入职的实习生。
权限设计的核心矛盾:太松不安全,太紧烦死人。设计原则:
- 高频安全操作默认放行(读个文件还要弹框确认,用户会疯)
- 低频危险操作坚决拦截(
rm -rf这种,一次都不能放过)
Claude Code 的四种权限模式:
| 模式 | 行为 | 适用场景 |
|---|---|---|
plan | 只读、搜索,绝不写入 | 设计方案阶段 |
default | 读取自动允许,写入需确认 | 日常开发 |
acceptEdits | 文件编辑自动放行,但 Bash 命令仍需确认 | 信任模型改代码,但不信任它执行命令 |
bypassPermissions | 绕过所有检查,完全放权 | 测试环境(沙箱) |
注意 acceptEdits 模式的精妙之处:它背后的判断是——模型改错代码,最多 git revert;模型跑错命令,可能直接删库。两种"写"操作的风险等级完全不同。
除了全局模式,还支持细粒度权限规则:可以针对每个工具、甚至每个参数模式设置权限。
小结
这一篇我们给 Agent 装上了手脚:
- Function Calling 的真相:模型不调用函数,只是生成一段"我想调用 XX"的 JSON,由我们的代码代为执行
- 第一原则:模型生成的输入不可信,中间必须有一条"验证→校准→拦截→授权"的管线
- 工具不是越多越好:50+ 个工具准确率跌破 50%,所以要延迟加载
- MCP 是 USB 接口:统一工具接入标准,但会带来 Token、安全、复杂度三个硬伤,需要强监管
- Skills 是说明书:用模型最擅长的"读文件"能力传递知识,与 MCP 分工合作
- 权限是镣铐:高频安全操作放行,低频危险操作拦截,还有四种模式按信任程度分级
有了手脚,新的问题出现了:Agent 一跑 50 轮,读了几十个文件,上下文窗口塞不下怎么办?这正是下一篇——上下文工程要回答的问题。它被称为"Agent 开发最重要的工程能力,没有之一"。