第 11 章 工具发现:让 Agent 在几百个工具里找到对的
工具一多,Agent 就"迷路"
上一章把接口设计好了。但还有一道坎:工具多了,Agent 怎么找到该用的那一个?
真实系统里工具不是几个,是几十上百个。MCP 生态里,一个配置了 7 个 server 的环境,光工具描述就能吃掉 67k token。全都塞进上下文,Agent 连"有哪些工具"都看不完,更别说挑对;不塞进去,Agent 又不知道有这些工具。这就像把整个图书馆的书单印在一张纸上让读者找书,纸比书还厚。
这一章讲四个模式,从"怎么浏览"到"怎么统一",回答"工具发现"的四个层次:
- Progressive Tool Discovery(渐进发现):把工具组织成文件系统,按需往下钻。
- Tool Search Lazy Loading(工具搜索懒加载):不预载全部工具,搜索到了才加载。
- Static Service Manifest(静态服务清单):在众所周知的 URL 上挂一份机器可读的清单。
- Unified Tool Gateway(统一工具网关):所有工具走一个网关,一次接入全部可用。
模式一:渐进发现(Progressive Tool Discovery)
问题
工具目录大到一定程度(几十到上千个),把所有工具定义一次性载入上下文就是灾难:
- 绝大多数工具在当前任务里根本用不到,预载纯属浪费。
- 上下文被工具定义挤占,真正干活的空间变小。
- 工具越多,模型越难从一堆描述里挑出对的那个。
方案
把工具组织成一个文件系统式的层级,让 Agent 通过"逛目录"来按需发现能力。
三个层次的发现粒度,从省 token 到完整:
- 只看名字:浏览用的最小上下文。
- 名字 + 描述:够判断这个工具是干嘛的。
- 完整定义 + schema:确定要用时,才加载全部参数细节。
工具按集成/领域组织成目录,Agent 可以:
# Agent 的工作流
1. list_directory("./servers/")
→ 返回: ["google-drive/", "slack/", "github/", ...]
2. search_tools(pattern="google-drive/*", detail_level="name+description")
→ 返回: Google Drive 工具的简要描述
3. get_tool_definition("servers/google-drive/getDocument")
→ 返回: 带参数、类型、示例的完整 JSON schema典型的目录结构:
servers/
├── google-drive/
│ ├── getDocument.ts
│ ├── listFiles.ts
│ └── shareFile.ts
├── slack/
│ ├── sendMessage.ts
│ └── getChannels.ts
└── github/
├── createIssue.ts
└── listRepos.ts证据
- 来自 Anthropic 工程团队的实践(
established,成熟),生产数据表明初始上下文消耗能降 70%-90%。 - 这个思路在 MCP、Cloudflare Code Mode、OpenAI、LangChain 等主流平台都被验证过。
- 和 RAG 的思想同源:先检索再精读,而不是把整个语料库搬进窗口。
怎么用
- 工具或集成超过 20 个的系统就该考虑。
- 层级要清晰(按集成、按领域、按功能),每层要有有意义的名字和描述。
- 支持通配符搜索(glob/regex);频繁一起请求的工具定义做缓存。
- 工具定义用 OpenAPI / JSON Schema 兼容的格式,方便后面接别的工具。
取舍
- 好处:初始上下文降 70%-90%;能扩展到上千个工具;Agent 通过探索自然了解工具生态;天然对接"代码优先"的接口(第 10 章);版本和废弃管理更优雅。
- 代价:多了一次"发现"的开销(执行前多了工具调用);组织方式和命名要花心思;如果任务本来就要用大部分工具,收益变小;找对工具可能需要多次往返。
模式二:工具搜索懒加载(Tool Search Lazy Loading)
问题
渐进发现解决的是"怎么浏览"。还有个更常见的痛点:MCP server 一多,工具描述本身就吃爆上下文。
文档记录的案例:7+ 个 server 同时启用,光工具描述就消耗 67k+ token。带来的连锁反应:
- 上下文膨胀:预载所有工具描述,把本该用于任务的 token 挤掉了。
- 延迟:工具越多,每次请求的处理开销越大。
- 发现困难:Agent 要在大量无关工具里扫描,才能找到相关的。
- 内存压力:大型工具目录可能超出实际上下文限制。
方案
不预载,搜索到了才加载。 核心是一个 ToolSearchTool:让 Agent 通过搜索动态地把工具载入上下文,而不是初始化时全部预载。
function initialize_mcp_servers(servers):
total_tool_tokens = calculate_tool_tokens(servers)
if total_tool_tokens > CONTEXT_THRESHOLD:
# 懒加载模式:只载入元数据,暴露搜索工具
tool_registry = load_tool_metadata_only(servers)
return ToolSearchTool(tool_registry)
else:
# 传统模式:直接预载全部
return preload_all_tools(servers)
function tool_search(query, tool_registry):
# Agentic search:不是基础 RAG,是智能搜索
relevant_tools = agentically_search(tool_registry, query)
return load_tool_definitions(relevant_tools)四个关键动作:
- 阈值检测:监测工具描述是否会超过上下文阈值(Claude Code 用的是窗口的 10%)。
- 搜索接口:提供 ToolSearchTool,Agent 用它搜索工具元数据、选择性地加载工具。
- Server 指令:利用 MCP server 的 instructions 字段,引导 Agent"什么时候该去搜这个 server 的工具"。
- 智能搜索:用 agentic search(让模型理解意图再搜),而不是基础向量 RAG。
怎么用
如果你是 MCP server 作者:
- 写好 server instructions:开了工具搜索之后,这个字段更关键,它告诉 Agent 什么时候该搜你的工具。
- 元数据要丰富:描述、标签写清楚,方便被搜到。
- 逻辑分组:把相关工具组织在一起,让发现更直观。
如果你是 MCP client 作者:
- 实现 ToolSearchTool;用智能搜索而不是基础 RAG。
- 阈值按场景定(Claude Code 用 10%);提供开关,允许用户关掉搜索回到预载。
- 常用工具可以渐进预载(LRU 缓存最近用过的),长尾工具留在搜索模式。
取舍
- 好处:基线上下文大幅下降(67k+ token 压到只剩元数据);能扩展到 100+ 工具;不需要工具时冷启动更快;可以同时启用更多 MCP server。
- 代价:动态加载有额外延迟;要建搜索基础设施和维护元数据;可能错过"逛完整目录"时的偶然发现;server instructions 变得关键,写不好会坑 Agent。
模式三:静态服务清单(Static Service Manifest)
问题
前两个模式假设"工具已经在 Agent 的视野里"。但换个场景:Agent 第一次遇到一个陌生平台,怎么知道它提供什么服务?
现在的几种方式都不太行:
- 把工具列表硬编码进系统提示词:不灵活,平台一更新就过时。
- 运行时探索工具目录:上下文开销大,还不一定找得到。
- 解析人工写的文档:格式不统一,解析成本高。
Agent 要么在不需要的完整目录上浪费上下文,要么根本没有办法了解这个平台,只能等人介入。
方案
在众所周知的 URL 上放一份静态、机器可读的清单,描述平台提供什么能力、有哪些服务、认证方式、使用限制。Agent 拉取一次、廉价解析,就能决定调哪些服务。不需要运行时工具调用的开销,也不需要人工整理。
两种互补的格式已经出现:
llms.txt(来自 llmstxt.org 社区约定):放在站点根目录的纯文本/markdown 文件,给人给机器看的站点能力摘要。类比robots.txt的反向:robots.txt说"哪些禁止访问",llms.txt说"哪些可以用"。agent.json/ai-plugin.json:结构化 JSON 清单(放根目录或/.well-known/),声明服务端点、认证方案、速率限制、能力元数据,schema 稳定,Agent 可以确定性解析。
// agent.json 示例
{
"name": "Platform Name",
"description": "这个平台提供什么",
"auth": { "type": "api_key", "header": "X-API-Key" },
"services": [
{
"name": "memory",
"path": "/v1/memory",
"description": "持久化的键值和向量存储",
"methods": ["GET", "POST", "DELETE"]
},
{
"name": "scheduler",
"path": "/v1/scheduler",
"description": "基于 cron 的任务调度",
"methods": ["GET", "POST"]
}
]
}# llms.txt 示例
> Platform Name: 一个 API key,多个基础设施服务。
## 可用服务
- Memory: 持久化存储,支持向量搜索 (/v1/memory)
- Scheduler: 基于 cron 的任务调度 (/v1/scheduler)
- Event Bus: 发布/订阅消息 (/v1/events)
## 认证
所有端点都需要 X-API-Key 请求头。
## 速率限制
每个 key 每分钟 100 次请求。Agent 的工作流:
1. fetch("{base_url}/llms.txt") → 自然语言概览
2. fetch("{base_url}/agent.json") → 结构化服务目录
3. 从清单里挑出要用的服务
4. 只调用这些端点怎么用
- 最适合:在单一基础 URL 后面暴露多个服务的 API 平台;需要先发现能力再规划的基础设施提供商;不同 API key 解锁不同服务子集的多租户平台。
- 实施要点:
llms.txt用 text/plain 或 text/markdown 放站点根目录;agent.json用 application/json 放根目录或/.well-known/。清单保持小(llms.txt小于 4K token,agent.json小于 8KB)。带版本字段让 Agent 能检测清单变化。只列稳定、公开文档化的端点。服务变化时,把清单更新加进 CI/CD。 - 和渐进发现互补:静态清单提供初始目录,渐进发现处理运行时细节加载。
取舍
- 好处:能力发现零运行时开销(一次 HTTP 拉取);不绑定 MCP / function calling / 任何特定框架;Agent 可以"先规划再行动",减少无效工具调用;实现简单,就是个静态文件。
- 代价:还没有统一的通用标准(多种格式竞争);不同步的话清单会和真实 API 状态漂移;只描述"有什么",不描述"每个端点怎么用"(完整 schema 还得靠 OpenAPI 或 MCP);如果没配好认证,可能把攻击面暴露给恶意 Agent。
模式四:统一工具网关(Unified Tool Gateway)
问题
Agent 能力越强,要接的外部工具越多:网页搜索、图像生成、竞品调研、安全扫描、视频制作……每个工具都有一套自己的 API key、认证流程、限流逻辑、计费方式。问题叠加起来:
- 凭据泛滥:要管几十个提供商的 API key,认证方案还各不相同。
- 集成税:每接一个新工具,都要写错误处理、重试、schema 转换、响应归一化的定制代码。
- 计费碎片化:用量散落在各提供商的 dashboard 上,成本追踪和预算管控很难。
- 发现开销:Agent 没有统一的方式知道"有哪些工具可用、我能访问什么"。
方案
在 Agent 和所有外部工具提供商之间放一个网关。网关统一处理发现、认证、路由、执行、计费,向 Agent 暴露一个统一接口,不管底层是哪个提供商。
四个层次:
- 发现层:统一注册表,Agent 通过一个端点/协议查询可用工具和能力(比如 MCP 的
tools/list)。 - 认证层:网关替 Agent 持有各提供商的凭据。Agent 只向网关认证一次,网关处理各提供商的认证。
- 路由层:进来的工具调用被路由到正确的提供商,必要时做 schema 转换。长时间运行的工具派发给异步 worker。
- 计量层:每次调用都记录、计量、计费,支持预算上限和用量可见性。
// Agent 侧:一个 key,一个协议
gateway = ToolGateway(api_key="tr_xxx")
// 发现
tools = gateway.discover(category="research")
// → [{name: "web_search", skills: ["search", "deep_research"]}, ...]
// 执行:网关处理提供商认证、重试、计费
result = gateway.call("web_search", "search", {query: "AI agent patterns"})
// 长任务异步执行
job = gateway.call("video_production", "generate_clip", {prompt: "..."})
// → {job_id: "abc", status: "processing"}
result = gateway.get_result(job.job_id)怎么用
- 最适合:需要 10+ 外部能力的 Agent 团队;向多个 Agent 或用户提供工具访问的平台;需要集中计费、限流、审计日志的场景。
- 协议选择:MCP 是天然选择,Agent 本来就讲 MCP,网关直接当 MCP server 暴露所有工具。
- 同步 vs 异步:短工具(搜索、查资料)内联执行;长工具(视频生成、大规模抓取)返回 job ID,在后台 worker 上跑。
- 凭据管理:用户可以自带 API key(BYOK)降低成本,网关提供默认 key 图方便。
- schema 归一化:各提供商的响应统一成一致的输出 schema,Agent 不用写针对某个提供商的解析逻辑。
- 限流:在网关层做按用户、按工具的限流,防滥用、控成本。
取舍
- 好处:Agent 只集成一次而不是每个提供商一次,集成面大幅缩小;所有工具的计费和用量追踪集中;新工具上线,所有 Agent 无需改代码就能用;限流、日志、访问控制集中在一个点;凭据隔离,Agent 永远看不到原始提供商 key。
- 代价:单点故障,网关挂了所有工具都用不了;多一跳网络延迟;网关运营商通常会在提供商成本上再加价;用私有协议会有供应商锁定风险;对提供商特有功能或优化的控制变弱。
四个模式怎么选
| 场景 | 推荐模式 |
|---|---|
| 工具 20+ 个,预载吃爆上下文 | 渐进发现 / 懒加载 |
| MCP server 多,光描述就 67k token | 懒加载(阈值触发) |
| Agent 要发现陌生平台的能力 | 静态服务清单 |
| 团队要接 10+ 外部工具,凭据和计费要集中 | 统一工具网关 |
四个模式其实是一套工具的四个入口:静态清单让 Agent 知道"这平台有啥",渐进发现让它"按需往下钻",懒加载解决"预载太贵",网关把"多个提供商"收敛成"一个入口"。小团队从渐进发现开始就够,规模上来再接网关。
实践清单
- 工具 20+ 个:按集成/领域组织成文件系统层级,提供 list / search / get 三级发现
- 工具描述超上下文 10%:启用懒加载,只载元数据,暴露 ToolSearchTool
- 给平台挂
llms.txt和agent.json,保持小于 4K token / 8KB,随 CI/CD 更新 - 清单带版本字段,只列稳定端点
- 10+ 外部工具:收敛到统一网关,一次接入全部可用
- 网关层做凭据隔离、统一计费、按用户按工具限流
- 长工具走异步 job,短工具内联执行
- 检查:Agent 看到的是"整个目录",还是"它需要的几个工具"?
本章小结
- 渐进发现:工具组织成文件系统,三级粒度按需加载,初始上下文降 70%-90%。
- 懒加载:超阈值就只载元数据 + 搜索工具,67k token 压到只剩元数据。
- 静态服务清单:
llms.txt+agent.json放众所周知的 URL,零运行时开销发现能力。 - 统一工具网关:一个入口收敛所有提供商,集中认证、路由、计费、限流。
- 发现做得好,Agent 才找得到工具;找到工具,接口设计(第 10 章)才派得上用场。
下一章讲执行环境:工具找到了,Agent 要在哪跑?智能 Bash、Shell 输出上下文化、CLI 优先的技能设计、虚拟机操作。