1
00:00:00,100 --> 00:00:06,432
本内容改编自小山学堂《学 AI 产品，从入门到精通》，为二次演绎配音版。

2
00:00:06,382 --> 00:00:08,930
这是 Codex 系列的第五集。

3
00:00:08,930 --> 00:00:19,579
上一章我们讲了 ContextualUserFragment，它是往模型上下文里塞一段带标记文字的登记口，编译器从此只接受实现了这个 trait 的结构体。

4
00:00:19,579 --> 00:00:21,586
听起来已经很稳了。

5
00:00:21,586 --> 00:00:31,514
这一集要讲的是，为什么就算类型系统放行了，一份改动仍然可能把缓存打掉、把窗口撑满、或者让旧会话恢复失败。

6
00:00:31,514 --> 00:00:34,543
一句话说清本集要解决的问题。

7
00:00:34,543 --> 00:00:42,079
类型是入口，它证明形状；但形状对了，成本还可以不合法，又长、又勤、又无界。

8
00:00:42,079 --> 00:00:49,579
挡住这些后果的，是仓库根上十行禁令，外加一份会把同一节原文再读一遍的评审 skill。

9
00:00:49,579 --> 00:00:51,406
为什么值得关心。

10
00:00:51,406 --> 00:00:55,757
因为这类成本编译器证明不了，日常命令也补不上。

11
00:00:55,757 --> 00:01:05,552
just fmt 和 just test 挡得住格式、锁文件漂移和一部分接口破坏，它们读不懂你是不是每轮注入了一次完整的 git status。

12
00:01:05,552 --> 00:01:10,000
这类过程属性，换语言重写也还得另开一扇门。

13
00:01:10,000 --> 00:01:25,436
这一集走一遍四份看起来都很负责的改动，看六条禁令各拦住哪一份；再讲第二条盯的缓存前缀，以及第一条和压缩的关系；最后看六条里哪几条落在代码里，哪几条只能靠人和 skill 守。

14
00:01:25,436 --> 00:01:27,431
先记住这句骨架。

15
00:01:27,431 --> 00:01:30,640
类型管形状，评审管成本。

16
00:01:30,770 --> 00:01:33,195
先看这六条禁令本身。

17
00:01:33,145 --> 00:01:40,357
它们写在仓库根的 AGENTS.md 里，标题叫 Model visible context，位置是第九十一到一百行。

18
00:01:40,357 --> 00:01:46,559
同一段原文还被抄进了 .codex/skills/code-review-context/SKILL.md 的第七到十三行。

19
00:01:46,559 --> 00:01:50,946
评审机器人读到的就是这六条，没有第二套解释。

20
00:01:50,946 --> 00:01:57,148
开头有一句定位，Codex maintains a context，说的是发给模型的那段消息历史。

21
00:01:57,148 --> 00:02:01,403
第一条，No history rewrite，上下文必须增量构建。

22
00:02:01,403 --> 00:02:06,691
第二条，避免频繁改动上下文，那会造成缓存未命中。

23
00:02:06,691 --> 00:02:14,564
第三条，不允许无界条目，注入模型上下文的每一样东西都必须有尺寸上界和硬上限。

24
00:02:14,564 --> 00:02:17,701
第四条，单条不得超过一万 token。

25
00:02:17,701 --> 00:02:24,251
第五条，新的单条如果可能超过一千 token，标成 P0，需要额外的人工评审。

26
00:02:24,251 --> 00:02:33,013
第六条，所有注入的片段必须在 core 下的 context 目录里定义成结构体，并实现 ContextualUserFragment 这个 trait。

27
00:02:33,013 --> 00:02:35,934
念完你会发现它们分成了三组。

28
00:02:35,934 --> 00:02:42,761
第六条管入口，第三条和第四条管上界，第一条、第二条和第五条管时机与注意力。

29
00:02:42,761 --> 00:02:45,645
三条一组，各盯一类成本。

