2026 年,MCP(Model Context Protocol)已成为 AI Agent 调用外部工具的事实标准。但一个被严重低估的问题是:同样的工具,换一种描述方式,AI 的调用准确率可以从 40% 飙升到 95%。工具描述(Tool Description)不是写给人看的文档,而是写给大模型看的「调用合同」——措辞、参数命名、类型约束的每一个细节都在直接影响 AI 的决策质量。本文将从实战出发,拆解工具描述工程的核心方法论。
🔧 一、工具描述的核心原则
大多数开发者写 Tool Description 的方式是「顺便写两句」——名字随意取,描述一句话带过,参数类型用 any。这种做法在简单工具上勉强能用,但在复杂业务场景下会导致 AI 频繁误调用、传参错误、甚至调用不该调用的工具。
1.1 命名即语义:工具名和参数名的黄金法则
工具名和参数名是大模型理解工具功能的第一信号。一个模糊的名字会直接导致 LLM 在工具选择阶段就犯错。
❌ 错误写法:
{
"name": "doStuff",
"description": "处理数据",
"inputSchema": {
"type": "object",
"properties": {
"d": { "type": "string" },
"n": { "type": "number" }
}
}
}
✅ 正确写法:
{
"name": "search_user_by_email",
"description": "根据精确邮箱地址查询单个用户信息。仅支持完全匹配,不支持模糊搜索。找不到时返回 null。",
"inputSchema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "用户的完整邮箱地址,如 user@example.com"
}
},
"required": ["email"]
}
}
⚡ 关键结论: 工具名应遵循
动词_名词_限定词的命名模式(如search_user_by_email),而不是泛化名称(如query或process)。参数名应该是自解释的英文单词,避免缩写。
1.2 描述不是文档,是给 LLM 的「调用说明书」
很多人把 Tool Description 当成 API 文档来写,这是根本性的错误。LLM 需要的不是技术文档,而是一份决策指引——告诉它什么时候该用这个工具、什么时候不该用、传什么值是对的。
以下是描述中必须包含的四个信息维度:
| 维度 | 说明 | 示例 |
|---|---|---|
| 功能声明 | 这个工具做什么 | “查询指定城市的实时天气数据” |
| 边界约束 | 什么时候不该用 | “不支持历史天气查询,仅返回当前时刻数据” |
| 参数语义 | 每个参数的含义和格式 | “city: 城市英文名,如 beijing、shanghai” |
| 返回值说明 | 调用成功/失败时返回什么 | “成功返回 JSON 对象,失败返回 { error: string }” |
✅ 完整的描述示例:
// MCP Server 中注册工具时的完整描述
server.tool(
"create_calendar_event",
"在用户的 Google Calendar 中创建一个新的日历事件。" +
"如果指定的时间段已有事件,不会自动冲突检测,需要调用方先用 check_calendar_availability 确认空闲。" +
"创建成功返回事件 ID,可用于后续的 update 或 delete 操作。",
{
title: z.string().describe("事件标题,1-200 字符"),
start_time: z.string().describe("开始时间,ISO 8601 格式,如 2026-06-15T10:00:00+08:00"),
end_time: z.string().describe("结束时间,ISO 8601 格式,必须晚于 start_time"),
attendees: z.array(z.string()).optional().describe(
"参会者邮箱列表,如 ['alice@corp.com']。最多 50 人,超出会报错"
),
location: z.string().optional().describe("线下会议地点,如 '北京朝阳区 xxx 大厦 3 楼会议室'"),
},
async (params) => {
// 实际调用 Google Calendar API
const event = await calendar.events.insert({
calendarId: "primary",
requestBody: {
summary: params.title,
start: { dateTime: params.start_time },
end: { dateTime: params.end_time },
attendees: params.attendees?.map(email => ({ email })),
location: params.location,
},
});
return { content: [{ type: "text", text: JSON.stringify({ eventId: event.data.id }) }] };
}
);
💡 提示: 描述中的「负面约束」(什么时候不该用)比「正面声明」更重要。LLM 在面对多个工具时,最常犯的错误不是选不到对的工具,而是不该用的时候用了。
1.3 参数类型约束:用 JSON Schema 做第一层防线
不要指望 LLM 会「猜对」参数格式。用类型约束(JSON Schema / Zod)把参数格式锁死,让错误在序列化阶段就被拦截。
import { z } from "zod";
// ❌ 弱类型约束 — LLM 可能传入任意字符串
const weakSchema = {
priority: { type: "string" }
};
// ✅ 强类型约束 — 枚举值 + 默认值 + 描述
const strongSchema = {
priority: z.enum(["low", "medium", "high", "critical"])
.default("medium")
.describe("任务优先级。critical 表示需要立即处理的 P0 问题,high 表示当天必须完成")
};
// ✅ 带正则校验的参数
const phoneSchema = {
phone: z.string()
.regex(/^\+?[1-9]\d{6,14}$/, "必须是 E.164 格式国际号码,如 +8613800138000")
.describe("用户手机号,E.164 格式(含国际区号)")
};
// ✅ 数值范围约束
const pageSchema = {
page: z.number().int().min(1).max(1000).default(1)
.describe("分页页码,从 1 开始。超过 1000 页会返回空结果"),
page_size: z.number().int().min(1).max(100).default(20)
.describe("每页数量,默认 20,最大 100。值过大会导致响应变慢")
};
⚠️ 警告: 永远不要给 LLM 一个
type: "object"且additionalProperties: true的参数。这等于告诉模型「随便传什么都行」,你会收到各种意想不到的数据结构。
🚀 二、高级描述策略:从「能用」到「好用」
掌握基础原则后,下面介绍几个进阶策略,能显著提升 AI Agent 的工具调用质量。
2.1 工具分组与描述一致性
当你有 10+ 个工具时,LLM 面临「选择过载」。解决方法是通过一致的描述模式降低 LLM 的认知负担:
// ❌ 描述风格不一致 — LLM 需要逐个理解
server.tool("getUser", "获取用户", { ... });
server.tool("fetch_orders", "拉取订单列表数据,支持分页和过滤", { ... });
server.tool("delete_item", "删除", { ... });
// ✅ 统一的描述模板 — LLM 可以快速扫描匹配
// 模板: [动作] + [对象] + [条件/限制] + [返回值]
server.tool(
"get_user_by_id",
"根据用户 ID 获取单个用户的详细资料。返回用户基本信息、注册时间和最近登录时间。找不到时返回 404 错误。",
{ user_id: z.string().describe("用户唯一 ID,格式为 usr_xxxxx") },
handler
);
server.tool(
"list_orders_by_user",
"获取指定用户的历史订单列表。支持按状态过滤和分页。返回订单数组,按创建时间倒序排列。",
{
user_id: z.string().describe("用户唯一 ID"),
status: z.enum(["pending", "paid", "shipped", "completed", "cancelled"]).optional()
.describe("订单状态过滤,不传则返回全部状态"),
page: z.number().int().min(1).default(1).describe("页码,从 1 开始"),
},
handler
);
server.tool(
"cancel_order",
"取消指定订单。只有 pending 和 paid 状态的订单可以取消。取消后不可恢复,需要重新下单。返回取消结果。",
{
order_id: z.string().describe("订单 ID,格式为 ord_xxxxx"),
reason: z.string().max(500).describe("取消原因,最多 500 字符"),
},
handler
);
2.2 用示例值引导 LLM 行为
在参数描述中嵌入具体的示例值,是提升 LLM 传参准确率的最简单方法。LLM 是模式匹配机器——看到示例后会自动模仿格式。
// 示例值策略对比
const examples = {
// ❌ 没有示例 — LLM 可能传入 "北京" 而不是 "beijing"
city: z.string().describe("城市名"),
// ✅ 有示例 — LLM 会模仿 "beijing" 格式
city: z.string().describe("城市英文名(小写),如 beijing、shanghai、guangzhou"),
// ✅ 用 enum 代替自由文本 — 100% 消除格式错误
city: z.enum(["beijing", "shanghai", "guangzhou", "shenzhen", "hangzhou"])
.describe("城市名称,仅支持以上五个城市"),
// ✅ 复杂格式用示例 + 模板
date_range: z.string().describe(
"日期范围,格式为 'YYYY-MM-DD~YYYY-MM-DD',如 '2026-06-01~2026-06-30'"
),
};
📌 记住: 在参数描述中给出 2-3 个示例值(覆盖不同场景),比写一大段文字说明更有效。LLM 对 pattern 的理解力远超对规则的理解力。
2.3 多工具编排的描述策略
当你的 MCP Server 暴露了一组相关工具时,要在每个工具的描述中暗示工具间的调用关系,引导 LLM 形成正确的调用链:
// 在描述中嵌入调用链提示
server.tool(
"upload_image",
"上传图片到 CDN 并返回 URL。上传成功后,如果需要在文章中引用该图片,请使用 insert_image_to_article 工具将 URL 嵌入。" +
"支持 jpg/png/webp 格式,单文件最大 10MB。",
{
file: z.string().describe("图片的 Base64 编码内容"),
filename: z.string().describe("文件名,如 photo.jpg"),
},
handler
);
server.tool(
"insert_image_to_article",
"将已上传的图片 URL 插入到指定文章的指定位置。" +
"注意:必须先通过 upload_image 工具获得图片 URL,不能直接使用外部 URL(会因防盗链被拒绝)。",
{
article_id: z.string().describe("文章 ID"),
image_url: z.string().url().describe("通过 upload_image 获取的 CDN 图片 URL"),
position: z.enum(["top", "after_paragraph", "end"]).default("end")
.describe("插入位置:top=文章开头,after_paragraph=指定段落后,end=文章末尾"),
after_paragraph_index: z.number().int().optional()
.describe("当 position 为 after_paragraph 时,指定段落索引(从 0 开始)"),
},
handler
);
💡 三、避坑指南与性能优化
3.1 常见的描述反模式
在生产环境中,以下反模式是工具调用失败的主要原因:
| 反模式 | 问题 | 修复方案 |
|---|---|---|
| 描述太短(< 10 字) | LLM 无法判断何时使用 | 补充功能、边界和返回值说明 |
| 描述太长(> 500 字) | 增加 token 消耗,降低准确率 | 精简到 50-150 字,保留关键信息 |
| 参数无 describe | LLM 猜测参数含义 | 每个参数必须有描述 |
用 any / object 类型 |
LLM 传入任意结构 | 用明确的 JSON Schema 约束 |
| 工具名用缩写 | LLM 无法理解语义 | 使用完整的英文动词短语 |
| 没有错误说明 | LLM 不知道何时会失败 | 描述常见错误场景和返回格式 |
3.2 工具描述的 Token 预算管理
MCP 协议在每次对话开始时会将所有工具定义发送给 LLM。如果你有 30 个工具,每个工具描述 200 字,光工具定义就消耗 6000+ tokens。这会直接压缩你的上下文窗口。
// Token 预算优化策略
interface ToolBudgetConfig {
maxTools: number; // 单次对话最多暴露的工具数
maxDescriptionLength: number; // 每个工具描述的最大字符数
maxParamDescriptionLength: number; // 每个参数描述的最大字符数
}
const DEFAULT_BUDGET: ToolBudgetConfig = {
maxTools: 20,
maxDescriptionLength: 200,
maxParamDescriptionLength: 100,
};
// 动态工具暴露:根据用户意图只暴露相关工具
function getRelevantTools(userIntent: string, allTools: Tool[]): Tool[] {
// 简化示例:根据意图关键词匹配相关工具
const intentKeywords = extractKeywords(userIntent);
const scored = allTools.map(tool => ({
tool,
score: calculateRelevance(tool, intentKeywords),
}));
return scored
.sort((a, b) => b.score - a.score)
.slice(0, DEFAULT_BUDGET.maxTools)
.map(s => s.tool);
}
💡 提示: 对于工具数量超过 20 个的 MCP Server,强烈建议实现动态工具暴露策略——根据用户的意图和上下文,只暴露当前场景需要的工具子集。这不仅能减少 token 消耗,还能显著提升 LLM 的工具选择准确率。
3.3 描述的 A/B 测试方法论
工具描述不是写完就结束了。你需要像优化产品文案一样,持续测试和迭代:
// 工具描述 A/B 测试框架
interface DescriptionVariant {
id: string;
name: string;
description: string;
paramDescriptions: Record<string, string>;
}
const searchToolVariants: DescriptionVariant[] = [
{
id: "v1_baseline",
name: "search_documents",
description: "搜索文档",
paramDescriptions: {
query: "搜索关键词",
},
},
{
id: "v2_detailed",
name: "search_documents",
description:
"在知识库中全文搜索文档。返回按相关性排序的文档列表,最多 10 条。" +
"支持中英文混合搜索。如果结果太少,建议缩短关键词或用同义词重试。",
paramDescriptions: {
query: "搜索关键词,如 'API 认证方式' 或 'authentication method'。建议 2-6 个词",
},
},
{
id: "v3_with_examples",
name: "search_documents",
description:
"在企业知识库中全文搜索文档内容。支持自然语言查询。" +
"返回最多 10 条结果,每条包含标题、摘要和文档 URL。" +
"常见使用场景:查找公司政策、技术文档、会议纪要。",
paramDescriptions: {
query:
"自然语言搜索词。示例:'请假审批流程'、'如何申请 VPN 权限'、'Q2 销售数据报告'。" +
"越具体越好,避免用单个词如 '流程'",
},
},
];
// 记录每个变体的调用成功率和用户满意度
interface VariantMetrics {
variantId: string;
totalCalls: number;
successfulCalls: number;
userSatisfied: number; // 用户确认结果正确
avgLatencyMs: number;
}
⚡ 关键结论: 通过 A/B 测试,v2 和 v3 版本通常比 v1 baseline 的调用准确率高出 30-60%。投入在工具描述优化上的时间,ROI 远高于优化 prompt 或切换模型。
📊 四、工具描述质量检查清单
在发布 MCP Server 之前,用以下清单逐项检查每个工具的描述质量:
| 检查项 | 说明 | 权重 |
|---|---|---|
| ✅ 工具名可读 | 使用 动词_名词 格式,避免缩写 |
高 |
| ✅ 描述包含功能 | 明确说明工具做什么 | 高 |
| ✅ 描述包含边界 | 说明什么时候不该用 | 高 |
| ✅ 描述包含返回值 | 说明成功/失败时返回什么 | 中 |
| ✅ 每个参数有 describe | 无遗漏的参数描述 | 高 |
| ✅ 参数有示例值 | 关键参数包含 2-3 个示例 | 中 |
| ✅ 参数类型精确 | 使用 enum、正则、范围约束 | 高 |
| ✅ 必填参数已标记 | required 字段正确设置 |
高 |
| ✅ 描述长度适中 | 工具描述 50-200 字,参数描述 20-100 字 | 中 |
| ✅ 无矛盾信息 | 描述之间不互相冲突 | 高 |
🎯 总结
工具描述工程是 MCP 开发中最被低估的技能点。一个写得好的 Tool Description,胜过换一个更贵的模型。
核心原则回顾:
- ✅ 工具名用
动词_名词_限定词格式,让 LLM 一眼看懂用途 - ✅ 描述必须包含四要素:功能、边界、参数语义、返回值
- ✅ 用 JSON Schema / Zod 约束参数类型,不让 LLM 有猜测空间
- ✅ 在参数描述中嵌入 2-3 个具体示例值
- ✅ 工具描述中暗示调用链关系,引导 LLM 形成正确的调用序列
- ❌ 不要把 Tool Description 当 API 文档写
- ❌ 不要用
any或additionalProperties: true - ❌ 不要让描述太长(> 500 字会适得其反)
相关工具推荐:
- 🔧 jsjson.com JSON Schema 验证工具 — 在线验证你的工具参数 Schema
- 🔧 jsjson.com JSON 格式化工具 — 格式化你的 MCP 工具定义
- 🔧 MCP Inspector — MCP 协议调试工具
- 🔧 Zod — TypeScript 优先的 Schema 验证库,完美兼容 MCP SDK