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

2
00:00:06,382 --> 00:00:09,038
这是 Grok Build 系列的第三集。

3
00:00:09,038 --> 00:00:16,334
前两集我们看了骨架和几个默认值，这一集看工具层，以及上下文容量到底怎么算。

4
00:00:16,334 --> 00:00:23,906
工具层要回答的问题是，一个产品里同时存在好几套工具协议，代码上怎么组织才不乱。

5
00:00:23,906 --> 00:00:30,480
Grok Build 的答案是实现族加注册表，再加一条运行时发现外部工具的通路。

6
00:00:30,480 --> 00:00:34,927
容量这部分要回答的是，那些百分比到底怎么算。

7
00:00:34,927 --> 00:00:43,076
八十五这个数字我们上一集见过一次，这一集把它彻底算清楚，包括等号边界，以及什么叫预留空间。

8
00:00:43,076 --> 00:00:50,516
两个话题看起来不相关，其实有一条共同的线，它们都在解决同一件事有多个来源的问题。

9
00:00:50,516 --> 00:00:55,444
工具有多套协议，token 有本地估算和服务端计量两种来源。

10
00:00:55,444 --> 00:01:00,228
而这两处的解法也相似，定义一层稳定的公共契约。

11
00:01:00,228 --> 00:01:02,692
还有一点值得先说清楚。

12
00:01:02,692 --> 00:01:09,302
这一集的内容比前两集更偏细节，全是字段名、默认值和算术式。

13
00:01:09,302 --> 00:01:15,132
听起来枯燥，但恰恰是这些细节决定了你对系统的理解是真是假。

14
00:01:15,132 --> 00:01:26,502
面试的时候、代码评审的时候、写设计文档的时候，能随口说出准确字段名的人，和只能说出大概思路的人，给人的信任感完全不同。

15
00:01:26,502 --> 00:01:28,112
我们开始。

16
00:01:28,252 --> 00:01:30,089
先看工具层。

17
00:01:30,039 --> 00:01:36,926
xai-grok-tools 这个 crate 里，同时容纳了多套内置工具实现和运行时接入的 MCP 工具。

18
00:01:36,926 --> 00:01:41,325
所谓实现族，就是按协议来源把工具分成几组。

19
00:01:41,325 --> 00:01:44,426
源码里的 namespace 枚举给出了这些族。

20
00:01:44,426 --> 00:01:52,635
第一族叫 grok_build，是主要的产品工具族，包含 ReadFile、SearchReplace、Bash、Task 这些实现。

21
00:01:52,635 --> 00:02:02,142
第二族叫 grok_build_concise，精简工具族，目录里有 read_file、search_replace 和 bash。

22
00:02:02,142 --> 00:02:10,002
第三族叫 grok_build_hashline，带 hashline 语义的 read_file、edit 和 grep 实现。

23
00:02:10,002 --> 00:02:19,053
第四族叫 codex，包含 apply_patch、read_file、list_dir、grep_files 这些兼容实现。

24
00:02:19,053 --> 00:02:26,337
第五族叫 opencode，包含 read、write、edit、bash、glob、grep、skill 和 todowrite。

25
00:02:26,337 --> 00:02:31,072
还有按能力单独拆出来的 memory、lsp、skills 模块。

26
00:02:31,072 --> 00:02:37,058
namespace 枚举里还有一个值叫 MCP，专门给运行时接进来的外部工具用。

27
00:02:37,058 --> 00:02:38,849
为什么要这样分。

28
00:02:38,849 --> 00:02:45,471
因为这些协议各有各的参数命名和返回格式，硬统一成本很高，也不必要。

29
00:02:45,471 --> 00:02:51,938
按族分开放，各自保留自己的形态，再通过一层归一化对外提供共同词汇。

30
00:02:51,938 --> 00:02:54,726
这就是后面要讲的 canonical。

31
00:02:54,726 --> 00:02:57,034
然后是注册表和桥接。

