跳到内容

第 13 卷 / 第 12 章 / 第 3 课

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,恢复执行时还需还原已持久化的计数和预算预留。

python
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 就不会失控:还要限制工具、权限、费用、数据和副作用。

练习 ​

  1. 为“创建工单”设计 strict input schema、权限和 idempotency contract。
  2. 画出工具调用从 proposed 到 validated、authorized、approved、executed 的状态迁移。
  3. 为 timeout with unknown outcome 写恢复流程。
  4. 区分一项任务中的对话历史、工作状态和长期记忆。
  5. 构造五条会暴露重复调用或权限绕过的 Agent 测试。

小结 ​

Agent 的核心不是让模型获得更多自主权,而是把不确定的动作建议放进受控执行循环。Typed schema、逐调用授权、显式状态、幂等、预算和轨迹评估共同限定风险;宿主程序始终对实际副作用负责。

下一课定义这条执行循环的任务、证据、轨迹、统计单位与发布门槛。

Built with VitePress | Software Systems Atlas