阶段 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

相关推荐
京东云开发者1 小时前
【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手(高级篇)
前端·agent·音视频开发
云水初2 小时前
【agent篇】RAG 知识库构建避坑指南
开发语言·python·学习·agent·rag
新知图书2 小时前
8.1 智能体的心跳执行模式:以定时器为核心(智能体工程)
人工智能·agent·ai agent·智能体
墨心@2 小时前
阶段 4:事件总线
人工智能·语言模型·大语言模型·agent·codex·harness
前端开发江鸟2 小时前
学完 Agent 开发基础后,我准备把它真正用在文字创作里
agent
Smoothcloud_润云2 小时前
从“模型服务”到“Agent 调度”:AI 推理基础设施为什么正在重构?
llm·agent·gpu
安逸sgr2 小时前
卷积神经网络 CNN 是什么?为什么适合处理图像?
人工智能·ai·大模型·agent·智能体
ShallJason2 小时前
Java与Python MCP跨语言调用中的时间序列化问题:根因剖析与完整解决方案
agent
心再无旁骛2 小时前
anywhere-labs/deepseek-harness-desktop 如何围绕上游演进:Submodule、版本溯源与非 Fork 架构
agent
用户3126874877202 小时前
一条 Prompt 就能劫持你的 Agent!OWASP Agentic Top 10 与零信任防御实战
agent