第 10 章 工具接口哲学:让 Agent 能用、好用、用得起
工具的脾气,就是 Agent 的天花板
第二部分我们把上下文管明白了。从这一章开始,进入第三部分"工具与环境",让 Agent 真正"动手做事"。
动手靠工具。而一个残酷的事实是:你给 Agent 什么工具,它就有什么能力;工具接口设计得别扭,Agent 再聪明也白搭。 人用 API 靠读文档、靠经验、靠 IDE 的自动补全,模型用 API 全靠接口本身"长什么样"。一个为人类设计的、层层嵌套的接口,对模型来说就是一场灾难:它要么报错,要么胡说。
这一章讲三个模式,回答"怎么设计工具,Agent 才好用":
- LLM-Friendly API Design(LLM 友好的 API 设计):把接口设计成"模型一看就懂"。
- Code-First Tool Interface(代码优先的工具接口):与其让模型一步步调用工具,不如让它写代码来编排工具。
- Code-Over-API(代码优于直接调用):数据处理别走上下文,在沙箱里用代码搞定。
模式一:LLM 友好的 API 设计(LLM-Friendly API Design)
问题
接口设计通常只考虑人类:版本号藏在 Release Notes 里、函数参数靠 IDE 提示、错误信息写给开发人员看。这些设计对人没问题,但对模型就是三座大山:
- 版本不透明:模型调用 API 时不知道自己在用哪个版本,按旧版参数调用新版接口,直接失败。
- 间接层太深:代码库七拐八绕,一个函数要穿过三四层抽象才找到真正的实现,模型很难推理"这个接口到底是干嘛的"。
- 错误信息没用:报错只说"调用失败",不告诉模型错在哪、怎么改,模型无从自我纠正。
一个直觉验证:ReAct 这类框架(Yao 等人,ICLR 2023)的核心就是把"推理"和"动作"交替起来,让模型边想边调工具。可一旦工具接口本身让人困惑,模型的每一步推理都建立在错误的理解上,想得越多错得越远。
方案
在设计和改造接口时,把"模型会怎么读这个接口"当成一等公民。 具体动作:
- 显式版本:把版本号做成模型"看得见"的东西。比如在函数名、端点路径、参数 schema 里直接体现(
create_vpc_v2、/api/v2/vpcs),而不是只在文档里提一句。 - 自描述功能:函数名、参数名、类型 schema(JSON Schema / OpenAPI)、文档都要能直接说明"这个 API 干什么、怎么用"。模型没有 IDE,它读的就是这些名字和 schema。
- 简化交互:能用一次调用解决的,别拆成三步;能扁平就别嵌套,减少模型出错的机会。
- 清晰的错误信息:错误响应要"可行动":告诉模型哪里错了、参数应该是什么、下一步怎么办。
- 减少间接层:把代码和库的结构压到两层间接以内,而不是 n 层,让模型能推理代码库。
# 面向人的设计(模型容易翻车)
POST /resources
body: { type: "compute", spec: { size: "large", image: 42, net: { sg: "web", vpc: "main" } } }
# 出错时只返回: {"error": "bad request"}
# 面向模型的设计(看一眼就懂)
POST /v2/compute_instances
body: {
"instance_type": "m4.large", # 参数名直白,带版本
"image_id": "ami-0421", # 类型清晰
"security_group_id": "sg-123", # 扁平,不嵌套
"vpc_id": "vpc-456"
}
# 出错时返回:
{"error": {"code": "INVALID_INSTANCE_TYPE",
"detail": "m4.large 不存在,可选: m4.xlarge / m5.large / t3.medium",
"hint": "把 instance_type 改成其中一个有效值"}}证据
- 来自 Cursor 的实践(Lukas Möller 的分享,
emerging):API 设计已经在朝"让模型更舒服"的方向调整,比如把版本号做得对模型可见、把代码结构压到两层间接。 - Gorilla(Berkeley,2023)证明:微调模型可以学会"按文档调用 API",但前提是接口文档得够好,它才会调用得够准。
- MCP(Model Context Protocol)本身就是在做这件事:用标准化的工具 schema 让模型理解工具。
怎么用
- 当"Agent 能不能可靠调用工具"决定成败时,先审视接口设计。
- 从窄工具面开始:先暴露少量、参数校验严格的工具,别一上来铺一大堆。
- 给工具调用加可观测性:记录延迟、失败、回退路径,你才知道哪个接口在坑模型。
- 错误信息当成"给模型写的调试信息"来设计,这是投入产出比最高的动作。
取舍
- 好处:工具调用成功率明显上升,失败率下降。
- 代价:接口设计要花心思,会和"人用的老接口"产生集成耦合;要为不同环境维护不同的接口形态。
模式二:代码优先的工具接口(Code-First Tool Interface / Code Mode)
问题
上一节的思路是"把接口改得让模型好调用"。但就算接口再友好,让模型一个工具一个工具地调用,本身就有硬伤:
- Token 浪费:每次工具调用,中间结果都要回灌进模型上下文。经典 MCP 模式是这样的:
LLM → 工具#1 → 大 JSON 响应 → LLM 上下文
LLM → 工具#2 → 大 JSON 响应 → LLM 上下文
LLM → 工具#3 → 大 JSON 响应 → LLM 上下文
→ 最终答案- Fan-out 爆炸:批量场景直接崩。处理 100 封邮件做个性化外联:传统 MCP 要 100 次独立工具调用,每封邮件的元数据可能上千 token,还没开始干活上下文就 10 万+ token,直接溢出。
方案
与其让模型调用工具,不如让模型写代码来编排工具。 这个思路叫 Code Mode:加一层"临时执行层"(ephemeral execution layer),模型生成代码,代码在沙箱里一次性编排多次工具调用,只有最终结果回流到上下文。
分工是这样的:
- MCP Server 管持久层:凭据、鉴权、限流、配额、Webhook、长连接、安全策略。这些"脏活"留在服务器。
- Code Mode 管临时层:多步编排、数据转换、业务逻辑。一次执行搞定,中间结果不进上下文。
架构骨架:
- Schema 发现:运行时动态拉取 MCP server 的工具 schema。
- API 转换:把 MCP 工具 schema 转成带类型和文档注释的 TypeScript 接口。
- LLM 感知:模型拿到完整的 TypeScript API 文档,知道有哪些工具可用。
- 临时代码生成:模型生成一段代码,在一次脚本里编排多次工具调用。
- V8 Isolate 执行:轻量、安全的沙箱,执行完即销毁,无持久状态。
- 受控绑定:安全的桥接,真正持有凭据和逻辑的仍然是 MCP server。
# 传统 MCP:一次调用一次回灌
rows = tool_call("spreadsheet.getRows", sheet="abc")
# 1000+ token 塞进上下文
for row in rows:
result = tool_call("sendEmail", to=row.email, body=row.text)
# 又是 1000+ token 回灌
# Code Mode:写一段代码,沙箱里跑完
def 批量外联(contacts):
vpc = create_vpc(name="demo") # 第一次工具调用
igw = create_internet_gateway(vpc.id) # 第二次
sg = create_security_group(vpc.id, ssh=True) # 第三次
inst = launch_ec2(type="m4.large", vpc=vpc.id) # 第四次
tag_resources([vpc, igw, sg, inst]) # 第五次
return {"instanceId": inst.id, "ssh": inst.ssh} # 只有这一个结果回流注意关键点:模型知道该写什么代码,不是因为它猜,而是因为它拿到了完整的、强类型的接口文档。schema 从 MCP server 动态生成,接口是"喂"给它的,不是它编的。
什么时候该用 Code Mode
- 流程型问题:步骤能提前画出来。比如基础设施开通、数据管道编排、批量操作。
- Fan-out 场景:对 100+ 条数据跑 for 循环,比让模型连调 100 次工具强得多;N 越大,传统方式越慢,Code Mode 反而更快。
- 自调试:生成的代码自带错误处理、日志、重试逻辑(CaMeL 风格的"Agent 自己改自己作业")。
反模式(别用):
- 开放式研究循环:每一步都要决定"下一步干嘛"的问题,Code Mode 很难提前写死。
- 执行中途需要智能:比如给 100 个联系人写"个性化"邮件,正文必须靠模型在循环里逐条生成。这又退回传统 Agent 模式,Code Mode 的省 token 优势全没了。
- 高度动态的工作流:步骤顺序严重依赖中间结果的不可预测变化时,传统 MCP 的逐步方式更合适。
证据
- Cloudflare Code Mode(2025,
established):核心数据是 Anthropic 报告的在 1 万行电子表格上 token 从 150K 降到 2K(75 倍)。 - Anthropic 工程团队的 Code-Over-API 分析(详见模式三)。
- CaMeL(Beurer-Kellner 等人,2025):代码优先的工具使用还能做形式化验证和污点分析(安全场景的静态检查)。
取舍
- 好处:多步工作流 token 省 10-100 倍;批量场景又快又稳;凭据留在 MCP server,不进模型上下文,更安全。
- 代价:要搭 V8 isolate 沙箱基础设施;执行成败依赖模型的代码生成能力;动态研究场景和"执行中途要智能"的场景不适用;运行时错误要单独处理。
模式三:代码优于直接调用(Code-Over-API)
问题
当 Agent 直接调用 API 或工具时,所有中间数据都必须流经模型的上下文窗口。处理电子表格、过滤日志、变换数据集这类"数据重"的工作流,代价高得吓人:拉 1 万行电子表格再过滤,光搬数据就能吃掉 15 万+ token,又慢又贵。
方案
别直接调工具,写代码让代码去调工具。 数据处理、过滤、变换都在执行环境里完成,只有结果回流到模型上下文。
核心洞察和模式二同源:模型"写代码去调用 API"比"直接调用 API"更擅长。因为训练数据里有海量开源代码,模型见过几百万种"用代码调接口"的写法。
# 直接调 API:1 万行数据全进上下文 → 15 万 token
rows = api_call("spreadsheet.getRows", sheet_id="abc123")
filtered = [r for r in rows if r.status == "active"] # 还要再花 token 处理
return filtered
# Code-Over-API:数据处理在沙箱里,只有摘要回流 → 约 2K token
def process_spreadsheet():
rows = spreadsheet.getRows(sheet_id="abc123") # 工具调用发生在执行环境
filtered = [r for r in rows if r.status == "active"] # 过滤在代码里,不进上下文
print(f"总 {len(rows)} 行,活跃 {len(filtered)} 行") # 只给模型看摘要
print(f"前 5 条活跃行: {filtered[:5]}")
return filtered模型看到的是日志输出和返回值,完整数据集永远不会进入它的上下文。
怎么用
- 最适合:数据重的工作流(电子表格、数据库、日志)、多步变换或聚合、中间结果不需要模型逐条检查的场景、对 token 成本敏感的应用。
- 前置条件:安全沙箱执行环境;执行环境能访问工具/API;要有资源限制(CPU、内存、时间)防止失控执行。
- 实施步骤:模型分析任务 → 写代码(在环境内调工具 + 过滤变换聚合 + 只打印摘要/样本 + 返回最终结果)→ 沙箱执行 → 只有日志和返回值回流。
执行环境有几种选择:
- V8 Isolate:毫秒级启动、内存极小、隔离强(Cloudflare Code Mode 的选择)。
- 容器:2-5 秒启动、语言灵活(Modal、Docker)。
- 虚拟机:完全隔离,适合破坏性操作(Cognition/Devon 这类)。
取舍
- 好处:token 大幅下降(报告案例 150K → 2K);延迟更低(大上下文调用少了);数据处理的天然契合;中间数据被关在执行环境里。
- 代价:要建安全执行基础设施;比直接调用工具复杂;模型得能写出正确的代码;调试变难(错误发生在执行环境,不在上下文里);需要监控、资源限制和沙箱。
三个模式怎么选
| 场景 | 推荐模式 |
|---|---|
| 现有接口让模型频繁翻车,报错看不懂 | LLM 友好 API 设计 |
| 多步编排、批量处理,想省 token | Code-First / Code Mode |
| 数据处理重,中间结果不需要模型看 | Code-Over-API |
三个模式是层层递进的关系:LLM 友好 API 是把"单次调用"做对,Code Mode 是把"多步编排"做对,Code-Over-API 是把"批量数据处理"做对。底层接口友好是地基,上面的编排和数据处理才立得住。
还有一个贯穿的判断标准:如果中间结果对模型没有价值,就别让它流经上下文。 这条标准和第二部分的"上下文经济"完全呼应。工具设计不只是"让 Agent 能用",更是"让 Agent 用得起"。
实践清单
- 接口显式带版本(函数名/路径/schema 里体现),别让模型猜版本
- 函数名、参数名、schema、文档做到"自描述"
- 交互扁平化:能一次调用就别拆三步,能扁平就别嵌套
- 错误信息写给模型看:哪里错、参数该是什么、下一步怎么办
- 代码库间接层压到两层以内
- 多步编排场景,考虑让模型写代码而不是逐步调工具
- 数据处理在沙箱里做,只把摘要和结果回流上下文
- 先上窄工具面 + 严格参数校验,再慢慢加
- 给工具调用加可观测性:延迟、失败率、回退路径
本章小结
- LLM 友好 API:显式版本、自描述、扁平化、可行动的错误信息、两层间接,让模型一看就懂。
- Code-First 工具接口:模型写代码编排工具,多步流程一次执行,省 10-100 倍 token。
- Code-Over-API:数据处理在沙箱完成,只有摘要回流,150K → 2K token。
- 判断标准:中间结果对模型没价值,就别让它进上下文。
- 底层接口友好是地基,编排和数据处理建立在它之上。
下一章讲工具发现:接口设计好了,模型还得"找得到"工具。渐进发现、懒加载、静态服务清单、统一工具网关。