TL;DR: 工具 (Tool) 是 AI Agent 与外部世界交互的双手。一个设计精良的工具能让 LLM 准确理解何时调用、传什么参数、如何处理结果;一个设计糟糕的工具则会引发调用混乱、参数错误甚至安全事故。本文从命名、Schema、描述、错误处理、安全护栏、可测试性六个维度,系统总结为 AI Agent 编写高质量 MCP Tools 的工程实践。

核心要点

  • 工具是 Agent 的"肌肉记忆":LLM 只能通过工具的名称和描述来决定是否调用,工具设计本质上是在对模型做 Prompt Engineering
  • Schema 即契约:严格的 JSON Schema 定义是防止 LLM 幻觉式传参的第一道防线
  • 单一职责原则:每个工具只做一件事,避免"万能工具"导致模型选择困难
  • 防御性返回:永远返回结构化结果,永远不要让工具抛出未捕获异常
  • 安全是底线:输入校验、权限控制、操作确认三层护栏缺一不可

如果你还不了解 MCP 协议的基础架构,建议先阅读 MCP 协议深度解析;如果你还没搭过 MCP Server,可以先完成 Node.js 快速入门教程

1. 为什么工具设计如此重要

在传统软件开发中,API 的调用者是人类程序员——他们能读文档、看示例、理解隐含约定。但在 Agent 工作流中,调用者变成了 LLM。模型不会主动去读你的 README,它对工具的全部认知仅来自三个字段:namedescriptioninputSchema

这意味着:

  1. 名称不直观 = 永远不会被调用。如果你把"查询数据库"工具命名为 tool_7,没有任何 LLM 能猜到它的用途。
  2. 描述不精确 = 调用时机错误。一个描述为"处理数据"的工具,模型无法判断它是用来清洗 CSV 还是查询 SQL。
  3. Schema 不严格 = 参数乱传。如果你定义了一个 any 类型的参数,模型会用各种意想不到的格式填充它。

这不是理论推演——在 Function Calling 的生产实践中,超过 60% 的调用失败源自工具定义不清晰,而非模型能力不足。

2. 命名:让 LLM 一眼看懂

2.1 命名公式

好的工具名称遵循 动词_名词动词_名词_限定词 的模式:

差的命名 好的命名 原因
data query_database 明确了动作和对象
process validate_json_schema 限定了处理的具体内容
do_stuff create_github_issue 动词+平台+实体,完全自描述
tool_1 search_documents 语义化命名

2.2 命名陷阱

避免同义工具名称冲突。 如果你同时注册了 search_filesfind_files,LLM 会陷入选择困难。每个动作只保留一个标准名称。

避免过度缩写。 del_usr_rec 对人类都难以理解,更不用说 LLM。写全 delete_user_record,token 开销微乎其微,但消歧义效果显著。

避免过于宽泛的名称。 manage_resource 可以是创建、读取、更新、删除中的任何一个操作。拆分为 create_resourceget_resourceupdate_resourcedelete_resource 四个独立工具。

3. Description:写给 LLM 看的 Prompt

工具的 description 字段不是给人类看的注释——它是直接注入到模型上下文窗口中的 Prompt。这个认知转变至关重要。

3.1 Description 四要素

一个高质量的 description 应该包含四个层面的信息:

typescript
server.tool(
  "query_database",
  // 四要素:做什么 + 何时用 + 何时不用 + 返回什么
  `Execute a read-only SQL query against the analytics database.
   Use this tool when the user asks for data insights, statistics,
   or specific records from the database.
   Do NOT use this for modifying data - use update_database instead.
   Returns: JSON array of matching rows, or an error message
   if the query is invalid.`,
  { query: z.string().describe("A valid SELECT SQL statement") },
  async ({ query }) => { /* ... */ }
);

四要素拆解:

  1. 做什么 (What):在分析数据库上执行只读 SQL 查询
  2. 何时用 (When):用户需要数据洞察、统计或特定记录时
  3. 何时不用 (When Not):不要用于修改数据,修改请用 update_database
  4. 返回什么 (Returns):JSON 行数组,或错误消息

3.2 跨工具消歧义

当多个工具的功能存在重叠时,必须在 description 中明确划定边界:

typescript
// 工具 A
"Search documents by keyword. Use for full-text search across all documents. "
+ "For filtering by metadata (date, author, tag), use filter_documents instead."

// 工具 B
"Filter documents by metadata fields (date range, author, tags). "
+ "For keyword-based content search, use search_documents instead."

