Agent 工具调用设计清单:参数、权限与失败处理

0阅读11分钟

工具是 Agent 的手

Agent 的能力边界,很大程度上取决于它能调用哪些工具。工具描述含糊时,模型会猜参数;错误返回不可读时,模型会在失败之后继续重试;权限没有分级时,一次误调用可能直接修改生产数据。

这些问题的根源在于工具设计没有为「非确定性调用者」做好准备。传统 API 面对的是人类开发者——能读文档、能推断意图、能处理模糊错误。Agent 面对的是 JSON schema 和自然语言描述,它需要的是严格的接口契约和可机读的错误信号。

Anthropic 在工程博客中将工具定义为「确定性系统与非确定性 Agent 之间的契约」1。OpenAI 的 Agent 实践指南同样强调,工具设计是 Agent 工程中投入产出比最高的环节2。LangChain 的 Agent 评测清单也指出:工具设计能一次性消除整类 Agent 错误,团队在工具上花的时间应该比 prompt 更多3

本文从工具描述、参数 schema、权限分级、错误处理和审计日志五个维度,给出一份可落地的设计清单。

工具调用在 Agent 架构中的位置

在讨论具体设计之前,先看工具调用在整个 Agent 循环里处于什么位置。一个典型的 Agent 执行流程如下:

流程图画布 · 115%
Mermaid 流程图加载中...

这个循环里有三个关键拦截点:参数校验、权限检查和错误处理。任何一个环节设计不清楚,Agent 都会陷入无效循环或产生不可预期的副作用。

工具描述:不是写注释,是写操作手册

描述的目的

工具描述会随 system prompt 一起注入模型的上下文窗口。模型在上下文中直接依赖这段文字做决策,而非先读文档再理解。这意味着工具描述的写作方式接近于给新同事写的操作手册,而不是给维护者看的代码注释。

Anthropic 的五项工具设计原则中,第一项就是「选择正确的工具」——按任务的自然分段来组织工具,而非把 API 端点逐一包装1

好描述 vs 坏描述

维度坏描述好描述
功能说明「搜索用户」「按姓名、邮箱或团队 ID 搜索组织内用户,返回匹配结果列表。当需要查找某人的信息或确认用户是否存在时调用」
使用场景未说明「适用于需要获取用户基本信息、确认用户身份或查找团队成员的场景。不适用于查询用户权限——权限查询请使用 get_user_permissions
参数说明query: stringquery: string — 搜索关键词,支持姓名、邮箱地址或团队 ID。至少 2 个字符
返回说明「返回结果」「返回最多 20 条匹配用户记录,包含 user_id、name、email、team。若无匹配返回空数组」
边界条件未说明「结果超过 20 条时返回 has_more: true,使用 cursor 参数翻页」

坏描述的问题在于,模型只能猜测什么时候该调用、参数该传什么、返回值长什么样。猜测的结果就是误调用和参数错误。

代码对比:工具描述

// ❌ 坏:描述模糊,模型需要猜测
const searchUserTool = {
  name: "search_user",
  description: "搜索用户",
  parameters: {
    type: "object",
    properties: {
      query: { type: "string" },
    },
    required: ["query"],
  },
};
// ✅ 好:描述明确,模型能做出正确决策
const searchUserTool = {
  name: "user_search",
  description: [
    "按姓名、邮箱或团队 ID 搜索组织内活跃用户。",
    "适用场景:需要查找某人的联系方式、确认用户是否存在、获取 user_id 用于后续操作。",
    "不适用:查询用户权限请用 `permission_get_user_permissions`;",
    "查询已注销用户请用 `user_search_archived`。",
    "返回最多 20 条结果,支持 cursor 分页。",
  ].join(" "),
  parameters: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: "搜索关键词,支持姓名、邮箱地址或团队 ID,至少 2 个字符",
        minLength: 2,
      },
      team_id: {
        type: "string",
        description: "可选,限定搜索范围为指定团队",
        format: "uuid",
      },
      limit: {
        type: "integer",
        description: "返回数量上限,默认 20,最大 50",
        default: 20,
        minimum: 1,
        maximum: 50,
      },
    },
    required: ["query"],
  },
};

好描述的关键区别:

  1. 命名带命名空间user_search 而不是 search_user,按服务/资源分组,避免与其他搜索工具混淆。Anthropic 的实践表明,命名空间能显著减少模型在大量工具中的选择错误1
  2. 说明何时调用、何时不调用:模型能区分边界,不会把权限查询误路由到用户搜索。
  3. 参数有约束minLengthformatdefaultmaximum 都是 JSON Schema 原生支持的校验关键字,模型和运行时都能用。
  4. 返回结构可预期:描述中明确了分页行为和字段含义。

参数 Schema:像写合同一样严谨

