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

2
00:00:06,382 --> 00:00:09,206
这是 DeepSeek Harness 系列的又一集。

3
00:00:09,206 --> 00:00:11,165
这一集讲两件事。

4
00:00:11,165 --> 00:00:18,641
第一件是，既然 bash 加上读写文件已经能干活了，为什么还要再配三套一等公民的工具。

5
00:00:18,641 --> 00:00:23,894
第二件是，权限这个大词在这套系统里是怎么被拆开的。

6
00:00:23,894 --> 00:00:25,721
为什么值得关心。

7
00:00:25,721 --> 00:00:35,540
因为这两件事都藏着同一个设计习惯，把复杂概念拆成正交的小变量，再让产品概念降级成给变量组合起的名字。

8
00:00:35,540 --> 00:00:40,132
看起来是源码细节，其实是一种很能迁移的思考方式。

9
00:00:40,132 --> 00:00:41,911
这一集分四块。

10
00:00:41,911 --> 00:00:56,189
先讲 terminal 怎么让状态活过一次调用，再讲 lsp 为什么只给四类查询，然后讲 jobs 怎么把三种后台任务收进一张列表，最后拆那两个正交的旋钮，收五条能带走的原则。

11
00:00:56,189 --> 00:01:00,853
先铺一句背景，免得误会这三套工具是功能堆砌。

12
00:01:00,853 --> 00:01:06,778
bash 加读写文件确实已经能干活了，绝大部分编码任务靠它们就够了。

13
00:01:06,778 --> 00:01:17,427
这三套工具不是拿来替代它们的，而是补三种它们补不了的缺口，状态跨调用存活、代码语义导航、后台任务统一收口。

14
00:01:17,427 --> 00:01:23,461
三个缺口都很具体，也都有明确的代价，补不补取决于你的运行环境。

15
00:01:23,461 --> 00:01:26,562
这也解释了为什么别的家没做全。

16
00:01:26,562 --> 00:01:32,223
Claude Code 是产品，交互式调试有 IDE 兜底，语义导航有编辑器兜底。

17
00:01:32,223 --> 00:01:40,060
而这里要讲的是运行时，运行时要在无头环境里独自把这些能力供给模型，没有别人可兜底。

18
00:01:40,060 --> 00:01:43,377
产品可以不做的事，运行时值得做。

19
00:01:43,377 --> 00:01:50,156
顺带提醒，这一集提到的行号都是二零二六年八月十三日核对源码的结果。

20
00:01:50,308 --> 00:01:53,346
先看最容易被低估的 terminal。

21
00:01:53,296 --> 00:01:55,652
一次性 bash 的问题很具体。

22
00:01:55,652 --> 00:02:02,407
你起一个 Python 的交互式解释器，定义几个变量，调一次工具，然后这一次调用结束了。

23
00:02:02,407 --> 00:02:07,888
下次再调用，变量全没了，模型只能把旧代码重新发一遍。

24
00:02:07,888 --> 00:02:10,520
轮数翻倍，token 也翻倍。

25
00:02:10,520 --> 00:02:16,518
REPL、调试器、远程登录这类有状态的程序，全都吃这个亏。

26
00:02:16,518 --> 00:02:20,412
这里的解法是把 PTY 会话做成一等资源。

27
00:02:20,412 --> 00:02:28,008
一共六个工具，open 建会话拿 id，之后的 send、read、signal、close、list 全按 id 操作。

28
00:02:28,008 --> 00:02:32,539
会话在后端和工具插件热重载期间照样活着。

29
00:02:32,539 --> 00:02:41,782
工具描述本身就是提示词工程的范本，在一千八百七十八行的工具目录里，发送这条把等待语义一句话就说清了。

30
00:02:41,782 --> 00:02:58,541
原话大意是，向持久终端发送文本，默认提交回车，然后等待提示符、等待输入、等待输出静默、等待超时或者会话退出；后台模式则返回一个供任务输出和任务终止使用的任务 id。

31
00:02:58,541 --> 00:03:03,336
一句话概括这套工具的价值，让状态活过一次调用。

32
00:03:03,336 --> 00:03:06,269
再补一层容易被忽略的收益。

33
00:03:06,269 --> 00:03:11,365
持久会话不只是省 token，它改变的是模型的工作方式。

