工具设计原则

状态:✅ 已补齐(2026-09-13) 一句话定义:工具是 Agent 的手;粒度、命名、参数 schema、错误信息、幂等 决定模型能不能用对。

你为什么要学这个

工具调用是 Agent 能力的核心,但很多工程师只关注「工具能不能跑」,忽略「模型会不会用」。好工具让模型用对 90% 的情况;坏工具让模型反复踩坑——误判参数、盲目重试、不知道何时该停止。

学完应能:判断工具粒度是否合理(一个工具一件事);写出模型能读懂的描述;设计可恢复的错误返回;理解权限标注与幂等的重要性;并回答「为什么万能搜索工具往往更差」。

一、一个工具一件事 vs 巨石工具

1.1 粒度原则:单一职责

反例:万能搜索工具

// 坏设计:一个工具干太多事,参数表长,模型容易错用
interface UniversalSearchParams {
  query: string;
  searchType: "users" | "orders" | "products" | "tickets";
  filters: {
    userId?: string;
    orderId?: string;
    productCategory?: string;
    ticketStatus?: "open" | "closed" | "pending";
    dateRange?: { start: string; end: string };
  };
  sortBy?: "date" | "relevance" | "popularity";
  limit?: number;
}

问题

  • 参数表太长(10+ 字段),模型容易漏填或乱填
  • searchType 是枚举,但每个枚举值的过滤字段不同(userId 只对 users 有效),模型需要「记忆」这些规则
  • 错误信息不够具体,模型不知道是哪个字段错了

正例:拆分为单一职责工具

// 好设计:每个工具只干一件事
async function searchUsers(params: {
  query?: string;
  userId?: string;
  limit?: number;
}): Promise<User[]> { /* ... */ }

async function searchOrders(params: {
  query?: string;
  orderId?: string;
  status?: "pending" | "paid" | "shipped";
  limit?: number;
}): Promise<Order[]> { /* ... */ }

async function searchProducts(params: {
  query?: string;
  category?: string;
  limit?: number;
}): Promise<Product[]> { /* ... */ }

async function searchTickets(params: {
  query?: string;
  ticketId?: string;
  status?: "open" | "closed" | "pending";
  limit?: number;
}): Promise<Ticket[]> { /* ... */ }

好处

  • 参数表短(3-5 字段),模型容易填对
  • 每个工具的字段含义清晰,不需要「记忆」复杂的枚举规则
  • 错误信息能精确到字段(「userId 必须为 24 字符 UUID」而非「参数无效」)

1.2 粒度判断原则

场景 粒度 理由
数据源相同(都查 MySQL) 拆分 过滤字段、排序规则不同,合在一起模型要记太多规则
动作相同(都是删除)但资源不同 拆分 deleteUserdeleteOrderdeleteProduct 权限不同
数据源不同(MySQL + Elastic) 拆分 搜索语义不同(MySQL 精确,Elastic 模糊)
工具调用有依赖(先 A 后 B) 拆分 让模型显式编排,而不是在工具里隐式串联
只有一个参数且含义相同 合并 getUserById(userId: string)getUser(userId: string) 合并

关键直觉工具的参数表应该是「一眼能看懂的」(5 个字段以内),超过就需要拆分。

二、描述文字怎么写才「模型可读」

2.1 三层描述结构

OpenAI / Anthropic 的 Function Calling 协议中,工具描述包含三层:

{
  "name": "searchUsers",
  "description": "搜索用户信息。支持按用户 ID 精确查询,或按姓名/邮箱模糊匹配。",
  "parameters": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "搜索关键词,可以是用户名或邮箱的一部分。不填则返回全部用户。",
      },
      "userId": {
        "type": "string",
        "description": "用户 ID(24 字符 UUID)。与 `query` 二选一,优先用此参数精确查询。",
      },
      "limit": {
        "type": "integer",
        "description": "返回结果数量限制,默认 10,最大 100。",
      },
    },
    "required": ["limit"], // ← 坏设计!limit 不该是必填
  },
}

2.2 描述三原则

原则一:说清楚「干什么」,不说「怎么干」

坏描述

{
  "name": "searchUsers",
  "description": "调用 MySQL 的 `SELECT * FROM users` 语句,支持 LIKE 匹配和 UUID 精确查询,默认按 `created_at` 降序排序。",
}

