Rufus' ink

Back

参考Trae的技术文章,学习应用MCP工具

越来越多的开发者开始为 AI Agent 开发工具,无论是通过 MCP(Model Context Protocol)、Skills 脚本、还是直接使用 OpenAI/Claude 的 function calling。但很快大家发现了一个令人困惑的现象:技术上实现完全正确的工具,Agent 却用不好。 工具能跑通,schema 定义正确,API 调用成功,但是 Agent 总是选错工具,传错参数,或者在明明应该调用工具的时候却回复「我无法完成这个任务」。

问题出在哪里?#

问题在于:我们用写 API 的思维在写 Agent 工具。

当你为人类设计 API 时,你可以假设他们会阅读文档、理解上下文、在出错后调试代码,但 Agent 不一样:

  • 它只能通过工具的名称、描述和参数 schema 来「理解」这个工具能做什么
  • 它「试错」的代价很高,每次调用都消耗 token,都可能影响用户体验
  • 它需要在可能几十上百个工具中,瞬间做出选择
  • 它有一定的试错能力,但是成本高且不稳定

这意味着,给 Agent 开发工具的真正挑战不是技术实现,而是设计出 Agent 能用好的工具接口,这也是我们在开发 TRAE 过程中一直在思考和解决的一个问题。

核心理念:Agent 工具是 Agent 的用户界面#

这里有一个关键的思维转换:Agent 工具是 AI Agent 的用户界面(User Interface),不是已有 REST API 的封装

传统 REST API 是为人类开发者设计的,我们假设开发者会阅读文档、理解上下文、在出错后调试代码,但 Agent 是完全不同的「用户」,它不会主动查阅文档,不擅长从上下文中推断隐含信息,每次调用都需要从头开始理解工具的用途。

换句话说:你不是在写 API,你是在教会一个智能体如何与这个世界交互。

这个智能体(LLM)有着独特的长处与局限性:

  • 擅长理解自然语言、推理意图、组合信息
  • 不擅长精确计算、记住长上下文、从模糊描述中猜测正确参数
  • 看不到你的代码实现,只能看到你暴露的 schema 和描述
  • 只具备有限的上下文,并且随着上下文被打满工具调用性能会明显下降

只有理解这个智能体的特性,你才能设计出它真正能用好的工具。本文将以 MCP 为主要切入点,因为它正在成为 Agent 工具开发的主流方式,但文中的设计原则适用于所有 Agent 工具开发场景。

LLM Tool Calling:完整的调用链路#

要设计好 Agent 工具,首先需要理解它是如何被 Agent 调用的,这条调用链路决定了你的设计将如何被「消费」。

LLM 原生的 Tool Calling 机制#

让我们从最底层开始:LLM 本身是如何调用工具的?

一个关键的认知:LLM 本身不会「执行」任何函数。 它只做一件事:生成文本,所谓的「function calling」或「tool calling」本质上是 LLM 与应用程序之间的一个多轮对话协议:

第一步:定义工具#

以 OpenAI API 为例,工具通过 tools 参数传递给模型。每个工具定义包含三个核心部分:

这三个部分:namedescriptionparameters,就是 LLM「看到」的工具的全部信息。它看不到你的代码实现,不知道函数内部做了什么。

第二步:LLM 决策与返回工具调用#

当用户说「深圳今天天气怎么样?」时,LLM 会分析这个请求,发现需要调用 get_weather 工具。但它不会执行任何代码,而是返回一个结构化的「工具调用请求」:

{
  "id": "fc_12345xyz",
  "type": "function_call",
  "name": "get_weather",
  "arguments": "{\"location\": \"深圳\", \"unit\": \"celsius\"}"
}
json

注意 arguments 是一个 JSON 字符串,LLM 本质上只是在「生成文本」,只不过这段文本遵循了特定的结构化格式,存在返回非法 JSON 格式的可能。

第三步:应用程序执行函数#

应用程序解析 LLM 返回的工具调用请求,执行实际的函数,这里以 Python 代码进行示例:

import json

