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

2
00:00:06,382 --> 00:00:08,497
你有没有过这种感觉。

3
00:00:08,497 --> 00:00:13,509
看一个开源项目的介绍文章，读完记住的全是形容词。

4
00:00:13,509 --> 00:00:20,889
高性能、可扩展、生产级，每个字都认识，但你还是不知道它到底是怎么跑起来的。

5
00:00:20,889 --> 00:00:22,824
今天我们换个路径。

6
00:00:22,824 --> 00:00:25,492
不看宣传，直接看仓库。

7
00:00:25,492 --> 00:00:33,413
这一集要解的工程问题是，一个真正上了生产环境的 Coding Agent，它的代码到底是怎么组织的。

8
00:00:33,413 --> 00:00:42,560
我们手上的样本是 Grok Build 的本地源码副本，一个 Rust 写的、带终端界面、能调工具、能跑长会话的命令行产品。

9
00:00:42,560 --> 00:00:44,338
整集分四步走。

10
00:00:44,338 --> 00:00:51,009
第一步，从一个有七十九个成员的 Cargo Workspace 清单里，把产品的主干读出来。

11
00:00:51,009 --> 00:01:01,466
第二步，聊 Rust 这门语言在这套系统里带来了什么，同时划清一条很重要的线，哪些是源码能证明的，哪些只是我们自己的推断。

12
00:01:01,466 --> 00:01:07,944
第三步，从真正的 main 函数出发，一路追到第一轮模型采样，把调用链走通。

13
00:01:07,944 --> 00:01:15,516
第四步也是最硬的一块，看会话是怎么被隔离的，状态归谁所有，取消信号怎么传递。

14
00:01:15,516 --> 00:01:17,512
为什么要关心这个。

15
00:01:17,512 --> 00:01:25,372
因为只要你写过带界面的命令行工具，或者做过需要长会话的 Agent，你迟早要回答这几个问题。

16
00:01:25,372 --> 00:01:33,197
界面和逻辑要不要分开，一次对话的状态放哪，用户按了取消之后怎么让后台任务干净地停下来。

17
00:01:33,197 --> 00:01:36,598
这套源码给了一份可以直接抄的答卷。

18
00:01:36,598 --> 00:01:38,569
顺带说一句边界。

19
00:01:38,569 --> 00:01:44,447
这套副本没有 git 元数据，所以我们不声称它对应某个具体的提交版本。

20
00:01:44,447 --> 00:01:47,800
我们讲的是结构事实，不是考古。

21
00:01:47,956 --> 00:01:49,781
先看最外层。

22
00:01:49,731 --> 00:01:55,500
根目录下的 Cargo.toml 第一行就写着，这是自动生成的 workspace root。

23
00:01:55,500 --> 00:02:02,723
这行注释本身就是个信号，说明这个清单大概率由构建脚本维护，人不直接手写它。

24
00:02:02,723 --> 00:02:05,223
清单里列了七十九个成员。

25
00:02:05,223 --> 00:02:11,918
七十九这个数字很容易让人产生错觉，以为这个产品有七十九个模块要理解。

26
00:02:11,918 --> 00:02:13,408
实际上不是。

27
00:02:13,408 --> 00:02:19,863
真正要紧的信息是分布，其中六十二个集中在 crates 目录下的 codegen 子目录里。

28
00:02:19,863 --> 00:02:26,966
codegen 这个名字基本已经告诉你答案了，这一大批是生成出来的，主要是协议和接口代码。

29
00:02:26,966 --> 00:02:31,822
剩下的分布在 build、common、prod 和 third_party 里。

30
00:02:31,822 --> 00:02:36,930
所以第一个能带走的方法就是，读一个大仓库先做减法。

31
00:02:36,930 --> 00:02:43,204
把生成物整块划掉，你会发现需要人工理解的部分只剩十几二十个 crate。

32
00:02:43,204 --> 00:02:48,336
划掉生成物之后，真实的产品主轴浮出来了，一共五条。

