OpenAI Agents SDK 工程笔记:streaming 流式输出与生产禁区

千笔-AIWritePaper · https://www.aiwritepaper.com

把 Agents SDK 的 streaming 当成「多打几个 print」会踩坑:Runner.run_streamed() 返回的是 RunResultStreaming,真正完成要以 stream_events() 迭代器结束 为准;最后几个可见 token 到齐之后,会话持久化、审批记账、历史压缩仍可能在收尾。官方 StreamingResults 写得很直:事件分 raw / run item / agent updated 三层;审批要先排空流再读 interruptionscancel() 后仍要继续消费事件。本文按工程笔记写法,钉死选型、可跑片段、审批续跑与生产禁区。示例模型名写作 gpt-4o以你账号可用快照与官方文档为准

图:上方 run vs run_streamed;中部三类 StreamEvent 与审批续跑;下方生产禁区对照。

目标说明

读完你应能独立完成五件事:

  1. 用一句话说清:run 等终态;run_streamed 边跑边推事件,但必须排空 stream_events() 才算完成。
  2. 写出可跑片段:token 级(ResponseTextDeltaEvent)与语义级(tool/message/agent_updated)两套消费循环。
  3. 会处理 HITL:流结束后读 interruptionsto_state() → approve/reject → 再 run_streamed
  4. 知道 cancel() / cancel(mode="after_turn") 的差异,以及取消后仍要继续消费事件。
  5. 列出生产禁区:把「已看到 delta」当业务成功、中途 break 丢收尾、审批未完成就开新用户回合、把流式 UI 卡顿当 SDK bug 却不查是否排空。

规格钉死(对照官方 Streaming / Stream events / Results):

  • 入口Runner.run_streamed(agent, input)RunResultStreaming
  • 事件联合RawResponsesStreamEvent | RunItemStreamEvent | AgentUpdatedStreamEvent
  • 完成条件 :异步迭代器结束;之后看 result.is_completefinal_output 在流未收完前可为 None
  • 审批 :流兼容 tool approval;先消费完,再处理 interruptions
  • 取消 :默认立即停;mode="after_turn" 让本回合干净结束后再停;取消后仍 drain。

适用边界

适合上 streaming

  • 聊天/客服 UI 需要逐字或逐段反馈,降低「卡住」感。
  • 需要在工具调用、handoff 切换时推送进度条(tool_called / agent_updated_stream_event)。
  • 人类审批工具:流式跑到 interruption,再续跑。
  • 观测:中途看 current_agent,不必等终态才知道路由。

更适合普通 Runner.run(不要硬上流)

  • 批处理、定时任务、只要终态 JSON,不需要中间 UI。
  • 下游强依赖完整 final_output / output_type 校验,且无增量渲染需求。
  • 测试断言只关心终答与 tracing 导出,流式只会增加 flake。

不该指望它单独搞定

  • 业务成功:token 到齐 ≠ 退款入账 ≠ 写库提交。
  • 用中途 break「优化体验」:提前退出迭代器会丢持久化与审批收尾。
  • 把 raw delta 当唯一真源:语义事件(message/tool/handoff)才适合做进度与审计。
  • 审批未决就 add_input 开新话题:官方要求先处理 interruption / 用 state 续跑。

风险提示

流式失败会在 stream_events() 上抛出;Python 侧没有单独的 error 属性可轮询。生产里若前端只订阅 delta、后端在异常时不落盘,会出现「用户看见半句话、系统无失败记录」的撕裂。共同解法:drain + 显式终态检查 + 业务状态机分离

步骤与机制

1. run vs run_streamed 对照

机制 谁拿到完整结果时机 中间可见性 失败时 典型用途
Runner.run / run_sync 返回时 无增量 异常抛出 批处理、单测
Runner.run_streamed 迭代器结束后 raw + 语义事件 stream_events 抛错 UI、进度、HITL
只打 LLM 原生流、自拼 Agent 你自己拼 易漏 tool/handoff 自担 不推荐替代 SDK
中途 break 不 drain 不定 半截 UI 持久化/审批可能未完 生产禁区