问题

  • 模型不需要知道底层实现(MySQL、表结构、排序规则)
  • 这些信息会干扰模型判断「该不该用这个工具」

好描述

{
  "name": "searchUsers",
  "description": "搜索用户信息。支持按用户 ID 精确查询,或按姓名/邮箱模糊匹配。",
}

原则二:说明「参数互斥」和「默认值」

坏描述

{
  "name": "searchUsers",
  "description": "搜索用户信息。",
  "parameters": {
    "query": { "type": "string", "description": "搜索关键词" },
    "userId": { "type": "string", "description": "用户 ID" },
  },
}

问题

  • 模型不知道 queryuserId 是二选一
  • 模型不知道不填会返回全部用户(可能触发大量结果)

好描述

{
  "name": "searchUsers",
  "description": "搜索用户信息。支持按用户 ID 精确查询,或按姓名/邮箱模糊匹配。",
  "parameters": {
    "query": {
      "type": "string",
      "description": "搜索关键词,可以是用户名或邮箱的一部分。不填则返回全部用户。",
    },
    "userId": {
      "type": "string",
      "description": "用户 ID(24 字符 UUID)。与 `query` 二选一,优先用此参数精确查询。",
    },
    "limit": {
      "type": "integer",
      "description": "返回结果数量限制,默认 10,最大 100。",
    },
  },
}

原则三:必填字段要「少」

坏设计

{
  "parameters": {
    "required": ["query", "limit", "sortBy"], // ← 太多必填
  },
}

问题

  • 模型遇到必填字段时会「强行凑值」(不填 limit 就填 1,不填 sortBy 就填 date
  • 强行凑的值往往不符合业务逻辑(limit=1 结果太少,sortBy=date 结果不准)

好设计

{
  "parameters": {
    "required": [], // ← 没有必填字段
    "limit": { "description": "返回结果数量限制,默认 10,最大 100。" },
    "sortBy": { "description": "排序方式,可选 `date`、`relevance`,默认 `relevance`。" },
  },
}

直觉必填字段越少,模型越不会强行凑值——宁可让工具返回全部结果(让前端分页),也不要让模型瞎填。

三、错误返回:让模型可恢复,而不是堆栈砸脸

3.1 三类错误

错误类型 模型能恢复吗 应如何返回
参数错误(格式不对、必填未填) ✅ 能,改参数重试 结构化错误信息 + 正确示例
权限错误(没权限执行) ✅ 能,换个工具或提示用户 说明权限等级 + 建议
业务错误(用户不存在、余额不足) ✅ 能,让模型解释给用户 业务层面的解释
系统错误(数据库挂了) ❌ 不能,降级或人工介入 返回友好提示 + 建议稍后重试

3.2 结构化错误返回

坏设计:堆栈砸脸

// 工具直接抛出异常
throw new Error("Invalid userId: must be 24 characters");

问题

  • 模型收到的是纯文本,无法解析「哪个字段错了」
  • 模型不知道「正确的格式是什么」

好设计:结构化错误

interface ToolResponse<T = any> {
  success: boolean;
  data?: T;
  error?: {
    type: "invalid_params" | "permission_denied" | "not_found" | "rate_limit";
    field?: string;           // 哪个字段错了
    message: string;          // 人类可读的错误信息
    hint?: string;            // 给模型的建议
  };
}

// 参数错误
return {
  success: false,
  error: {
    type: "invalid_params",
    field: "userId",
    message: "用户 ID 必须为 24 字符 UUID 格式",
    hint: "例如:5f8d0d55b54764421b7156d4,或先调用 searchUsers 获取正确 ID",
  },
};

// 权限错误
return {
  success: false,
  error: {
    type: "permission_denied",
    message: "您没有权限删除此订单",
    hint: "仅订单所有者或管理员可删除,请改用 requestOrderDeletion 工具发起删除申请",
  },
};

// 业务错误
return {
  success: false,
  error: {
    type: "not_found",
    field: "orderId",
    message: "订单不存在或已删除",
    hint: "请确认订单 ID 是否正确,或调用 searchOrders 搜索订单",
  },
};

3.3 让模型自纠

模型收到结构化错误后,通过 system prompt 引导自纠:

// System Prompt 的一部分
{
  role: "system",
  content: `你是一个智能助手,可以调用工具完成任务。
工具调用失败时,请根据错误类型采取不同策略:
- invalid_params:检查错误信息中的 field 和 hint,修改参数后重试
- permission_denied:提示用户权限不足,或建议使用其他工具
- not_found:提示用户资源不存在,或先调用搜索工具
- rate_limit:提示用户稍后重试

错误信息格式:
{
  "type": "invalid_params",
  "field": "userId",
  "message": "用户 ID 必须为 24 字符 UUID 格式",
  "hint": "例如:5f8d0d55b54764421b7156d4"
}`,
}

四、权限与副作用标注

4.1 权限标注:让模型知道「能不能用」

OpenAI 协议:工具定义时不支持权限标注,需要通过描述传递

{
  "name": "deleteUser",
  "description": "删除用户账户。⚠️ 仅管理员可使用,普通用户请改用 deactivateUser。",
  "parameters": { /* ... */ },
}

Anthropic 协议:支持 requires_approvalrisk_level

{
  "name": "deleteUser",
  "description": "删除用户账户。",
  "parameters": { /* ... */ },
  "requires_approval": true,      // ← 需要人工确认
  "risk_level": "L4",            // ← 风险等级,触发 HITL
}

MCP 协议:支持 dangerous 标注

{
  "name": "deleteUser",
  "description": "删除用户账户。",
  "parameters": { /* ... */ },
  "dangerous": true,             // ← 标记为危险操作
}

4.2 副作用标注:让模型知道「会有影响」

有副作用的操作

  • 删除、更新、下单、转账、发送邮件
  • 工具返回后,数据库状态改变

无副作用的操作

  • 查询、搜索、读取
  • 工具返回后,数据库状态不变

标注方式

{
  "name": "updateOrder",
  "description": "更新订单信息。此操作有副作用:会修改订单状态和数据库记录。",
  "parameters": { /* ... */ },
}

为什么重要:模型需要知道「哪些操作可以重试,哪些不行」

  • 无副作用操作:失败可重试(searchUsers 失败可重试 3 次)
  • 有副作用操作:失败不能重试(updateOrder 失败需人工介入)

五、与 MCP / Function Calling 的对应

5.1 三层协议映射

层级 MCP 协议 OpenAI Function Calling Anthropic Tool Use
工具定义 Tool schema function schema tool schema
描述 description description description
参数 inputSchema (JSON Schema) parameters (JSON Schema) input_schema (JSON Schema)
权限标注 dangerous 通过 description requires_approvalrisk_level
调用返回 ToolResult tool message tool_result message

5.2 JSON Schema 规范

所有协议都遵循 JSON Schema,核心字段:

{
  "type": "object",              // 必填
  "properties": {                // 参数定义
    "userId": {
      "type": "string",
      "format": "uuid",          // ← 格式校验
      "minLength": 24,
      "maxLength": 24,
    },
    "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
    },
  },
  "required": [],                // 必填字段
}

