Skip to main content

BLOGE 0.9.8-RC1 在线版 · 事实校验 2026-09-15 · English

第 17 章 —— 把非确定性 Agent 放进确定性生命周期

本章承诺: 你会让客服 Agent 只使用白名单只读工具,把轮次、工具并发和退出原因变成可观察事实,并用确定性门禁阻止不完整回答把工单误标为已解决。

学习目标

学完本章,你能够:

  1. 区分 Session 轮次、工单状态迁移和 Agent 模型轮次。
  2. 把 tool list 当成 capability allowlist,而不是提示词建议。
  3. 区分 finishReason、loop exit reason 和业务“已解决”。
  4. 正确处理正常停止、工具失败、max_turns 和可恢复 checkpoint。
  5. 在 Agent 之后放置确定性验证与人工交接门禁。

这一章只增加一个变量

客服故事已经有稳定的 ticketId、多轮 Session 和工单状态机。本章不再发明第四套生命周期;只在 triaging 阶段内加入一个非确定性的模型—工具循环。

Session round:客户与系统交流了几轮
Ticket state:open → triaging → assigned / escalated → resolved
Agent turn:模型响应 → 工具调用 → 观察 → 下一次模型响应

三种计数可以同时变化,但不能用一个“轮次”概括。

开场:一句“已经解决”能关闭工单吗

客户说:“上次退款还没到账。”Agent 搜索知识库后回答:“退款通常需要 3—5 个工作日,问题已经解决。”

先判断:此时 ticket.status 应该变成 resolved 吗?

不能。模型文本只是一项候选解释。系统仍不知道具体退款是否存在、是否超过 SLA、是否需要人工查账,更没有发生任何退款 effect。

图 17-1:Agent 循环位于工单生命周期内部,不拥有最终状态

最小客服 Agent:只开放两个只读工具

agent supportTriage {
model = "replaceable-support-model"
system_prompt = "Clarify the issue. Never claim a refund was issued."
max_turns = 4
max_tool_concurrency = 2
memory = sliding_window(6)

tool searchKnowledgeBase : KBSearchOperator {
description = "Search approved support articles"
input { query = tool_args.query }
}

tool readTicket : ReadTicketOperator {
description = "Read the current ticket snapshot"
input { ticketId = ctx.ticketId }
}

exit_condition = finish_reason == "stop"
}

注意工具参数的两个来源:query 由模型产生;ticketId 从外层受控上下文注入,模型不能换成别人的工单。

这里没有 refundcloseTicketchangeSla。提示词中的“不要退款”是行为指引,tool list 中不存在退款 capability 才是执行边界。

先观察 RC1 的真实循环语义

运行编译器和 loop 聚焦测试:

cd submodule/bloge
mvn -pl bloge-agent-ext \
-Dtest=AgentLoopOperatorTest,AgentDslCompileTest test

2026-09-14 在 commit cc38fbe5 上观察到:

AgentLoopOperatorTest Tests run: 6, Failures: 0
AgentDslCompileTest Tests run: 5, Failures: 0
Tests run: 11, Failures: 0, Errors: 0, Skipped: 0

这 11 项覆盖直接 stop、工具调用后继续、多工具并发上限、工具失败回注、max_turns 异常、DSL schema 推导和嵌入式 operator。

机制:一次 turn 里发生什么

AgentLoopOperator 的同步路径是:

组装 memory
→ 调用 llmChat
→ 无 tool call 或 finish_reason=stop:返回 AgentOutput
→ 有 tool call:只在声明的 toolBindings 中派发
→ 把 tool result 追加到 memory
→ 检查 exit_condition
→ 下一 turn 或退出

图 17-2:模型只能从已编译 toolBindings 选择能力

ToolDispatcher 按工具名查 AgentToolRef,后者指向编译时生成的子图和 operator。一个未声明的工具名没有可派发绑定。宿主仍需保证 registry 里的 operator 身份和权限正确;白名单不能修复一个本身越权的 operator。

四个退出结果必须分开读

运行结果RC1 行为下游应如何处理
模型 stop,无工具调用返回 AgentOutput仍需业务门禁
exit_condition 为真返回 AgentOutput;listener 记录 exit reason仍需业务门禁
工具失败错误变成 tool message,loop 可继续检查回答是否降级、是否需人工
达到 max_turnsMaxTurnsExceededException,携带 AgentCheckpointAgent 节点失败;保存/转人工,不得当最终输出

这里纠正一个常见误读:RC1 达到 max_turns不会把部分文本作为正常 AgentOutput 交给下游。部分进度存在异常携带的 checkpoint 中,包括 conversation、turn count 和最近 tool results。

正常返回的 AgentOutput.finishReason 是模型最后一轮给出的 reason;exit_condition 是 listener 的 loop-exit reason。两者都不是业务状态 resolved

