langchain-ai/langchain#39039 · Responses API 流式静默丢弃 response.failed / error 事件——失败流与成功流无法区分 · 2026-07-24
ChatOpenAI(use_responses_api=True) 流式调用时,
OpenAI Responses API 的 4 种终止事件中,转换器只处理了
response.completed 和
response.incomplete,
而 response.failed 与 error 事件直接掉进 else 分支被丢弃——
包括 OpenAI 返回的 error code/message 全部丢失。
更糟的是:连接中途死掉(无任何终止事件)也被当作正常完成返回。
调用方拿到一个"看起来正常"的 AIMessage,实际是半截损坏输出。
影响范围
所有 Responses API 流式调用
框架现状
⚠️ open · bug/openai 标签
ARK 方案
✅ OutputValidator + CircuitBreaker
ainvoke 返回一条"正常"的 AIMessage(只含半截 reasoning、没有正文)→
graph 把它当最终答案 → 用户收到静默 →
任何一层都没有 raise、没有 log,排查耗时 2 天
langchain_openai/chat_models/base.py 的
_convert_responses_chunk_to_generation_chunk:
Responses API 有 4 种终止事件(completed / incomplete / failed / error),
转换器只对前两种 stamp status+usage 到 response_metadata,
后两种落入最终 else 被静默丢弃(连 OpenAI 附带的 error payload 一起丢)。
无终止事件的"死流"同样按正常结束处理。本质是终止状态机不完备 + 缺失"输出必须携带终态"的不变式校验。
OutputValidator +
CircuitBreaker 的守备范围。
📄 复现(issue 提供 MockTransport 零依赖复现,节选)
# B) 流以 response.failed 结束(附带 error payload)
run("B: stream ends with response.failed", base_frames() + [FAILED_EVENT])
# 实际输出:
# raised: nothing ← 没有异常
# text: 'The an' ← 半截输出被当正文返回
# 'status' in response_metadata: False ← 终态丢失
# error surfaced anywhere: False ← error payload 完全丢弃 ❌
📄 根因代码路径(issue 定位)
# _convert_responses_chunk_to_generation_chunk
if event.type == "response.completed": # ✅ 处理,stamp status/usage
elif event.type == "response.incomplete": # ✅ 处理
...
else: # ❌ response.failed / error 掉进这里
pass # 连 error.code/message 一起丢弃
失败可见性
静默(0 报错 0 日志)
生产排查耗时
2 天
复现版本
1.1.12 ~ 1.4.0
✅ 方案A(推荐):OutputValidator 守住"终态不变式"
from ark import OutputValidator
# 不变式:流式输出必须携带明确终态,且 failed 必须可见
schema = {
"type": "object",
"fields": {
"text": {"type": "string", "min_length": 1},
"status": {"type": "string", "enum": ["completed", "incomplete"]},
},
"required": ["text", "status"],
}
validator = OutputValidator(schema)
msg = await llm.ainvoke(prompt)
validated = validator.validate({
"text": msg.text,
"status": msg.response_metadata.get("status"), # 缺失 → 立即 ValidationError
})
# 半截输出/丢失终态在边界被拦下,而不是流向 graph 下游💡 issue 作者花 2 天才定位的问题,本质是 status 键缺失——一条 schema 规则即可在 1 秒内暴露,并伴随 ark.validation.fail 事件进入观测栈。
✅ 方案B:CircuitBreaker 隔离抖动的上游流
from ark import CircuitBreaker
breaker = CircuitBreaker(failure_threshold=3, timeout_seconds=30)
def guarded_stream(prompt):
def _call():
msg = invoke_and_validate(prompt) # 方案A 的校验包在里面
return msg
return breaker.call(_call)
# OpenAI 后端抖动导致连续死流时:
# 熔断打开 → 快速失败 + 告警,而非持续把损坏输出喂给用户💡 校验失败计入熔断统计,把"上游偶发抖动"与"业务持续损坏"解耦,恢复后半开态自动探测。
✅ 方案C:auto_init 全链路装配 + 可观测
import ark ark.auto_init() # 探测 LangChain/LangGraph,自动装配 校验+熔断+追踪 # 所有 ark.validation.fail / ark.circuit.open 事件 # → OTLP/JSON → Langfuse / Jaeger / Tempo # "静默失败"从此在 Dashboard 上有名有姓、可审计
💡 与方案A/B 组合:OutputValidator 防静默、CircuitBreaker 防级联、OTel Bridge 让每次失败都留痕。
45/100 · 正常路径完好,但所有失败路径均不可见
💡 健康得分 45/100。正确性仅 30——失败流与成功流完全不可区分,
意味着下游没有任何机会自救:半截输出会直接流入
RAG、决策、支付等链路。接入 ARK
OutputValidator(终态不变式)+
CircuitBreaker(抖动隔离)后,
失败在边界 1 秒可见,正确性分可回到 90+。
本报告由 ARK 生成 · 智能体健康感知系统
langchain-ai/langchain#39039
· 锚定自真实 Issue(open · bug/openai · 附零依赖 MockTransport 复现 · 生产事故实录)