34
00:03:11,365 --> 00:03:19,779
没有持久会话的时候，模型只能把每一步都写成一次性的完整脚本，因为下一次调用什么都不会留下。

35
00:03:19,779 --> 00:03:26,461
有了持久会话，它可以像人一样先起环境、再逐步试探、边看输出边调整。

36
00:03:26,461 --> 00:03:33,408
前者是批量提交，后者是交互式迭代，两种工作模式的产出质量差得很远。

37
00:03:33,408 --> 00:03:37,423
还有一处细节能看出设计者对热重载的考虑。

38
00:03:37,423 --> 00:03:41,954
会话在后端和工具插件热重载期间照样活着。

39
00:03:41,954 --> 00:03:46,846
也就是说你改了工具插件的代码，正在跑的终端不受影响。

40
00:03:46,846 --> 00:03:53,324
这条对开发体验很重要，否则每次改插件都要把所有会话推倒重来。

41
00:03:53,476 --> 00:03:58,329
接着看并发纪律，这才是这套设计真正见功力的地方。

42
00:03:58,279 --> 00:04:02,210
一个 PTY 会话同一时刻只接受一个活动发送。

43
00:04:02,210 --> 00:04:09,890
两个调用抢同一个终端，第二个直接吃一个结构化报错，谁都别想把字符插进别人的命令中间。

44
00:04:09,890 --> 00:04:13,592
看把关的顺序，三道检查一道都省不掉。

45
00:04:13,592 --> 00:04:16,476
进门第一件事是校验归属。

46
00:04:16,476 --> 00:04:20,755
授权比较的是拥有这个会话的确切智能体实例。

47
00:04:20,755 --> 00:04:24,914
注意这里的关键，id 不是秘密，边界靠归属。

48
00:04:24,914 --> 00:04:28,880
别的智能体就算猜中了 id，也过不了这一关。

49
00:04:28,880 --> 00:04:34,385
第二道看会话是否正在关闭，关到一半的会话拒收任何新输入。

50
00:04:34,385 --> 00:04:37,354
第三道看上一条发送有没有结算。

51
00:04:37,354 --> 00:04:44,073
上一条还没结算，新来的这条直接抛一个带 SEND_ACTIVE 错误码的结构化失败。

52
00:04:44,073 --> 00:04:49,241
模型看到的是一条明确的失败，等上一条结算完再来就行。

53
00:04:49,241 --> 00:05:01,020
检查全过之后，本次操作被登记为这个会话唯一的活动发送，等它的完成承诺结算，成功失败都算，登记才清空，下一条发送才有资格进门。

54
00:05:01,020 --> 00:05:05,707
出处是终端包源码第二百四十三到二百五十四行。

55
00:05:05,707 --> 00:05:09,842
把这条纪律单独拎出来记一下，归属即边界。

56
00:05:09,842 --> 00:05:12,065
不靠保密，靠校验。

57
00:05:12,065 --> 00:05:16,272
等会儿讲任务列表的时候，它还会再出现一次。

58
00:05:16,420 --> 00:05:20,528
第二套能力是 lsp，治的是文本搜索没有语义。

59
00:05:20,478 --> 00:05:27,942
它把语言服务器的能力收窄到恰好四类查询，跳定义、找引用、跳实现、悬停提示。

60
00:05:27,942 --> 00:05:31,380
为什么不直接把整套协议透出去给模型。

61
00:05:31,380 --> 00:05:37,834
因为那样模型面对的是一个无底洞一样的 schema，换一家提供方行为就漂一次。

62
00:05:37,834 --> 00:05:39,853
这里的做法是闭合。

63
00:05:39,853 --> 00:05:48,820
接缝层、提供方、工具三层共享同一个四操作联合，加第五个操作会让编译失败，直到三层都改完。

64
00:05:48,820 --> 00:05:57,113
这是用编译期错误来挡住词汇表膨胀，跟上一集讲的用 never 类型挡住非法组合是同一类思路。

65
00:05:57,113 --> 00:06:01,524
它故意不开通用协议逃生口，模型玩不出协议花活。

66
00:06:01,524 --> 00:06:08,134
选路也简单粗暴，按文件扩展名找提供方，一个扩展名只属于一家。

67
00:06:08,134 --> 00:06:11,368
重点在没有提供方的时候会怎样。

