Files

87 lines
4.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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[分类<br/>结构化输出校验] -->|不合法| H[转人工]
A --> B[起草回复<br/>RetryPolicy 重试]
B -->|小额| D[执行退款 + 发送<br/>幂等键=工单号]
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 看到每一步的进度。”