2. 可跑:token 级流式

pip install openai-agents,并导出 OPENAI_API_KEY

python 复制代码
import asyncio
from openai.types.responses import ResponseTextDeltaEvent
from agents import Agent, Runner

MODEL = "gpt-4o"  # 占位:以账号可用快照为准

async def main():
    agent = Agent(
        name="Assistant",
        instructions="你是简洁助手,用中文回答。",
        model=MODEL,
    )
    result = Runner.run_streamed(agent, "用三句话解释什么是流式输出。")
    async for event in result.stream_events():
        if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
            print(event.data.delta, end="", flush=True)
    print()
    print("complete=", result.is_complete)
    print("final=", result.final_output)

asyncio.run(main())

验收:终端出现增量字符;循环结束后 is_complete 为真;final_output 非空。若你提前 break,不要宣称「流已跑通」。

3. 可跑:语义事件(忽略 raw)

python 复制代码
import asyncio
from agents import Agent, ItemHelpers, Runner
from agents.decorators import tool

@tool
def ping() -> str:
    return "pong"

async def main():
    agent = Agent(
        name="Toolish",
        instructions="先调用 ping 工具,再简短汇报。",
        model=MODEL,
        tools=[ping],
    )
    result = Runner.run_streamed(agent, "请探测一下。")
    async for event in result.stream_events():
        if event.type == "raw_response_event":
            continue
        if event.type == "agent_updated_stream_event":
            print("agent ->", event.new_agent.name)
        elif event.type == "run_item_stream_event":
            if event.item.type == "tool_call_item":
                print("tool_called")
            elif event.item.type == "tool_call_output_item":
                print("tool_output", event.item.output)
            elif event.item.type == "message_output_item":
                print("message", ItemHelpers.text_message_output(event.item))

asyncio.run(main())

要点:RunItemStreamEvent.namemessage_output_createdtool_calledtool_outputhandoff_requestedhandoff_occured(官方保留拼写)等。handoff 请求走 handoff_requested,不要指望它再冒充一次普通 tool_called

4. 审批:排空 → interruptions → 续跑

python 复制代码
# 伪结构:工具需审批时
result = Runner.run_streamed(agent, "执行需要审批的操作...")
async for _ in result.stream_events():
    pass

if result.interruptions:
    state = result.to_state()
    for interruption in result.interruptions:
        state.approve(interruption)  # 或 reject
    result = Runner.run_streamed(agent, state)
    async for _ in result.stream_events():
        pass

验收:未 approve 前不应出现业务副作用提交;续跑后再次 drain;审计能回答「谁在何时批准了哪次 interruption」。

5. 取消与 after_turn

  • result.cancel():尽快停。
  • result.cancel(mode="after_turn"):让当前回合干净结束。
  • 取消后继续消费 stream_events(),否则清理不完整。
  • after_turn 停在 tool 回合,用 to_input_list(mode="normalized") 续跑 last_agent 时,不要误开「全新用户回合」。

生产禁区

禁区 为什么危险 替代做法
看见 delta 就写业务成功 收尾未完;最终可能失败 以迭代器结束 + is_complete 为准
中途 break「省流量」 丢 session/审批记账 必须 drain;UI 可节流渲染
审批未决开新 user turn 状态机错乱 interruptions → to_state → 续跑
只订 raw、不订语义事件 丢 tool/handoff 进度与审计 双通道:UI 用 raw,审计用 item
把 cancel 当事务回滚 SDK 不停业务库 业务幂等键 + 补偿
流式 UI 卡顿就删 max 保护 放大 runaway tool loop 查是否未 drain / 网络;保留上限
final_output 为 None 就当空成功 流未完成 先查 is_complete

