你已经构建了MCP服务器,工具也能正常工作。接入LLM智能体后观察其运行,却发现:它会忽略工具、使用奇怪的参数调用工具、分不清应该使用哪个工具,还会以古怪的顺序将工具串联起来。
这就是“存在的工具”与“LLM能够正确使用的工具”之间的差距。大多数MCP服务器的投入都浪费在这里。企业构建了强大的能力,将其公开为工具,然后眼看着LLM无法有效使用。
一个有用的视角转换是:将工具设计视为UX设计,而LLM就是你的用户。工具描述是UI,Schema是表单,错误消息是反馈。把这些做好,LLM就能高效工作;做不好,再复杂的后端对智能体而言也等于不存在。
本文通过具体的有效与无效示例来介绍这些原则。
原则1:工具名称传达意图
工具名称是LLM最先看到的内容。它应以动作的形式说明工具做什么。
不佳:
customers(名词,没有动作)process_customer(含义模糊)do_x(毫无意义)
更好:
search_customers(动作明确)get_customer_by_id(操作具体)update_customer_email(变更具体)
为什么重要: LLM会扫描工具列表以寻找相关工具。描述性名称可以帮助它迅速找到正确工具。名称模糊会迫使它仔细阅读描述(而它有时不会这样做)。
一种实用模式是使用标准动词前缀。
- 读取操作使用
list_*、search_*、get_*。 - 写入操作使用
create_*、update_*、delete_*。 - 计算操作使用
analyze_*、summarize_*。
服务器内保持一致,有助于LLM建立心智模型。
原则2:描述就是提示词
工具描述是服务器中最重要的文本。LLM据此决定是否以及如何使用工具。
不佳的描述:
search_customers: 搜索客户数据库。
更好的描述:
search_customers: 按姓名、电子邮件地址或公司查找客户。最多返回10位匹配客户及其基本信息。当你需要识别用户所指的客户时使用。要按ID精确查询,请改用get_customer_by_id。
更好的版本做到了:
- 描述输入(“按姓名、电子邮件地址或公司”)。
- 描述输出(“最多返回10位匹配客户及其基本信息”)。
- 说明何时使用(“当你需要识别用户所指的客户时”)。
- 说明何时不应使用(“要按ID精确查询,请改用get_customer_by_id”)。
“何时不应使用”这一部分至关重要。缺少它时,LLM可能会在更适合 get_customer_by_id 的情况下调用 search_customers。
原则3:参数描述很重要
每个参数都需要描述,不能只依赖参数名。
不佳:
{
customer_id: string,
fields: string[]
}
更好:
{
customer_id: string, // "客户的唯一标识符。从search_customers的结果或用户明确提供的输入中获取。"
fields: string[] // "要返回的具体字段。可选值:name、email、phone、tier、created_at、last_active。如未指定,则返回name和email。"
}
这些描述:
- 告诉LLM如何获取值。
- 在适用时指定允许的值。
- 说明默认值。
原则4:错误引导恢复
工具发生错误时,错误消息会引导LLM的下一步操作。模糊的错误会让智能体陷入困惑。
不佳的错误:
{ "error": "输入无效" }
更好的错误:
{
"error": "validation_error",
"message": "电子邮件地址 '...' 的格式无效。格式应类似 'name@example.com'。",
"field": "email",
"suggestion": "请用户提供有效的电子邮件地址。"
}
现在LLM知道:
- 哪里出错了(email字段的验证错误)。
- 如何修复(使用有效的电子邮件格式)。
- 下一步做什么(询问用户)。
比较智能体面对这两种错误时的行为。第一种情况下,它可能会使用同一参数重试(浪费调用)、直接放弃(糟糕的UX),或凭空编造有效输入。第二种则会带来顺畅的用户交互。
原则5:输出决定下一步操作
工具的输出决定LLM接下来做什么。输出设计会影响智能体行为。
不佳的搜索输出:
[
{"id": "c1", "n": "John", "e": "john@..."},
{"id": "c2", "n": "Jane", "e": "jane@..."}
]
更好的输出:
{
"customers": [
{"id": "c1", "name": "John Smith", "email": "john@example.com", "tier": "pro"},
{"id": "c2", "name": "Jane Doe", "email": "jane@example.com", "tier": "free"}
],
"total_found": 2,
"summary": "找到2位与 'john' 匹配的客户。请注意,其中一位名叫 'Jane Doe',但电子邮件地址中含有 'john'。"
}
更好的输出:
- 使用可读的字段名。
- 包含元上下文(
total_found)。 - 包含自然语言
summary,帮助LLM组织接下来的回复。
摘要字段非常强大——它就像顺带向LLM提示应该如何解读结果。
原则6:一个工具只做一件事
同时做多件事的工具会让LLM困惑。LLM不仅要决定是否使用工具,还要决定使用哪种模式。
容易混淆:
manage_customer:
- mode: "search" | "get" | "update" | "delete"
- params: 取决于mode
LLM必须选择模式,而且经常选错。更糟的是,参数Schema会因为模式不同而变得复杂。
更好:拆分工具。
search_customers: 按姓名/电子邮件地址/公司搜索
get_customer: 按ID获取详细信息
update_customer: 更新特定字段
delete_customer: 归档客户
每个工具都没有歧义。LLM根据意图选择工具,Schema也很简单。
这意味着工具数量会增加,但每个工具都更清晰。LLM处理10个清晰工具的效果,优于处理3个多模式工具。
原则7:约束输入
应尽可能限制输入选项。枚举和验证可以防止LLM产生幻觉。
宽松:
{
status: string // 可以是任何内容
}
受约束:
{
status: "active" | "trial" | "churned" | "suspended"
}
该约束在Schema层执行(约束生成可防止LLM产生无效值)。
这同样适用于操作、严重程度、类型等枚举——凡是有效值集合已知的内容,都应加以约束。
日期应使用ISO 8601格式,并在描述中明确说明(“ISO 8601格式的日期,例如2026-05-15”)。如果不说明,LLM会生成各种格式的日期。
原则8:默认值减少幻觉
参数有合理默认值时,应将其设为可选,并由服务器应用默认值。
不佳:
{
query: string,
limit: number, // LLM必须提供某个值
include_archived: boolean,
sort_by: string
}
LLM必须为所有参数选值,而这些值可能不正确。
更好:
{
query: string,
limit: number = 10, // 合理默认值
include_archived: boolean = false, // 安全默认值
sort_by: "relevance" | "name" | "created_at" = "relevance" // 最常用
}
LLM只需指定与具体查询有关的参数。参数越少,产生困惑的空间越小。
在描述中注明默认值:“Limit:要返回的结果数。默认为10,最大为50。”
原则9:可组合性很重要
工具应能组合成LLM可以构建的工作流。粒度合适,复杂任务就会变得简单。
考虑一个任务:“告诉我排名前3的客户有哪些未关闭问题。”
不佳的工具集:
get_customer_summary(customer_id): 一次返回客户 + 工单 + 活动
LLM无法轻松完成“排名前3”筛选——这个工具一次返回一位客户的所有信息。要完成任务,LLM必须先知道哪些是排名靠前的客户,然后调用此工具3次。
更好的工具集:
list_customers(sort_by="value", limit=N): 返回包含优先级信息的客户摘要
list_tickets(customer_id, status): 返回某位客户的工单
LLM可以自然地组合:先列出排名靠前的客户,再逐一列出未关闭的工单。
原则是:考虑多工具工作流。组合良好的工具容易使用;无法组合的工具往往不好用。
原则10:明确传达幂等性
对于写入工具,应在描述中说明幂等性要求:
create_invoice: 为客户创建新发票。
重要:传入idempotency_key(由你生成的UUID)。如果重试此操作,请使用同一个UUID,以防生成重复发票。
参数:
- amount: ...
- customer_id: ...
- idempotency_key: 用于防止重试时重复创建的UUID。每个逻辑操作只生成一次。
这样LLM就知道要生成UUID,并在重试时继续使用同一个UUID。
如果没有这些指引,LLM可能完全跳过该键(没有幂等性),也可能每次重试都生成新的UUID(违背幂等性的目的)。
原则11:说明前置条件和后置条件
对于有前置条件或重要副作用的工具,应明确说明:
delete_customer: 归档客户记录。30天内可以恢复;30天后,数据将被永久删除。
前置条件:
- 客户不得有有效订阅。
- 客户不得有未关闭工单。
如果不满足前置条件,此工具会返回错误,说明需要先解决什么。
副作用:
- 客户的所有联系人也会一并归档。
- 客户将从活跃报表中移除。
- 将创建一条审计日志记录。
LLM现在知道调用前需要检查什么,也知道调用后会发生什么。它可以正确规划多步骤工作流(“先关闭工单,再删除”)。
原则12:拿不准时就提供示例
对于复杂工具,在描述中提供示例会有所帮助:
analyze_funnel: 根据事件数据分析转化漏斗。
参数:
- start_date: ISO 8601日期
- end_date: ISO 8601日期
- steps: 步骤定义数组,每项为 {event_name: string, filters?: object}
示例:
{
"start_date": "2026-01-01",
"end_date": "2026-01-31",
"steps": [
{"event_name": "signup"},
{"event_name": "first_login"},
{"event_name": "first_action", "filters": {"action_type": "create_project"}},
{"event_name": "subscription_started"}
]
}
示例比单独的Schema更能帮助LLM学会结构。
原则13:不要暴露内部实现
LLM不需要了解数据库结构或内部ID。应公开清晰的概念模型。
不佳:
get_user_by_pk(pk: number)
LLM必须知道要使用“主键”——这是数据库概念。
更好:
get_user(user_id: string)
隐藏数据库概念。LLM使用的是具有实际含义的user_id。
同理,不要暴露弃用字段、内部标志、调试参数,或任何与实现有关而非面向用户概念的内容。
原则14:避免魔法字符串
有些工具要求使用看起来像命令或代码的字符串,很容易出错。
不佳:
modify_record(record_id: string, change_string: string)
// change_string的形式类似 "field1=value1;field2=value2"
LLM必须按照特定字符串格式编码变更,很容易犯错。
更好:
update_record(record_id: string, updates: { field1?: any; field2?: any; ... })
将更新内容构造成对象,LLM可以直接使用任意字段。
原则15:使用真实LLM测试
工具描述对人类而言可能很易读,却仍可能让LLM困惑。只有测试才能确定。
一种实用工作流如下:
- 构建工具。
- 让LLM智能体仅使用你的工具尝试完成几项现实任务。
- 观察失败。
- 根据失败情况调整描述。
- 重复。
你会发现以下模式:
- LLM使用了错误工具 → 工具名称或描述不清晰。
- LLM传入错误参数值 → 参数描述或Schema需要改进。
- LLM遇到错误后放弃 → 错误消息需要改进。
- LLM没有尝试本可提供帮助的工具 → 工具不够醒目或命名不佳。
每个问题都指向一项具体修复。
诊断:工具设计不佳的迹象
以下模式表明工具设计存在问题:
LLM经常使用错误工具。 你会看到它在本应调用 get_customer_by_id 时调用 search_customers。修复:明确说明每个工具分别适用于什么情况。
LLM为完成一件事调用许多工具。 它串联5次工具调用,才完成本应由1次调用完成的任务。修复:可能需要更高层级的复合工具,或者当前粒度过细。
LLM遇到错误后放弃。 它尝试一次,收到错误后就告诉用户无法提供帮助。修复:改进错误消息并给出后续步骤。
LLM凭空编造参数值。 它虚构user_id、日期或ID。修复:明确如何获得有效值;增加约束;添加能够捕获并解释问题的错误处理。
LLM重复相同的失败调用。 同一个错误反复发生。修复:错误消息没有具体告诉LLM问题所在。
LLM不使用某个强大的工具。 你构建了优秀工具,LLM却从不调用。修复:改进发现能力(名称更清晰、描述更完善,并加入“在……时使用此工具”的指引)。
工具分类体系
一项有用的练习是将工具整理成分类体系。
读取工具(安全、幂等):
- search_customers
- get_customer_by_id
- list_tickets
- list_orders
计算工具(不改变状态):
- summarize_account_activity
- analyze_funnel
- calculate_lifetime_value
写入工具(改变状态,需要幂等性):
- create_customer
- update_customer_email
- create_ticket
- send_email
破坏性工具(需要谨慎授权):
- delete_customer
- cancel_subscription
- archive_record
分类体系可以帮助你:
- 应用适当的防护措施(幂等性、破坏性操作确认)。
- 在面向LLM的系统提示词中说明各个类别。
- 发现缺失的工具(如果某个类别为空,是否需要添加?)。
可以在系统提示词中加入:
可用工具类别:
- READ工具(可安全调用):search_customers、get_customer_by_id、...
- COMPUTE工具(无副作用):summarize_account_activity、...
- WRITE工具(有副作用,需包含idempotency_key):create_customer、...
- DESTRUCTIVE工具(需要人工确认):delete_customer、...
调用WRITE或DESTRUCTIVE工具前,先向用户确认。
这会在工作流层面塑造LLM使用工具的方式,而不只影响单次调用。
常见改进示例
下面通过改进前后的示例具体说明这些原则:
示例1:搜索工具
改进前:
// 搜索文档
{
name: "documents",
description: "搜索文档",
inputSchema: { query: "string" }
}
改进后:
{
name: "search_documents",
description: `搜索内部文档(知识库、Wiki页面、策略)。
返回匹配文档及其标题、摘录和链接。当用户询问公司策略、流程或内部文档时使用。按语义相似度返回最多10个最相关的匹配项。`,
inputSchema: {
query: {
type: "string",
description: "搜索查询。请尽量具体。好的示例:'2026年远程办公政策'。不佳示例:'与工作有关的文档'。"
},
document_type: {
type: "string",
enum: ["policy", "procedure", "guide", "faq", "any"],
default: "any",
description: "筛选特定类型的文档。"
},
limit: {
type: "number",
default: 5,
maximum: 10,
description: "结果数量。"
}
}
}
示例2:操作工具
改进前:
{
name: "send_email",
description: "发送电子邮件",
inputSchema: {
to: "string",
subject: "string",
body: "string"
}
}
改进后:
{
name: "draft_email_to_customer",
description: `根据近期互动起草一封发给客户的电子邮件。电子邮件将保存为草稿,供人工审核后发送——不会自动发送。用户必须在收件箱中批准草稿。
在以下情况使用:
- 你已经确定了一项需要跟进客户的操作。
- 你有发送邮件的具体原因和内容。
请勿在以下情况使用:
- 发送营销或推广内容。
- 用户没有明确提出请求。
- 回复退款或取消请求(应改为升级给人工处理)。`,
inputSchema: {
customer_id: {
type: "string",
description: "来自search_customers或get_customer的客户ID。"
},
subject: {
type: "string",
description: "电子邮件主题,4–8个词,内容具体。避免使用 '跟进一下' 之类笼统的主题。"
},
body: {
type: "string",
description: "电子邮件正文,纯文本。3–5句话。应个性化、具体,不要有模板感。"
},
tone: {
type: "string",
enum: ["professional", "friendly", "apologetic", "urgent"],
default: "professional",
description: "电子邮件的语气。"
},
idempotency_key: {
type: "string",
description: "此草稿使用的UUID。重试时使用同一个UUID,以免产生重复草稿。"
}
}
}
“改进后”的版本能为LLM提供更有效的指引。它们看起来很啰嗦,但完全值得。
核心结论
面向LLM的工具设计是一门独立学科。这些原则并非凭直觉就能掌握;你需要把LLM当作用户,并据此设计界面。
真正重要的模式包括:
- 使用动作动词命名。
- 使用丰富描述说明做什么、何时使用以及何时不应使用。
- 为每个参数提供描述、示例和约束。
- 提供结构化、可操作的错误消息。
- 让输出结构引导下一步操作。
- 每个概念对应一个工具。
- 设置合理默认值。
- 使用可组合的粒度。
- 明确说明幂等性。
- 记录前置条件和后置条件。
- 为复杂工具提供示例。
- 隐藏内部实现。
- 使用真实LLM测试。
大多数MCP服务器失败,不是因为协议太难,而是因为工具设计时没有考虑LLM。工具设计正确,服务器才会有效;设计错误,再复杂的后端也会白白浪费。
把LLM当作用户,并据此设计。这样的投入会通过工具实际使用效果得到多倍回报。



