MCP 工具描述工程:写出让 AI 精准调用的 Tool Description 实战指南

深入解析 MCP 工具描述工程的核心技巧,包含参数设计、描述优化、类型约束与实战代码示例,帮助开发者写出让 AI Agent 零失误调用的工具定义。

开发者效率 2026-06-12 12 分钟

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),而不是泛化名称(如 queryprocess)。参数名应该是自解释的英文单词,避免缩写。

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 文档写
  • ❌ 不要用 anyadditionalProperties: true
  • ❌ 不要让描述太长(> 500 字会适得其反)

相关工具推荐:

📚 相关文章