可验证清单

  • 能口述 run 与 run_streamed 的完成时机差异
  • 本地跑通 token 级与语义级两段示例
  • 有一条「必须 drain」的代码评审规则
  • HITL 路径:interruptions → approve → 再 streamed
  • cancel 用例会继续消费事件
  • 生产配置里没有「delta 到达即付款/删库」
  • tracing 能对照 tool_called / handoff_requested

踩坑

  1. 以为最后一个 delta 等于 run 完成:官方明确 post-processing 可能更晚。
  2. 前端断连就丢弃后端迭代器:应后端继续 drain,或显式 cancel 并记审计。
  3. 把 AgentUpdated 当 handoff 成功业务事件:那只是说话权变化。
  4. 单测里乱 break:制造「偶发 final_output 为空」。
  5. 混用自建 WebSocket 与 SDK 流却不统一完成语义:两套完成条件会双写。

与 handoffs / sessions / output_type 的协作位

解决什么 和 streaming 的交接面
streaming 增量可见性与 HITL 暂停点 必须 drain;中途可读 current_agent
handoffs 本轮说话权 agent_updated / handoff_* 事件可观测
sessions 跨轮记忆 持久化可能在最后 token 之后
output_type 终答形状 流未完成时不要强读 final_output

实践顺序:先定完成语义(何时算成功),再定 UI 订阅哪些事件,最后才加审批与 session。反过来先接 WebSocket,再补完成条件,事故率高于演示。

FAQ:为什么 UI 已经出字,后台还在忙?

因为可见 token 与会话收尾不是同一阶段。UI 可以渲染 delta;后台仍可能写 session、压缩历史、登记审批。产品文案不要写「字出完=任务完成」,应写「回复生成中 / 已提交 / 待审批」等业务态。

最小观测字段

建议至少落盘:run_idstarted_atdrain_finished_atis_completecancel_modeinterruption_countcurrent_agenterror_type。验收:出事五分钟内能回答「流是否排空、是否有未批准 interruption、最后 agent 是谁」。

发布前代码审一眼

  • 所有 run_streamed 调用处都有完整 async for 或明确的 drain 辅助函数。
  • 无「except: pass」吞掉 stream 异常。
  • 前端断连策略写进 runbook(继续 drain / cancel + 审计)。
  • 演示模型名非生产承诺。

端到端验收剧本(十分钟)

  1. 纯文本问题:token 增量可见;结束后 is_complete 真。
  2. 强制 tool:语义事件出现 tool_called → tool_output → message。
  3. 需审批 tool:第一段流结束后有 interruptions;未 approve 无副作用。
  4. approve 后续跑:再次 drain;业务态才允许「成功」。
  5. cancel(mode="after_turn"):仍 drain;审计有取消标记。

五步全过才算 smoke。任一步靠脑补,记入 _w/streaming-smoke-checklist.md

文档口径

对外:「流式改善等待体验;完成以服务端排空与业务状态为准。」对内 runbook:drain 辅助函数位置、审批续跑样例、取消与重连策略。

事件名速查(写进代码注释)

RunItemStreamEvent.name 固定集合里,生产最常用的是:

name 何时出现 UI/审计建议
message_output_created 一条完整消息产出 可刷新「最终气泡」
tool_called 普通函数工具被调用 进度:正在调用 X
tool_output 工具结果返回 记录输出摘要(注意脱敏)
handoff_requested 请求交接 不要当业务成功
handoff_occured 交接发生(官方拼写) 更新 current_agent 展示
reasoning_item_created 推理项(若模型提供) 默认不对终端用户全文展示
mcp_approval_requested MCP 审批请求 走与 tool 审批同类 HITL

把这张表贴进服务代码注释,比口头说「我们处理了 stream 事件」更可审。

前端节流与后端 drain 分离

常见错误是把「UI 每秒渲染」和「后端消费事件」绑死:UI 卡了就停迭代器。正确拆法:

  1. 后端任务负责 async for 直到结束或明确 cancel。
  2. UI 订阅环形缓冲,允许丢弃中间 delta,但不许要求后端停 drain。
  3. 断连时:标记 client_gone=true,后端仍 drain;必要时 cancel(mode="after_turn") 并写审计。