68
00:06:11,368 --> 00:06:19,048
工具照常挂在目录里，schema 一个字不变，调用返回带 LSP_UNAVAILABLE 错误码的结构化失败。

69
00:06:19,048 --> 00:06:23,110
为什么这比直接把工具从目录里摘掉更友好。

70
00:06:23,110 --> 00:06:29,541
因为模型学到的是这个项目没配语言服务器，而不是这个工具怎么突然消失了。

71
00:06:29,541 --> 00:06:37,041
工具消失会让模型反复去试一个不存在的东西，结构化降级则是一次就把结论讲清楚。

72
00:06:37,041 --> 00:06:40,226
降级要结构化，词汇表要稳定。

73
00:06:40,226 --> 00:06:42,329
把这个思路推广一下。

74
00:06:42,329 --> 00:06:46,945
凡是模型能看见的接口，稳定性都比丰富度重要。

75
00:06:46,945 --> 00:06:57,437
模型是靠上下文里的工具清单来规划动作的，清单今天有的工具明天没了，它没有任何渠道知道这件事，只能反复试错。

76
00:06:57,437 --> 00:07:05,478
反过来，一个永远在清单里、但会明确告诉你当前不可用的工具，模型一次就学会了不再依赖它。

77
00:07:05,478 --> 00:07:09,300
接口的消失比接口的失败昂贵得多。

78
00:07:09,436 --> 00:07:13,700
第三套能力是 jobs，治的是后台任务没人收尸。

79
00:07:13,650 --> 00:07:20,850
后台 bash 命令、终端发送的后台模式、后台子智能体，三种生产方形态完全不同。

80
00:07:20,850 --> 00:07:34,384
这里让它们全部注册进同一个注册表，领到按种类加序号发的 id，比如 bash 加一、子智能体加二，然后模型用同一组三个工具通吃，列任务、取输出、杀任务。

81
00:07:34,384 --> 00:07:41,908
分工很清楚，生产方拥有执行资源，注册表拥有身份、访问权限和生命周期状态。

82
00:07:41,908 --> 00:07:45,153
授权还是那句话，不靠 id 保密。

83
00:07:45,153 --> 00:07:50,754
id 按种类加序号顺序发放，完全可预测，防线是所有者授权。

84
00:07:50,754 --> 00:07:58,999
读、杀、等，全都校验调用方的会话与任务所有者是否一致，别人的任务连标签都看不到。

85
00:07:58,999 --> 00:08:01,102
还有两个细节值得记。

86
00:08:01,102 --> 00:08:11,607
第一，完成承诺在资源释放之后才算数，工作干完了还不算完，所有者被销毁时注册表会取消并等待任务，不留孤儿进程。

87
00:08:11,607 --> 00:08:23,843
第二，某个接口已经交付过终止状态时，一个已报告标记会抑制重复的完成通知，避免一次任务结束让模型收两遍消息、白白多开一轮。

88
00:08:23,843 --> 00:08:33,759
顺带点破一句，这三套能力都不在主干上，全是可选插件，极简预设一个都不挂，照样是一个完整的编码智能体。

89
00:08:33,759 --> 00:08:41,631
产品可以不做的事，运行时值得做，因为运行时要在无头环境里独自把这些能力供给模型。

90
00:08:41,788 --> 00:08:43,480
现在讲权限。

91
00:08:43,430 --> 00:08:49,127
先给结论，这里把权限这个大词拆成了两个互不打听的小问题。

92
00:08:49,127 --> 00:08:55,570
旋钮一叫沙箱模式，回答的是这条命令的文件效果允许到什么程度。

93
00:08:55,570 --> 00:08:59,957
取值三档，只读、工作区可写、完全访问。

94
00:08:59,957 --> 00:09:05,125
它只管文件系统，网络和进程可见性都不在它的词汇表里。

95
00:09:05,125 --> 00:09:10,281
只读要求后端必须拒绝写入，只保留一些 shell 必需的接收器。

96
00:09:10,281 --> 00:09:16,315
工作区可写是工作区根目录与后端承诺的临时区可写，界外一律拦。

97
00:09:16,315 --> 00:09:22,505
完全访问则是绕过隔离，消费方直接起原始命令，根本不调用沙箱。

98
00:09:22,505 --> 00:09:27,805
旋钮二叫审批策略，回答的是碰到需要人拍板的事问不问。

99
00:09:27,805 --> 00:09:30,834
取值只有两档，询问和从不。

