阶段 1:一次模型调用

阶段 1:一次模型调用(模型面)

对应代码:stage01_model.py

学习目标

理解"模型服务端只产生事件流,不循环"这件事。把多次请求串成闭环的永远是本地代码------这是理解整个 Agent 架构的第一块基石。

源码锚点

概念 位置 说明
客户端会话 codex-rs/core/src/client.rsModelClientSession 一个 Turn 内复用连接;sticky routing/WebSocket 是优化,先忽略
SSE 事件归约 codex-rs/codex-api/src/sse/responses.rs:387-528 response.failed 错误分类、response.completed 判定、流提前 EOF → Stream 错误
流空闲超时 codex-api/src/sse/responses.rs:505 timeout(idle_timeout, stream.next()),默认 5 分钟
provider 常量 model-provider-info/src/lib.rs:26-31 各类超时/重试的家

一条物理流的生命周期(分析文档 7.12 节的"物理响应流状态机"):

text 复制代码
Created -> OutputItemAdded -> (delta)* -> OutputItemDone* -> Completed

四层包含关系先记住形状:

text 复制代码
一个公开 Turn
  └─ 1..N 个模型轮次
       └─ 1..M 个物理流尝试
            └─ 0..K 个完成的 ResponseItem

代码走读

  • Item:对应 ResponseItem 的教学子集(message / function_call / function_call_output / reasoning 四种形态)。
  • FakeModel.stream():剧本模型。每个剧本元素吐 output_item.done 事件若干个 + 一个 response.completed之后每个阶段都用它离线测试 ------这正是 codex 自己 core/suite 集成测试的做法。
  • ResponsesClient.stream():真实客户端。SSE 一行一个 data: <json>,对应 responses.rs 的解析。需要 OPENAI_API_KEY,可选 CODEX_BASE_URL / CODEX_MODEL

运行与预期输出

bash 复制代码
python stage01_model.py
# 三行:一个 function_call 的 output_item.done、一个 assistant message、一个 response.completed

练习

  1. FakeModelreasoning 类型的 item(type="reasoning"),并让 stream 吐出来。
  2. ResponsesClient 加"流提前断开"模拟:在 completed 前抛 RuntimeError("stream closed before response.completed")------这就是真实代码里 CodexErr::Stream 的由来(protocol/src/error.rs 注释明确标注它可自动重试)。
  3. 观察:如果剧本用完了会发生什么?(FakeModel 会返回 "(no script left)"------真实系统里这是流异常,不是正常完成。)

与真实实现的差距

  • 真实流的 delta 是细粒度的(output_text.delta),本阶段只保留 output_item.done------delta 属于展示层(分析文档 9.1 节),不影响闭环。

代码

python 复制代码
"""积木 2:Agent Loop。

真实对应物(codex-rs/core/src/session/turn.rs):
  - run_turn() 主循环(L272-547):每轮 drain -> 采样 -> follow-up 判定
  - needs_follow_up = model_needs_follow_up or has_pending_input(分析文档 7.7 节)
  - 外层 Task 循环在 tasks/regular.rs:77-92(本阶段不做,阶段 6 补)

核心认知:Agent Loop 是"模型-工具-模型"的外部编排循环,不是一个 while 里调模型。
退出条件不是"模型说了话",而是 needs_follow_up == false。

运行:python stage02_loop.py
"""

from __future__ import annotations

import asyncio
import json
from typing import Awaitable, Callable

from stage01_model import FakeModel, Item, SampleResult, ToolSpec

ToolHandler = Callable[[str, dict], Awaitable[str]]  # (name, args) -> output text


async def sample(model, history: list[Item], tools: list[ToolSpec]) -> SampleResult:
    """一次逻辑采样。对应 try_run_sampling_request() 的归约部分:
    收集完成的 item,识别工具调用,计算 last_agent_message / needs_follow_up。"""
    result = SampleResult(items=[])
    async for ev in model.stream(history, tools):
        if ev["type"] == "output_item.done":
            item: Item = ev["item"]
            result.items.append(item)
            if item.type == "function_call":
                result.needs_follow_up = True          # 模型侧 follow-up(7.7 节)
            elif item.type == "message" and item.text:
                result.last_agent_message = item.text
        elif ev["type"] == "response.completed":
            if ev.get("end_turn") is False:
                result.needs_follow_up = True
    return result


