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

2
00:00:06,382 --> 00:00:09,939
这一集我们解决一个很具体的工程问题。

3
00:00:09,939 --> 00:00:21,875
你手里有一个 Agent 运行时，模型需要用到它天生没有的能力，比如查一次实时天气、查一张内部数据库的表、或者驱动一个只有你们公司才有的系统。

4
00:00:21,875 --> 00:00:23,786
这些能力从哪来。

5
00:00:23,786 --> 00:00:32,151
第一类情况，开源社区里已经有人把工具服务器写好了，你只想接过来用，最好一行业务逻辑都不改。

6
00:00:32,151 --> 00:00:39,435
第二类情况，这个需求世界上没人写过，只能让模型现场写一段代码当场跑起来。

7
00:00:39,435 --> 00:00:43,858
这两件事看着像同一件事，其实答案完全不同。

8
00:00:43,858 --> 00:00:46,935
DeepSeek Harness 把它们拆成了两条路。

9
00:00:46,935 --> 00:00:52,127
一条叫 MCP 桥，负责把外部协议生态里现成的工具接进来。

10
00:00:52,127 --> 00:00:56,850
一条叫 Extensions，让模型在会话里现写现跑插件。

11
00:00:56,850 --> 00:01:07,103
今天我们把这两条路拆开看，重点不是记住接口长什么样，而是理解它们各自的信任模型，以及为什么那座桥刻意只造了一半。

12
00:01:07,103 --> 00:01:15,216
理解了这个，你在自己的系统里设计扩展点的时候，就知道该在哪一侧配哪种看管方式。

13
00:01:15,364 --> 00:01:16,996
先对齐名词。

14
00:01:16,946 --> 00:01:20,913
MCP 是 Model Context Protocol，一个开放协议。

15
00:01:20,913 --> 00:01:27,836
任何人写一个工具服务器，任何支持这个协议的客户端都能连上去用它的工具。

16
00:01:27,836 --> 00:01:38,340
DeepSeek Harness 里的 mcp-client 插件就是这个协议的客户端，一个插件实例连一个服务器，stdio 子进程和 streamable-http 两种传输都支持。

17
00:01:38,340 --> 00:01:49,362
连上之后它做的事非常直白，拉一遍工具清单，把每个工具用一个公开名注册进上下文的工具注册表，模型从此把它们当原生工具一样用。

18
00:01:49,362 --> 00:01:57,211
另一条路的底座叫 Cordis，它是 DeepSeek Harness 的插件框架，整个运行时本身就是一棵 Cordis 插件树。

19
00:01:57,211 --> 00:02:04,663
Extensions 子系统做的事情，是让模型在会话进行中现写一个 Cordis 插件并且当场跑起来。

20
00:02:04,663 --> 00:02:20,568
写代码之前先用 cordis_inspect 查询当前运行时里有哪些服务和接口可用，然后 cordis_define 提交源码，cordis_run 启动，不要了就 cordis_stop 或者 cordis_undefine。

21
00:02:20,568 --> 00:02:24,871
这五个工具由 extensions 目录下的 tool-cordis 注册。

22
00:02:24,871 --> 00:02:27,431
一句话把两条路区分开。

23
00:02:27,431 --> 00:02:34,510
MCP 桥面对的是进程外面已经存在的能力，Extensions 面对的是模型现场生成的代码。

24
00:02:34,510 --> 00:02:38,308
一个是从外面接电，一个是自己发电。

25
00:02:38,452 --> 00:02:40,709
先看第一条路的实现。

26
00:02:40,659 --> 00:02:49,433
连接成功之后，客户端调用 listTools 拉一遍工具清单，把每个工具用公开名注册进上下文的工具注册表。

27
00:02:49,433 --> 00:02:52,306
这里冒出来的第一个设计点是命名。

28
00:02:52,306 --> 00:02:55,118
每个 MCP 工具其实有两个名字。

29
00:02:55,118 --> 00:03:00,611
原始名只在网线上出现，真正发起 tools/call 请求的时候用的是它。