这样用户刷新页面时,你仍能回答「那次 run 到底有没有完成」。

与 WebSocket 示例的对齐方式

官方示例仓库里有流式 + 审批循环的写法(如 examples/basic/stream_ws.py 一类)。你自己接 WebSocket 时,至少对齐三点:先消费事件;遇 interruptions 再问人;批准后用 state 续跑而不是新开用户句。缺任何一点,都会在「第二轮工具」上出现重复执行或丢审批。

测试策略(减少 flake)

  • 单测:对 drain 辅助函数注入假事件序列,断言必须读到哨兵「迭代结束」。
  • 集成:真实 key 下跑「短文本 + 强制 tool」两条。
  • 混沌:中途取消、断连、审批拒绝各一条。
  • 禁止:在 CI 里 break 后仍断言 final_output

成本与延迟笔记

流式不会让模型「算得更快」,它主要改善首字时间与进度可见性。若工具很多,语义事件比 raw 更适合做进度;raw 全量推到浏览器可能反而拖垮弱网用户。生产上对 raw 做聚合(每 50ms 刷一次)是体验优化,不是协议偷工。

半截输出的用户沟通模板

当 drain 未完成或出现 interruption 时,不要让前端显示「已完成」。可用三态:

  • generating:仍在收事件
  • needs_approval:流已排空且存在 interruptions
  • completed / failedis_complete 且业务态已落库

把 SDK 的流态与业务态分开命名,客服与值班才不会把「字出完了」理解成「单已办结」。

和 handoffs 联调时的最小断言

若 triage 可能交接:

  1. 订阅 agent_updated_stream_event,记录名字序列。
  2. 断言最终 current_agent(或 last_agent)符合路由预期。
  3. 不要只断言最终字符串包含「退款」------那可能是 triage 幻觉。

流式的价值之一,正是让路由过程可观测。

日志脱敏

tool_output 与 raw delta 可能含 PII。生产默认:审计存哈希与截断,完整内容进受限存储。UI 展示前做一次红线过滤。流式不等于「全量对外广播」。

总结

streaming 的工程价值是可控的增量可见性 ,不是「更快结束业务」。选型先问:要不要中间 UI / HITL?要就 run_streameddrain ;不要就 run。生产禁区的共同主题是:可见增量 ≠ 运行完成 ≠ 业务成功。把这三层拆开,流式才能从演示走进可运维。

相关推荐
Momo__8 小时前
AI 该不该"踩刹车"?三大巨头为何一边狂奔一边喊停
aigc·openai·ai编程
全栈弄潮儿9 小时前
让 AI 先写方案,再写代码
aigc·openai·ai编程
馒头不想说话1 天前
小芒果AI智能运营助手:AI写稿+自动发布,一键搞定20+平台
openai
VIP_CQCRE1 天前
在 Visual Studio 里接入 Ace Data Cloud:用 OpenAI 兼容接口提升 AI 编程效率
openai·api·ai编程·visual studio·ace data cloud
全栈弄潮儿1 天前
用 AI 拆一个真实需求:从模糊描述到开发任务清单
aigc·openai·ai编程
Sophnet云平台1 天前
2026:AI Agent应用元年,企业从试点到核心业务的跨越逻辑
网络·人工智能·llm·openai·agent
VIP_CQCRE1 天前
在 Visual Studio 里接入 Ace Data Cloud:用 LMLocal 打通 OpenAI 兼容 AI 编程体验
大模型·openai·ai编程·visual studio·ace data cloud
AIGC大时代1 天前
OpenAI Agents SDK 工程笔记:handoffs 多 Agent 交接与生产禁区
openai·multi agent·生产禁区·handoffs·triage
时空未宇1 天前
Codex 桌面版通过 SSH 访问 Docker 编译环境
语言模型·openai·codex