30
00:02:45,645 --> 00:02:48,446
这个分组不是课程自己加的。

31
00:02:48,446 --> 00:02:50,742
看源码落点就清楚了。

32
00:02:50,742 --> 00:03:13,474
第六条的落点是注入有没有登记成 ContextualUserFragment，第三条的落点是有没有硬上限，第四条的落点是单条会不会超过一万，第五条的落点是新种类若可能过一千就标 P0，第二条的落点是会不会每轮改已经发出去的前缀，第一条的落点是这次改动是追加新行还是改历史里的旧行。

33
00:03:13,474 --> 00:03:15,337
再补一层理解。

34
00:03:15,337 --> 00:03:19,748
第六条和第三条、第四条先问的是入口和上界。

35
00:03:19,748 --> 00:03:25,637
没走 trait，在处理器里直接 format 一段消息，编译能过，评审打回。

36
00:03:25,637 --> 00:03:29,147
走了 trait 但没写硬上限，同样打回。

37
00:03:29,147 --> 00:03:38,161
工具输出侧默认按字节一万截断，超了从中间砍，前面加一行警告，告诉模型原来的 token 数和总行数。

38
00:03:38,161 --> 00:03:39,928
出处要说清楚。

39
00:03:39,928 --> 00:03:49,027
这几条依据的是本地仓库 openai 斜杠 codex，核对的文件是 AGENTS.md，核对日期是二零二六年八月二十二日。

40
00:03:49,027 --> 00:03:51,683
代码块保留源码原文。

41
00:03:51,820 --> 00:03:53,813
现在过四份改动。

42
00:03:53,763 --> 00:04:04,171
甲，给 environment 上下文加一个 git_status 字段，每轮写入完整的工作区状态，理由是模型就不用猜工作区脏不脏。

43
00:04:04,171 --> 00:04:08,787
这份撞第二条，因为它每轮都在改已经发出去的前缀。

44
00:04:08,787 --> 00:04:13,907
乙，在 turn.rs 里 format 一句提示，提醒模型跑测试。

45
00:04:13,907 --> 00:04:19,220
这份撞第六条，没走 trait，是在处理器里直接拼一段消息。

46
00:04:19,220 --> 00:04:25,397
丙，新建一个 fragment，把整个源文件塞进模型可见文本，不写截断。

47
00:04:25,397 --> 00:04:30,421
这份撞第三条和第四条，无界，而且可能超过一万 token。

48
00:04:30,421 --> 00:04:35,926
丁，在 AGENTS.md 改动时，就地改写历史里那一条说明。

49
00:04:35,926 --> 00:04:43,234
这份撞第一条，还有 breaking change 的第五项，那一项点名的正是从已有 rollout 恢复会话。

50
00:04:43,234 --> 00:04:49,893
关键点是，这四份都能写出干净的结构体和干净的测试，类型系统会放行。

51
00:04:49,893 --> 00:04:58,462
编译器只看有没有结构体、有没有标记，不看这条文字每轮变不变、有多长、会不会改旧会话。

52
00:04:58,462 --> 00:05:02,104
类型放过的东西，正是禁令要拦的东西。

53
00:05:02,104 --> 00:05:04,027
换写法会怎样。

54
00:05:04,027 --> 00:05:10,938
甲和丙改成增量加硬上限，红灯会灭；可能超过一千的仍然要标 P0。

55
00:05:10,938 --> 00:05:14,676
乙没走类型，换写法也过不了第六条。

56
00:05:14,676 --> 00:05:19,256
丁动的是旧行本身，换写法仍然是给旧消息打补丁。

57
00:05:19,256 --> 00:05:21,515
这里补一句教学说明。

58
00:05:21,515 --> 00:05:31,804
这四份改动和写法切换是课程化设定，用来展示六条禁令各自盯的那一类成本；逻辑轨迹右侧的行号对应的是真实源码。