参数 schema 不只是类型声明,它是 Agent 和工具之间的接口合同。合同不清楚,调用方就会传错参数;合同有漏洞,恶意输入就能穿透防线。

Schema 设计的常见问题

问题表现后果
类型过宽data: any 或缺少 type模型传入不合法值,运行时崩溃
缺少枚举status: string 无枚举约束模型编造状态值,如 status: "已完成" 而非 "completed"
必填项缺失required 为空模型省略关键参数,工具执行失败
默认值未声明default模型不知道不传参时的行为
格式约束缺失URL 字段无 format: "uri"模型传入非 URL 字符串
嵌套过深多层 object 嵌套模型难以正确构造参数

代码对比:参数 Schema

// ❌ 坏:类型过宽,无约束
{
  "type": "object",
  "properties": {
    "user": {},
    "action": { "type": "string" },
    "data": { "type": "object" }
  }
}
// ✅ 好:类型精确,约束完整
{
  "type": "object",
  "properties": {
    "user_id": {
      "type": "string",
      "format": "uuid",
      "description": "目标用户 ID,从 `user_search` 返回值获取"
    },
    "action": {
      "type": "string",
      "enum": ["suspend", "activate", "reset_password"],
      "description": "要执行的操作类型"
    },
    "reason": {
      "type": "string",
      "minLength": 10,
      "maxLength": 500,
      "description": "操作原因,用于审计日志,至少 10 个字符"
    },
    "dry_run": {
      "type": "boolean",
      "default": false,
      "description": "为 true 时只校验不执行,用于预览操作影响"
    }
  },
  "required": ["user_id", "action", "reason"]
}

好的 schema 做了四件事:

  1. 参数命名语义明确user_id 而不是 user,消除歧义。
  2. 枚举约束可选值actionenum 限定三种合法操作,模型不会编造 delete_account 之类的值。
  3. 字符串有长度约束reason 要求至少 10 个字符,防止模型传空字符串或一句话应付。
  4. 提供 dry_run 选项:高风险操作前可以先预览影响,这是 Anthropic 推荐的安全模式1

权限分级:不是所有操作都应该直接执行

Agent 工具调用中最危险的情况是:模型有权限做它不该做的事。一个能删除数据的工具和一个只能查询数据的工具,不应该有相同的执行路径。

权限分级模型

风险等级操作类型示例执行策略
L0 - 只读查询、搜索、获取user_searchget_order_detail直接执行,无需确认
L1 - 低风险写入创建草稿、添加备注create_draftadd_comment直接执行,记录审计日志
L2 - 中风险写入修改状态、发送通知update_order_statussend_email执行前展示预览,需用户确认
L3 - 高风险操作删除数据、修改权限、支付delete_accountgrant_adminrefund_payment必须人工审批,工具只生成操作计划
L4 - 不可逆操作永久删除、批量变更purge_databatch_update_all拒绝 Agent 直接调用,需管理后台操作

Kevin Tan 在 Agent 错误处理实践中提出,高风险工具应该在执行前通过 escalation hook 拦截,将操作意图展示给人类审批4。这是对副作用不可逆性的尊重,而非对模型能力的不信任。

代码对比:权限拦截

// ❌ 坏:所有工具同等执行,无权限分级
async function executeTool(name: string, params: unknown) {
  const tool = tools[name];
  return await tool.execute(params);
}
// ✅ 好:基于风险等级分层执行
async function executeTool(
  name: string,
  params: unknown,
  context: AgentContext
) {
  const tool = tools[name];
  const riskLevel = tool.metadata.riskLevel;
 
  // L0/L1 直接执行
  if (riskLevel <= RiskLevel.LOW_WRITE) {
    return await tool.execute(params);
  }
 
  // L2 展示预览,等待确认
  if (riskLevel === RiskLevel.MEDIUM_WRITE) {
    const preview = await tool.preview(params);
    const confirmed = await context.requestUserConfirmation(preview);
    if (!confirmed) {
      return { status: "cancelled", reason: "user_declined" };
    }
    return await tool.execute(params);
  }
 
  // L3/L4 拒绝直接执行,生成操作计划
  return {
    status: "requires_approval",
    plan: await tool.generatePlan(params),
    approver: tool.metadata.requiredApprover,
  };
}

分层执行的核心思路是:L0/L1 操作可以放手让 Agent 自主执行,L2 操作需要人类确认,L3/L4 操作 Agent 只能生成计划而不能直接执行。

错误处理:让模型知道下一步该做什么

工具执行失败时,返回什么信息决定了 Agent 能否自我纠正。一个返回 &#123;"error": "failed"&#125; 的工具和返回结构化错误的工具,在 Agent 循环中的表现完全不同。

错误响应的层次