30
00:03:00,611 --> 00:03:08,436
而模型看到的公开名，是 mcp 两个下划线、服务器名、两个下划线、原始名拼起来的格式。

31
00:03:08,436 --> 00:03:14,938
这个格式跟 Claude Code 和 Codex 是一致的，mcp-client 的 README 自己点了这一句。

32
00:03:14,938 --> 00:03:17,642
那为什么非要有这套转换。

33
00:03:17,642 --> 00:03:26,008
因为公开名必须满足 DeepSeek 的函数名约定，最长六十四字符，只允许字母数字下划线和连字符。

34
00:03:26,008 --> 00:03:33,352
而外部服务器给你的工具名很可能不合规，可能是中文，可能带点号，可能长到离谱。

35
00:03:33,352 --> 00:03:35,563
转换的写法很讲究。

36
00:03:35,563 --> 00:03:46,765
先用正则把非法字符统一换成下划线，如果替换之后的字符串跟原串完全一样、长度也没超限，那就直接返回，一个字符都不动。

37
00:03:46,765 --> 00:03:54,902
否则就在尾部追加一段十二位的十六进制哈希，哈希的输入是服务器名加一个分隔符再加原始名。

38
00:03:54,902 --> 00:03:58,159
这个哈希解决的是一个很隐蔽的问题。

39
00:03:58,159 --> 00:04:08,195
假设两个工具，一个叫按天的预报，一个叫按天的预报扩展版，都被截断到六十四字符之后，极有可能折叠成同一个名字。

40
00:04:08,195 --> 00:04:13,364
加上哈希之后，两个不同的工具身份绝不可能被折叠成一个。

41
00:04:13,364 --> 00:04:18,460
更重要的是，整个函数是服务器名和原始名的纯函数。

42
00:04:18,460 --> 00:04:25,034
连接顺序变了、重新同步了、旁边又起了别的服务器，都改不了一个工具的名字。

43
00:04:25,034 --> 00:04:32,402
这一点等到讲断线重连的时候会再回来，你会发现它省下的不只是命名上的麻烦。

44
00:04:32,548 --> 00:04:34,613
第二个设计点叫世代。

45
00:04:34,563 --> 00:04:39,010
服务器的工具清单是会变的，变了就要重新同步。

46
00:04:39,010 --> 00:04:40,885
同步分两个阶段。

47
00:04:40,885 --> 00:04:49,779
第一个阶段把下一世代的全部工具定义拉完、建好，任何一步失败都不碰注册表，上一世代原样活着。

48
00:04:49,779 --> 00:04:54,587
第二个阶段才做交换，先注销旧世代，再注册新世代。

49
00:04:54,587 --> 00:04:57,928
交换阶段的写法值得单独拎出来讲。

50
00:04:57,928 --> 00:05:03,818
注册循环里每注册成功一个工具，就把它的注销函数存进一张表。

51
00:05:03,818 --> 00:05:17,087
任何一次注册抛出冲突，意味着有外来注册霸占了这台服务器的命名空间，catch 分支就把这张表里已经注册的全部注销掉，一个工具都不留，然后记一条错误日志。

52
00:05:17,087 --> 00:05:26,462
源码注释把意图写得很直白，回滚是为了让模型看到的要么是完整的一个世代，要么什么都没有，绝不能是半套。

53
00:05:26,462 --> 00:05:29,058
这个取舍背后的判断是什么。

54
00:05:29,058 --> 00:05:32,075
半套工具集比没有工具更危险。

55
00:05:32,075 --> 00:05:43,890
模型看到一半工具，会以为另一半压根不存在，于是自己找一条绕路，而那条绕路很可能根本走不通，或者更糟，走通了但走到错误的地方。

56
00:05:43,890 --> 00:05:48,734
宁可让它看到空，它至少会停下来说一句工具不见了。

57
00:05:48,734 --> 00:05:53,770
这是一个关于失败形态的判断，不是关于代码整洁的判断。

