从零理解 AI Agent(三):工具系统——Agent 的手脚

58 阅读10分钟

《从零理解 AI Agent》系列 · 第 3 / 7 篇

上一篇讲完了心跳(Loop),这一篇讲手脚(工具)。没有工具的 Agent 只能纸上谈兵——它知道该怎么改代码,但它摸不到你的文件系统。我们来搞清楚四件事:模型是怎么"调用"函数的、为什么工具不能太多、MCP 和 Skills 是什么、权限怎么设计


一、Function Calling:模型是怎么"调用"函数的?

你问 ChatGPT:"北京今天的天气怎么样?"它不会瞎编天气,而是返回这样一段东西:

{
  "type": "tool_use",
  "name": "get_weather",
  "input": { "city": "北京" }
}

问题来了:模型怎么知道要调用 get_weather?它又怎么知道参数叫 city?

真相是:模型从来不会自己调用函数。整个 Function Calling 流程是这样的:

  1. 我们提前塞了一份"工具菜单":在调用 API 时,附上一组 JSON Schema,描述每个工具的名字、参数格式、用途
  2. 模型看着菜单做决策:根据用户的问题,判断要不要用工具、用哪个
  3. 模型生成一段符合 JSON Schema 的 JSON——这就是所谓的"函数调用"(其实只是生成了一段文本!)
  4. 我们的代码解析这段 JSON,真正去触发对应的函数
  5. 执行结果塞回 messages,再发给模型,让它生成最终回复

另一个问题:模型凭什么保证输出的 JSON 一定合法?

两个手段:

  1. 训练:模型见过海量合法 JSON,学会了格式
  2. 约束解码(Constrained Decoding):模型每算一个 token 时,把不合法的选项直接排除掉。就像考试时把错误选项划掉,只剩正确答案可以选

约束解码这项技术不仅用于工具调用,还支撑了 Structured Output(结构化输出)——让模型严格按你定义的 JSON 结构返回数据,比如:

{ "score": 80, "issue": ["变量命名不规范", "缺少错误处理"], "pass": true }

这在上下文压缩(生成摘要)、生成式 UI、信息提取场景中非常常用。


二、工具系统的第一原则:模型生成的输入是不可信的

这是工具系统设计的金科玉律。模型可能幻觉出不存在的参数——你让它读 src/utils.ts,它可能输出 src/helper/utils.ts(一个不存在的路径)。

所以,从"模型表达意图"到"工具真正执行"之间,需要一条完整的处理管线:

  1. 验证:检查模型输出的 JSON 类型是否合法(约束解码)
  2. 校准:业务逻辑层面的语义校准
  3. 拦截补全:模型说要调 read_file,参数是 "src/index.ts"(相对路径)——需要拦截并转换为绝对路径
  4. 前置 Hook:防止模型生成的 JSON 中夹带恶意代码,在执行前先过一道检查
  5. 权限检查(三级):
    • 规则匹配:比如"读文件类工具直接放行"——够快够便宜
    • 分类器判断:用一个轻量 LLM 评估操作是否安全
    • 交互式询问:规则和分类器都拿不准时,弹框让用户手动确认
  6. 真正执行tool.call()。注意:工具的执行结果可能非常大(比如读了一个几万行的日志),直接塞给模型会导致上下文爆炸。Claude Code 设了 50K 字符阈值:超阈值的结果存到磁盘,只给模型传"内容摘要 + 文件路径"
  7. 后置 Hook:工具执行完后做收尾处理——过滤敏感信息、文件改动后自动 lint、记录审计日志

三、工具越多越好吗?——恰恰相反

一个反直觉的事实:工具函数超过 50 个,模型选对的准确率会跌破 50%

为什么?三个原因:

  1. 注意力稀释:还记得上一篇的 QKV 机制吗?工具越多,每个工具描述在上下文中的注意力权重就越小
  2. 语义碰撞:不同工具的描述可能语义相近("查询用户"和"搜索用户"),模型分不清该调哪个
  3. 预算挤压:每个工具的 Schema 都要占 token,工具多了,留给用户消息和历史对话的空间就少了

解法:延迟加载(Deferred Tool Loading)——不一次性把所有工具摊给模型,而是把不常用的标记为"延迟加载",模型需要时再取。

3.1 Claude Code 的做法:ToolSearch 机制

模型看到的不是完整的工具 Schema,而是一份"菜单名称列表":

以下工具可用,但需要通过 ToolSearch 获取完整 Schema: ["read_file", "write_file", "delete_file", ...]