预算:真正受控的和还没受控的

max_turns 给模型调用次数设硬上限,max_tool_concurrency 限制单轮工具并发。sliding_window(6) 只控制每轮发送多少历史,不是总 token/费用预算。

RC1 的 loop 会累计 token 并发给 listener,但本章源码范围内没有“超过 total tokens 自动停止”的硬门禁。若业务要求费用上限,宿主必须在 provider、operator/interceptor 或外层控制面建立可拒绝的预算,并把拒绝映射为明确状态。

图 17-3:轮次、工具并发、上下文窗口与总预算不是同一个旋钮

确定性下游门禁:回答可以变化,准入规则不能漂

Agent 正常返回后,ValidateResolutionOperator 只读取结构化事实:

ResolutionDecision validate(AgentOutput out, TicketSnapshot ticket) {
if (!"stop".equalsIgnoreCase(out.finishReason())) return HANDOFF;
if (out.content() == null || out.content().isBlank()) return HANDOFF;
if (!ticket.requiredFactsComplete()) return ASK_CUSTOMER;
if (ticket.refundOrSlaDispute()) return HUMAN_REVIEW;
return PROPOSE_RESOLUTION;
}

这段门禁不判断回答“写得像不像人”,只判断关闭工单所需事实是否完整。PROPOSE_RESOLUTION 仍是提议;状态机接收明确事件后才迁移到 resolved

图 17-4:AgentOutput 经过确定性门禁后才能提出状态事件

稳定的责任链是:

Agent 产生候选回答
→ deterministic gate 检查结构事实
→ state machine 接收明确事件
→ 人工或系统 owner 承担最终状态

单因素失败实验:把 max_turns 从 4 改成 2

保持模型序列和工具响应不变:第 1 轮请求知识库,第 2 轮再次请求工具,没有 stopAgentLoopOperatorTest.throwsWhenMaxTurnsAreExhausted 断言:

exception = MaxTurnsExceededException
maxTurns = 2
checkpoint.turnCount = 2

恢复不是把 checkpoint 中最后一句话直接展示为“答案”。先保存 checkpoint 和 exit reason,把工单保持 triaging,再由策略决定重新运行、请求客户补充或转人工。

另一个失败:工具报错但 loop 给出流畅回答

RC1 会把工具异常序列化为 tool message,让模型有机会解释或降级。测试中的 failing tool 抛出 boom,下一轮返回 Handled fallback.

技术上 loop 成功返回,不代表业务事实已获得。确定性门禁必须知道 required fact 是否来自成功工具结果;一句流畅的 fallback 不能把“查不到退款记录”变成“退款正常”。

现实映射:急诊分诊助手

急诊助手可以查询患者登记信息和分诊手册,但不能因为文本模型说“可以回家”就完成出院。模型建议后,规则门禁检查生命体征和必填检查,医生承担最终状态迁移。

客服系统急诊系统
只读工单/知识库工具患者资料/指南查询
max_turns问诊步骤上限
部分 checkpoint未完成的问诊记录
resolution gate必填检查与风险门禁
人工接管医生决策

映射的核心不是“都用了 AI”,而是非确定性建议被包在确定性责任边界里。

轮到你:为一个 Agent 画权限与退出卡

选择你系统里一个候选 Agent:

  1. 列出最多三个只读工具和零个默认写工具。
  2. 给每个参数标注模型提供或宿主注入。
  3. 定义 max_turns、单轮工具并发和真正的总预算 owner。
  4. 写出 stop、tool failure、max turns 三条结果路径。
  5. 设计一个只依赖结构化事实的 downstream gate。

产物是一张权限边界图和一张退出矩阵。停止条件是:任何自然语言输出都不能绕过门禁直接触发高风险 effect 或终态。

本章证明了什么,没有证明什么

RC1 聚焦测试证明:声明的 Agent DSL 可编译为嵌入式 operator;模型可调用白名单工具;并发上限被执行;工具失败可回注;耗尽轮次会抛出带 checkpoint 的异常。它没有证明模型回答正确、prompt 能构成权限边界、总 token 预算已受控,也没有证明工单可自动关闭。

参考字段、Builder、memory、streaming 和 listener 目录保留在附录 E。下一章先进入第 18 章——测试你的 Graph,建立测试层次;Arc 6 再把业务 verdict、trust 和 capability 分开。

实验验收卡

  • 预期与观察: 受限 Agent 以 finish reason、turn budget 和确定性门禁交付。
  • 失败与恢复: 把 max_turns 改成 2 或让工具报错;恢复为明确 partial result。
  • 证明边界: 证明 loop 和权限受控,不证明模型回答正确或获批。
  • 练习合同: 一张工单;只改 allowlist 或预算;交付退出卡;每种退出都有下游动作即停止。

精确事实入口

Coding Agent: Open the versioned task guide.