58
00:05:53,770 --> 00:05:56,522
断线重连也建在世代上。

59
00:05:56,522 --> 00:06:00,945
stdio 子进程崩了，supervisor 用指数退避重启它。

60
00:06:00,945 --> 00:06:08,241
首次延迟默认五百毫秒，之后逐次翻倍，上限三十秒，一次中断最多试十次。

61
00:06:08,241 --> 00:06:16,005
中断期间最后一个正常世代保持注册，模型这时候去调用会失败，但工具名不会凭空消失。

62
00:06:16,005 --> 00:06:19,972
这一点很关键，它意味着错误是可解释的。

63
00:06:19,972 --> 00:06:28,301
模型拿到的是调用失败，而不是工具不存在，这两种信号会让模型做出完全不同的后续行为。

64
00:06:28,301 --> 00:06:35,356
重连成功后重新发现，恢复的世代整体替换掉旧世代，工具既不重复也不泄漏。

65
00:06:35,356 --> 00:06:41,534
只要服务器名没变，新世代的名字逐字相同，连 KV cache 的前缀都保得住。

66
00:06:41,534 --> 00:06:43,589
预算这里也有讲究。

67
00:06:43,589 --> 00:06:56,726
连接存活超过三十秒就重置尝试预算，所以偶尔崩一次的服务器可以无限次恢复，而反复崩溃循环的服务器最终会耗尽预算被注销，不会永远重启下去。

68
00:06:56,726 --> 00:07:03,121
这是一条很朴素的工程直觉，容错要留给偶发故障，不能留给设计错误。

69
00:07:03,268 --> 00:07:09,095
最后一个设计点最容易被忽略，这座桥刻意只桥了 MCP 的工具能力。

70
00:07:09,045 --> 00:07:18,143
MCP 协议里其实还有 resources，也就是资源，以及 prompts，也就是提示词模板，这两类能力 DeepSeek Harness 一概没接。

71
00:07:18,143 --> 00:07:27,771
README 的已知限制那一节写得很坦白，原话是只桥接 MCP 的工具能力，资源和提示词没有消费接口，暂缓实现。

72
00:07:27,771 --> 00:07:29,369
为什么这么干。

73
00:07:29,369 --> 00:07:31,148
逻辑不难还原。

74
00:07:31,148 --> 00:07:39,718
运行时内部没有任何一个组件会去消费一个外部资源或者一个外部提示词，那先造桥墩就没有意义。

75
00:07:39,718 --> 00:07:45,415
工具不一样，工具有明确的消费方，就是 Agent 循环里的工具调用。

76
00:07:45,415 --> 00:07:48,492
所以先桥工具，这叫按需造桥。

77
00:07:48,492 --> 00:08:00,583
我特别喜欢这个例子，因为它示范了一种很稀缺的克制，协议支持什么是一回事，你的系统里谁会消费它是另一回事，中间那段没人走的桥，造出来就是负债。

78
00:08:00,583 --> 00:08:03,805
还有一处细节体现了同样的克制。

79
00:08:03,805 --> 00:08:12,951
图片、音频这类非文本结果做了有损投影，在模型上下文里变成一个占位符，二进制载荷不进上下文。

80
00:08:12,951 --> 00:08:17,266
上下文是最贵的资源，不该被一堆字节占住。

81
00:08:17,266 --> 00:08:20,199
那被搁置的能力面靠什么补上。

82
00:08:20,199 --> 00:08:22,050
靠第二条路。

83
00:08:22,204 --> 00:08:24,690
第二条路的气质完全不同。

84
00:08:24,640 --> 00:08:26,995
一个动态插件分两半。

85
00:08:26,995 --> 00:08:36,226
Host 半在 Node 侧的 vm 沙箱里跑逻辑，Browser 半在页面里渲染界面，两半的生命周期由动态 Cordis 运行器统一管。

86
00:08:36,226 --> 00:08:39,039
带 Browser 半的启动要走审批。

87
00:08:39,039 --> 00:08:47,404
请求运行这个事件把请求送到页面，用户点了允许才继续，还可以勾选一并放行这个插件的后续版本。

