学 AI 产品 · 专业 AI 产品经理播客第 4 章 · T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座 · EP 15
第 4 章 · EP 15

MCP 与 Extensions:外部工具接入的两条路

时长 14:26音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场1:15
1:15两条路先对齐名词1:23
2:38路A:MCP 桥怎么把外部服务器接进来1:54
4:32世代:要么全有要么全无2:30
7:03桥只桥 tools 是刻意的1:18
8:22路B:Extensions 让模型给自己长插件1:25
9:47两家对照,看清取向1:26
11:14一次断线重连的完整推演1:42
12:57可带走的设计原则1:28
解读全文

MCP 与 Extensions:外部工具接入的两条路

本集对应课程:DeepSeek Harness · 模型与外部接入 ·《MCP 与 Extensions:外部工具接入的两条路》
音频与本文配套,本文为文字版深度解读,可独立阅读。

一句话速览

桥接生态标准与原生扩展怎么分工。DSH 接外部能力有两条路,MCP 桥负责接协议生态里现成的工具服务器,Extensions 负责让模型在 harness 里现写现跑插件。两条路正交,各配各的信任模型:桥外靠隔离,桥内靠审批与沙箱。


一、本集要解决的工程问题

一个 Agent 运行时,模型的原生能力只有「生成文本」和「调用已注册工具」两件事。凡是超出这个范围的动作——查实时天气、查内部数据库、驱动公司私有系统——都必须从外部引入。

引入外部能力时,现实会分成两种截然不同的情况:

  1. 能力已存在:社区里已经有人把工具服务器写好了,你只想接过来用,最好一行业务逻辑都不改。
  2. 能力不存在:这个需求世界上没人写过,只能让模型现场写一段代码当场跑起来。

把这两种情况当成同一件事处理,是很多 Agent 框架设计失误的根源。因为它们的风险来源完全不同:前者是「别人的进程可能不可靠」,后者是「模型写的代码可能不安全」。用一套信任模型覆盖两种风险,必然出现要么过度收紧、要么过度放开的结果。

DeepSeek Harness(下称 DSH)的答案是拆成两条路。


二、能力地图

维度路 A:MCP 桥路 B:原生 Extensions
面对的对象进程外、已经存在的工具服务器模型现场生成的 Cordis 插件
核心源码packages/mcp/mcp-client/packages/extensions/
传输方式stdio 子进程、streamable-http进程内执行,无网络传输
能给模型什么只有工具(tools)工具、事件、服务、界面,四样都行
信任模型隔离:崩溃、垃圾返回、断线都被挡在桥外审批 + 沙箱:Host 半在 node:vm 里跑,Browser 半启动需用户点允许
失败形态调用失败,但工具名不消失未获审批则根本不启动
一句话比喻从外面接电自己发电

关键判断:两条路正交,不是替代关系

能力面上的差距是本质的。MCP 桥只能给模型「工具」这一种东西,而 Extensions 能加工具、能发事件、能注册服务、还能画界面。这不是版本先后的差距,是设计取向上的分工。


三、路 A 详解:MCP 桥的四个设计点

3.1 命名:公开名是纯函数

每个 MCP 工具有两个名字:

  • 原始名:只在网线上出现,tools/call 请求用它。
  • 公开名:模型看到的,格式为 mcp__<serverName>__<rawName>,与 Claude Code 和 Codex 同形。

转换规则(packages/mcp/mcp-client/src/tools.ts 第 96–102 行):

  1. 用正则把非法字符替换为下划线——公开名必须满足 DeepSeek 函数名约定(最长 64 字符,仅字母数字下划线连字符)。
  2. 若替换结果与原串完全一致且长度未超限 → 直接返回,一个字符都不动。
  3. 否则 → 截断并在尾部追加 12 位十六进制 SHA-256 哈希,哈希输入为 serverName\0rawName。

为什么要哈希:截断会引入碰撞风险。假设 get_forecast_daily 与 get_forecast_daily_x 都被截到 64 字符,两者可能折叠成同一个名字。哈希保证不同身份绝不折叠。

为什么要是纯函数:公开名只由 (serverName, rawName) 决定,与连接顺序、重新同步、其他服务器全部无关。这直接带来断线重连后名字不变、KV cache 前缀得以保留的实际收益。

3.2 世代:要么全有,要么全无

服务器工具清单会变,变了就要重新同步。同步分两阶段:

  • 阶段一:把下一世代的全部工具定义拉完、建好。任何一步失败都不碰注册表,上一世代原样存活。
  • 阶段二:交换——先注销旧世代,再注册新世代。

交换阶段的回滚写法(tools.ts 第 159–172 行):注册循环里每成功注册一个工具,就把它的注销函数存进一张表;任何一次注册抛出冲突(意味着有外来注册霸占了这台服务器的命名空间),catch 分支就把表里已注册的全部注销,一个不留,再记一条 error 日志。