59
00:05:31,950 --> 00:05:35,157
第二条盯的是时机，能追加就追加。

60
00:05:35,107 --> 00:05:36,753
为什么这么严。

61
00:05:36,753 --> 00:05:42,354
模型请求里的消息历史是从前往后拼的，缓存按前缀匹配。

62
00:05:42,354 --> 00:05:46,188
只要前面任何一处变了，后面全部作废。

63
00:05:46,188 --> 00:05:54,073
所以每轮往 environment 里写一次完整的 git status，等于每轮都改前缀，缓存命中率直接归零。

64
00:05:54,073 --> 00:05:57,655
这个代价是按轮翻倍的，不是一次性的。

65
00:05:57,655 --> 00:06:00,155
换成增量追加就不一样。

66
00:06:00,155 --> 00:06:06,549
旧内容不动，新内容挂在后面，前缀仍然对得上，缓存能从上一层继续用。

67
00:06:06,549 --> 00:06:08,664
有个容易忽略的细节。

68
00:06:08,664 --> 00:06:18,749
就算你的 git status 这一轮和上一轮内容一模一样，只要它是被重写而不是被追加，位置或格式上的任何抖动都会打断前缀。

69
00:06:18,749 --> 00:06:23,460
所以第二条说的是避免频繁改动，不是避免大改动。

70
00:06:23,460 --> 00:06:25,035
源码里怎么落。

71
00:06:25,035 --> 00:06:35,696
会话级客户端把跨 turn 稳定和 turn 内粘滞拆开了，sticky token 不准跨 turn 重放，出处是 client.rs 的第二百六十二到二百七十四行。

72
00:06:35,696 --> 00:06:45,648
Guardian 审查会话故意复用同一条 trunk，好保住 prompt cache key，出处是 guardian 下 review.rs 的第九百三十二到九百三十四行。

73
00:06:45,648 --> 00:06:47,679
反过来看什么是对的。

74
00:06:47,679 --> 00:06:53,112
能追加就追加，这一条不只适用于注入，也适用于工具清单。

75
00:06:53,112 --> 00:07:00,335
每轮换一次工具清单和每轮换一次环境描述，性质完全一样，都是在前缀上动刀。

76
00:07:00,335 --> 00:07:07,198
所以源码里客户端才要把跨 turn 稳定和 turn 内粘滞拆成两件事，分别管。

77
00:07:07,198 --> 00:07:15,383
还有一道集成测试盯着这件事，prompt_caching.rs 要求连续两轮的 instructions 和 tools 必须一致。

78
00:07:15,383 --> 00:07:20,011
这是六条里少有的、有自动化影子的地方之一。

79
00:07:20,170 --> 00:07:24,687
第一条是不许改写历史，上下文必须增量构建。

80
00:07:24,637 --> 00:07:27,978
最容易踩的场景是 AGENTS.md 改了。

81
00:07:27,978 --> 00:07:33,639
最省事的做法是找到历史里那一条 UserInstructions，把正文换掉。

82
00:07:33,639 --> 00:07:37,678
当前轮少占一条消息，token 看起来还降了。

83
00:07:37,678 --> 00:07:43,026
但从旧 rollout 恢复时，读到的是被改过的文本，会话对不上。

84
00:07:43,026 --> 00:07:47,822
压缩看起来也像在改历史，旧窗口从 live history 里消失。

85
00:07:47,822 --> 00:07:54,709
如果有人把压缩做成打开历史文件改一行，第一条和 breaking changes 第五项会一起被踩中。

86
00:07:54,709 --> 00:07:57,822
Codex 的做法是给压缩换一个定义。

87
00:07:57,822 --> 00:08:08,820
replace_compacted_history 把新表整表装进 live history，旧内容以带 replacement_history 的 CompactedItem 追加到 rollout，不回改旧行。

88
00:08:08,820 --> 00:08:12,762
注释里写明，压缩开启一个新的历史窗口。

