Streaming bypasses guardrails/middleware

当你在 LangChain 的 create_agent 中使用 PIIMiddleware 等输出中间件(middleware)、同时又用 agent.stream(..., stream_mode="messages") 流式输出时,模型生成的原始 token 会在中间件执行之前就打印到用户面前

快速结论:当你在 LangChain 的 create_agent 中使用 PIIMiddleware 等输出中间件(middleware)、同时又用 agent.stream(..., stream_mode="messages") 流式输出时,模型生成的原始 token 会在中间件执行之前就打印到用户面前,导致输出护栏被完全绕过。优先排查你的流式输出是否在中间件生效前就把 token 直接返回给了调用方。

适用环境:Issue 中报告的运行环境为 macOS(路径含 /Users/ 与 Homebrew),Python 3.10.16,使用 langchainlangchain-core 两个包,模型为 gpt-4o-mini。其余 CUDA、显卡等未在 Issue 中说明。

最快修复方案:暂无确认的一步修复方案。Issue 中维护者明确表示,在现有 Middleware 架构下没有直接的逐 token 执行中间件的方法,该问题需要 AgentMiddleware 层面的架构调整,Issue 因此长期保持 open 状态。可优先尝试的临时方案是:改用非流式的 agent.invoke() 先让完整输出经过中间件,再对最终消息做“伪流式”逐 token 输出(见下方解决步骤)。

注意事项:上述伪流式方案会牺牲真实流式的首字延迟体验,且属于社区提出的 workaround,并非官方修复;它在输出被中间件拦截时只能整段拒绝,无法保证真正的逐块安全。Issue 评论中另有人建议在流式过程中对缓冲区做增量扫描(buffered window scanning),但这依赖第三方工具且同样未获官方确认。另有评论建议至少在文档中加入警告提示,说明当前流式与输出护栏不兼容。

问题场景

用户在 LangChain 中通过 create_agent 创建 agent,并挂载了 PIIMiddlewarestrategy="block"apply_to_output=True)用于拦截输出中的邮箱等 PII 信息,然后用 agent.stream(stream_mode="messages") 逐条消费模型消息并直接打印 msg.content。结果是:即使启用了输出侧 PII 中间件,包含邮箱地址的完整内容依然先被打印出来,随后中间件才执行并触发拦截,抛出异常。用户同时还观察到,如果直接打印流式 token,敏感内容已被暴露给使用者。

报错原文

Sure! Here's a funny greeting incorporating your email:
...
you can reach me at my secret lair: **john.doe@example.com**!
...
Traceback (most recent call last):
  File ".../test_hiddenlayer.py", line 79, in <module>
    test_streaming_malicious()
  File ".../test_hiddenlayer.py", line 64, in test_streaming_malicious
    for msg, metadata in agent.stream(
  File ".../langgraph/pregel/main.py", line 2646, in stream
    for _ in runner.tick(
  File ".../langgraph/pregel/_runner.py", line 258, in tick
    _panic_or_proceed(
  File ".../langgraph/pregel/_runner.py", line 520, in _panic_or_proceed
    raise exc
  File ".../langgraph/pregel/_executor.py", line 80, in done
    task.result()
  ...

核心问题可概括为:Streaming bypasses guardrails/middleware。即流式输出在中间件完成检查之前就把原始内容交给了调用方。

原因分析

根据维护者在 Issue 中的回复,Middleware 节点只在整段流式内容全部 yield 回用户之后才执行一次,因此当前 Middleware 架构下没有直接办法在流式过程中逐 token 执行中间件。也就是说,输出侧的 PII 中间件本质上是“事后处理”,而 stream_mode="messages" 会在处理之前就把 token 交付给你并打印,从而出现“先泄漏、后拦截”的结果。Issue 评论进一步指出:任何把中间件实现为后处理步骤的系统,在流式场景下都会有同样的问题,修复需要在 LangGraph / Middleware 架构层面支持“流式感知的中间件”。

需要注意:这一原因来自维护者与评论者的分析,尚未在 Issue 中给出最终合并的官方修复结论。

环境排查

  • 确认 Python 版本,Issue 中为 3.10.16。
  • 确认 langchainlangchain-core 均已安装,并确认其版本。
  • 确认相关运行栈:Issue 堆栈中出现 langgraph/pregel/main.py_runner.py_executor.py,说明经过 LangGraph 执行层,需一并确认 langgraph 版本。
  • 确认模型:Issue 使用 gpt-4o-mini
  • 确认中间件配置:PIIMiddleware("email", strategy="block", apply_to_input=False, apply_to_output=True)
  • 确认调用方式是否为 agent.stream(..., stream_mode="messages") 并直接打印 token。

解决步骤

  1. 先复现确认:使用 Issue 中的最小示例,传入包含邮箱的 user 消息,观察邮箱是否在中间件拦截之前就被打印出来。
  2. 若确认存在绕过行为,可优先尝试社区给出的“伪流式”临时方案:不要用 stream_mode="messages" 直接消费 token,而是先用 agent.invoke() 拿到完整输出,让输出侧中间件完整执行(strategy="block" 时会被拦截)。
  3. output["messages"][-1] 取出最终消息内容,用分词器(Issue 示例使用 tiktoken.encoding_for_model("gpt-4o-mini"))将其编码为 token,再逐个 decode 并打印,模拟流式输出效果。
  4. try/except 包裹 invoke 与打印过程;一旦中间件因 PII 触发拦截,就在 except 中做统一降级处理,不要输出任何原始内容。
  5. 如仍需保留真实流式,考虑在流式过程中使用带缓冲的增量扫描方案(例如 Issue 评论提到的第三方工具 ClawMoat)。注意该方案并非官方修复,仅在你有能力自行维护状态机与误报处理时使用。
  6. 持续关注 Issue 中提到的修复 PR(#35470),其思路是在中间件侧缓冲模型输出,确保原始 token 不会在 after_model 拦截/脱敏之前暴露。该修复是否覆盖 sync/async 流式与各输出策略,需以 PR 最终状态为准。

验证方法

用同一个包含邮箱的提示词重新运行:如果改用 invoke + 伪流式,确认邮箱内容要么被 PIIMiddleware 阻止、要么在通过中间件后才显示,且不会出现“先完整打印敏感内容、再报错”的顺序。若采用缓冲扫描方案,则应验证邮箱的第一个字符出现之前就已被标记或阻断。建议同时覆盖同步与异步流式、以及 block 与脱敏策略,确认没有残留暴露路径。

参考来源

langchain-ai/langchain #35011

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 25185

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注