5.3 跨协议工具定义抽象

// 统一的工具定义
interface UnifiedToolDefinition {
  name: string;
  description: string;
  parameters: JSONSchema;
  riskLevel?: "L1" | "L2" | "L3" | "L4" | "L5";  // 通用
  dangerous?: boolean;                              // MCP / Anthropic
  requiresApproval?: boolean;                       // Anthropic
}

// 转换为 OpenAI 格式
function toOpenAI(tool: UnifiedToolDefinition) {
  return {
    type: "function",
    function: {
      name: tool.name,
      description: tool.dangerous ? `⚠️ 危险操作:${tool.description}` : tool.description,
      parameters: tool.parameters,
    },
  };
}

// 转换为 Anthropic 格式
function toAnthropic(tool: UnifiedToolDefinition) {
  return {
    name: tool.name,
    description: tool.description,
    input_schema: tool.parameters,
    requires_approval: tool.riskLevel === "L4" || tool.riskLevel === "L5",
    risk_level: tool.riskLevel,
  };
}

// 转换为 MCP 格式
function toMCP(tool: UnifiedToolDefinition) {
  return {
    name: tool.name,
    description: tool.description,
    inputSchema: tool.parameters,
    dangerous: tool.riskLevel === "L4" || tool.riskLevel === "L5",
  };
}

六、命名、参数与类型设计

6.1 命名规范

工具类型 命名模式 示例
查询 search{资源} searchUserssearchOrders
获取单条 get{资源} getUsergetOrder
创建 create{资源} createUsercreateOrder
更新 update{资源} updateUserupdateOrder
删除 delete{资源} deleteUserdeleteOrder
发送 send{资源} sendEmailsendNotification
取消 cancel{资源} cancelOrdercancelSubscription

