Flink Agents 与 AI 流式应用 —— 第 6 章 决策与推理控制
章节定位:第二篇的压舱石。Agent 会思考之后,“想太多"比"想不到"更危险——无限循环烧钱、自相矛盾的结论、格式错乱的动作指令,都是生产事故的种子。本章贯彻本系列主线"防失控先于智能”:ReAct 循环的工程化拆解、最大步数/循环检测/超时三道闸、系统提示词的生产写法、结构化决策输出与解析兜底、多步推理调试,实战电池告警多步诊断链。
版本基线:Apache Flink 2.3.0 + Flink Agents 0.3.x(JDK 17)
章节导读
- 6.1 ReAct 循环:推理与行动的节拍器
- 6.2 防失控三道闸:最大步数、循环检测与超时
- 6.3 系统提示词工程:生产级写法
- 6.4 结构化决策输出与解析兜底
- 6.5 多步推理调试:看清 Agent 在想什么
- 6.6 实战:电池告警多步诊断链
- 6.7 本章小结与下章预告
6.1 ReAct 循环:推理与行动的节拍器
6.1.1 循环解剖
┌──────────────────────────────────────────────────────┐ │ ReAct 单事件决策循环 │ │ │ │ InputEvent(告警) │ │ │ │ │ ▼ │ │ ┌──────┐ Thought: 先看故障码释义 │ │ │ LLM │ Action: read dtc://P0AA6 │ │ └──┬───┘ │ │ │ ToolRequestEvent │ │ ▼ │ │ ┌──────┐ Observation: "高压电池单体电压过高" │ │ │ Tool │ │ │ └──┬───┘ │ │ │ ToolResponseEvent 回填 │ │ ▼ │ │ ┌──────┐ Thought: 查该车历史工单 │ │ │ LLM │ Action: query_history(vin) ← 第2轮 │ │ └──┬───┘ │ │ ... │ │ ▼ │ │ ┌──────┐ Thought: 证据充分,出结论 │ │ │ LLM │ Final: OutputEvent(诊断+动作) │ │ └──────┘ │ │ │ │ Thought → Action → Observation → Thought → ... │ └──────────────────────────────────────────────────────┘6.1.2 Workflow Agent vs ReAct Agent 选型
| 维度 | Workflow Agent | ReAct Agent |
|---|---|---|
| 流程 | 预先编排,确定性 | LLM 现场决策,灵活 |
| 成本 | 可预估(固定调用次数) | 不定(1~N 轮) |
| 适用 | 流程明确的 SOP | 开放诊断、多路径排查 |
| 失控面 | 小 | 大,必须配三道闸(6.2) |
选型经验: 能画成流程图的 → Workflow(便宜、稳) 画不出来的 → ReAct(但必须上闸) 混合: 主干 Workflow,局部节点内嵌 ReAct(第 10、16 章实战)6.1.3 循环中的事件流
每轮循环 = 一对事件: ChatRequestEvent → LLM 收到上下文+工具清单 ChatResponseEvent → LLM 输出 Thought + 工具调用意图/最终答案 调试视角(第 14 章 Trace 的素材): 一个 InputEvent 触发的事件序列,就是一个决策 Trace: Input → ChatReq → ChatResp → ToolReq → ToolResp → ChatReq → ChatResp → Output6.2 防失控三道闸:最大步数、循环检测与超时
6.2.1 为什么 LLM 会失控
典型失控场景(全部来自真实事故): ① 工具返回语义化错误"缩小时间范围重试" → LLM 重试 → 又错 → 再重试…… 30 轮后预算烧光 ② 两个工具互相"踢皮球": LLM 认为该查 A,A 结果让它查 B,B 结果让它查 A…… ③ 永远差一步:"再确认一下更稳妥" → 连调 5 个只读工具,最后结论和第 2 步一样 (成本 ×5,价值 +0)6.2.2 三道闸配置
ReActAgentConfigguardConfig=ReActAgentConfig.builder()// 闸一: 硬步数上限——单事件最多 N 轮 LLM 调用.maxSteps(8)// 闸二: 循环检测——同一(工具+参数摘要)出现 2 次即截断.loopDetection(LoopPolicy.SAME_CALL_MAX_2)// 闸三: 单事件决策总预算——60 秒 / 15k tokens,先到先截.maxDecisionTime(Duration.ofSeconds(60)).maxTokensPerEvent(15_000).build();┌────────────┬──────────────────┬─────────────────────────┐ │ 闸 │ 防什么 │ 触发后行为 │ ├────────────┼──────────────────┼─────────────────────────┤ │ 最大步数 │ 无限循环 │ 强制出结论(降级版) │ │ 循环检测 │ 原地打转 │ 截断+提示"你已重复调用" │ │ 时间/token │ 慢决策、烧钱 │ 超时出结论或转人工 │ └────────────┴──────────────────┴─────────────────────────┘6.2.3 触发后的降级策略
闸触发 ≠ 事件丢弃。生产规范: 步数耗尽 → 用已有 Observation 强制生成结论, confidence 自动封顶 0.5,标记 "degraded": true 循环截断 → 回喂 LLM:"你已重复调用 X,请基于现有信息给结论" 超时 → 转人工队列(approval.required 事件,第 15 章) 关键: 降级结论必须带标记,下游工单系统据此降优先级、 加人工复核——不完整的推理不能当完整结论用。6.3 系统提示词工程:生产级写法
6.3.1 结构模板
┌─ 系统提示词五段式 ──────────────────────────────────┐ │ ① 角色与目标: 一句话,不堆形容词 │ │ ② 决策原则: 3~5 条硬规则,优先级排序 │ │ ③ 工具使用纪律: 何时用哪个,何时禁止用 │ │ ④ 输出契约: JSON schema + 字段含义 + 置信度校准 │ │ ⑤ 禁区清单: 明确禁止的行为(幻觉防控前置) │ └──────────────────────────────────────────────────┘6.3.2 电池诊断 Agent 系统提示词实例
你是车联网电池告警诊断专家。目标:基于告警与可查证据, 给出根因诊断、处置动作与置信度。 决策原则(按优先级): 1. 只依据工具返回的证据与告警原文下结论,禁止推测未见数据。 2. 历史同码工单复检 OK 且本次指标未恶化 → 倾向降级处置。 3. 涉及热失控特征(温升速率>1℃/min、绝缘阻值骤降) → 无条件 P0 + 转人工,不等待更多证据。 工具使用纪律: - 先读 dtc:// 资源确认故障码释义,再做任何检索。 - query_history 时间窗默认 6 个月,不足再扩。 - 无 approval_id 禁止调用 send_remote_diagnose。 输出契约(JSON,无多余文本): {"diagnosis":"根因,50字内","action":"P0|P1|P2|P3+动作描述", "confidence":0.0~1.0,"refs":["证据URI"], "degraded":false} 置信度校准:证据充分且多源一致≥0.8;单一来源0.6~0.8; 工具部分失败≤0.5 且 degraded=true。 禁区: - 禁止编造工单号、案例编号、故障码释义。 - 禁止在 refs 中引用未实际调用过的工具结果。6.3.3 提示词版本管理
提示词 = 代码,生产纪律: ① 版本命名: prompt.电池诊断_v3(与前作模型命名规范对齐) ② 纳入 Git 评审: 改提示词 = 改行为,必须走 MR ③ 灰度: 新提示词先发影子流量(第 14 章 AB 评估) ④ 环境注入: 禁止硬编码进 Agent 类, 从配置中心读 —— 改提示词不用重发作业6.4 结构化决策输出与解析兜底
6.4.1 解析三层兜底
publicDiagnosisResultparseDecision(Stringraw,ReActContextctx){// 第一层: 严格 JSON 解析try{returnmapper.readValue(raw,DiagnosisResult.class);}catch(Exceptione1){// 第二层: 提取混杂文本中的 JSON 块(```json ... ```)Optional<String>extracted=JsonExtractor.findJsonBlock(raw);if(extracted.isPresent()){try{returnmapper.readValue(extracted.get(),DiagnosisResult.class);}catch(Exceptionignored){}}// 第三层: 让 LLM 自我修复(仅一次机会,防循环)if(ctx.retryCount()==0){returnctx.askLlmToFix("上次输出不是合法JSON。请仅输出符合契约的JSON:\n"+raw);}// 兜底: 不丢弃,转人工returnDiagnosisResult.manualReview(raw,ctx.eventId());}}三层兜底的逻辑: ① 严格解析失败大概率是格式微调(多余逗号、注释) → 提取修复 ② 给 LLM 一次自我修正机会(成本远低于人工) ③ 最终兜底是"转人工"而非"丢弃"——事件永远有出口6.4.2 语义校验:格式对了还不够
JSON 合法 ≠ 结论可用。解析后追加语义校验: □ action 在枚举内(P0~P3),非自由文本 □ confidence 在 [0,1],且 degraded=true 时 ≤0.5 □ refs 每个 URI 都对应本轮真实发生的工具调用 (防幻觉引用——对比 ctx 中 ToolResponseEvent 列表) □ P0 决策必须含热失控特征证据,否则降级为人工复核 校验失败 → 回喂 LLM 修正一次 → 仍失败 → 转人工6.4.3 与前作的结构化输出对比
| 维度 | 前作 ML_PREDICT | 本作 Agent |
|---|---|---|
| 输出控制 | DDL 定义输出列 | 系统提示词契约 + 解析兜底 |
| 校验时机 | SQL 层类型强约束 | 应用层语义校验 |
| 失败出口 | 侧输出流 → Paimon | 转人工事件 → 审批队列 |
| 复杂度 | 单点推理,单层 | 多步推理,每层都可能抖 |
教训延续:任何 AI 输出都必须有"不丢弃"的出口。前作是侧输出流,本作是转人工队列——思想一致,载体不同。
6.5 多步推理调试:看清 Agent 在想什么
6.5.1 决策 Trace 还原
调试核心问题: "它为什么这么决策?" 事件流按 trace_id 串起来,就是完整推理链: [ALM001] 15:30:01 InputEvent 告警:压差187mV [ALM001] 15:30:02 ChatReq step1, prompt 1.2k tokens [ALM001] 15:30:04 ChatResp Thought:查故障码; Action:dtc://P0AA6 [ALM001] 15:30:04 ToolReq read dtc://P0AA6 [ALM001] 15:30:04 ToolResp "单体电压过高"(120ms) [ALM001] 15:30:05 ChatReq step2, prompt 1.6k tokens [ALM001] 15:30:07 ChatResp Thought:查历史; Action:query_history [ALM001] 15:30:08 ToolResp 3月前同码工单,复检OK [ALM001] 15:30:09 ChatResp Final:P2工单,confidence 0.82 [ALM001] 15:30:09 OutputEvent 决策完成,耗时8s,3步6.5.2 高频问题排查清单
| 症状 | 大概率原因 | 修法 |
|---|---|---|
| 步数经常打满 | 工具描述差,LLM 试错 | 重写描述(4.1.1 四要素) |
| 同一工具重复调 | 循环检测阈值太松 / 结果没进上下文 | 检查 ToolResp 回填;收紧阈值 |
| confidence 虚高 | 提示词缺校准规则 | 补 6.3.2 校准段 |
| refs 引用未见案例 | 幻觉引用 | 6.4.2 refs 真实性校验 |
| 偶发超长延迟 | 某工具 p99 高 | 工具线程池 + 超时复查(4.4) |
6.5.3 本地重放:最快的调试手段
生产事件 → 落 Paimon(第 12 章审计管道) → 本地导出单事件 → 本地起 Agent,关闭真实动作类工具(替换为 Mock) → 重放决策链,单步观察每轮 Thought 价值: 不碰生产、不烧线上预算、可反复单步。6.6 实战:电池告警多步诊断链
6.6.1 链路设计
输入: Kafka tsp-alert(压差/温升/绝缘三类告警) Workflow 主干(确定性) + 内嵌 ReAct 节点(灵活性): ① 特征预检(代码,非LLM): 温升>1℃/min → 直通 P0+人工 ② ReAct 诊断节点: 故障码→历史→案例,最多8步 ③ 结论校验节点(代码): 6.4.2 语义校验 ④ 分流: confidence≥0.8 → 自动建单 0.5~0.8 → 建单+人工复核标记 <0.5/degraded → 转人工队列6.6.2 关键配置与代码
// ReAct 诊断节点:三道闸 + 提示词版本化ReActAgentbatteryAgent=ReActAgent.builder().model(model).systemPrompt(promptCenter.get("prompt.电池诊断_v3")).tools(List.of(dtcResource,historyTool,casesTool,ticketTool)).guard(ReActAgentConfig.builder().maxSteps(8).loopDetection(LoopPolicy.SAME_CALL_MAX_2).maxDecisionTime(Duration.ofSeconds(60)).maxTokensPerEvent(15_000).build()).outputParser(this::parseDecision)// 6.4.1 三层兜底.semanticValidator(this::validateRefs)// 6.4.2 防幻觉引用.build();// 主干分流@Action(listenEventTypes={"diagnosis.completed"})publicEventroute(ActionContextctx,DiagnosisResultr){if(r.degraded||r.confidence<0.5)returnnewApprovalRequiredEvent(r);// 转人工if(r.confidence<0.8)returnticketTool.call(r.withReviewFlag());// 建单+复核returnticketTool.call(r);// 自动建单}6.6.3 上线验收指标
试运行 2 周(影子模式),达标再切真实建单: □ 平均决策步数 ≤ 3.5(超过说明工具描述有问题) □ 步数打满率 < 2% □ 转人工率 10%~25%(过低=校验失效,过高=提示词/工具不行) □ refs 真实性校验通过率 = 100% □ 单事件平均 LLM 成本 ≤ 预算红线(第 14 章成本归因)6.7 本章小结与下章预告
本章小结
┌──────────────────────────────────────────────────────────────┐ │ 第 6 章 要点回顾 │ └──────────────────────────────────────────────────────────────┘ ✓ ReAct 循环: Thought→Action→Observation 节拍; 每轮一对 Chat 事件,串起来就是决策 Trace ✓ 选型: 能画成流程图的用 Workflow,画不出来的用 ReAct; 生产推荐主干 Workflow + 局部 ReAct ✓ 三道闸: 最大步数/循环检测/时间token预算; 触发后降级出结论,degraded 标记必须传导下游 ✓ 提示词五段式: 角色/原则/工具纪律/输出契约/禁区; 版本化管理,配置中心注入,灰度先行 ✓ 输出兜底: 严格解析→JSON块提取→LLM自我修复→转人工; 语义校验防"格式对的假话"(refs 真实性是重点) ✓ 实战: 预检直通 P0 + ReAct 8步诊断 + 三档分流, 影子模式达标后切真下章预告
第 7 章 Agent 状态管理
单事件会思考还不够——跨事件的诊断连续性才是生产刚需。本章深入 Flink 状态体系:Keyed State 按 VIN 隔离会话、ForSt 状态后端配置、TTL 48h 防膨胀、Checkpoint 恢复语义,实战"渐进式诊断 Agent"——让 Agent 记住三天前那辆车的排查进度。
官方参考资料
- Flink Agents 0.3 Documentation(ReAct Agent 章节)
- ReAct: Synergizing Reasoning and Acting in Language Models
- Flink 2.3 Stable Documentation(State & Checkpointing)
本章实战代码清单
序号 内容 说明 1 ReAct 循环事件流解剖图 6.1.1 2 三道闸完整配置 6.2.2 3 系统提示词五段式模板 + 电池诊断实例 6.3 4 输出解析三层兜底实现 6.4.1 5 语义校验清单(防幻觉引用) 6.4.2 6 决策 Trace 还原与排查清单 6.5 7 电池告警多步诊断链(Workflow+ReAct 混合) 6.6