# 解析 LLM 返回的工具调用
tool_call = response.output[0]  # 获取第一个工具调用
args = json.loads(tool_call.arguments)

# 执行实际的函数(这是你的代码,不是 LLM 执行的)
weather_result = get_weather(args["location"], args.get("unit", "celsius"))

# 返回: {"temperature": 14, "condition": "晴", "humidity": 65}
python

第四步:将结果返回给 LLM#

执行结果需要通过 function_call_output 类型的消息返回给 LLM:

# 将工具执行结果添加到对话中
input_messages.append({
    "type": "function_call_output",
    "call_id": tool_call.call_id,  # 关联到具体的工具调用
    "output": json.dumps(weather_result)
})

# 再次调用 LLM,让它基于结果生成最终回复
final_response = client.responses.create(
    model="gpt-4",
    tools=tools,
    input=input_messages
)
python

第五步:LLM 生成最终回复#

LLM 收到工具执行结果后,会生成用户可读的最终回复:

“深圳今天天气晴朗,当前气温 14°C,湿度 65%。“

工具定义如何被 LLM「看到」?#

这是一个容易被忽视但非常重要的细节,要理解工具设计的约束,我们需要从 LLM 实现的角度来看工具调用是如何工作的。

1. JSON 只是中间格式,不是 LLM 真正「看到」的东西#

当你通过 API 传入 JSON 格式的工具定义时,LLM 提供商通常会将其转换为一种内部优化的格式。这是因为 JSON 对 LLM 来说并不是一个友好的格式:

  • 边界模糊:JSON 使用 {}[]" 等通用符号标记结构,这些符号在普通文本中也会频繁出现,容易产生歧义
  • 严格的语法要求:少一个逗号、多一个引号就会导致解析失败,而 LLM 生成文本时很容易犯这类错误
  • 字符串转义的噩梦:JSON 字符串中的引号需要转义为 \",反斜杠需要转义为 \\,换行需要转义为 \n。当参数内容包含代码片段时(这在 coding agent 中极为常见),LLM 需要正确处理代码中的所有引号、反斜杠和换行符,这是一个极易出错的环节
  • 远距离依赖:嵌套结构中,匹配的括号可能相隔很远,LLM 需要「记住」开始标记才能正确闭合
  • 缺乏显式结束标记:JSON 只依赖括号匹配,没有像 </function> 这样语义明确的结束信号

相比之下,许多 LLM 提供商内部使用类 XML 的格式来表示工具调用,相比 JSON 格式有以下优势:

  • 明确的边界:开始标签和结束标签清晰地标记了工具调用的范围
  • 自描述性:标签名本身携带语义信息,比 JSON 的键值对更不容易与内容混淆
  • 训练数据丰富:LLM 在预训练时见过大量 HTML/XML 文档,对这种格式更「熟悉」
  • 容错性更好:即使内容中包含类似符号,也不容易与结构标记产生冲突

实际上,不同提供商采用了不同的内部格式和特殊 token。这些格式在模型训练时就被专门优化过,使模型能够更准确地识别「何时应该调用工具」以及「如何正确构造调用参数」。

一些 LLM 提供商(如 OpenAI 的 Strict Mode)使用了 Constrained Decoding 技术来保证输出一定是合法的 JSON 结构。这种技术在解码时动态限制下一个 token 的候选集,确保生成的序列符合预定义的 schema。但这种约束并非没有代价:它可能影响生成速度,在某些边界情况下也可能影响模型的表达能力。

2. 工具定义是 System Prompt 的一部分#

工具定义会被注入到 LLM 的 system prompt 中,占用宝贵的 context window。这带来两个重要影响:

  • 上下文占用:工具定义占用的 token 越多,留给实际对话内容的空间就越少
  • Prompt Caching:现代 LLM API 通常会缓存 system prompt 的 KV cache 来加速推理。如果你动态修改工具列表,就会导致缓存失效,显著增加延迟和成本。

3. 为什么要用原生的 Function Calling?#