反例

{
  "name": "getUserData",          // ← 不够具体,data 是什么?
  "name": "queryUser",            // ← 动词混用,统一用 search
  "name": "deleteUserById",       // ← 方法名里不要加 ByXxx,参数里已有 userId
}

正例

{
  "name": "getUser",              // ← 明确,返回单个用户
  "name": "searchUsers",          // ← 明确,返回用户列表
  "name": "deleteUser",           // ← 明确,删除用户
}

6.2 参数命名

参数类型 命名模式 示例
ID {资源}Id userIdorderId
名称 {资源}Name userNameproductName
查询关键词 query query(通用)
筛选字段 filter{字段} filterStatusfilterDateRange
分页 pagelimitoffset pagelimit
排序 sortBysortOrder sortBysortOrder

反例

{
  "uid": { "type": "string" },               // ← 不够明确
  "name": { "type": "string" },              // ← 不够明确,是用户名还是商品名?
  "search": { "type": "string" },            // ← 动词混用,统一用 query
  "size": { "type": "integer" },             // ← 不够明确,是页大小还是记录数?
}

正例

{
  "userId": { "type": "string" },            // ← 明确
  "userName": { "type": "string" },          // ← 明确
  "query": { "type": "string" },             // ← 明确
  "limit": { "type": "integer" },            // ← 明确
}

6.3 类型设计

必填类型

业务场景 类型 示例
ID string + format: uuid "5f8d0d55b54764421b7156d4"
金额 number + minimum: 0 99.99
数量 integer + minimum: 1 10
日期 string + format: date-time "2026-09-13T12:00:00Z"
布尔 boolean truefalse
枚举 string + enum "pending""paid""shipped"

反例

{
  "userId": { "type": "string" },                    // ← 没说明格式,模型可能填任意字符串
  "price": { "type": "number" },                     // ← 没限制最小值,模型可能填负数
  "quantity": { "type": "integer" },                 // ← 没限制最小值,模型可能填 0 或负数
  "status": { "type": "string" },                    // ← 没说明枚举值,模型可能乱填
}

正例

{
  "userId": {
    "type": "string",
    "format": "uuid",
    "description": "24 字符 UUID,例如:5f8d0d55b54764421b7156d4。",
  },
  "price": {
    "type": "number",
    "minimum": 0,
    "description": "订单金额(元),必须 ≥ 0。",
  },
  "quantity": {
    "type": "integer",
    "minimum": 1,
    "description": "商品数量,必须 ≥ 1。",
  },
  "status": {
    "type": "string",
    "enum": ["pending", "paid", "shipped", "cancelled"],
    "description": "订单状态。",
  },
}

七、幂等性设计

7.1 幂等的定义

幂等操作:多次执行结果相同,不会产生副作用。

操作 幂等吗 理由
getUser(userId: string) ✅ 是 查询操作,不改变数据库
searchUsers(query: string) ✅ 是 查询操作,不改变数据库
createUser(params) ❌ 否 每次创建新用户
updateUser(userId, params) ✅ 是 多次更新到同一状态
deleteUser(userId) ✅ 是 删除后已无数据,再删不变
sendEmail(to, subject, body) ❌ 否 每次发送新邮件

7.2 幂等键设计

**非幂等操作(创建、发送)**必须支持幂等键,防止重试时重复执行。

{
  "name": "createOrder",
  "description": "创建订单。此操作有副作用:会扣减库存并创建订单记录。幂等键防止重复创建。",
  "parameters": {
    "idempotencyKey": {
      "type": "string",
      "description": "幂等键(UUID),相同键的重复请求会返回第一次创建的订单,不会重复创建。建议用客户端生成的 UUID。",
    },
    "productId": { "type": "string" },
    "quantity": { "type": "integer" },
  },
}

服务端实现

// 幂等键存储(Redis)
const idempotencyKeys = new Map<string, { result: Order; timestamp: number }>();

async function createOrder(params: {
  idempotencyKey: string;
  productId: string;
  quantity: number;
}): Promise<Order> {
  // 检查幂等键
  const cached = idempotencyKeys.get(params.idempotencyKey);
  if (cached) {
    return cached.result; // ← 返回缓存结果,不重复执行
  }

  // 执行创建逻辑
  const order = await createOrderInDB(params);

  // 缓存结果(24 小时过期)
  idempotencyKeys.set(params.idempotencyKey, { result: order, timestamp: Date.now() });

  return order;
}