32
00:02:57,034 --> 00:03:01,625
工具定义进入 registry，由 ToolBridge 把它接进会话层。

33
00:03:01,625 --> 00:03:10,976
ToolBridge 结构体里有两个关键东西，一个是 Arc 包着的 FinalizedToolset，也就是注册表本身，另一个是可选的终端后端。

34
00:03:10,976 --> 00:03:20,135
它还有一个 register_mcp_tools 方法，用来在运行时把外部工具连同它的 input schema 一起注册进去。

35
00:03:20,135 --> 00:03:22,298
所以职责很清楚。

36
00:03:22,298 --> 00:03:30,387
实现族解决多套协议的代码组织，registry 负责组合与运行时注册，ToolBridge 负责连接会话。

37
00:03:30,387 --> 00:03:32,635
三层各管一段。

38
00:03:32,788 --> 00:03:37,173
重点看动态 MCP，这是我觉得最巧的一处设计。

39
00:03:37,123 --> 00:03:47,315
如果每接入一个 MCP 服务器，就把它的工具全部塞进模型的工具列表，那列表会随着服务器数量膨胀，而且跨轮次不稳定。

40
00:03:47,315 --> 00:03:53,313
今天多一个服务器，模型的工具列表就变一次，这对提示词缓存是灾难。

41
00:03:53,313 --> 00:03:56,738
Grok Build 的做法是只给两个固定入口。

42
00:03:56,738 --> 00:03:58,553
第一个叫 SearchTool。

43
00:03:58,553 --> 00:04:02,519
它在 ToolIndex 里按 BM25 检索 MCP 工具。

44
00:04:02,519 --> 00:04:09,707
输入有两个字段，query 是字符串关键词，limit 是一个 Option 包着的 u8，默认五。

45
00:04:09,707 --> 00:04:15,103
返回的结果按 server 分组，并且带上工具描述和 input schema。

46
00:04:15,103 --> 00:04:16,774
第二个叫 UseTool。

47
00:04:16,774 --> 00:04:23,180
它接收发现之后的合格工具名，通过 InnerDispatch 或者托管网关去真正调用。

48
00:04:23,180 --> 00:04:35,127
输入也只有两个字段，tool_name 是字符串，通常形如 server 名加双下划线加工具名，tool_input 是一个按发现到的 schema 构造的 JSON 值。

49
00:04:35,127 --> 00:04:37,351
这个设计的收益是什么。

50
00:04:37,351 --> 00:04:42,639
模型的工具列表跨轮次保持稳定，永远只有这两个元工具。

51
00:04:42,639 --> 00:04:47,147
要用外部工具，先搜，拿到 schema，再按 schema 调。

52
00:04:47,147 --> 00:04:48,709
完整走一遍。

53
00:04:48,709 --> 00:05:01,377
模型先调 search_tool，传 query，从返回结果里读 input schema，然后把工具名和按 schema 构造的输入传给 use_tool，最后由 ToolBridge 把结果交回会话。

54
00:05:01,377 --> 00:05:03,252
代价也要说清楚。

55
00:05:03,252 --> 00:05:08,120
多了一轮检索，而且模型必须先知道该搜什么关键词。

56
00:05:08,120 --> 00:05:11,209
如果检索没命中，工具就用不了。

57
00:05:11,209 --> 00:05:16,041
这是用一次额外往返换列表稳定性，很典型的取舍。

58
00:05:16,041 --> 00:05:18,685
再补一个容易被忽略的对比。

59
00:05:18,685 --> 00:05:24,947
内置工具是静态注册进 registry 的，启动时就确定，模型直接看得到。

60
00:05:24,947 --> 00:05:31,570
MCP 工具是运行时注册进去的，模型默认看不见，必须走检索这条通路。

61
00:05:31,570 --> 00:05:41,197
一个静态一个动态，两套机制共存在同一个 registry 里，靠的就是 ToolBridge 上那个 register_mcp_tools 方法。

62
00:05:41,197 --> 00:05:46,618
想清楚静态和动态的分界，是理解这套工具层的关键。