这种"互相指引"的写法能极大提高 LLM 的工具选择准确率。在 MCP 协议 2025 版本中,这一设计模式被进一步强调。

4. Input Schema:用类型系统约束 LLM

JSON Schema 不仅是参数校验工具,更是防止 LLM 幻觉式传参的关键屏障。

4.1 Schema 设计原则

尽可能使用枚举而非自由文本。 当参数有固定选项时,枚举能将 LLM 的选择空间从无穷收敛到有限集合:

typescript
{
  format: z.enum(["json", "csv", "xml"])
    .describe("Output format. Must be one of: json, csv, xml"),
  
  // 而不是
  format: z.string()
    .describe("Output format like json, csv, etc.")
}

为每个参数添加 describe。 参数名只是标识符,describe 才是 LLM 理解参数含义的信息源:

typescript
{
  startDate: z.string()
    .describe("Start date in ISO 8601 format (YYYY-MM-DD). Example: 2026-01-15"),
  maxResults: z.number()
    .min(1).max(100)
    .describe("Maximum number of results to return. Default: 10, Max: 100")
}

明确标注必填和可选。 利用 .optional().default() 清晰表达参数的必要性:

typescript
{
  query: z.string().describe("Search query - required"),
  page: z.number().optional().default(1)
    .describe("Page number for pagination. Optional, defaults to 1"),
  includeArchived: z.boolean().optional().default(false)
    .describe("Whether to include archived results. Optional, defaults to false")
}

4.2 避免深度嵌套

LLM 处理扁平结构的准确率远高于深度嵌套的对象。如果你发现参数结构超过两层嵌套,应该考虑拆分为多个工具或扁平化参数:

typescript
// 差:深度嵌套
{
  config: {
    database: {
      connection: { host: string, port: number },
      query: { table: string, filters: {...} }
    }
  }
}

// 好:扁平化
{
  dbHost: z.string().describe("Database host address"),
  dbPort: z.number().describe("Database port number"),
  table: z.string().describe("Target table name"),
  filterColumn: z.string().optional().describe("Column name to filter by"),
  filterValue: z.string().optional().describe("Value to filter for")
}

5. 输出设计:结构化、可预测、可消费

工具的返回值不是给人类看的——它会被 LLM 消费并用于后续推理。因此,输出设计同样需要遵循严格的原则。

5.1 统一返回格式

为所有工具定义一致的返回结构,降低 LLM 的解析负担:

typescript
// 成功响应
{
  content: [{
    type: "text",
    text: JSON.stringify({
      success: true,
      data: { /* 业务数据 */ },
      metadata: { totalCount: 42, executionTimeMs: 123 }
    })
  }]
}

// 错误响应
{
  content: [{
    type: "text",
    text: JSON.stringify({
      success: false,
      error: {
        code: "INVALID_QUERY",
        message: "SQL syntax error near 'FORM'",
        suggestion: "Did you mean 'FROM'? Check your SQL syntax."
      }
    })
  }],
  isError: true
}

5.2 控制输出体积

LLM 的上下文窗口是有限资源。一个返回 10MB JSON 的工具会直接撑爆上下文,导致后续推理崩溃。必须在工具层面做好截断和分页:

typescript
const handleQuery = async ({ query, maxResults = 10 }) => {
  const allResults = await db.execute(query);
  const truncated = allResults.slice(0, maxResults);
  
  return {
    content: [{
      type: "text",
      text: JSON.stringify({
        results: truncated,
        totalCount: allResults.length,
        hasMore: allResults.length > maxResults,
        hint: allResults.length > maxResults
          ? `Showing ${maxResults} of ${allResults.length} results. Use 'page' parameter to see more.`
          : undefined
      })
    }]
  };
};

关键点在于:返回 hasMorehint 字段,给 LLM 一个明确的信号——数据被截断了,可以翻页获取更多。

6. 错误处理:让 Agent 能自我修复

AI Agent 开发中,错误不再是终止信号,而是 Agent 自我修复的信息源。你的工具抛出的每一个错误,都应该携带足够的信息让 LLM 理解"哪里错了"和"怎么修"。

6.1 错误分类与处理策略

错误类型 示例 LLM 应知道的信息
参数错误 日期格式不对 期望的格式是什么
权限不足 无权访问该资源 需要什么权限、如何获取
资源不存在 文件未找到 可能的正确路径、如何搜索
速率限制 API 配额已耗尽 何时可以重试
内部错误 数据库连接失败 是否值得重试

