--- id: ai.langgraph.scenario.refund-approval title: 退款工单 Agent:人工审批、任务状态和崩溃恢复 type: scenario domain: ai stack: [langgraph, fastapi, postgresql] tags: [人机协作, 审批流, 任务状态, 异常恢复, 幂等, 流式进度] level: advanced prerequisites: [ai.langgraph.persistence-hitl] est_hours: 3 related: [ai.langgraph.interview, ai.agent-ops.scenario.agent-quality-gate, backend.postgresql.scenario.order-state-audit] week: [12] --- # 退款工单 Agent:人工审批、任务状态和崩溃恢复 ## 业务背景与问题 客服系统接入 Agent 自动处理退款工单:识别工单类型和金额、查询订单、起草回复、执行退款。试运行发现: 1. 大额退款不能让 Agent 直接执行,需要主管审批,审批可能隔天才处理; 2. 模型接口偶尔超时,整个工单就失败了,要客服手动重新提交; 3. 一次发版重启,几十个“处理中”的工单状态卡住,不知道执行到哪一步; 4. 有工单重复退款了两次; 5. 客服页面只显示“处理中”,不知道 Agent 在做什么。 ## 约束 - 审批阈值:500 元及以上需要人工审批。 - 退款接口是第三方支付接口,支持幂等键。 - 服务多实例部署,可能随时重启。 ## 方案设计 ```mermaid flowchart LR A[分类
结构化输出校验] -->|不合法| H[转人工] A --> B[起草回复
RetryPolicy 重试] B -->|小额| D[执行退款 + 发送
幂等键=工单号] B -->|大额| C[interrupt 等待审批] C -->|同意| D C -->|拒绝| E[结束] ``` 1. **LangGraph + PostgresSaver**:每一步保存检查点,`thread_id = 工单号`。 2. **审批**:审批节点调用 `interrupt`,同时把工单状态改为 `waiting_approval` 写入业务表;主管在后台点击后,接口调用 `Command(resume=...)` 继续。 3. **临时错误**:模型调用节点配置 `RetryPolicy`,只重试超时 / 限流 / 5xx。 4. **崩溃恢复**:后台巡检任务定期找出状态是“处理中”但长时间没有更新的工单,用 `invoke(None, config)` 从检查点继续(多实例下要先抢占租约,避免两个实例同时恢复同一个工单)。 5. **幂等**:退款和发送都用工单号做幂等键;状态更新用条件更新(`WHERE status = ...`)。 6. **流式进度**:节点里用 `get_stream_writer()` 发送进度,接口转成 SSE 推给客服页面。 ## 关键代码 完整可运行示例:[examples/ticket-flow](../examples/ticket-flow/)。审批接口(FastAPI,示意): ```python @app.post("/tickets/{ticket_id}/approval") async def approve(ticket_id: str, body: ApprovalIn, user=Depends(require_role("supervisor"))): cfg = {"configurable": {"thread_id": ticket_id}} snap = await graph.aget_state(cfg) if snap.next != ("approve",): raise HTTPException(409, "工单不在待审批状态") await graph.ainvoke(Command(resume={"ok": body.ok, "note": body.note, "by": user.id}), cfg) return {"status": (await graph.aget_state(cfg)).values["status"]} ``` ## 取舍 | 选择 | 优点 | 缺点 | | --- | --- | --- | | **LangGraph 检查点(本方案)** | 自带暂停 / 恢复 / 历史,代码少 | 依赖框架;检查点表需要清理 | | 自己用状态机 + 任务表实现 | 完全可控,和业务表一体 | 要自己实现恢复、重试、事件流 | | 工作流引擎(Temporal 等) | 可靠性强,适合长流程 | 学习和运维成本高 | | 全部人工审批 | 最安全 | 失去自动化的意义;用阈值和抽检平衡 | ## 易错点 - 审批节点在 `interrupt` 之前就执行了退款。 - 检查点和业务表的状态不一致:以业务表为准对账,或者在同一个节点里同时更新。 - 恢复任务没有租约,多实例同时恢复同一个工单。 - 审批接口没有校验当前状态,重复点击导致重复恢复。 - 审批人看不到 Agent 的依据:审批数据要包含订单信息、起草内容、分类结果。 ## 面试怎么讲 > “退款 Agent 我用 LangGraph 做,检查点存在 PostgreSQL,thread_id 就是工单号。大额退款在审批节点 interrupt,状态写成待审批,主管点同意后用 Command resume 继续。模型超时用节点级重试;服务重启后巡检任务抢到租约,再从检查点续跑,已经完成的步骤不会重复执行;退款接口用工单号做幂等键,防止重复退款。前端通过 SSE 看到每一步的进度。”