MCP 工具设计模式与最佳实践

设计优秀的 MCP 工具需要兼顾 AI 可理解性、安全性和实用性。本文详解工具命名、参数设计、描述撰写、组合模式和结果格式化等 MCP 工具设计的核心方法论。

1. 工具设计的核心目标

MCP 工具的设计与其他 API 设计最大的区别在于:工具的调用者不是人类开发者,而是 AI 模型。这个根本差异导致了完全不同的设计考量:

AI 调用者的特点:

  • 理解有限:AI 不能阅读源代码或查看实现细节,只能通过 JSON Schema 和英文描述理解工具
  • 容易误解:模糊的描述会导致模型选择错误的工具或填入错误的参数
  • 上下文受限:模型每次只能看到工具列表的摘要,太长会被截断
  • 顺序推理:复杂任务需要分解为多个工具调用,工具之间需要有明确的依赖关系

设计目标优先级:

  1. 可发现性:AI 能准确判断何时使用该工具
  2. 可理解性:AI 能理解每个参数的含义和取值范围
  3. 健壮性:错误输入能被优雅处理,不应导致系统崩溃
  4. 组合性:工具可以与其他工具组合完成复杂任务

2. 工具命名策略

2.1 动词 + 名词命名法

// ✅ 清晰表明用途
"read_file"          // 读取文件
"write_file"         // 写入文件
"list_directory"     // 列出目录
"search_documents"   // 搜索文档
"execute_query"      // 执行查询
"send_notification"  // 发送通知

// ❌ 模糊命名
"handle"             // 处理什么?
"process"            // 处理什么?
"do_thing"           // 做什么?
"data"               // 什么数据?

2.2 避免命名冲突

当提供多个相关工具时,使用命名空间前缀避免冲突:

// 文件系统工具
"fs_read_file"
"fs_write_file"
"fs_list_directory"

// 数据库工具
"db_execute_query"
"db_list_tables"
"db_describe_table"

// Git 工具
"git_log"
"git_diff"
"git_status"

3. 参数设计的黄金法则

3.1 参数命名

// ✅ 自描述参数名
interface ReadFileArgs {
  file_path: string;       // ❌ 用 path 太泛
  encoding: string;        // ❌ 用 format 不准确
  max_lines?: number;      // ❌ 用 limit 不明确
}

// ✅ 更准确的命名
interface ReadFileArgs {
  absolute_file_path: string;  // 明确要绝对路径
  text_encoding: "utf8" | "ascii" | "latin1";
  maximum_line_count?: number; // 至少读多少行
}

3.2 参数类型选择

// ✅ 使用 enum 限制取值范围
interface SearchArgs {
  sort_order: "relevance" | "newest" | "oldest" | "alphabetical";
  result_limit: number; // 1-100
}

// ✅ 必填 vs 可选的明确区分
interface QueryArgs {
  // 必填参数放在前面
  table_name: string;
  select_columns: string[];

  // 可选参数用 ? 标记,提供合理的默认值
  where_conditions?: Record<string, unknown>;
  order_by?: string;
  limit?: number;        // 默认 100
  offset?: number;       // 默认 0
}

3.3 默认值的策略

// ✅ 安全的默认值
const ToolDefaults = {
  read_file: {
    max_size_bytes: 1024 * 1024,    // 1MB 上限,防止读取超大文件
    encoding: "utf8",
  },
  search: {
    max_results: 10,                 // 限制返回数量,避免上下文溢出
    timeout_ms: 5000,
  },
  write_file: {
    create_backup: true,             // 写文件前自动备份
    dry_run: false,
  },
};

4. 描述撰写的艺术

工具描述是 AI 理解工具用途的唯一来源,其重要性等同于 API 文档对于人类开发者。一个好的工具描述应该回答三个问题:这是什么?何时使用?怎么用?

4.1 工具描述模板