层次内容作用
状态标识status: "error"让模型知道这不是成功结果
错误类型error_type: "validation_error"让模型理解错误类别
错误详情detail: "user_id 格式无效,需要 UUID"让模型知道具体哪个字段有问题
修复建议suggestion: "使用 user_search 工具先获取有效的 user_id"让模型知道如何修正
可重试标识retryable: false让模型决定是否重试
关联 IDtrace_id: "abc123"用于后续排查

Nordic APIs 在 API 错误消息设计指南中提出,面向 Agent 的错误响应应该是「语义化的、带恢复路径的结构化数据」5。freeCodeCamp 的 API 设计指南推荐采用 RFC 7807 Problem Details 格式,用 typetitlestatusdetailerror_code 五个字段构建机器可读的错误响应6

代码对比:错误响应

// ❌ 坏:自由文本错误,模型难以解析
return { error: "Something went wrong while processing your request" };
// ❌ 稍好但仍然不够:有错误码但无修复建议
return { error_code: "USER_NOT_FOUND", message: "User not found" };
// ✅ 好:结构化错误,带修复建议和可重试标识
return {
  status: "error",
  error_type: "resource_not_found",
  error_code: "USER_NOT_FOUND",
  detail: `未找到 user_id 为 ${params.user_id} 的用户`,
  suggestion: "请确认 user_id 是否正确,可使用 user_search 工具按姓名或邮箱搜索",
  retryable: false,
  trace_id: context.traceId,
};
// ✅ 好:参数校验错误的结构化响应
return {
  status: "error",
  error_type: "validation_error",
  error_code: "INVALID_PARAMETER",
  detail: "参数校验失败",
  fields: [
    {
      field: "action",
      message: `"${params.action}" 不是合法操作类型`,
      allowed_values: ["suspend", "activate", "reset_password"],
    },
    {
      field: "reason",
      message: "操作原因至少需要 10 个字符",
      min_length: 10,
    },
  ],
  suggestion: "请修正上述字段后重试",
  retryable: true,
  trace_id: context.traceId,
};

好的错误响应做了三件事:告诉模型「什么错了」、解释「为什么错了」、建议「怎么修复」。retryable 字段让模型知道重试是否有意义——对于「资源不存在」这类错误,重试不会改变结果;对于「服务暂时不可用」,重试是合理的。

审计日志:每次工具调用都要留痕

Agent 的工具调用不是函数调用——函数调用失败可以 catch,工具调用失败可能已经产生了外部副作用。一封不该发的邮件、一条不该发的通知、一次不该执行的状态变更,都需要在事后能追溯。

审计日志的必要字段

字段类型说明
trace_idstring全局追踪 ID,贯穿整个 Agent 会话
tool_call_idstring本次工具调用的唯一 ID
tool_namestring工具名称
callerobject调用者信息(Agent ID、会话 ID、模型版本)
parametersobject调用参数摘要(敏感字段脱敏)
risk_levelenum操作风险等级
result_statusenum执行结果:success / error / cancelled / requires_approval
result_summarystring结果摘要(不含完整返回值)
duration_msnumber执行耗时
timestampISO 8601调用时间
human_approvedboolean是否经过人工审批
approval_bystring?审批人(如经过人工审批)

代码对比:审计日志

// ❌ 坏:只记录工具名和参数,无法追溯
console.log(`Tool called: ${name}`, params);
// ✅ 好:结构化审计记录
interface AuditRecord {
  trace_id: string;
  tool_call_id: string;
  tool_name: string;
  caller: {
    agent_id: string;
    session_id: string;
    model: string;
  };
  parameters: Record<string, unknown>; // 敏感字段脱敏后
  risk_level: RiskLevel;
  result_status: "success" | "error" | "cancelled" | "requires_approval";
  result_summary?: string;
  duration_ms: number;
  timestamp: string;
  human_approved: boolean;
  approval_by?: string;
}
 
async function executeWithAudit(
  name: string,
  params: unknown,
  context: AgentContext
) {
  const tool = tools[name];
  const startTime = Date.now();
  const sanitizedParams = sanitizeForAudit(params, tool.metadata.sensitiveFields);
 
  const record: AuditRecord = {
    trace_id: context.traceId,
    tool_call_id: generateToolCallId(),
    tool_name: name,
    caller: {
      agent_id: context.agentId,
      session_id: context.sessionId,
      model: context.model,
    },
    parameters: sanitizedParams,
    risk_level: tool.metadata.riskLevel,
    result_status: "success",
    duration_ms: 0,
    timestamp: new Date().toISOString(),
    human_approved: context.humanApproved ?? false,
    approval_by: context.approvedBy,
  };
 
  try {
    const result = await tool.execute(params);
    record.result_summary = summarizeResult(result);
    record.duration_ms = Date.now() - startTime;
    await auditLogger.write(record);
    return result;
  } catch (err) {
    record.result_status = "error";
    record.result_summary = err instanceof Error ? err.message : String(err);
    record.duration_ms = Date.now() - startTime;
    await auditLogger.write(record);
    throw err;
  }
}