63
00:05:46,756 --> 00:05:50,275
第二个话题，canonical input，稳定投影。

64
00:05:50,225 --> 00:05:52,509
先说它解决什么问题。

65
00:05:52,509 --> 00:06:00,502
不同 harness 可以用不同的原始参数名，比如同一个文件路径，有的叫 file_path，有的叫 path。

66
00:06:00,502 --> 00:06:06,547
如果每个工具各说各话，展示层、遥测、跨工具分析就都没法做。

67
00:06:06,547 --> 00:06:14,829
Grok Build 的做法是，把少量稳定语义投影到 x.ai/tool 这份元数据里，形成一套共同词汇。

68
00:06:14,829 --> 00:06:18,603
canonical 字段一共八个，不多，我们念一遍。

69
00:06:18,603 --> 00:06:34,736
path 是文件或搜索路径，offset 是归一化起始位置，limit 是读取或结果上限，command 是待执行命令，description 是命令描述，cwd 是工作目录，directory 是目录列表目标，pattern 是搜索模式。

70
00:06:34,736 --> 00:06:36,611
记住只有这八个。

71
00:06:36,611 --> 00:06:42,405
源码里不存在 content 这个 canonical field，旧一些的资料里出现过，已经移除。

72
00:06:42,405 --> 00:06:46,179
元数据合约叫 CanonicalToolMeta，七个字段。

73
00:06:46,179 --> 00:06:51,960
version、name、kind、namespace、label、read_only、input。

74
00:06:51,960 --> 00:06:54,977
version 是数字一，不是字符串。

75
00:06:54,977 --> 00:07:00,349
input 是可选的 JSON 值，没有稳定投影时会整体省略。

76
00:07:00,349 --> 00:07:02,909
这里有个很容易被误解的点。

77
00:07:02,909 --> 00:07:06,924
input 是 canonical projection，不是原始输入的镜像。

78
00:07:06,924 --> 00:07:08,342
什么意思。

79
00:07:08,342 --> 00:07:10,277
投影可能丢字段。

80
00:07:10,277 --> 00:07:15,253
grep 的 flags、replace_all 这类非共享字段可能被丢弃。

81
00:07:15,253 --> 00:07:22,945
编辑前后的文本、完整写入内容这种体量大的字段，不会进投影，它们留在 raw_input 里。

82
00:07:22,945 --> 00:07:29,388
所以做分析的时候，你要大字段就得回 raw_input 拿，别指望 input 里有。

83
00:07:29,388 --> 00:07:31,094
举个具体例子。

84
00:07:31,094 --> 00:07:40,061
一次编辑调用，原始输入有 file_path、old_string、new_string、replace_all 四个字段。

85
00:07:40,061 --> 00:07:52,381
canonical input 里只有 path 能进投影，old_string 和 new_string 属于大字段，replace_all 属于非共享字段，三个都留在 raw_input。

86
00:07:52,516 --> 00:07:54,905
第三个话题，token 估算。

87
00:07:54,855 --> 00:07:58,990
xai-token-estimation 这个 crate 提供共享的算术原语。

88
00:07:58,990 --> 00:08:02,007
它有两种数据来源，必须分清。

89
00:08:02,007 --> 00:08:04,014
第一种是本地估算。

90
00:08:04,014 --> 00:08:09,098
estimate_tokens 的实现是拿 UTF-8 字节长度除以四。

91
00:08:09,098 --> 00:08:17,403
这是个很粗的近似，但胜在快，可以在发请求之前、或者工具输出刚加进来的时候给出快速预测。

92
00:08:17,403 --> 00:08:22,307
单张低分辨率图片给的是一个固定估值，七百六十五 token。

93
00:08:22,307 --> 00:08:24,916
第二种是服务端 usage 观测。

94
00:08:24,916 --> 00:08:31,875
它描述的是已完成请求的实际计量，是真实值，但只在请求回来之后才有。