const toolDescriptions = {
  read_file: `
    读取指定路径的文件内容并返回文本。

    何时使用:
    - 需要查看文件内容时(代码文件、配置文件、日志文件)
    - 在修改文件前先读取其当前内容
    - 搜索文件内容的一部分时

    何时不使用:
    - 不需要查看文件内容时(使用 list_directory 代替)
    - 文件是二进制文件时(可能返回乱码)

    参数说明:
    - absolute_file_path: 文件的绝对路径(如 /home/user/project/src/main.ts)
    - text_encoding: 文本编码,默认为 utf8
    - maximum_line_count: 最多读取的行数,用于读取大文件的前半部分
  `,

  search_documents: `
    在文档集合中搜索与查询匹配的内容。

    何时使用:
    - 用户询问某个主题时,搜索相关文档
    - 需要查找包含特定关键词的文档
    - 不知道具体文件名但需要查找某段内容时

    搜索策略:
    - 先尝试精确匹配,再尝试模糊匹配
    - 支持布尔运算符: AND, OR, NOT
    - 支持短语搜索用引号: "exact phrase"

    参数说明:
    - query: 搜索查询字符串
    - document_types: 限制文档类型,如 ["markdown", "pdf"]
    - max_results: 最大返回结果数(1-50)
    - search_in_content: 是否在文档内容中搜索(默认 true)
  `,
};

4.2 参数描述技巧

const toolSchema = {
  name: "execute_sql_query",
  description: "在只读权限下执行 SQL SELECT 查询。不支持 INSERT/UPDATE/DELETE。",
  inputSchema: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: `
          完整的 SQL SELECT 查询语句。
          示例:
          - SELECT * FROM users WHERE age > 18
          - SELECT COUNT(*) FROM orders WHERE created_at > '2024-01-01'
          - SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id

          限制:
          - 只支持 SELECT 语句
          - 最多返回 1000 行
          - 查询超时 30 秒
        `,
      },
      timeout_seconds: {
        type: "number",
        description: "查询超时时间(秒)。默认 30 秒,最大 120 秒。复杂查询应适当增加。",
        minimum: 1,
        maximum: 120,
        default: 30,
      },
    },
    required: ["query"],
  },
};

5. 工具组合模式

5.1 分解复杂任务

将一个复杂操作分解为多个简单工具,让 AI 可以逐步推理:

// ❌ 过于复杂的单一工具
const badTool = {
  name: "manage_database",
  description: "/manage_database 可以查询、修改、备份、恢复数据库",
  // AI 难以决定何时使用以及传入什么参数
};

// ✅ 分解为多个原子工具
const goodTools = [
  {
    name: "db_list_tables",
    description: "列出数据库中的所有表",
    parameters: {},
  },
  {
    name: "db_describe_table",
    description: "获取指定表的结构信息(列名、类型、约束)",
    parameters: { table_name: "string" },
  },
  {
    name: "db_execute_select",
    description: "执行只读 SELECT 查询",
    parameters: { query: "string", limit: "number?" },
  },
  {
    name: "db_backup_table",
    description: "将表数据导出为 INSERT 语句备份",
    parameters: { table_name: "string" },
  },
];

5.2 状态机工具链

某些任务需要维护状态,通过工具返回值驱动下一步:

// 代码审查工作流
const reviewWorkflow = [
  {
    name: "review_start",
    description: "开始代码审查会话,创建审查上下文",
    returns: { review_session_id: "string" },
  },
  {
    name: "review_analyze_file",
    description: "分析指定文件的代码质量",
    parameters: {
      review_session_id: "string",
      file_path: "string",
      focus_areas: "string[]", // ["security", "performance", "readability"]
    },
    returns: { findings: "array" },
  },
  {
    name: "review_generate_report",
    description: "根据审查会话生成最终报告",
    parameters: { review_session_id: "string" },
    returns: { report_markdown: "string" },
  },
];

6. 结果格式化

6.1 结构化输出

AI 更容易处理结构化的返回:

// ❌ 纯文本返回难以解析
function badResult() {
  return "找到 5 个结果: file1.txt (80% 匹配), file2.txt (60% 匹配)...";
}

// ✅ 结构化 JSON 返回
function goodResult() {
  return {
    total_count: 5,
    results: [
      {
        file_path: "/docs/intro.md",
        relevance_score: 0.95,
        matched_lines: [12, 34, 56],
        preview: "...匹配内容预览...",
      },
      // ...
    ],
    query_time_ms: 120,
    has_more_results: true,
  };
}

6.2 错误结果的格式化

// 统一的错误响应格式
interface ToolError {
  error_code: string;        // 机器可读的错误码
  error_message: string;     // 人类可读的错误描述
  suggestion?: string;       // AI 的建议操作
  retryable: boolean;        // 是否可重试
}

