12.3 工具与 Agent:由宿主程序控制的执行状态机
以“找出延迟最高的服务并建修复工单”为例:监控查询可能成功,建单请求却超时。如果服务端早已建好工单,直接重试就会再建一张。模型把过程讲得再顺,也不能确定哪一步已经完成。
Agent 的执行循环由宿主控制:模型提出动作,应用验证和授权,执行器完成调用,再把有限结果交回模型。副作用周围的状态迁移与权限判断由确定性代码负责。
本课目标
- 把 Agent 实现为有界状态机;
- 设计 typed tool schema、授权与参数验证;
- 处理幂等、重试、超时和部分失败;
- 区分对话历史、工作状态与长期记忆;
- 评估工具选择、参数、结果和副作用。
1. 模型只能提出调用
Tool calling response 通常包含 tool name 和 arguments。它们都是不可信输入:模型可能编造工具名、漏字段、给错类型,或把文档中的 prompt injection 复制进参数。
工具注册表应定义:
- 稳定的 tool name、version 和用途;
- JSON Schema 或等价 typed input/output;
- required fields、枚举、长度、数值和格式限制;
- read-only 或 side-effect classification;
- timeout、rate limit、cost 和 data sensitivity;
- 调用者所需 permission;
- retry 与 idempotency contract。
对未知字段默认拒绝,避免模型把未审计参数偷偷传到底层 SDK。Tool output 同样不可信,要限制大小、转义控制内容,并把 data 与 instructions 分开。
2. 权限不会因模型建议而扩大
用户能让 Agent 做什么,取决于当前 principal 和业务政策,不取决于 prompt 写得多坚定。宿主程序应在每次调用前检查:
- 当前用户/服务身份;
- tenant、resource scope 与 operation;
- 数据分类和目的限制;
- 是否需要二次确认或审批;
- 是否超过预算、速率或时间窗口。
对外部可见或不可逆动作,按已有授权核对具体目标与影响。需要额外审批时,再展示解析后的操作供确认;审批记录应绑定动作和参数版本,不能让模型后续改参数后继续沿用旧授权。
不要把数据库超级用户凭据或生产部署 token 交给模型上下文。由受限执行器持有凭据,并根据已授权的 tool call 使用。
3. 用状态机表达循环
下面是 Python 形式的编排片段,依赖应用提供状态存储、model、policy、approval、executor、sanitizer 和完成 verifier。record_model_reply 只记回复,不累加步数;循环显式计数。此处展示新 run,恢复执行时还需还原已持久化的计数和预算预留。
def run_agent(task, principal, registry, limits):
state = new_run(task=task, principal=principal, limits=limits)
tool_calls = 0
while state.steps < limits.max_steps:
if state.deadline_exceeded() or state.cost_exceeded():
return state.stop("budget_exhausted")
state.steps += 1
reply = model.respond(
messages=state.visible_messages(),
tools=registry.schemas_for(principal),
)
state.record_model_reply(reply)
if reply.final_answer is not None:
if not verifier.accepts(state, reply.final_answer):
return state.stop("completion_unverified")
return state.finish(reply.final_answer)
calls = registry.parse_and_validate(reply.tool_calls)
if not calls:
return state.stop("invalid_or_empty_action")
for call in dependency_order(calls):
if state.deadline_exceeded() or state.cost_exceeded():
return state.stop("budget_exhausted")
if tool_calls >= limits.max_tool_calls:
return state.stop("max_tool_calls_reached")
tool_calls += 1
policy.authorize(principal, call)
approval.require_if_needed(call, principal)
result = executor.execute(
call,
timeout=min(registry.timeout(call.name), state.remaining_seconds()),
idempotency_key=state.idempotency_key(call),
)
state.record_tool_result(call, sanitize(result))
return state.stop("max_steps_reached")真实实现还要捕获模型、验证器和工具异常,并把它们归入可恢复、不可恢复和需人工处理的状态。不要无限把错误文本喂回模型重试。
执行前检查余额,不能预留未知的未来开销。严格费用上限还需原子预留、单次限额与事后结算;模型调用也要有 deadline。取消本地等待并不证明远端副作用已回滚。
4. 规划是策略,不是权限机制
有些任务适合 model 一步一调用;有些任务需要先产生 plan,再逐步执行;还有些任务可用固定 workflow,只让模型填写局部参数。越稳定、越高风险的流程,越适合确定性 orchestration。
公开展示模型的完整内部“Thought”并不是 Agent 必需条件。系统可以保留结构化 plan、decision summary、tool trace 和可验证依据,而不依赖自由文本 chain-of-thought 作为审计记录。
并行工具调用只适合相互独立的动作。若第二个调用依赖第一个输出,必须建立显式依赖图;否则模型可能使用尚不存在的 ID 或基于失败结果继续执行。
5. Side Effects 需要幂等和补偿
网络超时不代表服务端没有执行。客户端若直接重试“创建工单”,就可能重复产生副作用。
优先设计:
- 由 run/action identity 派生 idempotency key;
- 服务端保存 key 与操作结果;
- 重试返回原结果,而不是再次执行;
- 为不可幂等接口先查状态,或设计业务去重键;
- 多步骤事务定义补偿动作和人工接管点;
- 把“请求已接受”和“业务已完成”分开。
分布式系统通常无法靠一次 HTTP 调用获得普遍的 exactly-once guarantee。应用要在业务语义上处理 at-least-once delivery 和重复结果。
同一动作 ID 要跨重试和进程重启保留。服务端应原子地把幂等键绑定到参数 hash 与结果,同键不同参数要拒绝。两次有意执行的动作也可能参数相同,因此只用参数 hash 不能表示动作身份;键的保留期也限制去重保证。
6. 为每条路径设置预算
至少设置:
- max model steps;
- max tool calls 与单工具调用次数;
- token、money 和 wall-clock budget;
- 单次/总输出大小;
- query/data scan limits;
- 重试次数和 backoff;
- 可访问的工具 allowlist。
“模型说完成”只是一个候选终止信号。其他条件包括目标已由 verifier 满足、无合法动作、重复状态、预算耗尽、用户撤销和不可恢复错误。
检测重复调用时比较规范化 arguments 与相关 state,不要只比较自然语言响应。
7. 三种状态不要混在一起
对话历史
用于理解当前交互。历史过长会带来 token 成本和旧指令污染;summary 是有损压缩,关键约束应保存为结构化字段。
工作状态
包括 task graph、已完成动作、artifact IDs、approvals、预算和错误。它应由应用持久化并校验,不依赖模型“记得”。
长期记忆
跨会话保存用户偏好或事实时,需要 consent、来源、有效期、可更正/删除和访问控制。模型生成的总结不能未经验证就变成长期事实。
8. Error Recovery 要可解释
对失败建立有限策略:
- schema error:返回具体字段错误,最多修正若干次;
- auth denied:终止该动作,不能换工具绕过;
- rate limit/transient error:按 policy backoff;
- timeout/unknown outcome:先查询操作状态,再决定重试;
- partial success:记录已完成子步骤,执行补偿或请求人工;
- unsafe output:隔离内容,不继续传给高权限工具。
每次恢复都要记录触发原因、策略、调用版本和最终状态。Agent“自己想办法”不是恢复策略。
9. 评估完整执行轨迹
Final answer 正确仍可能伴随多余查询、越权尝试或重复副作用。Agent evaluation 应分层:
- tool selection accuracy;
- argument schema/semantic correctness;
- authorization decision;
- task completion;
- side-effect correctness 与 duplication rate;
- recovery success;
- steps、latency、tokens 和 cost;
- unsafe action attempt rate;
- trace 是否足以复盘。
测试集应包含 tool unavailable、malformed output、timeout、权限不足、prompt injection、同名实体和未知执行结果。对真实副作用使用 sandbox/fake tools 或隔离环境。
常见误区
- Agent 等于会思考的 LLM:产品行为由模型、工具、状态机和政策共同决定。
- Tool schema 能保证安全:它只解决部分结构验证,不能替代授权和业务约束。
- 超时后重试即可:原操作可能已经成功,需要幂等键或状态查询。
- 把全部历史塞回去就是记忆:那会增加成本、污染和隐私风险。
- 设置
max_steps就不会失控:还要限制工具、权限、费用、数据和副作用。
练习
- 为“创建工单”设计 strict input schema、权限和 idempotency contract。
- 画出工具调用从 proposed 到 validated、authorized、approved、executed 的状态迁移。
- 为 timeout with unknown outcome 写恢复流程。
- 区分一项任务中的对话历史、工作状态和长期记忆。
- 构造五条会暴露重复调用或权限绕过的 Agent 测试。
小结
Agent 的核心不是让模型获得更多自主权,而是把不确定的动作建议放进受控执行循环。Typed schema、逐调用授权、显式状态、幂等、预算和轨迹评估共同限定风险;宿主程序始终对实际副作用负责。
下一课定义这条执行循环的任务、证据、轨迹、统计单位与发布门槛。