6.2 可操作的错误消息

typescript
const handleTool = async (params) => {
  try {
    // 业务逻辑
    const result = await businessLogic(params);
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  } catch (error) {
    // 根据错误类型返回可操作的信息
    if (error.code === 'ENOENT') {
      return {
        content: [{
          type: "text",
          text: JSON.stringify({
            success: false,
            error: "FILE_NOT_FOUND",
            message: `File '${params.path}' does not exist.`,
            suggestion: "Check the file path. Use the list_files tool to see available files in the directory.",
            retryable: false
          })
        }],
        isError: true
      };
    }
    
    // 兜底处理
    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          success: false,
          error: "INTERNAL_ERROR",
          message: "An unexpected error occurred. This may be a temporary issue.",
          retryable: true
        })
      }],
      isError: true
    };
  }
};

注意 suggestion 字段——它直接指引 LLM 接下来应该调用 list_files 工具,形成自我修复的闭环。这正是 LLM Function Calling 的高阶用法。

7. 安全护栏:三层防御体系

将工具暴露给 LLM 意味着你在赋予 AI 执行真实操作的能力。一个没有护栏的写入工具就是一颗定时炸弹。

7.1 第一层:输入校验

在 Schema 层面做第一道拦截,但不要完全信任 Schema——在 handler 内部做二次校验:

typescript
const deleteFile = async ({ path }) => {
  // Schema 只能约束类型,业务规则需要手动校验
  const normalizedPath = path.normalize(path);
  
  // 防止路径穿越攻击
  if (normalizedPath.includes('..') || !normalizedPath.startsWith(ALLOWED_ROOT)) {
    return {
      content: [{ type: "text", text: "Access denied: path is outside the allowed directory." }],
      isError: true
    };
  }
  
  // 防止删除关键文件
  const PROTECTED_PATTERNS = ['.env', 'package.json', '.git'];
  if (PROTECTED_PATTERNS.some(p => normalizedPath.includes(p))) {
    return {
      content: [{ type: "text", text: "This file is protected and cannot be deleted." }],
      isError: true
    };
  }
  
  // 执行删除
  await fs.unlink(normalizedPath);
  return { content: [{ type: "text", text: `Deleted: ${normalizedPath}` }] };
};

7.2 第二层:操作分级

将工具按危险等级分为三级,不同级别采用不同的执行策略:

级别 操作类型 执行策略 示例
只读 查询、搜索、获取 直接执行 search_files, get_config
可逆写入 创建、更新 执行后返回回滚信息 create_file, update_config
不可逆写入 删除、发送、部署 需要确认机制 delete_database, send_email

7.3 第三层:操作确认

对高危操作引入确认机制(也称为 Human-in-the-Loop),在 MCP 协议进阶中有更详细的架构设计:

typescript
server.tool(
  "delete_database_table",
  "Permanently delete a database table and all its data. This action is IRREVERSIBLE.",
  {
    tableName: z.string().describe("Name of the table to delete"),
    confirmPhrase: z.literal("I understand this is irreversible")
      .describe("You must pass exactly 'I understand this is irreversible' to confirm")
  },
  async ({ tableName, confirmPhrase }) => {
    // confirmPhrase 的 literal 类型强制 LLM 明确传入确认文本
    await db.dropTable(tableName);
    return { content: [{ type: "text", text: `Table '${tableName}' has been permanently deleted.` }] };
  }
);

z.literal() 的巧妙运用迫使 LLM 必须"有意识地"传入确认字符串,而不是无意中触发危险操作。

8. 幂等性与副作用管理

在 Agent 工作流中,LLM 可能因为网络超时或推理错误而重复调用同一个工具。如果你的工具不具备幂等性,重复调用会导致重复创建、重复扣费等严重后果。

8.1 幂等设计模式

typescript
const createOrder = async ({ orderId, items }) => {
  // 幂等键:相同的 orderId 只会创建一次订单
  const existing = await db.orders.findOne({ orderId });
  if (existing) {
    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          success: true,
          data: existing,
          note: "Order already exists. Returning existing order."
        })
      }]
    };
  }
  
  const newOrder = await db.orders.create({ orderId, items });
  return {
    content: [{
      type: "text",
      text: JSON.stringify({ success: true, data: newOrder })
    }]
  };
};

核心思路:接受一个客户端生成的唯一标识符 (如 orderId),在执行前检查是否已存在,存在则直接返回已有结果。