88
00:08:47,404 --> 00:08:53,522
另外每个包版本是不可变的，改代码就是追加一个新版本，不是原地覆盖。

89
00:08:53,522 --> 00:09:03,966
这两个约束放在一起看，意图很清楚，版本不可变是为了让审批对象具体化，你批准的永远是某一版代码，而不是某个会变形的东西。

90
00:09:03,966 --> 00:09:10,625
生命周期工具一共五个，查询可用服务、提交源码、启动、停止、注销。

91
00:09:10,625 --> 00:09:17,596
这套设计里真正值得学的要点在于，模型不是直接执行代码，而是先探测再提交。

92
00:09:17,596 --> 00:09:21,587
先问运行时你能给我什么，再根据答案写插件。

93
00:09:21,587 --> 00:09:28,834
这比凭空猜接口名要稳得多，也是把模型从猜接口这件最容易出错的事情上解放出来。

94
00:09:28,834 --> 00:09:32,921
能力面上的差距，是两条路最本质的区别。

95
00:09:32,921 --> 00:09:42,789
MCP 桥能给模型的只有工具这一种东西，而 Extensions 能加工具、能发事件、能注册服务、还能画界面，四样都行。

96
00:09:42,789 --> 00:09:47,729
一个是接别人的电，一个是自己发电，代价完全不同。

97
00:09:47,884 --> 00:09:51,836
横向看两家，能把这个光谱照得更清楚。

98
00:09:51,786 --> 00:09:54,009
Claude Code 的实现厚得多。

99
00:09:54,009 --> 00:09:59,947
六种传输方式，七个配置来源层级，OAuth 认证加十五分钟缓存。

100
00:09:59,947 --> 00:10:02,916
工具命名跟 DeepSeek Harness 同形。

101
00:10:02,916 --> 00:10:07,086
权限规则能精确到工具级或者服务器级。

102
00:10:07,086 --> 00:10:19,731
它还有一个 DeepSeek Harness 没有的防御，工具描述截断到两千零四十八字符，因为观测到用 OpenAPI 自动生成的服务器，会往工具描述里塞十五到六十 KB 的文档。

103
00:10:19,731 --> 00:10:26,185
这个细节很妙，它说明防御是长在真实观测上的，不是拍脑袋加的上限。

104
00:10:26,185 --> 00:10:28,673
Grok Build 走的是另一条路线。

105
00:10:28,673 --> 00:10:37,844
仓库里 MCP 客户端和插件市场并存，MCP 负责协议兼容，市场负责分发与信任，走的是集中审核的生态路线。

106
00:10:37,844 --> 00:10:40,860
三家放在一起，光谱就出来了。

107
00:10:40,860 --> 00:10:50,909
Grok 靠市场集中管信任，Claude Code 靠七层配置和权限规则分散管信任，DeepSeek Harness 把两条路拆开，各配各的信任模型。

108
00:10:50,909 --> 00:10:53,781
差异不在功能多少，在取向。

109
00:10:53,781 --> 00:10:58,541
Claude Code 把 MCP 当成唯一的官方扩展点，做深做全。

110
00:10:58,541 --> 00:11:03,985
DeepSeek Harness 把桥做薄，只桥工具，把重能力留给原生 Extensions。

111
00:11:03,985 --> 00:11:09,719
前者的扩展跑在进程外，后者多给了一条跑在进程内的路。

112
00:11:09,719 --> 00:11:14,707
这两条路是正交的，各管一头，不存在谁替代谁。

113
00:11:14,860 --> 00:11:18,475
我们用一节课的练习把前面讲的串起来。

114
00:11:18,425 --> 00:11:29,159
假设天气服务器在模型刚拿到工具清单之后崩溃了，八秒后被 supervisor 拉起来，而这次它的工具清单多了一个 get_alerts。

115
00:11:29,159 --> 00:11:32,608
请你在脑子里按时间顺序推演一遍。