审计日志的关键在于「可追溯性」和「存储成本」之间取得平衡。参数记录摘要而非完整值,敏感字段(密码、token、个人信息)必须脱敏。

工具调用的执行流程

将前面讨论的参数校验、权限检查、错误处理和审计日志整合在一起,一个完整的工具执行流程如下:

流程图画布 · 115%
Mermaid 流程图加载中...

设计清单:15 项检查

以下是工具调用设计的完整检查清单。每个工具在上线前都应该逐项核对。

工具描述

  • 描述明确说明工具用途:包含「做什么」和「什么时候用」
  • 说明不适用的场景:指出容易混淆的相邻工具及其区别
  • 工具命名遵循命名空间规范:按「服务_资源_操作」格式,如 user_searchemail_send
  • 描述中说明返回结构:让模型知道调用后能拿到什么

参数 Schema

  • 每个参数有明确的 type:不使用 any 或空 schema
  • 枚举值用 enum 约束:不允许模型自由发挥的字段必须枚举
  • 必填项正确声明required 数组包含所有不可缺少的参数
  • 默认值明确标注default 值与文档描述一致
  • 格式约束完整:邮箱用 format: "email",UUID 用 format: "uuid",URL 用 format: "uri"
  • 提供 dry_run 或预览选项:高风险操作支持先预览再执行

权限与执行

  • 风险等级正确标注:L0-L4 分级与实际副作用匹配
  • L2 及以上操作需人工确认:执行前展示预览,等待用户确认
  • L3/L4 操作拒绝 Agent 直接执行:只生成操作计划,提交审批

错误处理

  • 错误响应结构化:包含 statuserror_typedetailsuggestionretryable
  • 校验错误列出具体字段:指明哪个字段有问题、合法值是什么
  • 错误消息可机读:避免纯自然语言描述,使用 error_code 辅助判断

审计与可观测性

  • 每次工具调用记录审计日志:包含 trace_id、参数摘要、结果、耗时
  • 敏感字段脱敏:密码、token、个人信息不出现在日志中
  • 支持幂等性:写操作支持 idempotency key,防止重复执行

验证方法

设计完工具之后,怎么验证设计是否合理?仅靠「调用成功」是不够的。

Anthropic 建议用真实场景构建评测任务,而不是用简单的沙盒测试1。例如:

评测场景测试内容通过标准
正常调用参数完整、格式正确工具执行成功,返回预期结果
参数缺失省略必填参数返回 validation_error,指出缺失字段
格式错误email 字段传入非邮箱字符串返回 validation_error,说明期望格式
权限不足L2 工具在无确认情况下执行返回 requires_confirmation,展示预览
资源不存在传入不存在的 user_id返回 resource_not_found,建议搜索
外部服务故障依赖的下游 API 超时返回 service_unavailable,标记 retryable: true
重复执行同一写操作调用两次第二次返回幂等结果,不产生副作用
边界值limit=0、limit=999返回参数越界错误,说明合法范围

LangChain 的评测清单也强调,Agent 工具调用的评测不只看成功率,还要看失败后的行为——是停下来请求帮助、继续盲目重试、还是能根据错误信息自我纠正3

Kevin Tan 在 Agent 错误处理实践中给出了一个参考数据:实施分层错误处理后,因失控循环导致的 token 消耗从每次 $180 降至 $3-5,问题发现时间从数小时缩短到 3 次调用之内4

小结

工具调用设计本质上是 API 设计的一个特殊分支,只是调用方从人类开发者变成了 LLM Agent。人类能读文档、能推断意图、能容忍模糊错误;Agent 只能依赖 JSON schema 和自然语言描述做决策。

把工具描述写成操作手册,把参数 schema 写成接口合同,把错误响应写成恢复指南,把权限分级写成安全策略,把审计日志写成事后追溯依据——这五件事做好了,Agent 工具调用的稳定性会有本质提升。

Anthropic 的经验是,他们在工具设计上花的时间比 prompt 设计多得多1。工具是 Agent 与世界交互的唯一通道——通道本身不可靠,Agent 再聪明也无济于事。这属于必要投入,而非过度工程。

参考资料

Footnotes

  1. Anthropic Engineering, "Writing Effective Tools for AI Agents", 2025 2 3 4 5 6

  2. OpenAI, "A Practical Guide to Building Agents", 2025

  3. LangChain, "Agent Evaluation Readiness Checklist", 2026 2

  4. Kevin Tan, "AI Agent Error Handling: 5 Patterns to Catch Silent Failures", 2026 2

  5. Nordic APIs, "Designing API Error Messages for AI Agents", 2025

  6. freeCodeCamp, "How to Design APIs for AI Agents", 2026

评论 0

0 / 1000