9. 工具数量与上下文预算

每个注册的工具都会占用 LLM 的上下文窗口。一个典型的 MCP Tool 定义(name + description + inputSchema)大约消耗 200-500 tokens。如果你注册了 50 个工具,仅工具描述就可能占用 10,000-25,000 tokens。

9.1 工具精简策略

策略 适用场景 示例
合并同质工具 CRUD 操作可用参数区分 manage_config --action=get/set/delete
按领域拆分 Server 工具数超过 20 个 数据库 Server、文件系统 Server、API Server 各自独立
动态注册 工具与用户角色相关 管理员才能看到 delete_* 系列工具
渐进式暴露 复杂工作流 先暴露高频工具,按需激活低频工具

这一点在 MCP 2025 规范更新中得到了协议层面的支持——Server 可以动态通知 Client 工具列表的变化。

10. 可测试性:三层测试金字塔

工具的质量不能只靠肉眼检查。建立一套自动化测试体系,确保工具在持续迭代中不会退化。

10.1 单元测试层

测试工具函数的纯业务逻辑,不涉及 MCP 协议层:

typescript
describe('query_database tool', () => {
  it('should return results for valid query', async () => {
    const result = await queryHandler({ query: 'SELECT * FROM users LIMIT 5' });
    expect(result.content[0].text).toContain('"success":true');
  });

  it('should return error for invalid SQL', async () => {
    const result = await queryHandler({ query: 'INVALID SQL' });
    expect(result.isError).toBe(true);
    expect(result.content[0].text).toContain('INVALID_QUERY');
  });

  it('should respect maxResults limit', async () => {
    const result = await queryHandler({ query: 'SELECT * FROM users', maxResults: 2 });
    const parsed = JSON.parse(result.content[0].text);
    expect(parsed.results.length).toBeLessThanOrEqual(2);
    expect(parsed.hasMore).toBeDefined();
  });
});

10.2 Schema 一致性测试

确保 Schema 定义与实际处理逻辑一致:

typescript
describe('Schema validation', () => {
  it('should reject missing required fields', () => {
    const schema = toolDefinitions['query_database'].inputSchema;
    const result = schema.safeParse({});
    expect(result.success).toBe(false);
  });

  it('should accept valid enum values', () => {
    const schema = toolDefinitions['export_data'].inputSchema;
    const result = schema.safeParse({ format: 'csv', query: 'SELECT 1' });
    expect(result.success).toBe(true);
  });
});

10.3 端到端集成测试

使用 MCP Inspector 或编程式 Client 进行完整的协议级测试,验证从自然语言到工具调用的全链路。

11. 实战检查清单

在发布任何新的 MCP Tool 之前,逐项核对以下清单:

命名与描述

  • 名称是否遵循 动词_名词 格式
  • Description 是否包含四要素(做什么、何时用、何时不用、返回什么)
  • 与相似工具是否有明确的消歧义说明

参数设计

  • 是否为每个参数添加了 .describe()
  • 枚举值是否覆盖了所有合法选项
  • 是否避免了两层以上的嵌套
  • 可选参数是否设置了合理的默认值

输出规范

  • 输出是否使用了统一的 JSON 结构
  • 大量数据是否做了分页/截断处理
  • 是否包含 hasMore 等分页提示

错误处理

  • 是否所有异常都被 catch 并返回了 isError: true
  • 错误消息是否包含修复建议
  • 是否标注了 retryable 属性

安全性

  • 读操作与写操作是否分离为独立工具
  • 危险操作是否有确认机制
  • 输入是否做了路径穿越/注入攻击的防护
  • 写入操作是否具备幂等性

总结

AI Agent 编写工具是一种全新的编程范式。你的"用户"不再是人类,而是一个通过自然语言理解世界的 LLM。这要求开发者转变思维——把 description 当作 Prompt 来写,把 inputSchema 当作契约来定义,把错误消息当作"下一步指令"来设计。

掌握这些原则后,你构建的工具集不仅能被 LLM 准确调用,还能在复杂的多步推理和多 Agent 协作场景中保持稳定可靠。这正是从"能用"到"好用"到"可信赖"的跨越。

如果你想进一步了解 MCP 协议的完整能力图谱,推荐阅读本专栏的 MCP 协议深度解析;如果你关注 2025 年协议的最新变化(包括 OAuth 认证和远程 Server 部署),请参阅 MCP 2025 规范解读