第八篇:人在回路与内容安全 —— 当 AI 说“让我请示一下“

Human-in-the-loop 看起来只是一个"审批按钮",背后是一整套状态机、超时策略和框架 hack。

LangGraph 内置方案的致命限制

LangGraph 的 HumanInTheLoopMiddleware 基于 interrupt()------序列化图状态、挂起、等待外部信号。文档上的示例干净利落:

python 复制代码
# 文档里的理想用法
def my_node(state):
    result = interrupt("请管理员审批")  # ← 等待外部审批信号
    return result

问题出在我们实际接入时。我们的退款审批逻辑在 request-return tool 内部------tool 的执行上下文不是 graph node ,是 LangChain 的 ToolMessage 回调。当我们在 tool 函数内调用 interrupt(),整个图状态机挂起了,但 tool 的执行栈没有正确序列化------ToolInvocation 对象卡在 pending 状态,没有任何恢复机制能正确重建 tool 执行上下文。

具体表现:ReAct Agent 执行到 request-return tool → tool 调用 interrupt("确认退款 ¥199 → 用户 138xxx") → Agent 线程 hang 住,不抛异常,不超时,Langfuse trace 停在 tool_call start 再也没有后续。我们等了 30 分钟,trace 还是停在原地。

翻 LangGraph 源码确认了根因:checkpoint() 序列化的是 graph node 级别的 Channel 状态,不包含 tool 执行帧。当 interrupt() 在 tool 内部被调用时,恢复后 graph 从 checkpoint 恢复 node 的输入状态重新执行------但 tool 的中间产出的数据(已生成的退款明细)已经被丢掉了。

退款审批恰好发生在 tool 内部(request-return tool 里的"生成退款明细→等待确认→执行退款")。框架给了 80% 的路径------node 间审批------但你业务长在另外 20% 上------tool 内部审批。而这个 20% 恰恰是 LangGraph 的架构边界。


自研方案:一个标志位 + Redis

Tool 正常返回(不抛异常),返回内容带 _pending_approval 标志 + 退款明细摘要。Orchestrator 检测到标志位 → 摘要存入 Redis _INTERRUPT_STORE → Agent 暂停 → 审批员 API 同意/拒绝 → 恢复 Agent。

三个魔鬼细节:

  1. 退款明细随中断一起返回 → 审批者无需额外查询
  2. 审批超时 30 分钟自动拒绝 → 避免挂死
  3. 工具正常返回而非抛异常 → 避免 Langfuse 被 "ApprovalRequiredError" 刷屏

什么情况该换工作流引擎? 标志位+Redis 适合审批路径简单(同意/拒绝,2-3 个状态)的场景。如果你遇到这些信号就该升级了:

  • 多级流转:主管→经理→财务,审批链随金额/类型动态路由
  • 会签或签:多人并行审批,需全部通过或任意一人通过
  • SLA 告警升级:审批超时后不是直接拒绝,而是逐级上升
  • 驳回回退:审批者退回上一级修改,不是简单的"同意→执行→完成"

这时别再在 agent 里手写状态机了。两种选择:

  • Temporal:代码即工作流,Saga 模式原生支持长活事务的补偿与重试。适合「审批跨越几小时甚至几天」的分布式场景。缺点是没有可视化流程建模,审批规则改一次就要改代码、发版。
  • Conductor(Netflix 开源):微服务编排引擎,独立部署,官方 Python SDK 完善。WAIT 节点天然支持「暂停→等待外部回调→继续」的人工审批模式。缺点是流程定义不走 BPMN 标准,需要手写 JSON DSL(自带 UI 可以可视化查看和监控流程图,但不能像 BPMN 设计器那样拖拽建模),如果要业务人员拖拽方式创建审批流,可以考虑使用flowable通过API集成的方式。

核心原则:把审批流程从 agent 中剥离成独立的工作流服务,agent 只负责发起和接收回调。 HITL 的复杂度在于"流程",不在于"AI"------不要让 agent 承担它不该承担的编排责任。


HITL 暂停期间新消息的处理

用户挂了退款审批,又去问"那我的优惠券还能用吗"。推荐"轻量分支":只合并历史、不合并意图,两个请求独立处理。


内容安全:四层纵深防御

90% 的拦截在规则层(L1,<1ms)就结束了:

  • L1 输入规则过滤(正则+敏感词库)→ 直接拒绝
  • L2 本地 LLM 安全审查 → RAG Step2 与问题改写并行
  • L3 云端 LLM 复核 → 可选,仅边界模糊 case
  • L4 输出规则过滤 → 生成后硬阻断

让贵的组件做少的事。


隐私合规要点

三存储体系(Milvus + Redis + DB)全链路清理。假名化 ≠ 匿名化------hash 替代手机号是可逆的,GDPR 对两者要求不同。

相关推荐
Bruce_Liuxiaowei几秒前
2026年10月第3周网络安全形势周报
安全·web安全·网络安全
破茧蝴蝶6 小时前
Python分布式爬虫完整教程
开发语言·python
benchmark_cc6 小时前
量化系统架构设计:为什么应该将数据获取与策略逻辑解耦?
python·数据分析·pandas·量化交易·股票数据·quantdash
数据库小学妹7 小时前
国产数据库怎么选?兼容、迁移、核心案例与融合架构四道关
数据库·架构·国产数据库·数据库迁移·融合数据库
凤山老林7 小时前
Spring Boot 启动冲刺:耗时剖析、懒加载策略与事件驱动初始化优化
spring boot·后端·python
霸道流氓气质7 小时前
Serverless 架构入门与实战:FaaS/BaaS 模型、Spring Cloud Function 适配与 AI 推理冷启动优化的 Java 全栈指南
spring cloud·架构·serverless
小白快快跑哦7 小时前
python-字符串全解(六):正则表达式-逻辑运算或
python·正则表达式·字符串·逻辑运算
Thinker QAQ7 小时前
并发编程(九):BlockingQueue 与 Channel——语言层的原子性、可见性与有序性
java·python·go·并发编程·channel·blockingqueue·message passing
道满科技7 小时前
Function Calling 参数总出错?只修参数就行
linux·windows·python
霸道流氓气质9 小时前
RAG 可观测性与生产监控实战:四层遥测模型、五大仪表盘与 OpenTelemetry 全链路追踪的 Java 生产级实现
java·人工智能·python