95
00:08:31,875 --> 00:08:33,581
关键点在这里。

96
00:08:33,581 --> 00:08:38,101
百分比函数不会去获取数据，也不判断数据来自哪。

97
00:08:38,101 --> 00:08:40,889
它只处理调用方传进来的数值。

98
00:08:40,889 --> 00:08:47,151
所以调用链可以在不同阶段选择用估算总量，还是用已更新的服务端 usage。

99
00:08:47,151 --> 00:08:51,202
这个设计很值得学，把算术和数据来源解耦。

100
00:08:51,202 --> 00:08:55,553
函数只管算，不管你喂给它的是估算还是实测。

101
00:08:55,553 --> 00:08:57,379
代价也很明确。

102
00:08:57,379 --> 00:09:07,476
调用方必须自己清楚此刻传的是哪一种，否则会把估算当成实测用，然后得出一个看起来精确、实际上完全不可信的百分比。

103
00:09:07,476 --> 00:09:11,442
这类 bug 很难查，因为数值看起来完全合理。

104
00:09:11,442 --> 00:09:13,365
那什么时候用哪种。

105
00:09:13,365 --> 00:09:19,447
发请求之前，你只有本地估算，用它做粗判，比如要不要提前压缩。

106
00:09:19,447 --> 00:09:25,565
请求回来之后，服务端 usage 是真实计量，用它做准确判断和计费对账。

107
00:09:25,565 --> 00:09:28,281
两个阶段用两个数，不要混。

108
00:09:28,281 --> 00:09:32,295
顺带说一句，除以四这个系数是怎么来的。

109
00:09:32,295 --> 00:09:36,815
它就是一个经验近似，英文大致四个字符一个 token。

110
00:09:36,815 --> 00:09:44,026
中文和代码的误差会明显更大，所以它只适合做量级判断，不适合做精确预算。

111
00:09:44,026 --> 00:09:51,057
源码里并没有按中英文或者代码类型分别计价的公式，别在复述里编一个出来。

112
00:09:51,196 --> 00:09:54,463
最后把百分比和阈值彻底算清楚。

113
00:09:54,413 --> 00:09:55,819
三个函数。

114
00:09:55,819 --> 00:09:59,509
第一个叫 usage_percentage，算使用率。

115
00:09:59,509 --> 00:10:05,422
total 等于零的时候返回零，否则算百分比，并且把结果上限压到一百。

116
00:10:05,422 --> 00:10:09,954
也就是 used 除 total 乘一百，再取和一百的较小值。

117
00:10:09,954 --> 00:10:16,540
注意它返回的是浮点数，而且有上限保护，不会出现百分之一百二十这种数字。

118
00:10:16,540 --> 00:10:20,927
第二个叫 exceeds_threshold，判是否超阈值。

119
00:10:20,927 --> 00:10:23,163
窗口为零返回 false。

120
00:10:23,163 --> 00:10:28,788
否则 used 乘一百，窗口乘阈值百分比，然后比较，用的是大于等于。

121
00:10:28,788 --> 00:10:33,283
整数饱和乘法，避免浮点舍入改变触发边界。

122
00:10:33,283 --> 00:10:40,939
第三个叫 exceeds_threshold_with_headroom，在阈值之前预留固定 token 空间。

123
00:10:40,939 --> 00:10:43,656
窗口为零仍然返回 false。

124
00:10:43,656 --> 00:10:48,764
比较式右边多减一项，headroom 乘一百，减法也用饱和减法。

125
00:10:48,764 --> 00:10:52,117
现在算边界，这是最容易错的地方。

126
00:10:52,117 --> 00:10:57,862
exceeds_threshold，used 八百五十，窗口一千，阈值八十五。

127
00:10:57,862 --> 00:11:06,288
八百五十乘一百等于八万五千，一千乘八十五等于八万五千，两边相等，大于等于成立，返回 true。

128
00:11:06,288 --> 00:11:12,514
把 used 换成八百四十九，八万四千九百小于八万五千，返回 false。

