附录 F|一枪采样:模型看见什么、失败走哪条、怎么钉住
先别把 messages[] 和「能跑」当成同一件事
读完 B 组和 F3,下一件错事是打开 Responses 请求,把 input 当成聊天记录,把 5xx 和沙箱拒绝丢进同一个 retry(),再用肉眼看 TUI 证明循环对了。
判断先说清楚:一次采样的合同是 Prompt。 三块:input(历史投影)、tools(这一 Step 冻住的 spec)、base_instructions(模型/协作模板)。课程判断在 B7;这一附录只钉 怎么测:mock SSE、失败分流、test_codex。
这一附录服务 F3 的 P0/P1,不新开产品层。
1. Prompt 三块,不要焊成一段 system
build_prompt(core/src/session/turn.rs:1508-1525)几乎只做赋值:
| 字段 | 从哪来 | 不是什么 |
|---|---|---|
input | history.for_prompt(modalities)(:1571-1576) | 不是 TUI transcript,也不是 raw AGENTS.md 文件 |
tools | step_context.tool_router.model_visible_specs() | 不是 Handler 注册表全文;看不见的不能 call |
base_instructions | Session::get_prompt_base_instructions()(session/mod.rs:1409-1426) | 不是项目说明书。provenance 是 Model 或 Custom(/review 换的是这一块) |
for_prompt 按模型 modality 剥图/音频(context_manager/history.rs:392-396)。历史里已经落盘的 fragment——环境、AGENTS.md、TurnAborted——以 user 角色的带标记 item 出现,不是改 base 文本。C4 的 Personality 只可能动 base 里 # Personality 一节;C1 的说明书走 B4 的差量注入。
parallel_tool_calls 在 build_prompt 里写死 true(turn.rs:1518)。能不能真并行,仍看 B2 的门闩:工具自己报名,默认不准。
没有「三块拆开」会怎样?你会把 AGENTS.md 写进 system,换模型就丢政策;或把 MCP 目录整份塞进 base,compact 永远在救火。B4 禁止 system +=,合同就在这个 struct。
2. 一次 apply_patch 在两枪之间发生什么
不要背 JSON。跟 tool_harness.rs 里那条路径(:324-388):
- 用户话进
TurnInput→start_or_steer_turn。 - 第一枪 mock SSE:
ev_apply_patch_custom_tool_call。模型「看见」的是上一节那份Prompt;吐出的是 freeform 补丁,不是sed。 - Orchestrator:审批(测试里
Never)→ 选沙箱 → Runtime 落盘。Event:FileChangestarted/completed。 - 补丁结果变成
input里的 tool output。 - 第二枪 mock SSE:
ev_assistant_message("patch complete")。同一ModelClientSession(B1),同一 Turn,新的 Step 可以复用 router。
第二枪的 input 比第一枪多了一对 call/output。resume 能重建,是因为这两枪的 item 都进了 rollout(B4),不是因为测试进程还活着。
shell 里的 apply_patch <<EOF 会被拦截成同一套 Runtime(附录 E)。学习者抄循环时,断言应该落在 文件内容和 Event,不要断言命令字符串。
3. 失败谱:采样、窗口、工具、额度不是一种 retry
run_sampling_request 的内层 loop(turn.rs:1566-1647)只重试 Responses 流。默认最多 5 次(DEFAULT_STREAM_MAX_RETRIES,model-provider-info/src/lib.rs:30, 443-445)。用尽且还能切传输,才 Session 级 HTTP fallback(responses_retry.rs:92-106,B1)。远程 compact 故意把流重试压到 2(附录 C),别混用。
| 失败 | 谁处理 | 不准做的 |
|---|---|---|
| 流断开 / 可重试 5xx | handle_retryable_response_stream_error;重试时重新 for_prompt | 每枪新建 ModelClientSession;把 compact 的重试预算用光 |
ContextWindowExceeded | 立刻返回(turn.rs:1613-1615),交给采样前 compact / 外层 | 在采样 loop 里截断 input |
UsageLimitReached | 记 rate limits,返回(:1617-1622) | 当连接失败去重连 |
| 不可重试的采样错误 | !err.is_retryable() 直接失败(:1632-1633) | 盲 sleep 再 POST |
| 沙箱拒绝 | Orchestrator 升级再试(B3) | 把 exit 2/126/127 当拒绝 |
| 工具/MCP 超时 | 各工具自己的顶(MCP 默认 300s,D3);结果回模型 | 卡死整条 Turn 当「模型挂了」 |
| 额度低 | 记忆管道跳过(C2);自动化不自动停(E7) | 当成 --yolo 或关沙箱 |
窗口满必须发生在 下一枪采样之前(B4)。采样已经超窗,再改数组就是 rewrite。工具失败是下一枪的 item,不是采样重试。
没有这张表会怎样?一个 retry() 把 compact、审批升级、MCP hang 搅在一起:要么把用户最后一句吞掉,要么把 typo 升出沙箱。
4. 对照:test_codex + mount_sse_once 钉的是循环,不是 widget
仓库自己的集成测试在 core/tests/suite/。夹具:
test_codex()(core/tests/common/test_codex.rs:1363)起一套连 mock provider 的 Sessionmount_sse_once(responses.rs:1108)只允许一次/responsesPOSTev_*+sse(...)编模型吐出的事件wait_for_event听EventMsg,不是读 stdout 颜色
P0 合同可以缩成 apply_patch_tool_executes_and_emits_patch_events(tool_harness.rs:324)在观察什么:
- 第一枪是补丁 call,第二枪是「做完了」
- 磁盘上出现
notes.txt - 过程中有 FileChange 类 Event
抄自己的 harness 时,用同等断言。不要新写「prompt 里包含某字符串」——那是钉实现。F3 的完成定义是可观测行为:审批(或 Never)、patch、完成。
跑这一条即可(在 codex-rs/):
just test -p codex-core apply_patch_tool_executes_and_emits_patch_events不要为此开全量 just test。完整套件要问过再跑(仓库 AGENTS.md)。
没有 mock SSE 会怎样?你只能对着真模型手工点。P0「能跑完一轮」会变成不可回归的演示。
5. 结语:带走一句话
一枪采样的合同是 Prompt 的三块——历史投影、这一 Step 的 tools、base 指令——AGENTS.md 在 input 里;流才重试,窗口满走 compact,工具失败当下一枪的 item;用 test_codex 钉文件和 Event,不要钉 TUI。判断回 B7;枪怎么飞出去回 G1。