33
00:02:48,336 --> 00:02:52,819
第一条是组合入口，就是那个叫 xai-grok-pager-bin 的 crate。

34
00:02:52,819 --> 00:03:02,892
它的 Cargo.toml 里明确定义了一个二进制目标，名字叫 xai-grok-pager，注释写得很直白，说这是 Grok Build 终端界面的 composition root。

35
00:03:02,892 --> 00:03:07,651
第二条是交互界面，也就是 pager 库本身，负责画界面。

36
00:03:07,651 --> 00:03:14,490
第三条是 Agent 宿主，xai-grok-shell，它同时提供 leader、stdio 和 headless 三种入口点。

37
00:03:14,490 --> 00:03:18,949
第四条是领域能力，工具和协议这些各占一个 crate。

38
00:03:18,949 --> 00:03:25,235
第五条是推理与状态，xai-grok-sampler 管模型请求，xai-chat-state 管对话状态。

39
00:03:25,235 --> 00:03:28,312
这里有个值得停下来想一想的安排。

40
00:03:28,312 --> 00:03:33,084
为什么终端界面和 Agent 宿主是两个 crate，不是一个。

41
00:03:33,084 --> 00:03:40,764
答案在第三条主轴里，因为 shell 要能被无头模式、标准输入输出模式和 leader 模式共用。

42
00:03:40,764 --> 00:03:48,048
如果界面和 Agent 逻辑耦合在一个 crate 里，你要跑无头模式就得把整套界面依赖也拖进来。

43
00:03:48,048 --> 00:03:52,399
分开之后，无头路径完全不知道界面的存在。

44
00:03:52,540 --> 00:03:58,752
第二步聊技术选型，但我想先立一条规矩，这也是本集最值钱的一句话。

45
00:03:58,702 --> 00:04:05,192
分析一个技术选择的时候，先给每条结论贴标签，源码事实还是课程推断。

46
00:04:05,192 --> 00:04:07,259
先看事实这一栏。

47
00:04:07,259 --> 00:04:14,122
第一，workspace 的 package 段里明确设置 edition 等于二零二四，这是 Rust 二零二四版。

48
00:04:14,122 --> 00:04:19,663
第二，依赖声明用 Tokio 一系，并且打开了 full 特性，也就是全功能。

49
00:04:19,663 --> 00:04:29,242
第三，入口代码里创建的是 Tokio 多线程 runtime，用的是 new_multi_thread 加 enable_all 这套写法。

50
00:04:29,242 --> 00:04:36,634
第四，类型系统被用来承载领域边界，enum、Result、newtype 和 Actor 的 handle 到处都是。

51
00:04:36,634 --> 00:04:44,110
第五，发布配置 release-dist 里调了 panic 策略、LTO 和 codegen units 这些原生发布参数。

52
00:04:44,110 --> 00:04:47,055
这五条都有文件可以指给你看。

53
00:04:47,055 --> 00:04:49,110
再看推断这一栏。

54
00:04:49,110 --> 00:04:59,735
原生二进制方便把命令行工具和运行时一起交付，这个从发布配置能推出来，但交付方式是不是最初的选型动机，源码没写。

55
00:04:59,735 --> 00:05:08,882
所有权和 Send 边界有助于管理多线程会话，这个从代码形态能看出来，但团队是不是因为这个才选 Rust，不知道。

56
00:05:08,882 --> 00:05:15,829
强类型适合复杂协议和工具参数的建模，这是经验之谈，不是仓库证据。

57
00:05:15,829 --> 00:05:23,581
编译时间长、生命周期约束烦、学习门槛高，这些代价也真实存在，但同样属于推断。

58
00:05:23,581 --> 00:05:26,286
最容易翻车的陈述长这样。

59
00:05:26,286 --> 00:05:28,269
多线程一定更快。

60
00:05:28,269 --> 00:05:32,115
或者，xAI 为了降低内存占用选了 Rust。

61
00:05:32,115 --> 00:05:36,634
两句话听起来都很合理，但都没有仓库直接证据。

62
00:05:36,634 --> 00:05:41,538
把它们标成推断，你就不会在别人追问的时候下不来台。

