阶段4(上):LangGraph、Function Calling、MCP(SDK 2.x)、Agent 评测与运维、Agent 设计专题;run_examples 设置 RUN_EXAMPLES=1

This commit is contained in:
Shikui Wang committed 2026-10-23 16:06:54 +08:00
1 parent 6650f7eb6d
commit caaec830b3
38 files changed
+3515 -64

No files matched your search

@@ -0,0 +1,86 @@
---
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 看到每一步的进度。”