模型需要时通过 ToolSearch 查询,支持三种方式:

  1. 精确选择:直接指定工具名,如 "webSearch"
  2. 模糊匹配:输入"搜索"、"获取"等关键词,系统结合上下文推测要哪个工具
  3. 必选 + 排序:一次查询多个,按相关性排序返回

哪些工具会被延迟加载?两类:所有 MCP 接入的外部工具(用户自己装的,不可控),以及内置工具中标记了 shouldDefer: true 的。

顺便一提,延迟加载还有个隐藏福利:它是 KV Cache 的守护者。如果把所有工具 Schema 全量塞进上下文,每新增一个工具,整个 prompt 前缀都变了,KV Cache 全部失效重算。

3.2 其他 Agent 的思路

  • OpenClaw(工具配置文件):把工具按场景分组——minimal(最基础交互)、coding(代码相关)、messaging(通讯相关)、full(全部能力)。不在当前场景的工具全部延迟加载。
  • Manus(小工具集哲学):用最少的工具实现最大的功能:
    1. 原子工具:读文件、写文件、发 HTTP 请求等最基础的操作
    2. CLI 工具:通过沙箱暴露 gitnpm 等命令行——一个 CLI 背后抵得上几十个专用工具
    3. 现场写脚本:复杂逻辑让模型现用 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 可以对外暴露三种东西:

  1. Tool:可被模型调用的工具
  2. Resource:可被模型读取的数据源(文件、数据库)
  3. Prompt:预设的提示词模板

4.2 MCP 的三个硬伤

  1. Token 占用:一个 MCP Server 暴露 15~20 个工具是常态,每个都有名称、参数、描述——还记得"工具越多准确率越低"吗?
  2. 安全风险:一个恶意 MCP Server 可以在返回内容里藏一段提示词(提示词注入攻击),诱导 Agent 干坏事
  3. 复杂度:每个 MCP Server 都要一个额外进程、一套配置、一条通信链路

所以接了一堆 MCP 之后,常见两个症状:Agent 变迟钝、回答质量下降;Agent 做一些你从没要求的额外动作。

4.3 Claude Code 怎么管 MCP?三个字:强监管

  1. 命名空间隔离:所有 MCP 工具强制改名 mcp_<serverName>_<toolName>,解决工具名冲突;内置工具拥有优先执行权
  2. 全部延迟加载:MCP 工具一律不默认进上下文
  3. 共享权限管线:MCP 工具没有任何特权,照样走内部那条"验证→校准→拦截→授权"的管线

五、Skills:与其教新协议,不如用旧能力

MCP 之外还有一条路:Skills。它的哲学是——与其教模型使用新的协议,不如让它使用它最擅长的能力:读文件

一个 Skill 本质上就是一个文件夹:一个 Markdown 文件 + 几个脚本。模型会用 Read 工具读它,就等于"学会"了这项技能。

问题:100+ 个 Skill,全塞进上下文不可能。怎么办?三层渐进式加载:

  1. Frontmatter 技能头(永远加载):每个 skill.md 开头有一段 YAML 元信息,只占几十 token:

    ---
    name: weather-info
    description: 获取全球任意城市的实时天气信息和天气预报。
                  当用户询问天气、温度、降雨、风速等气象信息时,使用此技能。
    ---
    

    这就像书的目录——模型永远知道"有哪些技能可翻"。

  2. 完整内容(按需加载):模型判断当前任务与某个 Skill 相关时,才加载该 Skill 的正文。

  3. 引用文件(再按需加载):Skill 文件夹里的 scripts/(可执行脚本)、references/(参考文档)从不主动加载,模型需要时再用 Read 读取。

Skills vs MCP:不是竞争,是分工

MCPSkills
路线协议标准化文件约定
优点跨平台、通用简单轻量,模型天然会用
缺点协议本身有 Token 开销没有标准化跨平台能力
定位能力(能做什么)知识 + 能力(知道怎么做)

一句话总结:Skills 负责"知道怎么做",MCP 负责"实现它"。它们是合作关系。


六、权限系统:给手脚戴上镣铐

Agent 能删文件、能跑命令,如果不加约束,等于把 root 密码交给一个刚入职的实习生。

权限设计的核心矛盾:太松不安全,太紧烦死人。设计原则:

  1. 高频安全操作默认放行(读个文件还要弹框确认,用户会疯)
  2. 低频危险操作坚决拦截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 开发最重要的工程能力,没有之一"。