当接口的调用者变成 LLM:MCP Tool 与 Progressive Disclosure

15 阅读7分钟

引言:一个熟悉却被忽略的问题

在现代软件工程中,我们已经非常习惯“配置式”的库函数设计。

以 axios、Vue 等为代表的框架,往往对外提供一个能力极强的“超级函数”,调用者通过传入大量配置项,来精细地控制行为。这种设计对人类开发者而言是成功的:它降低了上手门槛,同时又保留了足够的灵活性。

但当我第一次把类似的设计思想,原封不动地搬到 LLM / Agent 的 tool(尤其是 MCP tool)中时,问题才真正显现出来。

tool 的 schema 完整、参数齐全,调用也总是“成功返回”。
然而系统行为开始出现一种奇怪的特征:

每一步看起来都对,但整体结果却持续偏离预期。

本文尝试从一个工程视角出发,讨论三个问题:

  1. 人类库函数(如 axios)与 LLM tool 的本质差异
  2. MCP tool 在编写时应当如何权衡抽象与灵活性
  3. 当 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”,
那么许多我们习以为常的工程经验,就不应被机械继承,而需要被重新审视。