async def execute_tool(call: Item, handler: ToolHandler) -> Item:
    """对应 stream_events_utils.rs handle_output_item_done 的 Ok(Some(call)) 分支
    + parallel.rs 的结果转换:普通失败也包装成 output 回给模型(8.1 节),
    只有 fatal 才终止采样。"""
    try:
        out = await handler(call.name, json.loads(call.arguments or "{}"))
    except Exception as e:                              # 普通错误不终止采样
        out = f"tool error: {e}"
    return Item(type="function_call_output", call_id=call.call_id, output=out)


async def run_turn(model, user_text: str, tools: list[ToolSpec],
                   handler: ToolHandler) -> str | None:
    """对应 run_turn():输入用户消息,返回最终 agent message。

    不变量:call -> output 配对按模型发出调用的顺序进入历史(7.6 节)。
    """
    history: list[Item] = [Item(type="message", role="user", text=user_text)]
    while True:
        result = await sample(model, history, tools)
        history.extend(result.items)                    # 先记调用(事务语义)
        if not result.needs_follow_up:                 # follow-up 判定是唯一的退出条件
            return result.last_agent_message
        calls = [i for i in result.items if i.type == "function_call"]
        outputs = await asyncio.gather(*(execute_tool(c, handler) for c in calls))
        history.extend(outputs)                         # 工具结果 -> 下一轮 Prompt 的新事实


# ---- 演示:一个只会 echo 的工具 ----


async def shell_handler(name: str, args: dict) -> str:
    print(f"  [tool] {name} {args}")
    return "hi"


async def demo() -> None:
    model = FakeModel(
        [
            {"tool_calls": [{"call_id": "c1", "name": "shell", "arguments": {"cmd": "echo hi"}}]},
            {"text": "命令输出了 hi"},
        ]
    )
    tools = [
        ToolSpec(
            "shell",
            "run a command",
            {"type": "object", "properties": {"cmd": {"type": "string"}}},
        )
    ]
    final = await run_turn(model, "跑一下 echo", tools, shell_handler)
    print("final:", final)


if __name__ == "__main__":
    asyncio.run(demo())

原文为: https://github.com/3127651234jk-creator/learn-to-codex/blob/main/stage01-model.md

相关推荐
FanetheDivine16 分钟前
学习Agent开发9 OM 与前缀缓存
agent·ai编程
吴佳浩1 小时前
从 OpenClaw、Codex 到 Hermes,看懂 AI Agent 架构为什么正在收敛
人工智能·llm·agent
空堂与归3 小时前
迁移学习怎么落地?Transformers 库微调实战
人工智能·机器学习·自然语言处理·transformer·迁移学习
糖墨夕4 小时前
理解大语言模型:Agent 的“大脑”
前端·agent
冬奇Lab4 小时前
一天一个开源项目(第209篇):holaOS - Agent 原生的本地工作台
人工智能·开源·agent
荣合技术服务5 小时前
Codex 实战:用 AI 写运维脚本
运维·codex
夏文强5 小时前
DeepSeek Harness 权限与审批:给 Agent 上一把 human-in-the-loop 的安全阀
人工智能·开源·大模型·agent·deepseek
loong_XL6 小时前
生产级 Agent 开发方法论:速度、质量、价格与工程化
ai·大模型·agent·loop·智能体·vibe
jimidou7 小时前
子 Agent 能并行,却不能互相说话:Claude Code 里哪些活不该委派
agent·ai编程
DeepAgent7 小时前
AI Agent 项目赏析:DeerFlow 2.0 —— 一个真正“长跑“的 SuperAgent 是怎么设计出来的?
github·agent