const errorExamples = {
  file_not_found: {
    error_code: "FILE_NOT_FOUND",
    error_message: "文件 /path/to/file.txt 不存在",
    suggestion: "请使用 list_directory 查看可用文件,或使用搜索功能查找正确路径",
    retryable: false,
  },

  query_timeout: {
    error_code: "QUERY_TIMEOUT",
    error_message: "查询执行超过 30 秒限制",
    suggestion: "尝试添加更精确的 WHERE 条件,或减少 SELECT * 改为只选需要的列",
    retryable: true,
  },

  permission_denied: {
    error_code: "PERMISSION_DENIED",
    error_message: "无权访问 /etc/passwd(超出允许访问的路径范围)",
    suggestion: "只能访问 /home/user/workspace 目录下的文件",
    retryable: false,
  },
};

7. 常见反模式

7.1 万能工具

// ❌ 反模式:试图一个工具做所有事情
const badTool = {
  name: "do_everything",
  description: "这是一个万能工具可以做很多事情",
  parameters: {
    action: { type: "string", description: "操作类型" },
    data: { type: "object", description: "操作数据" },
  },
};
// AI 不知道 "action" 可以取什么值,"data" 需要包含什么

// ✅ 正解:拆分为具体工具
const goodTools = [
  { name: "create_task", description: "创建新任务", parameters: { title: "string", priority: "enum" } },
  { name: "update_task", description: "更新任务状态", parameters: { task_id: "string", status: "enum" } },
  { name: "delete_task", description: "删除任务", parameters: { task_id: "string" } },
];

7.2 参数神谕

// ❌ 反模式:期望 AI 生成复杂参数
const badTool = {
  name: "run_code",
  parameters: {
    code: { type: "string", description: "要执行的代码" },
  },
};
// AI 不知道环境中有什么库可用,生成的代码容易出错

// ✅ 正解:提供受限的特定功能
const goodTools = [
  { name: "math_add", parameters: { a: "number", b: "number" } },
  { name: "string_reverse", parameters: { text: "string" } },
  { name: "json_parse", parameters: { json_string: "string" } },
];

7.3 副作用隐藏

// ❌ 反模式:描述中隐藏副作用
const misleadingTool = {
  name: "get_config",
  description: "读取应用配置",
  // 实际上这个"读取"操作会触发配置重载并重启服务!
};

// ✅ 正解:明确标注副作用
const honestTool = {
  name: "reload_config",
  description: `
    重新加载配置文件并重启服务以应用新配置。
    ⚠️ 副作用:服务将重启约 5 秒,期间不可用。
    ⚠️ 请确保在非生产时段使用,或先确认当前无活跃请求。
  `,
};

8. 测试工具可用性

8.1 模拟 AI 决策

// 测试:给定用户请求,AI 是否能选择正确的工具?
const testCases = [
  {
    user_request: "帮我读取 README.md 的内容",
    expected_tool: "read_file",
    expected_args: { file_path: "README.md" },
  },
  {
    user_request: "查找所有包含 TODO 的文件",
    expected_tool: "search_files",
    expected_args: { query: "TODO", search_in_content: true },
  },
  {
    user_request: "数据库里有多少用户",
    expected_tool: "db_execute_select",
    expected_args: { query: "SELECT COUNT(*) FROM users" },
  },
];

async function testToolSelection(
  llm: LLMClient,
  tools: Tool[],
  testCases: TestCase[]
): Promise<number> {
  let correct = 0;
  for (const tc of testCases) {
    const response = await llm.selectTool(tools, tc.user_request);
    if (response.tool === tc.expected_tool) {
      correct++;
    }
  }
  return correct / testCases.length;
}

9. 总结

设计优秀的 MCP 工具需要在"简洁"和"精确"之间找到平衡:

维度好实践坏实践
命名动词+名词,语义明确模糊的单字或缩写
参数少而精,有明确约束过多可选参数,默认值不清晰
描述说明何时用、何时不用只重复工具名称
组合原子操作,可链式调用试图一个工具解决所有问题
结果结构化 JSON,包含元数据无格式的纯文本
错误错误码+建议,可重试性明确仅返回 “error” 字符串

记住:你的工具使用者是一个只能看到你的 schema 和描述的 AI。把设计当作给这个"特殊的用户"写 API 文档——它需要比常规 API 更精确、更详尽、更考虑边界情况。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. 排序学习与搜索召回排序系统
  2. 数据版本控制与血缘:DVC 与 LakeFS
  3. 模型可解释性:SHAP、LIME 与注意力归因