前端调用

// 每次创建订单生成新的幂等键
const idempotencyKey = uuidv4();
const order = await createOrder({ idempotencyKey, productId, quantity });

7.3 幂等键放在哪一层?

层级 放在哪 理由
客户端 生成 唯一标识一次业务操作,跨请求保持一致
网关 检查 先查幂等键,命中则直接返回,不调用下游
服务端 校验 & 缓存 最后校验,防止绕过网关直连
数据库 唯一索引 幂等键作为唯一索引,防止并发重复插入

最佳实践

  1. 客户端生成幂等键(UUID)
  2. 网关先查幂等键(Redis),命中直接返回
  3. 服务端最后校验幂等键,执行业务逻辑
  4. 数据库用幂等键做唯一索引兜底

八、端到端落地示例

6.1 坏工具改造前后对比

改造前:万能搜索工具

{
  "name": "universalSearch",
  "description": "搜索所有资源,包括用户、订单、产品、工单。",
  "parameters": {
    "searchType": {
      "type": "string",
      "enum": ["users", "orders", "products", "tickets"],
    },
    "query": { "type": "string" },
    "filters": {
      "type": "object",
      "properties": {
        "userId": { "type": "string" },        // 只对 users 有效
        "orderId": { "type": "string" },       // 只对 orders 有效
        "status": { "type": "string" },        // 各资源 status 含义不同
      },
    },
  },
}

问题

  • 模型调用成功率:62%
  • 参数错误率:31%
  • 平均重试次数:2.3 次

改造后:拆分为 4 个单一工具

[
  {
    "name": "searchUsers",
    "description": "搜索用户信息。支持按用户 ID 精确查询,或按姓名/邮箱模糊匹配。",
    "parameters": {
      "query": { "type": "string", "description": "搜索关键词,可以是用户名或邮箱的一部分。" },
      "userId": { "type": "string", "description": "24 字符 UUID,与 query 二选一。" },
      "limit": { "type": "integer", "description": "返回数量,默认 10。" },
    },
  },
  {
    "name": "searchOrders",
    "description": "搜索订单信息。支持按订单 ID 精确查询,或按商品名称模糊匹配。",
    "parameters": {
      "query": { "type": "string", "description": "搜索关键词,可以是商品名称或订单号。" },
      "orderId": { "type": "string", "description": "24 字符 UUID,与 query 二选一。" },
      "status": {
        "type": "string",
        "enum": ["pending", "paid", "shipped", "cancelled"],
        "description": "订单状态筛选。",
      },
      "limit": { "type": "integer", "description": "返回数量,默认 10。" },
    },
  },
  // ... searchProducts, searchTickets
]

改造后指标

  • 模型调用成功率:94%
  • 参数错误率:6%
  • 平均重试次数:0.8 次

6.2 工具验收清单

  • 粒度:每个工具参数表 ≤ 5 个字段
  • 命名:动词 + 名词(searchUsersdeleteOrder
  • 描述:说清楚「干什么」,不说「怎么干」
  • 必填字段:尽量少(0-1 个)
  • 参数互斥:在描述中说明「二选一」
  • 默认值:在描述中说明「默认 10」
  • 错误返回:结构化,包含 typefieldhint
  • 权限标注:危险操作在描述中加 ⚠️
  • 副作用标注:有副作用操作在描述中说明

学习要点(应能回答)

  • 为什么「万能搜索工具」往往更差? 参数表太长(10+ 字段),模型容易漏填或乱填;不同资源的过滤字段不同,模型需要「记忆」复杂规则;错误信息不够具体,模型不知道是哪个字段错了。拆分为单一职责工具后,参数表短(3-5 字段),模型容易填对。
  • 工具失败时返回什么字段最有用? 结构化错误信息,包含 type(错误类型)、field(哪个字段错了)、message(人类可读信息)、hint(给模型的建议)。这样模型能根据 type 采取不同策略,根据 field 修改参数,根据 hint 生成正确示例。

已有相关文档(先读这些)

参考资料

  • OpenAI Function Calling 文档:工具定义最佳实践
  • Anthropic Tool Use 文档:requires_approvalrisk_level
  • MCP 协议规范:dangerous 标注
  • JSON Schema 规范:参数校验标准