89
00:08:12,762 --> 00:08:22,774
出处是 session 下 mod.rs 的第三千三百七十三到三千四百一十八行，还有 AGENTS.md 的第九十五行和第一百一十行。

90
00:08:22,774 --> 00:08:29,961
生产路径里那个就地换表的函数叫 replace_history，上面标了只在测试下编译。

91
00:08:29,961 --> 00:08:34,805
也就是说，就地换表这条路在生产里是被关掉的。

92
00:08:34,805 --> 00:08:37,197
远端压缩还有一层滤网。

93
00:08:37,197 --> 00:08:46,644
服务端送回来的 transcript 不可信，developer 消息直接丢掉，再由本地按当前 world state 把带 marker 的 fragment 重新渲染进去。

94
00:08:46,644 --> 00:08:55,202
历史继续增量构建，压缩继续换窗口，出处是 compact_remote.rs 的第三百五十四到三百七十二行。

95
00:08:55,202 --> 00:08:57,786
为什么这个形状长期成立。

96
00:08:57,786 --> 00:09:02,750
追加写、用快照换窗口，是日志系统的通用形状。

97
00:09:02,750 --> 00:09:09,312
事件溯源换的是投影，LSM 树换的是 SSTable，都不回改已经写下的旧行。

98
00:09:09,312 --> 00:09:12,606
禁止就地更新，恢复才有得对。

99
00:09:12,750 --> 00:09:17,159
一个很实在的事实，六条禁令里零条有专用 lint。

100
00:09:17,109 --> 00:09:24,476
第一条和第二条有集成测试的影子，前面说过的 prompt_caching.rs 盯的就是第二条。

101
00:09:24,476 --> 00:09:27,782
第三条和第四条靠局部 cap 兜住。

102
00:09:27,782 --> 00:09:36,796
工具输出侧默认按字节一万截断，超了从中间砍，前面加一行警告，告诉模型原来的 token 数和总行数。

103
00:09:36,796 --> 00:09:47,577
出处是 protocol.rs 的第三千一百一十二行、model_info.rs 的第一百六十七行，以及 output-truncation 下 lib.rs 的第十二到二十四行。

104
00:09:47,577 --> 00:09:52,938
通用附加上下文另卡在一千 token，这正好是第五条的门槛。

105
00:09:52,938 --> 00:09:58,960
已有的种类被代码截到一千，新的、可能超过一千的种类才需要人看。

106
00:09:58,960 --> 00:10:08,563
源码里没有 P0 枚举，也没有 lint 去估一个新结构体的 body 会不会超过一千，出处是 additional_context.rs 的第五行。

107
00:10:08,563 --> 00:10:10,883
所以第五条纯靠人。

108
00:10:10,883 --> 00:10:18,226
第六条拦得住没实现 trait 就走 render_full 的路，拦不住在处理器里直接拼一段消息。

109
00:10:18,226 --> 00:10:21,147
这也解释了为什么 skill 要存在。

110
00:10:21,147 --> 00:10:28,058
同一段六条原文被抄进 SKILL.md，评审机器人读到的就是这六条，没有第二套解释。

111
00:10:28,058 --> 00:10:30,967
规则只有一份，就不会漂移。

112
00:10:30,967 --> 00:10:33,996
执行主体是评审，不是编译器。

113
00:10:33,996 --> 00:10:36,952
总量谁来管，六条没写数字。

114
00:10:36,952 --> 00:10:38,924
代码用两层补上。

115
00:10:38,924 --> 00:10:49,176
模型窗口的 full_context_window_limit 是硬顶；会话树的 RolloutBudget 按加权 token 记账，用尽就对整棵 thread 停写。

116
00:10:49,176 --> 00:10:55,342
四十条都合法、每条九千，总量仍然会被满窗或会话预算拦住。

117
00:10:55,342 --> 00:11:01,051
出处是 context_window.rs 和 rollout_budget.rs。

