引言:一个熟悉却被忽略的问题
在现代软件工程中,我们已经非常习惯“配置式”的库函数设计。
以 axios、Vue 等为代表的框架,往往对外提供一个能力极强的“超级函数”,调用者通过传入大量配置项,来精细地控制行为。这种设计对人类开发者而言是成功的:它降低了上手门槛,同时又保留了足够的灵活性。
但当我第一次把类似的设计思想,原封不动地搬到 LLM / Agent 的 tool(尤其是 MCP tool)中时,问题才真正显现出来。
tool 的 schema 完整、参数齐全,调用也总是“成功返回”。
然而系统行为开始出现一种奇怪的特征:
每一步看起来都对,但整体结果却持续偏离预期。
本文尝试从一个工程视角出发,讨论三个问题:
- 人类库函数(如 axios)与 LLM tool 的本质差异
- MCP tool 在编写时应当如何权衡抽象与灵活性
- 当 tool 数量规模化(上百、上千)时,tool call 应该如何被编排
这些问题的背后,其实都指向同一个核心:
抽象边界,应该画在哪里?
一、配置式超级函数:对人类友好,但并非没有代价
“配置式 API”在软件工程中并不新鲜:
createSomething({
retry: true,
timeout: 3000,
adapter: ...,
hooks: ...
})
这种设计的优点非常明显:
- 上手成本低
- 一个接口覆盖大量使用场景
- 高级用户可以通过配置获得精细控制
但与此同时,它也带来了一些工程上早已被反复讨论的代价:
- 抽象被参数稀释:接口不再清晰表达“我在做什么”,而是在说“你可以怎么调我”
- 行为边界模糊:不同配置组合下的行为很难通过直觉判断
- 调用者对系统整体形态的把控下降
在以人类为调用者的场景下,这些问题是“可以接受的复杂性”。因为人类可以:
- 阅读文档
- 理解隐含约定
- 主动意识到“这里我其实不太懂”
但当调用者换成 LLM,这些前提就不再成立了。
二、当调用者变成 LLM,问题被指数级放大
LLM 与人类调用 API 的方式存在一个根本差异:
人类是“理解后调用”,而 LLM 更多是“语义猜测后调用”。
在配置式接口下,这种差异会被急剧放大:
- LLM 会把所有参数都视为自己应该参与决策的自由度
- 很容易构造出语法合法、但语义错误的调用
- 一旦调用成功但语义偏航,系统往往很难察觉
这在 tool call 场景中尤其危险:
- tool 看似执行成功
- 但系统状态已经悄然偏离预期
- 后续推理全部建立在错误结果之上
因此,同样的接口设计,在 LLM 场景下并不是“稍微不优雅”,而是系统性风险。
三、MCP Tool 的本质:不是 API,而是能力边界
在 MCP / Agent 体系中,tool 并不仅仅是一个“可调用函数”。
更准确地说:
tool 是 LLM 对“我能做什么”的世界模型的一部分。
从这个角度看,tool 的各个组成部分承担着完全不同的角色:
- 函数名:表达意图(Intent)
- 参数:暴露给 LLM 的可控自由度
- 输出结构:后续推理所依赖的事实基础
一旦 tool 退化成“配置项集合”,这个抽象就已经失败了。
因此,给 LLM 设计 tool 时,更合理的类比不是 axios 这类基础设施 API,而是 DDD 中的领域服务(Domain Service) 。
四、为什么要避免配置式 MCP Tool
配置式 MCP tool 往往意味着:
- 参数即决策点
- 参数即认知负担
- 参数即错误空间
对 LLM 来说,这些参数不会被“自然地忽略”,而是会被主动组合、尝试、探索。
一个典型的反例如下:
❌ 配置式 tool(反例)
{
"name": "fetch_data",
"description": "Fetch data with flexible options",
"parameters": {
"type": "object",
"properties": {
"source": { "type": "string" },
"format": { "type": "string" },
"retry": { "type": "boolean" },
"timeout": { "type": "number" },
"strategy": { "type": "string" },
"hooks": { "type": "array" }
}
}
}
从人类视角看,这是“强大而灵活”;
从 LLM 视角看,这是一个组合搜索空间。
五、更适合 MCP 的 tool 形态
实践中,更稳健的 MCP tool 通常具备以下特征:
1. 函数名本身就能表达完整意图
{
"name": "fetch_user_profile",
"description": "Retrieve a user's public profile information"
}
2. 参数只包含不可避免的输入
{
"parameters": {
"type": "object",
"properties": {
"user_id": { "type": "string" }
},
"required": ["user_id"]
}
}
如果一个参数在大多数情况下都有合理默认值,它更适合被内化到实现中,而不是暴露给 LLM。
3. 输出结构稳定、可预测
避免因为参数变化而改变返回 schema,否则会直接污染后续推理。
需要强调的是:
对 LLM 来说,tool 应当“看起来简单”,而不是“内部真的简单”。
复杂度应该被隐藏,而不是被暴露。
六、当 tool 数量爆炸:问题不在 tool,而在编排
当系统中存在上百甚至上千个 tool 时,新的问题出现了:
- 不可能一次性全部暴露给 LLM
- tool selection 本身成为一个推理任务
一种更可扩展的思路是:
- 分阶段暴露 tool:先选择能力类别,再选择具体 tool
- 将 tool 作为 RAG 的一部分:通过相似度检索提供候选 tool
- 允许回退与重选:承认 tool call 本质上是搜索,而非确定性决策
在这种设计下:
- LLM 面对的是“恰好足够多”的选项
- 而不是一个不可理解的能力全集
这与上下文工程中的“减少与隔离”原则是高度一致的。
从这个角度看,MCP tool 的设计问题,其实可以被统一理解为一个更经典的概念:Progressive Disclosure(渐进式暴露) 。
在 LLM 场景中,Progressive Disclosure 不再只是用户体验层面的优化,而是一种必要的认知约束机制。
每一个被暴露给 LLM 的 tool、参数或选项,都会被视为“我应该纳入推理空间的可能性”。如果在意图尚未收敛的阶段暴露过多能力,等价于人为放大了搜索空间和语义歧义。
Claude skill 等系统中所采用的分层能力暴露、语义路由、本质上都是在实现同一件事:
延迟复杂度的出现,直到决策已经足够具体。
从这个意义上说,tool 编排并不是能力管理问题,而是一次面向 LLM 的 Progressive Disclosure 设计。
七、一些实践性的自检问题
在实际编写 MCP tool 时,可以快速问自己几个问题:
- 这个 tool 的名字,是否一句话就能解释清楚?
- 参数是否真的不可或缺?
- 是否存在可以内化的策略或默认值?
- 输出结构是否稳定?
- 如果只看 schema,不看实现,是否还能猜对用途?
如果这些问题中有多个答案变得犹豫,往往意味着抽象边界还不够清晰。
结语:接口对象变了,工程假设也必须跟着变
配置式 API 在人类软件工程中是一个成功的模式,但它隐含了一个前提:
调用者知道自己在做什么。
在 MCP / Agent 场景下,这个前提不再成立。
我们面对的,并不是“如何写好 tool”,而是一个更根本的问题:
接口设计,是否正在经历一次调用对象的迁移?
如果接口的对象从“人类开发者”变成了“LLM”,
那么许多我们习以为常的工程经验,就不应被机械继承,而需要被重新审视。