源码注释的意图很直白:回滚是为了让模型看到的要么是完整的一个世代,要么什么都没有,绝不能是半套。

提示1:半套工具集比没有工具更危险

这不是代码整洁问题,是失败形态问题。模型看到一半工具,会以为另一半压根不存在,于是自己找一条绕路——而那条绕路很可能走不通,或者更糟,走通了但走到错误的地方。宁可让它看到空,它至少会停下来说一句「工具不见了」。

设计推论:凡是模型会感知到的资源状态,都应该保证「全有或全无」的原子性,中间态的代价由模型的错误推理来放大。

3.3 断线重连:建在世代之上

  • stdio 子进程崩溃 → supervisor 指数退避重启:首次延迟默认 500ms,逐次翻倍,上限 30s,一次中断最多试 10 次。
  • 中断期间最后一个正常世代保持注册——模型调用会失败,但工具名不会凭空消失。
  • 重连成功后重新发现,恢复的世代整体替换旧世代,工具既不重复也不泄漏。
  • 服务器名未变 → 新世代名字逐字相同 → KV cache 前缀保得住。
  • 预算重置:连接存活超过 30s 就重置尝试预算。所以偶尔崩一次的服务器可无限恢复,反复崩溃循环的服务器最终耗尽预算被注销,不会永远重启。

提示2:可解释的错误 vs 不可解释的错误

「调用失败」是可解释的,模型会重试或换参数;「工具不存在」是不可解释的,模型会以为自己记错了工具名,进而编造一个根本不存在的工具名——这是最坏的一种失败。

设计推论:断线时保持旧注册表的决策,本质上是在购买错误的可解释性。任何会让模型怀疑自身记忆的状态,都应该被显式避免。

3.4 桥只桥 tools:按需造桥

MCP 协议里还有 resources(资源)和 prompts(提示词模板)两类能力,DSH 一概没接。README 的「已知限制与暂缓事项」写得坦白:

「只桥接 MCP 的工具能力:资源和提示词没有 harness 消费接口,暂缓实现。」

理由很清楚:运行时内部没有任何组件会消费一个外部 resource 或外部 prompt,先造桥墩没有意义。工具有明确消费方(Agent loop 的工具调用),所以先桥工具。

配套的另一处克制:图片、音频这类非文本结果做有损投影,在模型上下文里变成占位符,二进制载荷不进上下文。

提示3:协议支持什么 ≠ 你的系统需要什么

这是本集最值得迁移的一条。协议支持什么是一回事,你的运行时里谁会消费是另一回事。中间那段没人走的桥,造出来就是负债——它不只是代码量,还是长期的维护面和测试面。

设计推论:实现协议时先问「消费方是谁」。答不上来就先不实现,等消费方出现再补。


四、路 B 详解:Extensions 让模型给自己长插件

Cordis 是 DSH 的插件框架,整个 harness 就是一棵 Cordis 插件树。Extensions 子系统让模型在会话里现写一个 Cordis 插件、当场跑起来。

4.1 五个生命周期工具(由 packages/extensions/tool-cordis 注册)

工具作用
cordis_inspect查询当前运行时有哪些服务和接口可用
cordis_define提交插件源码
cordis_run启动插件
cordis_stop停止插件
cordis_undefine注销插件

先探测再提交是这套设计的核心——先问运行时「你能给我什么」,再根据答案写插件。这把模型从「猜接口名」这件最容易出错的事情上解放了出来。

4.2 一个动态插件分两半

  • Host 半:Node 侧 node:vm 沙箱里跑逻辑。
  • Browser 半:页面里渲染 UI。
  • 两半生命周期由 ctx.dynamicCordisRunner(packages/extensions/cordis-host-runner/src/index.ts 第 124 行起)统一管理。

带 Browser 半的启动要走审批:cordis/request-run 事件把请求送到页面,用户点了允许才继续,还可勾选一并放行该插件的后续版本(runHostHalf 的 approveFutureVersions 参数)。

4.3 版本不可变

每个 Package 版本不可变,改代码就是追加新版本,不是原地覆盖。

这条约束与审批配合起来看,意图很清楚:版本不可变是为了让审批对象具体化——你批准的永远是某一版代码,而不是某个会变形的东西。否则「已批准」这个状态会立刻失去意义。


五、横向对比:三家怎么接外部能力