116
00:11:32,608 --> 00:11:35,589
崩溃的瞬间，注册表里有什么。

117
00:11:35,589 --> 00:11:39,615
是上一个正常世代的整套工具，一个不少。

118
00:11:39,615 --> 00:11:45,493
崩溃本身不会触发任何注销动作，因为注销只发生在交换阶段。

119
00:11:45,493 --> 00:11:49,988
中断期间模型调用那个查预报的工具，会得到什么。

120
00:11:49,988 --> 00:11:53,882
会得到一次调用失败，而不是工具不存在。

121
00:11:53,882 --> 00:12:06,502
这两者的区别前面讲过，前者模型会重试或者换参数，后者模型会以为自己记错了工具名，然后开始编造一个根本不存在的工具名，这是最坏的一种失败。

122
00:12:06,502 --> 00:12:09,711
重连成功之后注册表经历了什么。

123
00:12:09,711 --> 00:12:16,947
走一遍完整的世代替换，先拉新清单、建好新世代，再注销旧世代、注册新世代。

124
00:12:16,947 --> 00:12:20,132
那个查预报工具的公开名变了吗。

125
00:12:20,132 --> 00:12:27,704
没变，因为名字是服务器名和原始名的纯函数，服务器名没变，名字就逐字相同。

126
00:12:27,704 --> 00:12:32,272
新增的 get_alerts 则作为一个新工具补进来。

127
00:12:32,272 --> 00:12:40,108
最后一个追问，如果两个不同的服务器 weather 和 weather2 都暴露了 get_forecast，它们会冲突吗。

128
00:12:40,108 --> 00:12:41,082
不会。

129
00:12:41,082 --> 00:12:47,800
因为它们各自有独立的命名空间，公开名里带着服务器名这一段，天然不重叠。

130
00:12:47,800 --> 00:12:56,983
而如果同一个命名空间下真的撞了，世代替换的回滚逻辑会把整代注销掉并且报错，不会留下一半。

131
00:12:57,124 --> 00:13:00,174
最后给你几条能直接拿走的东西。

132
00:13:00,124 --> 00:13:03,129
第一，名字要设计成纯函数。

133
00:13:03,129 --> 00:13:10,641
公开名只由服务器名和原始名决定，跟连接顺序、重试次数、其他服务器全都无关。

134
00:13:10,641 --> 00:13:18,935
这样断线重连之后名字不变，KV cache 前缀保得住，这是纯函数换来的真实收益，不只是代码好看。

135
00:13:18,935 --> 00:13:23,778
第二，注册要按世代整体替换，失败就整代回滚。

136
00:13:23,778 --> 00:13:29,307
让模型看到的工具集要么完整要么为空，永远不要有中间态。

137
00:13:29,307 --> 00:13:35,473
半套工具比没有工具更危险，因为它会诱导模型用错误的路径去绕。

138
00:13:35,473 --> 00:13:40,473
第三，断线期间保持旧世代注册，不要让工具凭空消失。

139
00:13:40,473 --> 00:13:51,098
调用失败是可解释的错误，工具不存在是不可解释的错误，模型对这两种信号的后续行为完全不同，前者重试，后者幻觉。

140
00:13:51,098 --> 00:13:54,536
第四，没有消费方就不要造桥墩。

141
00:13:54,536 --> 00:13:59,596
协议里有什么能力是一回事，运行时里谁会消费是另一回事。

142
00:13:59,596 --> 00:14:06,555
按需造桥能省下大量永远不会被调用的抽象，这些抽象最后都会变成维护成本。

143
00:14:06,555 --> 00:14:13,358
第五，进程外的能力靠隔离挡风险，进程内的能力靠审批和沙箱看风险。

144
00:14:13,358 --> 00:14:17,781
这两种信任模型不能混用，也不能互相替代。

145
00:14:17,781 --> 00:14:23,454
想清楚你的扩展到底跑在哪一侧，再决定给它配哪种看管方式。

146
00:14:23,454 --> 00:14:25,557
这一集就到这里。