63
00:05:41,538 --> 00:05:44,375
顺带做一个公开行为的对照。

64
00:05:44,375 --> 00:05:51,454
Grok Build 这边我们能验证的是 Rust workspace、原生二进制目标、Tokio runtime 和 crate 边界。

65
00:05:51,454 --> 00:06:01,875
Claude Code 那边，公开安装文档给了原生安装、Homebrew、WinGet 等几种方式，用户可见行为包括终端交互、无头模式和工具调用。

66
00:06:01,875 --> 00:06:09,627
这里只比公开可验证的部分，不推断它的内部实现、语言和并发架构，那条线不该越。

67
00:06:09,772 --> 00:06:11,645
第三步进代码。

68
00:06:11,595 --> 00:06:16,294
真正的入口在 xai-grok-pager-bin 的 src 目录下的 main.rs。

69
00:06:16,294 --> 00:06:25,248
这个入口干的第一件事是建 Tokio 多线程 runtime，第二件事是解析命令和交互模式，然后分四条路走。

70
00:06:25,248 --> 00:06:34,984
第一条路叫 run_headless，Agent 无头模式，命令行里敲 grok agent headless 就走这条，进的是 shell 提供的无头宿主。

71
00:06:34,984 --> 00:06:43,842
第二条叫 run_stdio_agent，通过 ACP 协议的标准输入输出收发请求，适合外部客户端来驱动它。

72
00:06:43,842 --> 00:06:52,039
第三条叫 run_leader，起一个 leader 进程，承载长生命周期的 Agent，再通过连接通道服务客户端。

73
00:06:52,039 --> 00:07:01,414
第四条是默认分支，进 app 模块里的 run 函数，也就是终端交互界面，而界面这条路径也可以经由 leader 建立连接。

74
00:07:01,414 --> 00:07:05,044
四条路汇合之后，真正的交接开始了。

75
00:07:05,044 --> 00:07:13,770
第一步是 connect_or_spawn，这个名字很实在，能连上现有的 leader 就连，连不上就按需起一个进程。

76
00:07:13,770 --> 00:07:18,854
第二步是 MvpAgent，它响应 ACP 请求并管理会话句柄。

77
00:07:18,854 --> 00:07:26,402
第三步是 spawn_session_on_thread，为一个会话创建一条操作系统线程。

78
00:07:26,402 --> 00:07:31,306
第四步是 SessionActor，它推进 turn loop，协调状态和工具。

79
00:07:31,306 --> 00:07:36,450
第五步是 SamplerActor，为这次请求创建流式采样任务。

80
00:07:36,450 --> 00:07:39,672
到这里，第一轮对话才真正发出去。

81
00:07:39,672 --> 00:07:42,292
值得注意的细节在第三步。

82
00:07:42,292 --> 00:07:57,897
spawn_session_on_thread 用的是标准库里的 thread Builder，线程有名字，栈大小显式设成八兆，而且线程里面建的不是多线程 runtime，是 new_current_thread，再配一个 LocalSet。

83
00:07:57,897 --> 00:08:00,300
为什么是单线程加 LocalSet。

84
00:08:00,300 --> 00:08:08,389
因为这样可以放非 Send 的任务，那些带引用的、跨 await 点不能安全送走的 Future，在这个环境里能跑。

85
00:08:08,389 --> 00:08:14,988
代价是这个会话的所有异步工作都挤在一条线程上，所以它必须另外起线程。

86
00:08:14,988 --> 00:08:20,673
会话级别的隔离靠的是操作系统线程，不是 Tokio 的任务调度。

87
00:08:20,836 --> 00:08:24,644
第四步看会话运行时，这块最值得抄。

88
00:08:24,594 --> 00:08:30,135
SessionActor 的主循环叫 run_session，它同时接收四类东西。

89
00:08:30,135 --> 00:08:35,135
SessionCommand、ChatStateEvent、SessionEvent，还有 turn 完成的通知。

