1. 工具设计的核心目标
MCP 工具的设计与其他 API 设计最大的区别在于:工具的调用者不是人类开发者,而是 AI 模型。这个根本差异导致了完全不同的设计考量:
AI 调用者的特点:
- 理解有限:AI 不能阅读源代码或查看实现细节,只能通过 JSON Schema 和英文描述理解工具
- 容易误解:模糊的描述会导致模型选择错误的工具或填入错误的参数
- 上下文受限:模型每次只能看到工具列表的摘要,太长会被截断
- 顺序推理:复杂任务需要分解为多个工具调用,工具之间需要有明确的依赖关系
设计目标优先级:
- 可发现性:AI 能准确判断何时使用该工具
- 可理解性:AI 能理解每个参数的含义和取值范围
- 健壮性:错误输入能被优雅处理,不应导致系统崩溃
- 组合性:工具可以与其他工具组合完成复杂任务
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 更精确、更详尽、更考虑边界情况。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。