129
00:11:12,514 --> 00:11:17,850
所以结论是，八十五这个边界取等号，等于阈值时立刻为 true。

130
00:11:17,850 --> 00:11:19,882
再看 headroom 的影响。

131
00:11:19,882 --> 00:11:23,908
窗口十万，阈值八十五，headroom 四千。

132
00:11:23,908 --> 00:11:29,028
原本的触发点是十万乘八十五除一百，也就是八万五千。

133
00:11:29,028 --> 00:11:33,680
加 headroom 之后，右边要减去四千乘一百，也就是四十万。

134
00:11:33,680 --> 00:11:36,721
于是触发点前移到八万一千。

135
00:11:36,721 --> 00:11:39,148
再算一道练习巩固一下。

136
00:11:39,148 --> 00:11:42,333
窗口十二万八千，阈值八十五。

137
00:11:42,333 --> 00:11:48,992
无 headroom 的最早触发 used 是十二万八千乘八十五除一百，等于十万八千八百。

138
00:11:48,992 --> 00:11:54,184
如果 headroom 是四千，就再往前移四千，等于十万四千八百。

139
00:11:54,184 --> 00:11:56,504
两题都要保留等号。

140
00:11:56,644 --> 00:11:59,190
收尾，列能带走的。

141
00:11:59,140 --> 00:12:01,940
第一，多套协议别硬统一。

142
00:12:01,940 --> 00:12:07,036
按实现族分开放，再用一层归一化对外提供共同词汇。

143
00:12:07,036 --> 00:12:15,798
Grok Build 有 grok_build、concise、hashline、codex、opencode 这些族，还有 memory、lsp、skills 和 MCP。

144
00:12:15,798 --> 00:12:19,765
第二，外部工具不要直接塞进模型工具列表。

145
00:12:19,765 --> 00:12:24,284
给两个固定元工具，先检索发现，再按 schema 调用。

146
00:12:24,284 --> 00:12:27,818
列表跨轮次稳定，代价是多一次往返。

147
00:12:27,818 --> 00:12:32,385
第三，canonical 字段只有八个，元数据版本是数字一。

148
00:12:32,385 --> 00:12:38,286
input 是投影不是镜像，大字段和非共享字段留在 raw_input。

149
00:12:38,286 --> 00:12:40,955
别在 input 里找完整内容。

150
00:12:40,955 --> 00:12:45,065
第四，估算和服务端 usage 是两种数据来源。

151
00:12:45,065 --> 00:12:51,784
共享函数只管算术不管来源，所以调用方必须清楚自己此刻传的是哪一种。

152
00:12:51,784 --> 00:12:56,387
第五，阈值比较用整数饱和乘法，而且取等号。

153
00:12:56,387 --> 00:12:59,368
八十五这个边界，等于就触发。

154
00:12:59,368 --> 00:13:03,875
headroom 会把触发点往前移，移的量就是 headroom 本身。

155
00:13:03,875 --> 00:13:06,027
第六，也是总纲。

156
00:13:06,027 --> 00:13:08,887
这一集和上一集的共同点是什么。

157
00:13:08,887 --> 00:13:12,084
都是在处理同一件事有多个来源。

158
00:13:12,084 --> 00:13:14,729
工具多协议，token 多来源。

159
00:13:14,729 --> 00:13:21,339
解法也一样，定义一层稳定的公共契约，让上层只面对契约，不面对多样性。

160
00:13:21,339 --> 00:13:23,130
最后留一个问题。

161
00:13:23,130 --> 00:13:29,608
你的系统里，有没有哪一处是多个来源各自为政，而你没有定义公共契约的。

162
00:13:29,608 --> 00:13:34,921
如果有，那大概率是你最难做遥测、最难做展示的地方。

163
00:13:34,921 --> 00:13:37,962
从那里开始抽象，收益最明显。

164
00:13:37,962 --> 00:13:41,351
本集到此，我们下一集继续往下拆。