118
00:11:01,210 --> 00:11:03,251
横向看另外两家。

119
00:11:03,201 --> 00:11:09,835
DeepSeek 的 Harness 在仓库根 AGENTS.md 第一百零七行写的是，Model-visible 等价于 logged。

120
00:11:09,835 --> 00:11:18,033
送到模型请求里的东西，必须能从会话日志重建；新的模型可见输入，必须对应一条 session 事件。

121
00:11:18,033 --> 00:11:25,436
它管的是可见与落盘对齐，不管这一条是不是无界、是不是每轮改、单条是不是超过一万。

122
00:11:25,436 --> 00:11:28,009
它还有个做法值得单独学。

123
00:11:28,009 --> 00:11:30,160
被否决过的路会立档。

124
00:11:30,160 --> 00:11:45,152
一篇讨论要不要把压缩的定义包和唯一实现折在一起的笔记，状态写明 rejected，还单独留下一节被考虑过的替代方案，说明将来可能有远端或召回后端，但这不构成现在就拆包的理由。

125
00:11:45,152 --> 00:11:54,118
Codex 这一侧没有这样的目录告诉后来者，某次每轮注入 git status 为什么被撤回，后来者只能从测试和注释反推。

126
00:11:54,118 --> 00:11:56,342
Claude Code 这一格是空的。

127
00:11:56,342 --> 00:12:06,402
用 Model visible context、ContextualUserFragment、unbounded context 这几个词去检索还原源码，找不到公开的上下文注入评审规范。

128
00:12:06,402 --> 00:12:18,445
REVIEW.md 是给审查模型看的产品化规则，用来标记该不该在审查里指出某类问题，和运行时往模型上下文里塞东西的工程红线不是同一层。

129
00:12:18,445 --> 00:12:22,243
这一格空着，等新的还原源码来填。

130
00:12:22,243 --> 00:12:25,236
收尾，四条可以带走的原则。

131
00:12:25,236 --> 00:12:28,601
一，类型管形状，评审管成本。

132
00:12:28,601 --> 00:12:36,883
别指望类型系统替你管住每隔一轮就变一次的文字，那是过程属性，换语言重写也得另开一扇门。

133
00:12:36,883 --> 00:12:41,017
日常命令也补不上这扇门，它们读不懂语义。

134
00:12:41,017 --> 00:12:44,010
二，先问入口，再问上界。

135
00:12:44,010 --> 00:12:49,839
任何注入都先过登记，再问有没有硬上限，最后才谈内容写得对不对。

136
00:12:49,839 --> 00:12:52,616
顺序错了会浪费一整轮评审。

137
00:12:52,616 --> 00:12:55,332
三，只追加，不改旧行。

138
00:12:55,332 --> 00:13:00,560
压缩不是改写历史，而是开一个新窗口并留下可回放的记录。

139
00:13:00,560 --> 00:13:04,719
就地更新一时省 token，代价是恢复时对不上。

140
00:13:04,719 --> 00:13:06,847
四，规则只有一份。

141
00:13:06,847 --> 00:13:12,556
把禁令原文抄进评审 skill，让机器人和人读到的是同一段文字。

142
00:13:12,556 --> 00:13:15,765
有两套解释，就会有两套执行。

143
00:13:15,765 --> 00:13:17,520
留一道练习给你。

144
00:13:17,520 --> 00:13:26,113
有人要在 session 下的 turn.rs 里 format 一段 workspace_map，把当前目录树塞进去，声称只有调试时才开。

145
00:13:26,113 --> 00:13:31,173
按六条逐条过一遍，哪几条亮红，改成什么样才能留。

146
00:13:31,173 --> 00:13:40,272
进阶一问是，如果目录树最坏超过一千 token，PR 标题要不要标 P0，以及源码里有没有对应的属性宏替你标。

147
00:13:40,272 --> 00:13:42,375
这一集就到这里。

