雨天小六

读懂 Codex(5.14):Text Delta 的转发和完整 Message 组装

· 更新于 2026-08-02 · 专栏:读懂 Codex

#Codex#Agent Runtime#Responses API#流式协议#软件架构

文字 Delta 用来降低首字延迟,完整 Assistant Message 才是可记录事实。Codex 还在两者之间放了流式文本解析器,避免把 citation 标记、Plan 模式控制块或尚未判定的前缀直接展示。

具体问题与启用条件

本节只处理 Assistant 文本的暂态展示和最终提交;Reasoning 在 5.15,工具参数在 5.16—5.17。

条件来源决定字段或状态对本机制的影响
活动类型active_item 可投影为 AgentMessageText Delta 进入消息 parser
扩展贡献者TurnItemContributor 非空推迟流式 Started/Delta,防止最终改写不一致
Plan 模式ModeKind::Plan解析 proposed plan 段而非普通显示

协议、类型与状态所有权

状态或协议所有者生命周期关键不变量
AssistantMessageStreamParsers一次采样按 item_id 保存到 Done/Completed仅暂态,不写 History
AgentMessageContentDeltaSession event stream即时事件携带 thread/turn/item ID
完整 Message ResponseItem服务端 Done 事件History/Rollout最终文本是权威内容
last_agent_message采样结果当前 Turn来自完成项而非 Delta 拼接

机制调用链如下:

OutputItemAdded(Message)
→ 建立/seed parser 与 ItemStarted
→ OutputTextDelta
→ parser 按 item_id 处理 citation/plan/空白前缀
→ AgentMessageContentDelta
→ OutputItemDone(full Message)
→ flush parser 尾部
→ finalize TurnItem
→ History + ItemCompleted
Assistant Message 的暂态 Delta 与完整项双路径
图 5.14-1:UI 低延迟接收可见片段,History 只接收服务端完成项。

机制怎样工作

Item Added 可能已带一段初始文本,parser 先 seed_item_text,确保后续 delta 不与它重复。每个 Text Delta 读取 active item ID;普通 AgentMessage 经过 parser,去掉不向用户展示的 citation 标记,并按 Plan 模式分割 proposed plan。解析器会缓存尚不能确定是否为控制标记的行和前导空白,所以 Done 与整个 Response Completed 都必须显式 flush。

最终完整 Message 不靠客户端 Delta 累加生成,而是来自 OutputItemDone 的服务端 ResponseItem。它再经过贡献者、citation 与 plan 隐藏标记清理,发 ItemCompleted 并立即记录到 History/Rollout。若存在 TurnItemContributor,流式展示被延迟,因为贡献者可能修改最终 TurnItem;即时显示旧版本会让 Started/Delta 与 Completed 内容不一致。

Python 风格伪代码

这段伪代码保留生产实现中会改变结果的状态、分支和异步边界;认证 SDK、遥测字段和 Rust 所有权样板被折叠为明确的领域对象。

class MessageStreamState:
    def __init__(self, plan_mode: bool):
        self.parsers: dict[str, AssistantTextParser] = {}

    async def start(self, item: MessageItem, stream_to_client: bool):
        parser = self.parsers.setdefault(item.id, AssistantTextParser())
        visible_seed = parser.seed(item.raw_text)
        if stream_to_client:
            await emit_item_started(item.with_text(visible_seed))

    async def delta(self, item_id: str, text: str, stream_to_client: bool):
        if not stream_to_client:
            return
        parsed = self.parsers[item_id].push(text)
        if parsed.visible_text:
            await emit_content_delta(item_id, parsed.visible_text)

    async def done(self, full_item: MessageItem):
        tail = self.parsers.pop(full_item.id).finish()
        if tail.visible_text:
            await emit_content_delta(full_item.id, tail.visible_text)
        finalized = strip_hidden_markup(full_item)
        await emit_item_completed(finalized)
        await history.record(full_item)  # 记录协议完整项

失败、取消与恢复

文字解析缓冲和最终消息提交边界
图 5.14-2:parser 只控制展示;完整协议 Item 独立进入持久历史。
故障或边界已发生的状态对上层的结果能否直接重试恢复动作
Text Delta 无 active item无法给出 item_id内部错误中止/诊断协议序列
流在 Item Done 前断开UI 可能见到暂态片段不生成完整 Message重试时不把片段进 History
parser 尾部未 flush最后标记/文字仍缓存UI 缺尾部不适用Done 与 Completed 双重 flush
Contributor 改写最终项流式旧版本可能已展示因此提前禁用 stream不适用只展示最终 Item

设计取舍与验证

服务端完整项作为权威来源避免客户端拼接差异和断流污染,代价是 Delta 与最终项要走两条路径。解析隐藏标记会增加一点展示延迟,却阻止半个 citation/plan tag 闪到 UI。对贡献者禁用流式输出牺牲 TTFT,换取扩展改写的一致性。

可验证契约证据方式预期结果
Delta 共享同一 item_iditems 集成测试Started、所有 Delta、Completed ID 一致
Review/Contributor 场景抑制 Delta专用事件断言客户端只见最终项
citation/plan 控制文本不显示parser 单元与模式测试可见文字正确,原始项仍可记录

Mini Codex 对照

Mini Codex 可以先只处理普通文本,但仍应维护 item_id -> parser,并把 UI buffer 与 History 分开。隐藏标记 parser 可作为可替换策略,避免把产品语法焊死在事件循环。

本节边界

Assistant 文字的两阶段交付已说明。5.15 用相同活动项边界处理两套 Reasoning 流:摘要和原始内容。

评论


← 返回文章列表