TL;DR: 工具 (Tool) 是 AI Agent 与外部世界交互的双手。一个设计精良的工具能让 LLM 准确理解何时调用、传什么参数、如何处理结果;一个设计糟糕的工具则会引发调用混乱、参数错误甚至安全事故。本文从命名、Schema、描述、错误处理、安全护栏、可测试性六个维度,系统总结为 AI Agent 编写高质量 MCP Tools 的工程实践。
核心要点
- 工具是 Agent 的能力契约:名称、描述和 Schema 会影响工具选择,但模型、策略和运行时上下文同样重要
- Schema 即契约:严格的 JSON Schema 定义是防止 LLM 幻觉式传参的第一道防线
- 单一职责原则:每个工具只做一件事,避免"万能工具"导致模型选择困难
- 防御性返回:对预期错误返回结构化结果,对未知异常记录并安全降级
- 安全是底线:输入校验、权限控制、操作确认三层护栏缺一不可
如果你还不了解 MCP 协议的基础架构,建议先阅读 MCP 协议深度解析;如果你还没搭过 MCP Server,可以先完成 Node.js 快速入门教程。
1. 为什么工具设计如此重要
在传统软件开发中,API 的调用者是人类程序员——他们能读文档、看示例、理解隐含约定。但在 Agent 工作流中,调用者变成了 LLM。模型不会主动去读你的 README,它对工具的全部认知仅来自三个字段:name、description 和 inputSchema。
这意味着:
- 名称不直观 = 永远不会被调用。如果你把"查询数据库"工具命名为
tool_7,没有任何 LLM 能猜到它的用途。 - 描述不精确 = 调用时机错误。一个描述为"处理数据"的工具,模型无法判断它是用来清洗 CSV 还是查询 SQL。
- Schema 不严格 = 参数乱传。如果你定义了一个
any类型的参数,模型会用各种意想不到的格式填充它。
工具定义不清晰确实会造成选择和参数错误,但失败比例取决于模型、任务、工具集合和评测方法。没有统一数据集时,不应把某个百分比写成普遍规律;应在自己的工作负载上分别测量选择、参数、执行和结果解释错误。
2. 命名:让 LLM 一眼看懂
2.1 命名公式
好的工具名称遵循 动词_名词 或 动词_名词_限定词 的模式:
| 差的命名 | 好的命名 | 原因 |
|---|---|---|
data |
query_database |
明确了动作和对象 |
process |
validate_json_schema |
限定了处理的具体内容 |
do_stuff |
create_github_issue |
动词+平台+实体,完全自描述 |
tool_1 |
search_documents |
语义化命名 |
2.2 命名陷阱
避免同义工具名称冲突。 如果你同时注册了 search_files 和 find_files,LLM 会陷入选择困难。每个动作只保留一个标准名称。
避免过度缩写。 del_usr_rec 对人类都难以理解,更不用说 LLM。写全 delete_user_record,token 开销微乎其微,但消歧义效果显著。
避免过于宽泛的名称。 manage_resource 可以是创建、读取、更新、删除中的任何一个操作。拆分为 create_resource、get_resource、update_resource、delete_resource 四个独立工具。
3. Description:写给 LLM 看的 Prompt
工具的 description 字段不是给人类看的注释——它是直接注入到模型上下文窗口中的 Prompt。这个认知转变至关重要。
3.1 Description 四要素
一个高质量的 description 应该包含四个层面的信息:
server.tool(
"get_sales_summary",
// 四要素:做什么 + 何时用 + 何时不用 + 返回什么
`Return an approved sales summary for a named region and date range.
Use this for aggregate reporting, not arbitrary database exploration.
The server applies tenant, row, query, and result-size policy.
Returns a bounded summary with an optional next cursor.`,
{
region: z.enum(["APAC", "EMEA", "AMER"]),
from: z.string().date(),
to: z.string().date()
},
async ({ region, from, to }) => { /* ... */ }
);
四要素拆解:
- 做什么 (What):返回受控的销售汇总
- 何时用 (When):用户需要指定区域和日期范围的聚合数据时
- 何时不用 (When Not):不要把它当作任意 SQL 或跨租户查询入口
- 返回什么 (Returns):有界的汇总结果和分页信息
3.2 跨工具消歧义
当多个工具的功能存在重叠时,必须在 description 中明确划定边界:
// 工具 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 的选择空间从无穷收敛到有限集合:
const schema = {
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 理解参数含义的信息源:
const dateRangeSchema = {
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() 清晰表达参数的必要性:
const searchSchema = {
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 处理扁平结构的准确率远高于深度嵌套的对象。如果你发现参数结构超过两层嵌套,应该考虑拆分为多个工具或扁平化参数:
// 差:深度嵌套
const nestedSchema = {
config: {
database: {
connection: { host: "string", port: "number" },
query: { table: "string", filters: "object" }
}
}
};
// 好:扁平化
const flatSchema = {
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 的解析负担:
// 成功响应
const successResponse = {
content: [{
type: "text",
text: JSON.stringify({
success: true,
data: { /* 业务数据 */ },
metadata: { totalCount: 42, executionTimeMs: 123 }
})
}]
};
// 错误响应
const errorResponse = {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: {
code: "INVALID_DATE_RANGE",
message: "The end date must not precede the start date.",
suggestion: "Provide an ISO date range with from <= to."
}
})
}],
isError: true
};
5.2 控制输出体积
LLM 的上下文窗口是有限资源。一个返回 10MB JSON 的工具会直接撑爆上下文,导致后续推理崩溃。必须在工具层面做好截断和分页:
const handleSalesSummary = async ({ region, from, to, cursor }) => {
const page = await reports.getSalesSummary({ region, from, to, cursor });
return {
content: [{
type: "text",
text: JSON.stringify({
results: page.rows,
hasMore: page.nextCursor !== null,
nextCursor: page.nextCursor
})
}]
};
};
关键点在于:返回 hasMore 和 nextCursor,给 LLM 一个明确的信号——结果是有界页面,可以在服务端再次执行权限检查后继续翻页。
6. 错误处理:让 Agent 能自我修复
在 AI Agent 开发中,错误不再是终止信号,而是 Agent 自我修复的信息源。你的工具抛出的每一个错误,都应该携带足够的信息让 LLM 理解"哪里错了"和"怎么修"。
6.1 错误分类与处理策略
| 错误类型 | 示例 | LLM 应知道的信息 |
|---|---|---|
| 参数错误 | 日期格式不对 | 期望的格式是什么 |
| 权限不足 | 无权访问该资源 | 需要什么权限、如何获取 |
| 资源不存在 | 文件未找到 | 可能的正确路径、如何搜索 |
| 速率限制 | API 配额已耗尽 | 何时可以重试 |
| 内部错误 | 数据库连接失败 | 是否值得重试 |
6.2 可操作的错误消息
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 内部做二次校验:
import pathModule from "node:path";
const deleteFile = async ({ path: requestedPath }) => {
// Schema 只能约束类型,业务规则需要手动校验
const normalizedPath = pathModule.resolve(ALLOWED_ROOT, requestedPath);
const relativePath = pathModule.relative(ALLOWED_ROOT, normalizedPath);
// 防止路径穿越攻击
if (relativePath.startsWith('..') || pathModule.isAbsolute(relativePath)) {
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 协议进阶中有更详细的架构设计:
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 }) => {
// 文本只是用户意图信号;真正的授权、资源检查和一次性审批在服务端完成
const approved = await approvalService.consume({
action: "delete_database_table",
resource: tableName,
confirmation: confirmPhrase
});
if (!approved) {
return { content: [{ type: "text", text: "Approval required." }], isError: true };
}
await db.dropTable(tableName);
return { content: [{ type: "text", text: `Table '${tableName}' has been permanently deleted.` }] };
}
);
z.literal() 只能约束参数形状,不能阻止模型或恶意客户端伪造确认文本。高影响操作还需要服务端根据可信 Principal 做对象授权,并校验带有精确 Action Digest、短期有效期和一次性使用标记的审批记录。
8. 幂等性与副作用管理
在 Agent 工作流中,LLM 可能因为网络超时或推理错误而重复调用同一个工具。如果你的工具不具备幂等性,重复调用会导致重复创建、重复扣费等严重后果。
8.1 幂等设计模式
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 的上下文窗口,但实际开销取决于 Schema、描述、序列化方式和模型 Tokenizer。不要把 200-500 tokens 或 20 个工具当作协议阈值;应在目标模型和真实工具集合上测量上下文占用、选择混淆率、延迟和任务成功率。
9.1 工具精简策略
| 策略 | 适用场景 | 示例 |
|---|---|---|
| 合并同质工具 | CRUD 操作可用参数区分 | manage_config --action=get/set/delete |
| 按领域拆分 Server | 工具预算、选择混淆或权限边界需要隔离 | 数据库 Server、文件系统 Server、API Server 各自独立 |
| 动态注册 | 工具与用户角色相关 | 管理员才能看到 delete_* 系列工具 |
| 渐进式暴露 | 复杂工作流 | 先暴露高频工具,按需激活低频工具 |
如果所用 MCP 版本和 Client 支持工具列表变更通知,可以按需更新 Capability;这不是让 Server 绕过授权或动态暴露高风险工具的理由。
10. 可测试性:三层测试金字塔
工具的质量不能只靠肉眼检查。建立一套自动化测试体系,确保工具在持续迭代中不会退化。
10.1 单元测试层
测试工具函数的纯业务逻辑,不涉及 MCP 协议层:
describe('get_sales_summary tool', () => {
it('should return results for valid query', async () => {
const result = await summaryHandler({ region: 'APAC', from: '2026-01-01', to: '2026-03-31' });
expect(result.content[0].text).toContain('"success":true');
});
it('should return error for an invalid date range', async () => {
const result = await summaryHandler({ region: 'APAC', from: '2026-04-01', to: '2026-01-01' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('INVALID_DATE_RANGE');
});
it('should return a bounded page', async () => {
const result = await summaryHandler({ region: 'APAC', from: '2026-01-01', to: '2026-03-31' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.hasMore).toBeDefined();
expect(parsed.nextCursor).toBeDefined();
});
});
10.2 Schema 一致性测试
确保 Schema 定义与实际处理逻辑一致:
describe('Schema validation', () => {
it('should reject missing required fields', () => {
const schema = toolDefinitions['get_sales_summary'].inputSchema;
const result = schema.safeParse({});
expect(result.success).toBe(false);
});
it('should accept valid enum values', () => {
const schema = toolDefinitions['get_sales_summary'].inputSchema;
const result = schema.safeParse({ region: 'APAC', from: '2026-01-01', to: '2026-03-31' });
expect(result.success).toBe(true);
});
});
10.3 端到端集成测试
使用 MCP Inspector 或编程式 Client 进行完整的协议级测试,验证从自然语言到工具调用的全链路。
11. 实战检查清单
在发布任何新的 MCP Tool 之前,逐项核对以下清单:
命名与描述
- 名称是否遵循
动词_名词格式 - Description 是否包含四要素(做什么、何时用、何时不用、返回什么)
- 与相似工具是否有明确的消歧义说明
参数设计
- 是否为每个参数添加了
.describe() - 枚举值是否覆盖了所有合法选项
- 是否避免了两层以上的嵌套
- 可选参数是否设置了合理的默认值
输出规范
- 输出是否使用了统一的 JSON 结构
- 大量数据是否做了分页/截断处理
- 是否包含
hasMore等分页提示
错误处理
- 预期业务错误是否按 SDK 语义返回
isError: true,未知异常是否安全降级并记录 - 错误消息是否包含修复建议
- 是否标注了
retryable属性
安全性
- 读操作与写操作是否分离为独立工具
- 危险操作是否有确认机制
- 输入是否做了路径穿越/注入攻击的防护
- 写入操作是否具备幂等性
总结
为 AI Agent 编写工具是一种全新的编程范式。你的"用户"不再是人类,而是一个通过自然语言理解世界的 LLM。这要求开发者转变思维——把 description 当作 Prompt 来写,把 inputSchema 当作契约来定义,把错误消息当作"下一步指令"来设计。
掌握这些原则后,你构建的工具集不仅能被 LLM 准确调用,还能在复杂的多步推理和多 Agent 协作场景中保持稳定可靠。这正是从"能用"到"好用"到"可信赖"的跨越。
如果你想进一步了解 MCP 协议的完整能力图谱,推荐阅读本专栏的 MCP 协议深度解析;如果你关注 2025 年协议的最新变化(包括 OAuth 认证和远程 Server 部署),请参阅 MCP 2025 规范解读。