90
00:08:35,135 --> 00:08:42,202
里面有个方法叫 maybe_start_running_task，负责把待处理的 turn 启动起来。

91
00:08:42,202 --> 00:08:46,781
turn 完成之后再走 completion、turn end 和后续通知。

92
00:08:46,781 --> 00:08:48,211
状态归谁。

93
00:08:48,211 --> 00:08:51,637
答案很干脆，ChatStateActor 专属拥有。

94
00:08:51,637 --> 00:08:56,409
对话内容、token、配置、持久化，全在它手里。

95
00:08:56,409 --> 00:09:00,736
它通过一个 mpsc 的无界接收端串行处理命令。

96
00:09:00,736 --> 00:09:02,983
串行这两个字是关键。

97
00:09:02,983 --> 00:09:07,490
因为串行就意味着没有锁竞争，状态变更天然有序。

98
00:09:07,490 --> 00:09:15,856
这是 Actor 模型最实在的收益，不是什么高深的并发技巧，就是把可变状态关进一个单线程的消息循环里。

99
00:09:15,856 --> 00:09:17,358
取消怎么做。

100
00:09:17,358 --> 00:09:20,723
用的是 CancellationToken，协作式终止。

101
00:09:20,723 --> 00:09:27,623
注意协作式这三个字，它不是强杀，是把信号发出去，各段代码自己检查自己退。

102
00:09:27,623 --> 00:09:33,067
源码里还有一句兜底，如果所有 handle 都被丢弃了，循环也会结束。

103
00:09:33,067 --> 00:09:39,918
这其实是两道保险，一道是你主动取消，一道是引用计数归零自然收摊。

104
00:09:39,918 --> 00:09:42,574
这里要纠正一个常见误解。

105
00:09:42,574 --> 00:09:49,582
很多人会想当然地认为，既然是 Actor 消息队列，那用户中途插话应该是插到队首去。

106
00:09:49,582 --> 00:09:52,430
这套源码里没有这个通用设计。

107
00:09:52,430 --> 00:09:56,841
取消 token 和消息优先级是两个概念，不要混。

108
00:09:56,841 --> 00:10:03,344
本源码没有高优先级消息插队首的通用机制，你就别在自己的复述里发明一个。

109
00:10:03,484 --> 00:10:07,893
最后看 Agent 本身的字段边界，这块最容易被写错。

110
00:10:07,843 --> 00:10:11,256
源码给 Agent 定义的字段大致是这些。

111
00:10:11,256 --> 00:10:16,845
definition，也就是 AgentDefinition，定义身份、模式和策略输入。

112
00:10:16,845 --> 00:10:23,179
prompt_context，一个支持检查、重渲染和序列化的提示词上下文。

113
00:10:23,179 --> 00:10:28,359
system_prompt，从上下文渲染出来并缓存的字符串。

114
00:10:28,359 --> 00:10:34,105
tool_bridge，一个 Arc 包着的工具注册与会话上下文桥梁。

115
00:10:34,105 --> 00:10:37,951
reminder_policy，会话级的提醒策略。

116
00:10:37,951 --> 00:10:43,311
compaction_policy，管自动压缩、memory flush 和两遍处理。

117
00:10:43,311 --> 00:10:48,251
hosted_tools，发给接口的那批后端托管工具定义。

118
00:10:48,251 --> 00:10:53,624
还有 backend_search_enabled，构建时的服务端搜索开关。

119
00:10:53,624 --> 00:10:59,513
看到这份字段清单，你大概就能猜到这个系统的复杂度落在哪了。

120
00:10:59,513 --> 00:11:02,001
不在模型调用，在策略。

121
00:11:02,001 --> 00:11:09,249
提醒策略、压缩策略、托管工具、搜索开关，全是产品决策，全挂在 Agent 上。

122
00:11:09,249 --> 00:11:11,893
然后是最关键的一处措辞。

123
00:11:11,893 --> 00:11:17,170
源码注释里说，Agent 构建之后是 effectively immutable，有效不可变。

124
00:11:17,170 --> 00:11:22,710
很多人看到这句就直接翻译成 Agent 是不可变的，这就错了。