LLM 在训练过程中已经对原生的工具调用格式进行了专门的优化。使用原生格式,模型更容易准确识别何时应该调用工具、正确选择要调用的工具、生成符合 schema 的参数。这也是为什么主流 Agent 框架都直接使用各 LLM 提供商原生的 function calling 机制。

4. 工具数量对模型效果的影响#

工具数量Token 消耗
10 个工具2,500-3,000 tokens
20 个工具5,000-6,000 tokens
50 个工具12,500-15,000 tokens
100 个工具25,000-30,000 tokens

当工具数量增加到几十甚至上百个时,模型会面临选择困难、注意力稀释、Prompt 拥挤等问题。OpenAI 官方建议:尽量将工具数量控制在 20 个以内

MCP 的定位:标准化的工具协议层#

MCP(Model Context Protocol)并没有改变 tool calling 机制,它解决的是另一个问题:如何标准化地定义和暴露工具。在 MCP 出现之前,你需要为每个 LLM 单独适配工具 schema,这就是经典的 N×M 问题。MCP 引入标准化中间层,MCP Server 只需按协议暴露工具,MCP Client 负责转换成各 LLM 能理解的格式。

MCP 工具的命名约定与潜在问题#

MCP 工具通常添加前缀:mcp_<server-name>_<tool-name>。这会带来:

  1. 与自带工具的冲突或歧义aa
  2. 工具名过长(如 TRAE 有 60 字符限制)
  3. 工具数量爆炸(多个 MCP Server 可累计超过 20 个建议上限)
  4. 命名空间污染(多个 search 工具描述相似)
  5. Schema 兼容性问题(不同 LLM 对 JSON Schema 支持差异大)
  6. LLM 参数传递的不确定性(类型错误、格式不符、必填字段缺失等)

设计建议:尽量使用简单、扁平的 schema 结构,做好防御性编程。

Agent 如何「看」工具?#

Agent 眼中的工具:三元组#

每个工具就是一个简单的三元组:工具 = (名称, 描述, 参数 Schema)。没有代码实现,没有注释,没有文档链接。

Agent 依赖显式语义,而非隐含上下文#

# ❌ 依赖隐含上下文
def get_user(id):
    """获取用户信息"""
    pass

# ✅ 显式语义化
def get_user_by_uuid(user_uuid: str):
    """
    根据 UUID 获取用户信息。
    参数:user_uuid: 用户的唯一标识符,格式为 'usr_xxxxxxxx'
    返回:用户信息的 JSON 对象,包含 name、email、created_at 等字段
    """
    pass
python

设计 Agent 工具的核心原则:防呆式语义化,假设 Agent 会完全按字面意义理解你的工具,不会做任何「显然」的推断。

Agent 的「试错」成本#

Agent 需要尽量一次做对,因为成本高昂、用户体验差、上下文被污染、且没有跨会话记忆复用。

上下文窗口:稀缺的认知资源#

  • 工具数量要克制(1-5 个精心设计的工具 > 20 个随意堆砌的工具)
  • 描述要精准而简洁
  • 参数要必要且充分

如何命名:让工具「自解释」#

  • 动词优先create_github_issuesend_slack_message
  • 命名即分类:使用一致前缀帮助 Agent 快速筛选
  • 长度适中:保持工具名在 30-50 字符以内

描述的艺术:精准的契约#

好的工具描述应回答四个问题:做什么?什么时候用?有什么限制?返回什么?

参数描述善用示例、标注必填/可选、说明默认值和失败情况、引导工具选择顺序。

关键洞察#

设计工具的本质,是在设计 Agent 的认知体验。好的工具设计,就是不断减少 Agent 的认知负担。


原文链接:如何让你的 Agent 更准确:MCP 工具设计技巧(上) 作者:小夏,TRAE 技术专家

如何让你的 Agent 更准确:MCP 工具设计技巧(上)
https://blog.arminos.cn/blog/mcp-tool-design-tips-part1
Author 范铭哲 Rufus
Published at 2026年7月28日
Comment seems to stuck. Try to refresh?✨