业务工具与售后工作流 DAG从「每意图一张图」的弯路到「审批是事件」语言 / Language中文 系列第七章目录 上一章决策层、持久化与闸门硬化项目Agentdemo007 —— 电商智能客服 Agent技术栈Java 17 / LangGraph4j固定工作流子图/ LC4j 工具前向 / HITL L2 三级持久化周期2026-09-12设计 方向纠偏→ 09-15工作流冲刺→ 09-19~20提交制 对账修复验证规模workflow 74 例 HITL 84 例 业务工具 36 例单测全量回归随各 Phase 交付源码github.com/Gavincui123/Agentdemo007前言业务工具层要回答的问题是当对话需要真的办事——查订单、办退款、建工单——怎么把 LLM 的不确定性关进确定性系统的笼子。这一章的主线不是设计展示是弯路和事故清单我先在每意图动态生成 DAG上犯了一次系统性过度设计设计推演阶段自己否决了自己落地后又接连踩到 LangGraph4j 的迭代预算陷阱、一个被用户看成系统说谎的小写订单号、一次管理台必现的 409 对账故障。最后靠两次原则级收口定型——审批是事件、建单走四态幂等门——每一段弯路的尽头都站着一条现在还立着的规矩。一、方向纠偏「每意图动态 DAG」被否决的经过业务工具层的第一份设计per-intent-dag雄心勃勃每个意图动态生成一张 DagSpec 图按需编排节点。这个方向在设计推演阶段就被我自己否决了回滚记录就写在计划文档的开头RoutePlan 只是每请求的结构化路径计划本身无 DAG低风险意图靠现有固定 Order 链加 #135 自跳过也无 DAGDAG 的落点只有一处——高风险操作时在 LangGraph 构建固定工作流子图。否决的理由今天看依然成立动态图的拓扑随 LLM 输出漂移。不可测——每个输入都可能长出一张新图用例没法写不可审计——事后说不清当时到底跑了哪张图不可回归——golden 用例锚不住拓扑。固定子图加明确的进入条件售后动作 订单号把要不要跑图交给上游决策层第六章图本体保持死板。死板是特性不是缺陷。同一份设计里还埋着一个运行时坑MAX_ITERATIONS从 10 放大到 20。原因很反直觉——LangGraph4j 的迭代预算按 generator yield 计数不是按节点数每个节点内部的多次 yield、条件边的重入都计数10 在 deny-retry 边界就会 trip。这类框架计量单位与直觉不符的坑属于不写进文档就一定会有人再踩一次的。二、售后工作流图五个节点与两种裁决机械事实不符ORDER_NOT_FOUND / ORDER_NOT_OWNED或 Agent 裁决 INELIGIBLE政策资格 → Agent 裁决 pass / uncertain进入refund_request / return_requestquery_user用户信息query_order订单信息query_policy政策召回RAG 单通道validateRejected业务驳回·非系统失败submit_ticket建 HITL 工单挂起等审批决议 管理台状态事件APPROVED / REJECTED / TIMEOUT落地的工作流子图很克制五个节点、一条条件边。查用户、查订单、查政策是三步串行取证validate 是唯一的裁决点submit_ticket 把通过者送进 HITL 工单挂起等审批Rejected 是业务驳回出口。关键设计在 validate 节点里两种裁决的分工// capability/workflow/AfterSaleWorkflowGraph.java —— 机械事实与政策资格分层// 机械事实校验存在/归属——非政策判断保留代码内短路即驳if(ordernull){ReasonfailReason.ORDER_NOT_FOUND;returnMap.of(FAIL_KEY,fail,OUTCOME_KEY,(AfterSaleWorkflowOutcome)newAfterSaleWorkflowOutcome.Rejected(…));}StringuidresolveUserId(ctx);if(uid!null!uid.equals(order.userId())){ReasonfailReason.ORDER_NOT_OWNED;returnMap.of(FAIL_KEY,fail,OUTCOME_KEY,(AfterSaleWorkflowOutcome)newAfterSaleWorkflowOutcome.Rejected(…));}// 政策资格 → Agent 裁决2026-09-19 用户裁决政策知识query_policy 召回 实时事实//订单记录 当前日期一并交模型三态裁决裁决器内部全降级失败UNCERTAIN fail-safe// 到人工绝不冒充业务驳回、绝不盲目放行机械事实订单在不在、是不是你的留在代码里——确定性判断不需要模型意见短路即驳政策资格7 天无理由适不适用这一单交给模型三态裁决——它需要读召回的政策、比对订单记录和当前日期。裁决失败的落点既不是拒绝也不是放行而是UNCERTAIN进人工审批且管理员重点复核。不确定是一种合法输出fail-safe 到人模型意见永远不可能单独驳回或放行一笔业务。业务驳回还有一层语义收口Rejected是业务终态话术直接答复这单不符合条件不是DegradationScenario——系统没坏是业务说不。把业务结果混进降级枚举监控就会把正常业务拒绝算成故障率。三、一次误拒的两个教训小写订单号与空表对账3.1 用户说 “ord-001”入口归一化实测用户小写输入 “ord-001” 原样进OrderQueryService的精确键查找 → miss → 误判ORDER_NOT_FOUND答复订单不存在。订单明明就在 mock 里用户视角这就是系统说谎。修复在服务端入口做防御性归一化// capability/business/OrderQueryService.java —— mock 三笔订单覆盖 validate 全部分支publicOptionalOrderRecordfindByOrderId(StringorderId){if(orderIdnull){returnOptional.empty();}// 防御性归一化trim大写调用方可能传用户原话里的 ord-001精确键会 missreturnOptional.ofNullable(ORDERS.get(orderId.trim().toUpperCase(java.util.Locale.ROOT)));}教训一句话标识符在系统边界处归一化一次而不是要求每个调用方记得归一化。这条后来写进了订单号提取的统一契约raw 优先、standardQuery 兜底《决策层、持久化与闸门硬化》§2.4。3.2 更疼的一个mock 与 DB 各说各话2026-09-20 联调管理台点批准退款必然 409「订单不存在」——而工作流校验明明通过了。排查结论写在BizOrderSeedRunner的 javadoc 里是一教科书级的数据源分裂数据源分裂根因工作流图校验订单存在/归属走 OrderQueryService 内存 mock……而 HitlBusinessGate 对账走 biz_order 表——表只有 DDL 无种子运行时恒空 → 管理台 confirm 必然 fail-closed「订单不存在」。两条链路各自正常校验查内存 mock查得到对账查 biz_order 表按 fail-closed 拒绝——单看都对合起来必坏。修复是启动时BizOrderSeedRunner写入与 mock同源对齐的三笔演示订单配两条铁律fill-if-absent只补空缺不覆盖——业务表是对账锚点种子绝不回写业务状态否则测试数据会污染真实审批结果app.biz-order.seed.enabledfalse可关生产接真数据。mock↔DB 对齐由BizOrderSeedRunnerTest断言钉死——对齐不是口头约定是测试。四、审批是事件一次原则翻转最初的设计是请求内等待工作流发起审批后请求线程挂起等管理台决议桥唤醒机制恢复执行。语义直观但三个毒副作用很快显形请求线程被审批时长绑架Tomcat 线程池会被审批队列占满审批等待与超时语义纠缠HITL_TIMEOUT 话术发出去之后决议才到算谁的恢复路径复杂桥的状态机要处理进程重启。2026-09-20 将裁决整体翻转管理台人工hitl_ticket 表售后工作流管理台人工hitl_ticket 表售后工作流请求线程到此结束——不存在请求内等待决议不受任何请求超时影响等待窗口/桥唤醒已整体退役建单幂等键 wfa:{action}:{orderId}即返回1话术「已提交等待人工审批」短路收尾2confirm / reject纯状态变更事件3confirm 批准 → AfterSaleBusinessExecutor 执行wfa 单不经 resumeresume 属 hitl: 检查点单恢复通道4原则的原话落在代码注释里原则审批是事件、Agent 最小权限——建单即返回无请求内等待。……决议 状态事件Agent 只查询进度。……等待窗口/桥唤醒已整体退役超时不可能影响决议。——TicketApprovalSubmitter / AfterSaleWorkflowGraph翻转后的世界简单了很多请求线程生命周期到建单为止WORKFLOW_APPROVAL_TIMEOUT场景保留只为指标兼容决议语义上已不可能超时恢复是显式管理动作——先过HitlBusinessGate业务对账再 CAS 消费检查点第六章的三级持久化正好接住。等待一个人类从来就不该发生在请求线程里这一条适用于一切带人工环节的系统。三个环节的实测截图正好凑成一次完整闭环五、建单幂等四个状态的门高风险动作的幂等要答两个问题幂等键怎么选命中之后每个状态去哪。工单按业务幂等键查询工作流审批单wfa:{action}:{orderId}、L2 检查点单hitl:{action}:{entity}两套单的分派口径同构只有一处刻意不同——这正是本节最想讲清的地方。先看 L2 检查点单HitlStep610的四个去向// capability/hitl/HitlStep.java —— 建单幂等2026-09-18 L2OptionalHumanTicketexistingticketService.findByIdempotencyKey(idempotencyKey);if(existing.isPresent()){HumanTicketpriorexisting.get();switch(prior.status()){casePENDING-{// 复用挂起单重复请求/网关重试/重开会话不再爆单checkpoint 缺失则补挂重启丢失兜底context.setHitlTicketId(prior.id());ensureCheckpoint(context,prior,idempotencyKey);// …省略 log/metricsreturnnewStepOutcome.ShortCircuit(DegradationScenario.HITL_TIMEOUT);}caseAPPROVED-{// 幂等放行人工已批准过该业务动作恢复锚定的放行语义带外预批准同语义// …省略 setHitlTicketId/log/metricsreturnnewStepOutcome.Proceed();}caseREJECTED-{// 人工已驳回该业务动作不再重审防驳回后换会话重提绕过审批// …省略 log/metricsreturnnewStepOutcome.ShortCircuit(DegradationScenario.HITL_TIMEOUT);}caseTIMEOUT-{// 超时单人工未决议允许重新建单键索引指向新单走下方正常流程// …省略 log无 return——落到方法下方的正常建单流程}}}四个去向各有一个为什么PENDING 复用是防重复轰炸——重复请求、网关重试、重开会话都不再爆单checkpoint 缺失还要补挂兜住重启丢失APPROVED 放行是审批语义的兑现——这笔钱人工批过了再问一遍不能再执行一遍TIMEOUT 重建是给人改口的机会键索引指向新单REJECTED这一格两套单刻意走了两个方向检查点单上面这段HitlStep代码驳回后不再重审而退款退货真正走的工作流审批单TicketApprovalSubmitterwfa:键工作流审批单的幂等键javadoc 写明差异REJECTED 不封禁再申请——PENDING 复用同单、APPROVED 已受理不重建防重复业务动作、REJECTED/TIMEOUT 重建新单。驳回是那张工单的终局事件不是对用户的永久禁令重建的是一张新工单照样要人工审审批权始终在人手里放开重建不构成绕过。幂等门真正要堵的是已批准的工单被重复兑现和同一申请轰炸出十张挂起单——这两条两套门都堵死了。六、这一章带走的六条动态图是系统性过度设计的典型症状。LLM 决定要不要进图图内部保持固定——灵活性与可测试性的边界画在图的门口而不是图里面。框架的计量单位要先核实再设预算。LangGraph4j 的 yield 计数坑属于不记录必复发类。机械事实归代码价值判断归模型不确定归人。validate 节点的三态裁决pass / rejected / uncertain让模型的意见永远不可能单独驳回或放行一笔业务。数据源分裂是集成事故的第一大来源。两条链路各自测试全绿照样联调必坏对齐要用种子加断言钉死且种子不得回写业务状态。带人工环节的流程等待必须发生在请求之外。审批是事件建单即返回、决议是状态变更、恢复是显式管理动作——超时语义才可能与审批语义彻底解耦。幂等门的每个状态都是一个产品决策。复用、放行、不重审检查点单、允许重建工作流单——枚举不全的幂等只是防重复提交枚举全了才防得住重复执行。七、已知边界诚实清单userId 客户端声明订单归属校验的uid来自ChatRequest.userIdnull 兜底 baked 10086 供演示真鉴权接入后 per-request 注入收口。单实例语义工单状态机与检查点消费为单实例口径多实例需 DB 乐观锁第六章已登记。演示身份与 mock 数据ORD-001/002/003 覆盖 validate 全部分支是演示口径真实接入后 mock 退役、对账链路不变。本文机制出处capability/workflow/AfterSaleWorkflowGraph / WorkflowExecutionStep / TicketApprovalSubmitter、capability/hitl/HitlStep / HitlResumeService、capability/business/OrderQueryService / BizOrderSeedRunner设计沿革见 business-tools-workflow-dag 与 per-intent-dag。相关阅读系列目录 · 第六章·决策层、持久化与闸门硬化 · 第四章·L0 业务键注册表 · 下一章全链路延迟与稳定性调优