Claude CodeGrok BuildDeepSeek Harness
MCP 实现厚度满配:六种传输(stdio、sse、sse-ide、http、ws、sdk)、七个配置来源层级、OAuth + 15 分钟缓存MCP 客户端与插件市场并存薄:只桥 tools
额外扩展点无(MCP 是唯一官方扩展点)插件市场(集中审核分发)原生 Extensions(进程内)
信任管理分散:七层配置 + 权限规则(工具级/服务器级)集中:市场审核拆分:桥外隔离,桥内审批
特色防御工具描述截断到 2048 字符—世代回滚、哈希防碰撞

Claude Code 那条观测驱动的防御

工具描述截断到 2048 字符,原因是观测到用 OpenAPI 自动生成的服务器会往描述里塞 15–60KB 的文档(services/mcp/client.ts 第 217–219 行注释)。

值得留意的是这条防御的性质:它不是拍脑袋加的上限,而是长在真实观测上的。


六、审查清单

在设计你自己的外部能力接入方案时,逐条对照:

  • 外部能力的名字是不是纯函数生成的?连接顺序、重试次数、其他实例会不会影响它?
  • 工具注册有没有世代概念?失败时是整代回滚,还是留下半套?
  • 断线期间旧注册表是保留还是清空?保留的话,模型拿到的是「调用失败」还是「工具不存在」?
  • 重连后的名字是否与重连前逐字相同?KV cache 前缀是否保得住?
  • 重启预算有没有重置机制?崩溃循环会不会无限重启?
  • 协议里那些没有消费方的能力,你是不是也顺手实现了?
  • 非文本结果有没有做投影?二进制载荷会不会进上下文?
  • 进程内执行的代码有没有沙箱?有没有审批?审批对象是不是一个不可变的版本?
  • 模型的扩展点是不是「先探测再提交」,而不是凭空猜接口?
  • 两条路(进程外/进程内)的信任模型是不是分开配的?有没有互相替代的错觉?

七、约束说明

  1. 本集取材范围:DSH 的 packages/mcp/mcp-client/ 与 packages/extensions/ 两个子系统,外加 Claude Code 与 Grok Build 的横向对照。源码核对日期为 2026-08-13,依据本地仓库 deepseek-harness-master。
  2. 源码引用边界:只引用了公开可读的仓库结构与注释,未引入仓库外的推测性实现。凡是文中标注「出处」的行号,均以核对日期的快照为准,后续版本可能偏移。
  3. 非文本结果:图片、音频在模型上下文里被投影为占位符,这一结论仅针对 DSH 的实现,不代表其他框架同样处理。
  4. 被搁置的能力:resources 与 prompts 的「暂缓实现」是核对日期当下的状态,不是永久设计决策。若未来 harness 内部出现消费方,这条取舍可能改变。
  5. 横向对比的口径:Claude Code 与 Grok Build 的对照基于各自开源仓库的静态阅读(Claude Code 侧依据 claude-code-sourcemap-main/study/chapters/08-mcp.md),不涉及运行时实测。
  6. 本集不含题库内容:课程中的练习推演已改写为正文讲解,不在集页另行出题。

八、可带走的设计原则

  1. 名字要设计成纯函数。 输出只由确定的输入决定,与时序、顺序、其他实例无关。它换来的不只是代码好看,而是重试后状态可复用(KV cache 前缀保住)这类真实收益。
  2. 注册要按世代整体替换,失败整代回滚。 让模型看到的工具集要么完整要么为空,永远不要有中间态。
  3. 断线期间保持旧世代注册。 调用失败是可解释的错误,工具不存在是不可解释的错误,后者会诱发幻觉。
  4. 没有消费方就不要造桥墩。 按需实现,避免永远不会被调用的抽象变成维护负债。
  5. 进程外靠隔离,进程内靠审批与沙箱。 两种信任模型不能混用,也不能互相替代。先想清楚扩展跑在哪一侧,再决定配哪种看管方式。

九、一次断线重连的完整推演

假设 weather 服务器在模型刚拿到工具清单后崩溃,8 秒后被 supervisor 拉起,且这次工具清单多了一个 get_alerts。

  1. 崩溃瞬间注册表里有什么? 上一个正常世代的整套工具,一个不少。崩溃本身不触发任何注销动作,因为注销只发生在交换阶段。
  2. 中断期间调用 mcp__weather__get_forecast 会得到什么? 一次调用失败,而不是工具不存在。前者模型会重试或换参数,后者可能诱发编造工具名。
  3. 重连后注册表经历了什么? 完整世代替换:拉新清单 → 建新世代 → 注销旧世代 → 注册新世代。get_forecast 的公开名不变(纯函数,服务器名未变),get_alerts 作为新工具补进来。
  4. 两个服务器 weather 与 weather2 都暴露 get_forecast,会冲突吗? 不会。各自有独立命名空间,公开名里带服务器名这一段,天然不重叠。若同一命名空间下真撞了,世代回滚会把整代注销并报错,不会留下一半。

*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*