125
00:11:22,710 --> 00:11:31,713
因为它还提供了一个叫 finalize_prompt 的方法，签名是 &mut self，会更新时间戳并重新渲染 system prompt。

126
00:11:31,713 --> 00:11:38,083
所以准确的说法是，以有效不可变为主，同时保留一个显式的重渲染入口。

127
00:11:38,083 --> 00:11:41,869
差一个字，你对这套系统的理解就偏了。

128
00:11:41,869 --> 00:11:47,999
你会以为提示词是构建期一锤子买卖，实际上它可以在运行期刷新。

129
00:11:47,999 --> 00:11:50,811
顺着这条线还能推一件事。

130
00:11:50,811 --> 00:11:58,359
既然提示词能在运行期重渲染，那渲染出来的内容就可能随时间变化，比如构建时间戳。

131
00:11:58,359 --> 00:12:06,689
这意味着你在做缓存、做对账、做可复现测试的时候，不能默认提示词字符串是恒定的。

132
00:12:06,844 --> 00:12:10,507
收个尾，把今天能带走的东西列清楚。

133
00:12:10,457 --> 00:12:13,282
第一，读大仓库先做减法。

134
00:12:13,282 --> 00:12:18,703
七十九个成员里六十二个是生成物，划掉之后主干只有五条。

135
00:12:18,703 --> 00:12:23,354
别让成员总数吓住你，也别在生成代码上浪费时间。

136
00:12:23,354 --> 00:12:26,131
第二，技术选型要分两栏写。

137
00:12:26,131 --> 00:12:29,364
左边源码事实，右边课程推断。

138
00:12:29,364 --> 00:12:34,796
Rust 二零二四、Tokio、类型边界、二进制目标，这些进左栏。

139
00:12:34,796 --> 00:12:39,736
性能收益、团队动机，这些进右栏，并且接受被证伪。

140
00:12:39,736 --> 00:12:42,369
这条习惯能救你很多次。

141
00:12:42,369 --> 00:12:45,445
第三，组合入口和宿主分开。

142
00:12:45,445 --> 00:12:55,469
界面一个 crate，Agent 宿主一个 crate，因为无头模式、标准输入输出模式、leader 模式都要复用宿主，而它们都不需要界面。

143
00:12:55,469 --> 00:12:59,075
这是依赖方向上的取舍，不是目录洁癖。

144
00:12:59,075 --> 00:13:06,215
第四，会话隔离用操作系统线程加 LocalSet，状态所有权交给单个 Actor 串行处理。

145
00:13:06,215 --> 00:13:12,933
想要并发安全，最省事的办法往往不是加锁，是让可变状态只有一个所有者。

146
00:13:12,933 --> 00:13:19,171
第五，取消用协作式 token，别指望强杀，也别自己发明消息优先级。

147
00:13:19,171 --> 00:13:23,078
源码没有的东西，就不要写在你的复述里。

148
00:13:23,078 --> 00:13:29,304
第六，遇到 effectively immutable 这类措辞，翻回代码看有没有 &mut self 的口子。

149
00:13:29,304 --> 00:13:33,342
有，就写有效不可变加显式重渲染入口。

150
00:13:33,342 --> 00:13:35,542
没有，才写不可变。

151
00:13:35,542 --> 00:13:40,518
这六条合起来其实是一件事，工程判断要长在证据上。

152
00:13:40,518 --> 00:13:45,926
源码写了什么就说什么，源码没写的，你再喜欢也要标成推断。

153
00:13:45,926 --> 00:13:50,674
下次你打开一个陌生的 Rust 仓库，不妨就按这个顺序来。

154
00:13:50,674 --> 00:13:56,647
先减生成物，再画主轴，再分事实和推断，最后找状态的所有者。

155
00:13:56,647 --> 00:14:02,212
走完这四步，你对该项目的理解会超过大多数只看过 README 的人。

156
00:14:02,212 --> 00:14:05,602
本集到此，我们下一集继续往下拆。