100
00:09:30,834 --> 00:09:35,798
询问这一档是默认，它把请求交给一条应答者链去处理。

101
00:09:35,798 --> 00:09:41,724
链上一个应答者都没有，走到头返回拿不到答案，照样按拒绝处理。

102
00:09:41,724 --> 00:09:50,365
从不这一档是确定性返回拒绝，不分发任何应答者，这是持续集成和无人值守场景的标准姿势。

103
00:09:50,365 --> 00:09:57,505
注意这两档的分工，询问不是「问了就一定答」，它是「启动一条链去要答案」，要不到就当拒绝。

104
00:09:57,505 --> 00:10:05,786
把「要不到答案」和「明确拒绝」在执行方眼里统一成同一个结果，是这套设计里最省心的决定之一。

105
00:10:05,786 --> 00:10:14,596
两个旋钮在日志里也是两种独立事件，执行、提示词、回放，全都只读这两种事件的折叠结果。

106
00:10:14,596 --> 00:10:20,005
这就是正交的实际含义，拧任何一个，另一个纹丝不动。

107
00:10:20,164 --> 00:10:22,241
然后是那个下拉框。

108
00:10:22,191 --> 00:10:26,205
它维护一张表，名字映射到一个旋钮组合。

109
00:10:26,205 --> 00:10:33,513
默认表就两行，工作区可写对应工作区可写加询问，完全访问对应完全访问加从不。

110
00:10:33,513 --> 00:10:38,465
它是可选能力，不在主干上，也不拥有任何强制执行。

111
00:10:38,465 --> 00:10:41,205
切预设的时候到底发生什么。

112
00:10:41,205 --> 00:10:43,777
三条事件，顺序固定。

113
00:10:43,777 --> 00:10:48,441
先追加一条只记日志的预设事件，记下你的意图。

114
00:10:48,441 --> 00:10:55,592
然后通过两个旋钮各自的规范写入器去写生效值，而且只写真的变了的那个。

115
00:10:55,592 --> 00:10:58,862
重新选中当前预设，什么都不追加。

116
00:10:58,862 --> 00:11:03,645
所以切预设没有第三个执行路径，也没有隐藏状态。

117
00:11:03,645 --> 00:11:07,503
日志里追加了什么，执行层就折叠出什么。

118
00:11:07,503 --> 00:11:17,924
回放时执行层只认旋钮事件，预设事件的唯一作用，是当两个预设共享同一组合时，记住你当初选的到底是哪一个名字。

119
00:11:17,924 --> 00:11:20,256
还有个保留字叫 custom。

120
00:11:20,256 --> 00:11:25,112
表里不许有叫这个名字的条目，插件加载时就抛异常。

121
00:11:25,112 --> 00:11:32,035
只有当你手动把旋钮拧到一个表里没有的组合，当前状态才派生出 custom 给客户端显示。

122
00:11:32,035 --> 00:11:38,753
它只出不进，可以是当前状态，永远成不了切换目标，也绝不出现在事件内容里。

123
00:11:38,753 --> 00:11:41,157
为什么对 custom 这么苛刻。

124
00:11:41,157 --> 00:11:44,102
因为它是一个派生值，不是事实。

125
00:11:44,102 --> 00:11:53,561
日志里的事实只有两条旋钮事件，custom 只是客户端拿着当前旋钮值去表里反查、查不到时给的一个显示名。

126
00:11:53,561 --> 00:12:02,467
如果允许把 custom 当目标写进事件，日志里就会多出一个无法解释执行层行为的条目，回放也会跟着分叉。

127
00:12:02,467 --> 00:12:06,157
派生值只出不进，这条纪律值得记住。

128
00:12:06,157 --> 00:12:09,583
再看查表和派生这两件事的对称性。

129
00:12:09,583 --> 00:12:17,768
查表是名字到组合，派生是组合到名字，两个方向的函数都写死了，而且都不允许半路插队。

130
00:12:17,768 --> 00:12:24,030
切预设走查表，显示当前状态走派生，各走各的路，互不干扰。

131
00:12:24,172 --> 00:12:26,489
最后这块最值得学。

132
00:12:26,439 --> 00:12:32,016
唯一的放行值是 allowed-once，而且只授权所询问的那一个操作。

