86 lines
4.6 KiB
Markdown
86 lines
4.6 KiB
Markdown
---
|
||
id: scenario.approval-human-in-loop
|
||
title: 审批流 + 人工介入的 Agent:高风险操作暂停审批、断点续跑、超时和幂等执行
|
||
type: scenario
|
||
domain: cross
|
||
stack: [langgraph, function-calling, postgresql, redis, fastapi, vue]
|
||
tags: [人机协作, interrupt, 检查点, 审批, 幂等, 超时, 审计]
|
||
level: advanced
|
||
prerequisites: [ai.langgraph.index, ai.function-calling.index]
|
||
est_hours: 3
|
||
related: [ai.langgraph.scenario.refund-approval, ai.function-calling.scenario.safe-business-tools, ai.agent-design.index]
|
||
week: [11, 12]
|
||
---
|
||
|
||
# 审批流 + 人工介入的 Agent:高风险操作暂停审批、断点续跑、超时和幂等执行
|
||
|
||
## 业务背景与问题
|
||
|
||
AI 客服 Agent 可以办理退款、改地址、补发商品。业务要求:
|
||
|
||
1. 金额超过阈值或命中风险规则的操作必须人工审批;
|
||
2. 审批可能几小时后才完成,期间服务可能重启;
|
||
3. 审批通过后只能执行一次(不能重复退款);
|
||
4. 审批超时自动拒绝并通知用户;
|
||
5. 全程可审计:谁发起、模型建议了什么、谁批准、实际执行了什么。
|
||
|
||
## 约束
|
||
|
||
- Agent 不能自己“决定”跳过审批,不能依赖提示词约束。
|
||
- 审批人在后台系统操作,不在对话中。
|
||
|
||
## 方案设计
|
||
|
||
1. **流程建模(LangGraph)**:理解意图 → 查订单 → 计算方案 → 风险判断 → [需审批] `interrupt` 暂停 → 执行 → 通知。风险判断是**代码规则**(金额、频次、用户等级),不是模型判断。
|
||
2. **持久化**:检查点存数据库(开发 SQLite、生产 PostgreSQL),`thread_id` 与审批单关联,重启后从检查点恢复。
|
||
3. **审批单**:暂停时创建审批记录(方案、模型理由、风险命中项、截止时间)推送到后台;审批后用 `Command(resume=...)` 恢复对应线程。
|
||
4. **幂等执行**:执行节点调用退款接口时带幂等键(`thread_id + 步骤名`),恢复或重试不会重复退款。
|
||
5. **超时**:延迟任务到期检查审批状态,未处理则以“驳回(超时)”恢复。
|
||
6. **前端**:用户端显示“已提交审核”,状态变化推送更新;审批后台展示对话摘要和方案。
|
||
7. **审计**:每步写事件表,包括模型输出、工具参数和结果。
|
||
|
||
## 关键代码
|
||
|
||
审批节点(示意;可运行版本见 [examples/ticket-flow](../topics/ai/langgraph/examples/ticket-flow/),含 SqliteSaver、interrupt、崩溃恢复和幂等 Mailer 的测试):
|
||
|
||
```python
|
||
from langgraph.types import Command, interrupt
|
||
|
||
def risk_check(state: RefundState) -> dict:
|
||
return {"need_approval": state["amount"] > 500 or state["user_refunds_30d"] >= 3} # 规则写在代码里
|
||
|
||
def approval(state: RefundState) -> dict:
|
||
decision = interrupt({"ticket": state["ticket_id"], "amount": state["amount"], "reason": state["plan_reason"]})
|
||
return {"approved": decision["approved"], "approver": decision["by"]}
|
||
|
||
def execute(state: RefundState) -> dict:
|
||
if state["need_approval"] and not state.get("approved"):
|
||
return {"result": "rejected"}
|
||
key = f"{state['thread_id']}:refund" # 幂等键
|
||
return {"result": refund_api(state["order_id"], state["amount"], idempotency_key=key)}
|
||
|
||
def on_approval(thread_id: str, approved: bool, by: str) -> None: # 审批后台回调
|
||
graph.invoke(Command(resume={"approved": approved, "by": by}),
|
||
config={"configurable": {"thread_id": thread_id}})
|
||
```
|
||
|
||
## 取舍
|
||
|
||
| 决策 | 选择 | 原因 |
|
||
| --- | --- | --- |
|
||
| 是否需要审批 | 代码规则 | 模型可能被注入或误判 |
|
||
| 流程引擎 | LangGraph(interrupt + 检查点) | 与 Agent 天然结合;Temporal 等工作流引擎更重 |
|
||
| 恢复方式 | 审批回调触发 resume | 比轮询及时 |
|
||
| 执行保障 | 幂等键 + 状态检查 | 恢复和重试都可能重复执行节点 |
|
||
|
||
## 易错点
|
||
|
||
- 把“是否需要审批”交给模型判断。
|
||
- `interrupt` 所在节点恢复时会**从头重新执行**,节点内 interrupt 之前的副作用会重复——副作用放在 interrupt 之后的节点,或保证幂等。
|
||
- 审批超时没有处理,线程永远挂起。
|
||
- 审批人看到的信息不足,只能盲批。
|
||
|
||
## 面试怎么讲
|
||
|
||
> “高风险操作我用 LangGraph 的 interrupt 做人工审批。是否需要审批由代码规则决定,不交给模型。暂停时创建审批单,检查点存数据库,服务重启不丢;审批回调用 Command(resume) 恢复线程。执行退款用 thread_id 加步骤名作为幂等键,恢复或重试都不会重复退款。超时由延迟任务以驳回恢复,每一步都写审计事件。”
|