难度
进阶
从工具 schema 设计、调用循环、失败处理到评测闭环,建立一套可落地的 Agent 与工具调用实践框架。
难度
进阶
阅读时长
约 135 分钟
更新日期
2026/03/24
主题
Agent / 工具调用 / 结构化输出 / Prompting / 评测
读完这篇教程后,你应该能:
如果你已经读过 Prompt Engineering 系统指南、ReAct:Synergizing Reasoning and Acting in Language Models 和 Toolformer:Language Models Can Teach Themselves to Use Tools,这篇文章会把相关概念真正落到工程实践。
现在很多产品只要接了一个外部 API,就会自称“Agent”。但工程上最好先分清三层能力:
这三者最大的差别,不是“听起来谁更高级”,而是:
很多团队真正需要的,其实是“带工具调用的工作流”,而不是一个完全开放的 Agent。默认原则应该是:
能用固定流程解决,就不要过早引入开放式循环。
从系统视角看,一个最小 Agent 往往由下面几部分组成:
| 组件 | 作用 | 常见问题 |
|---|---|---|
| 模型 | 负责理解任务、选择动作、整合结果 | 乱调工具、漏掉关键条件 |
| 工具注册表 | 暴露可用工具及 schema | 描述含糊、参数约束过松 |
| 执行器 | 真正调用 API、数据库、检索系统 | 异常未捕获、超时失控 |
| 状态存储 | 保存历史消息、工具结果、临时计划 | 上下文越来越乱,无法收敛 |
| 安全护栏 | 控制权限、确认高风险动作、限制步数 | 写操作被误触发 |
| 评测与日志 | 记录调用轨迹、失败样本、完成率 | 只能凭感觉调 prompt |
很多 Agent 项目失败,不是因为“模型不够聪明”,而是因为后面四项做得太弱。
如果把这些部件连成一张运行图,会更容易看清一个可控 Agent 到底靠什么稳定下来:
<g>
<rect x="36" y="92" width="130" height="66" rx="16" fill="#e8f1ff" stroke="#98b7e1" />
<text x="101" y="130" text-anchor="middle" font-size="18" font-weight="700">用户请求</text>
</g>
<g>
<rect x="204" y="76" width="220" height="98" rx="20" fill="#f8fbf4" stroke="#d5e4c6" />
<text x="314" y="108" text-anchor="middle" font-size="20" font-weight="700">模型决策层</text>
<text x="314" y="132" text-anchor="middle" font-size="13" fill="#4b5563">回复用户 / 选择工具 / 继续循环</text>
<text x="314" y="153" text-anchor="middle" font-size="13" fill="#4b5563">受 system prompt、历史状态约束</text>
</g>
<g>
<rect x="462" y="76" width="176" height="98" rx="20" fill="#fff4dc" stroke="#e2c36f" />
<text x="550" y="108" text-anchor="middle" font-size="20" font-weight="700">工具注册表</text>
<text x="550" y="132" text-anchor="middle" font-size="13" fill="#4b5563">名称 / 描述 / schema</text>
<text x="550" y="153" text-anchor="middle" font-size="13" fill="#4b5563">读写动作显式分离</text>
</g>
<g>
<rect x="676" y="64" width="248" height="122" rx="22" fill="#f9fafb" stroke="#d7e2f1" />
<text x="800" y="96" text-anchor="middle" font-size="20" font-weight="700">执行前护栏</text>
<text x="800" y="120" text-anchor="middle" font-size="13" fill="#4b5563">schema 校验</text>
<text x="800" y="141" text-anchor="middle" font-size="13" fill="#4b5563">高风险写操作确认</text>
<text x="800" y="162" text-anchor="middle" font-size="13" fill="#4b5563">最大步数 / 重复动作检测</text>
</g>
<g>
<rect x="700" y="238" width="198" height="74" rx="18" fill="#fce7ef" stroke="#e2a8bd" />
<text x="799" y="268" text-anchor="middle" font-size="19" font-weight="700">工具执行器</text>
<text x="799" y="291" text-anchor="middle" font-size="13" fill="#4b5563">API / DB / 检索 / 工作流</text>
</g>
<g>
<rect x="420" y="250" width="224" height="62" rx="18" fill="#dff4f0" stroke="#8dc7bd" />
<text x="532" y="277" text-anchor="middle" font-size="18" font-weight="700">Observation 回写</text>
<text x="532" y="298" text-anchor="middle" font-size="13" fill="#4b5563">结构化工具结果进入上下文</text>
</g>
<g>
<rect x="154" y="250" width="216" height="62" rx="18" fill="#ede9fe" stroke="#b7a8ea" />
<text x="262" y="277" text-anchor="middle" font-size="18" font-weight="700">最终回答或转人工</text>
<text x="262" y="298" text-anchor="middle" font-size="13" fill="#4b5563">收敛、失败退出、升级处理</text>
</g>
<line x1="166" y1="125" x2="204" y2="125" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
<line x1="424" y1="125" x2="462" y2="125" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
<line x1="638" y1="125" x2="676" y2="125" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
<line x1="799" y1="186" x2="799" y2="238" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
<line x1="700" y1="275" x2="644" y2="281" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
<path d="M 420 281 C 388 281, 366 226, 366 182" fill="none" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
<path d="M 314 174 L 314 228" fill="none" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
<line x1="314" y1="250" x2="370" y2="281" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
<text x="58" y="356" font-size="13" fill="#64748b">读操作可以更自由探索,写操作必须额外经过确认与审计,这就是“工具调用”与“业务动作”之间的安全边界。</text>
</g>
模型能否稳定调用工具,关键不在模型名字,而在工具定义是否清晰。一个好的工具 schema 至少要满足四件事:
下面用一个客服支持场景举例。假设我们有三个工具:
[
{
"name": "search_help_center",
"description": "查询帮助中心文章,返回最相关的证据片段。",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"top_k": { "type": "integer", "enum": [3, 5] }
},
"required": ["query"]
}
},
{
"name": "check_order_status",
"description": "根据订单号查询订单状态。",
"input_schema": {
"type": "object",
"properties": {
"order_id": { "type": "string" }
},
"required": ["order_id"]
}
},
{
"name": "create_support_ticket",
"description": "在确认需要人工介入时创建工单。",
"input_schema": {
"type": "object",
"properties": {
"order_id": { "type": "string" },
"issue_type": {
"type": "string",
"enum": ["refund", "payment", "delivery", "account", "other"]
},
"summary": { "type": "string" }
},
"required": ["issue_type", "summary"]
}
}
]
这里最重要的设计点,不是格式好看,而是:
search_help_center 只负责找证据check_order_status 只负责读订单状态create_support_ticket 是明确的写动作,应该受额外确认保护如果你把这三件事混在一个“solve_customer_problem”工具里,模型就很难学会稳定决策,日志也会失去可分析性。
工具调用最常见的失败,不是模型不会填参数,而是它总想调工具,或者在不该调的时候调错工具。一个实用的系统提示,最好同时说明:
例如,你可以把策略写成这样:
你是订单支持助手。
决策规则:
1. 如果问题可以仅凭已有上下文回答,不要调用工具。
2. 如果回答需要订单状态,必须先调用 check_order_status。
3. 如果问题涉及规则说明,优先调用 search_help_center 获取证据。
4. 如果缺少订单号,不要猜测,先向用户追问。
5. create_support_ticket 属于高风险写操作,只有在信息充足且用户明确同意后才能调用。
6. 连续两次工具失败后,停止循环并建议人工处理。
这个阶段的重点不是“写得像论文一样复杂”,而是让模型知道哪些动作属于默认动作,哪些属于受限动作。
一个常见误解是,Agent 必须无限自由地“自主思考”。实际上,工程里更稳的做法是给它一个有限循环:
下面是一个足够说明问题的最小伪代码:
import json
MAX_STEPS = 6
def run_agent(user_query: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_query},
]
for step in range(MAX_STEPS):
result = call_model(messages, tools=TOOLS)
if result["type"] == "message":
return result["content"]
if result["type"] == "tool_call":
tool_name = result["name"]
arguments = validate_tool_args(tool_name, result["arguments"])
if needs_confirmation(tool_name, arguments):
return "这个操作需要你确认后才能继续。"
observation = execute_tool(tool_name, arguments)
messages.append(
{
"role": "assistant",
"tool_calls": [result],
}
)
messages.append(
{
"role": "tool",
"name": tool_name,
"content": json.dumps(observation, ensure_ascii=False),
}
)
return "已达到最大处理步数,建议转人工继续。"
这个循环里最关键的不是 for step in range(MAX_STEPS) 这句代码,而是三层控制:
如果缺了这三层,系统就很容易出现“明明只是一个小任务,却无限循环或误操作”的问题。
ReAct 的核心价值,在于把“思考”和“行动”放进同一个闭环里。但产品工程里,不一定要把模型的完整推理链都暴露出来。更实用的做法通常是:
换句话说,ReAct 的工程重点更像是:
| 关注点 | 更推荐的做法 |
|---|---|
| 计划表达 | 一句简短行动意图或结构化状态 |
| 工具结果 | 严格结构化,方便后续消费 |
| 最终回答 | 面向用户重新组织,不直接转储中间轨迹 |
| 排障方式 | 看调用轨迹、步数、参数与 observation,而不是只看最终一句回复 |
真正让 Agent 变稳的,往往不是更长的“思维链”,而是更干净的状态和 observation。
工具调用系统最容易出现的错误,大致可以分成下面几类:
| 失败类型 | 典型表现 | 常见防护 |
|---|---|---|
| 选错工具 | 该检索时去创建工单 | 强化决策规则,增加 few-shot 反例 |
| 参数缺失 | 漏掉订单号、日期、用户 ID | schema 校验 + 缺参追问 |
| 参数格式错 | 日期格式、枚举值不合法 | 解析前校验,失败后让模型修复 |
| 工具超时 | API 卡住,循环停滞 | 超时控制、熔断、降级回复 |
| 幻觉 observation | 模型编造工具结果 | 工具结果只能来自执行器,不允许模型自己写 |
| 写操作误触发 | 未确认就发起退款或建单 | 高风险动作二次确认 |
| 无限循环 | 一直重复检索或重复同一个动作 | 最大步数、重复动作检测 |
很多时候,Agent 的“稳定性”不是靠更强模型自然涌现出来的,而是靠你是否把这些失败路径提前堵住。
在工程里,你最好默认:
例如:
一旦进入写操作,你应该额外补上:
否则用户一句模糊表达,就有可能被系统错误解释成真实业务动作。
Agent 系统如果没有评测,通常会出现一个错觉:
演示时看起来很聪明,线上一跑就到处漏水。
最小可用的评测维度,建议至少覆盖下面这些:
如果你要做自动评测,可以把样本设计成下面这种结构:
{
"input": "我的订单已经扣款,但后台还是显示未支付。",
"expected_tool_path": ["check_order_status", "create_support_ticket"],
"required_args": {
"issue_type": "payment"
},
"success_criteria": [
"先查询订单状态",
"写操作前要求用户确认或已满足预设确认条件",
"最终回复解释清楚下一步"
]
}
评测数据不一定要很多,但必须覆盖真实失败类型。否则你会不停优化看起来“更聪明”的案例,却对高频坏案例没有任何约束。
下面这个判断框架通常很有用:
| 场景 | 更适合什么方案 |
|---|---|
| 固定表单抽取、固定 JSON 输出 | 结构化输出,不一定要 Agent |
| 明确 DAG 的审批或 ETL 流程 | 工作流编排 |
| 需要查多个工具、根据中间结果继续决策 | Agent 或半 Agent |
| 强合规、低容错写操作 | 更强人工审核或传统流程 |
| 强实时、超低延迟要求 | 尽量减少循环,优先固定流程 |
真正成熟的系统往往不是“全站 Agent 化”,而是:
工具过多会显著增加选择难度和歧义。先把最核心的 3 到 5 个工具做稳定,通常比一口气接 20 个工具更有效。
最终答案看起来像是对的,不代表路径是对的。线上事故往往藏在“工具其实调错了,但刚好结果还能圆回来”的轨迹里。
模型可以辅助判断,但权限校验必须在后端做。特别是涉及用户资产、隐私和外部系统写入时,不能只靠提示词约束。
一个会在关键时刻优雅转人工的 Agent,通常比一个永远想自己硬做到底的 Agent 更可靠。
从相近主题继续深入,建立连续学习链路。