133
00:12:32,016 --> 00:12:38,639
被拒、被取消、拿不到答案，这三种结果在执行方眼里统统是拒绝。

134
00:12:38,639 --> 00:12:49,228
没有应答者、应答者抛异常、返回了词汇表外的怪值、弹窗期间用户关掉了界面，全部归一化成拿不到答案或者已取消。

135
00:12:49,228 --> 00:12:53,074
任何一环拿不到明确的放行，就不放行。

136
00:12:53,074 --> 00:12:55,478
审批还不进模型上下文。

137
00:12:55,478 --> 00:13:01,091
询问与决定这两条事件成对写入会话日志，只做审计。

138
00:13:01,091 --> 00:13:05,862
模型看到的是工具结果和运行时上下文快照。

139
00:13:05,862 --> 00:13:14,132
请求体还故意不带工具参数，用调用 id 指向已经流式输出的那次调用，避免渲染一份会漂移的副本。

140
00:13:14,132 --> 00:13:16,103
提权也是一次性的。

141
00:13:16,103 --> 00:13:30,742
写操作被沙箱拦下后，模型可以带沙箱权限请求提权重试，审批通过给一次性放行，这次重试以显式更宽的模式解析一次策略，用完即弃，会话的旋钮设置不会因此改变。

142
00:13:30,742 --> 00:13:33,338
还有一个边界问题值得抠。

143
00:13:33,338 --> 00:13:42,665
如果一个插件在审批服务挂载之后，抢在应答者链最前面塞一个全部放行的应答者，能不能绕过从不这个策略。

144
00:13:42,665 --> 00:13:46,572
答案是不能，因为从不根本不在链里执行。

145
00:13:46,572 --> 00:13:57,148
源码注释把设计意图讲完了，从不的裁决点在服务自己的请求路径上，任何监听器形态的门都保不住与注册顺序无关这个承诺。

146
00:13:57,148 --> 00:14:02,112
出处是审批包源码第三百零四到三百一十二行。

147
00:14:02,260 --> 00:14:04,012
收五条原则。

148
00:14:03,962 --> 00:14:07,087
第一条，让状态活过一次调用。

149
00:14:07,087 --> 00:14:14,599
凡是模型需要跟一个有状态的程序反复对话，就该问一句，这个状态能不能跨调用保留。

150
00:14:14,599 --> 00:14:19,611
重发一遍旧代码的代价，比多写几个工具的代价大得多。

151
00:14:19,611 --> 00:14:23,193
第二条，闭合词汇表，拒绝逃生口。

152
00:14:23,193 --> 00:14:27,797
给模型的接口宁可窄而稳定，也不要宽而漂移。

153
00:14:27,797 --> 00:14:33,866
加一类操作要付出编译期代价，这个代价正是词汇表不膨胀的保证金。

154
00:14:33,866 --> 00:14:36,655
第三条，降级要结构化。

155
00:14:36,655 --> 00:14:42,280
能力不可用时，返回一个带错误码的失败，而不是让工具消失。

156
00:14:42,280 --> 00:14:46,883
前者教会模型一件事，后者只会让它反复试探。

157
00:14:46,883 --> 00:14:49,311
第四条，归属即边界。

158
00:14:49,311 --> 00:14:54,107
不要靠 id 保密做授权，id 迟早会被猜到或者泄露。

159
00:14:54,107 --> 00:14:59,227
把归属校验放在每一个入口上，顺序发放的 id 也很安全。

160
00:14:59,227 --> 00:15:02,869
第五条，把大概念拆成正交变量。

161
00:15:02,869 --> 00:15:11,114
沙箱管文件效果，审批管问不问人，两个变量互不打听，产品概念降级成给组合起的名字。

162
00:15:11,114 --> 00:15:15,465
拆对了，你能表达出单轴上根本不存在的组合。

163
00:15:15,465 --> 00:15:16,883
留一道练习。

164
00:15:16,883 --> 00:15:23,470
审批弹窗弹出后，用户直接关掉了浏览器，界面应答者随着销毁被移除。

165
00:15:23,470 --> 00:15:28,482
这次请求最终以什么结果结算，工具调用是放行还是拒绝。

166
00:15:28,482 --> 00:15:32,580
答案是不拿到答案就拒绝，工具调用不放行。

167
00:15:32,580 --> 00:15:37,532
想清楚这一条，这套审批设计的取向就全懂了。

