Agent 工具调用设计清单:参数、权限与失败处理
工具是 Agent 的手
Agent 的能力边界,很大程度上取决于它能调用哪些工具。工具描述含糊时,模型会猜参数;错误返回不可读时,模型会在失败之后继续重试;权限没有分级时,一次误调用可能直接修改生产数据。
这些问题的根源在于工具设计没有为「非确定性调用者」做好准备。传统 API 面对的是人类开发者——能读文档、能推断意图、能处理模糊错误。Agent 面对的是 JSON schema 和自然语言描述,它需要的是严格的接口契约和可机读的错误信号。
Anthropic 在工程博客中将工具定义为「确定性系统与非确定性 Agent 之间的契约」1。OpenAI 的 Agent 实践指南同样强调,工具设计是 Agent 工程中投入产出比最高的环节2。LangChain 的 Agent 评测清单也指出:工具设计能一次性消除整类 Agent 错误,团队在工具上花的时间应该比 prompt 更多3。
本文从工具描述、参数 schema、权限分级、错误处理和审计日志五个维度,给出一份可落地的设计清单。
工具调用在 Agent 架构中的位置
在讨论具体设计之前,先看工具调用在整个 Agent 循环里处于什么位置。一个典型的 Agent 执行流程如下:
这个循环里有三个关键拦截点:参数校验、权限检查和错误处理。任何一个环节设计不清楚,Agent 都会陷入无效循环或产生不可预期的副作用。
工具描述:不是写注释,是写操作手册
描述的目的
工具描述会随 system prompt 一起注入模型的上下文窗口。模型在上下文中直接依赖这段文字做决策,而非先读文档再理解。这意味着工具描述的写作方式接近于给新同事写的操作手册,而不是给维护者看的代码注释。
Anthropic 的五项工具设计原则中,第一项就是「选择正确的工具」——按任务的自然分段来组织工具,而非把 API 端点逐一包装1。
好描述 vs 坏描述
| 维度 | 坏描述 | 好描述 |
|---|---|---|
| 功能说明 | 「搜索用户」 | 「按姓名、邮箱或团队 ID 搜索组织内用户,返回匹配结果列表。当需要查找某人的信息或确认用户是否存在时调用」 |
| 使用场景 | 未说明 | 「适用于需要获取用户基本信息、确认用户身份或查找团队成员的场景。不适用于查询用户权限——权限查询请使用 get_user_permissions」 |
| 参数说明 | query: string | query: 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"],
},
};好描述的关键区别:
- 命名带命名空间:
user_search而不是search_user,按服务/资源分组,避免与其他搜索工具混淆。Anthropic 的实践表明,命名空间能显著减少模型在大量工具中的选择错误1。 - 说明何时调用、何时不调用:模型能区分边界,不会把权限查询误路由到用户搜索。
- 参数有约束:
minLength、format、default、maximum都是 JSON Schema 原生支持的校验关键字,模型和运行时都能用。 - 返回结构可预期:描述中明确了分页行为和字段含义。
参数 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 做了四件事:
- 参数命名语义明确:
user_id而不是user,消除歧义。 - 枚举约束可选值:
action用enum限定三种合法操作,模型不会编造delete_account之类的值。 - 字符串有长度约束:
reason要求至少 10 个字符,防止模型传空字符串或一句话应付。 - 提供
dry_run选项:高风险操作前可以先预览影响,这是 Anthropic 推荐的安全模式1。
权限分级:不是所有操作都应该直接执行
Agent 工具调用中最危险的情况是:模型有权限做它不该做的事。一个能删除数据的工具和一个只能查询数据的工具,不应该有相同的执行路径。
权限分级模型
| 风险等级 | 操作类型 | 示例 | 执行策略 |
|---|---|---|---|
| L0 - 只读 | 查询、搜索、获取 | user_search、get_order_detail | 直接执行,无需确认 |
| L1 - 低风险写入 | 创建草稿、添加备注 | create_draft、add_comment | 直接执行,记录审计日志 |
| L2 - 中风险写入 | 修改状态、发送通知 | update_order_status、send_email | 执行前展示预览,需用户确认 |
| L3 - 高风险操作 | 删除数据、修改权限、支付 | delete_account、grant_admin、refund_payment | 必须人工审批,工具只生成操作计划 |
| L4 - 不可逆操作 | 永久删除、批量变更 | purge_data、batch_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 能否自我纠正。一个返回 {"error": "failed"} 的工具和返回结构化错误的工具,在 Agent 循环中的表现完全不同。
错误响应的层次
| 层次 | 内容 | 作用 |
|---|---|---|
| 状态标识 | status: "error" | 让模型知道这不是成功结果 |
| 错误类型 | error_type: "validation_error" | 让模型理解错误类别 |
| 错误详情 | detail: "user_id 格式无效,需要 UUID" | 让模型知道具体哪个字段有问题 |
| 修复建议 | suggestion: "使用 user_search 工具先获取有效的 user_id" | 让模型知道如何修正 |
| 可重试标识 | retryable: false | 让模型决定是否重试 |
| 关联 ID | trace_id: "abc123" | 用于后续排查 |
Nordic APIs 在 API 错误消息设计指南中提出,面向 Agent 的错误响应应该是「语义化的、带恢复路径的结构化数据」5。freeCodeCamp 的 API 设计指南推荐采用 RFC 7807 Problem Details 格式,用 type、title、status、detail、error_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_id | string | 全局追踪 ID,贯穿整个 Agent 会话 |
tool_call_id | string | 本次工具调用的唯一 ID |
tool_name | string | 工具名称 |
caller | object | 调用者信息(Agent ID、会话 ID、模型版本) |
parameters | object | 调用参数摘要(敏感字段脱敏) |
risk_level | enum | 操作风险等级 |
result_status | enum | 执行结果:success / error / cancelled / requires_approval |
result_summary | string | 结果摘要(不含完整返回值) |
duration_ms | number | 执行耗时 |
timestamp | ISO 8601 | 调用时间 |
human_approved | boolean | 是否经过人工审批 |
approval_by | string? | 审批人(如经过人工审批) |
代码对比:审计日志
// ❌ 坏:只记录工具名和参数,无法追溯
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、个人信息)必须脱敏。
工具调用的执行流程
将前面讨论的参数校验、权限检查、错误处理和审计日志整合在一起,一个完整的工具执行流程如下:
设计清单:15 项检查
以下是工具调用设计的完整检查清单。每个工具在上线前都应该逐项核对。
工具描述
- 描述明确说明工具用途:包含「做什么」和「什么时候用」
- 说明不适用的场景:指出容易混淆的相邻工具及其区别
- 工具命名遵循命名空间规范:按「服务_资源_操作」格式,如
user_search、email_send - 描述中说明返回结构:让模型知道调用后能拿到什么
参数 Schema
- 每个参数有明确的
type:不使用any或空 schema - 枚举值用
enum约束:不允许模型自由发挥的字段必须枚举 - 必填项正确声明:
required数组包含所有不可缺少的参数 - 默认值明确标注:
default值与文档描述一致 - 格式约束完整:邮箱用
format: "email",UUID 用format: "uuid",URL 用format: "uri" - 提供
dry_run或预览选项:高风险操作支持先预览再执行
权限与执行
- 风险等级正确标注:L0-L4 分级与实际副作用匹配
- L2 及以上操作需人工确认:执行前展示预览,等待用户确认
- L3/L4 操作拒绝 Agent 直接执行:只生成操作计划,提交审批
错误处理
- 错误响应结构化:包含
status、error_type、detail、suggestion、retryable - 校验错误列出具体字段:指明哪个字段有问题、合法值是什么
- 错误消息可机读:避免纯自然语言描述,使用
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
-
Anthropic Engineering, "Writing Effective Tools for AI Agents", 2025 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
OpenAI, "A Practical Guide to Building Agents", 2025 ↩
-
LangChain, "Agent Evaluation Readiness Checklist", 2026 ↩ ↩2
-
Kevin Tan, "AI Agent Error Handling: 5 Patterns to Catch Silent Failures", 2026 ↩ ↩2
-
Nordic APIs, "Designing API Error Messages for AI Agents", 2025 ↩
-
freeCodeCamp, "How to Design APIs for AI Agents", 2026 ↩