本集对应课程:DeepSeek Harness · 模型与外部接入 ·《MCP 与 Extensions:外部工具接入的两条路》
音频与本文配套,本文为文字版深度解读,可独立阅读。
桥接生态标准与原生扩展怎么分工。DSH 接外部能力有两条路,MCP 桥负责接协议生态里现成的工具服务器,Extensions 负责让模型在 harness 里现写现跑插件。两条路正交,各配各的信任模型:桥外靠隔离,桥内靠审批与沙箱。
一个 Agent 运行时,模型的原生能力只有「生成文本」和「调用已注册工具」两件事。凡是超出这个范围的动作——查实时天气、查内部数据库、驱动公司私有系统——都必须从外部引入。
引入外部能力时,现实会分成两种截然不同的情况:
把这两种情况当成同一件事处理,是很多 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 能加工具、能发事件、能注册服务、还能画界面。这不是版本先后的差距,是设计取向上的分工。
每个 MCP 工具有两个名字:
tools/call 请求用它。mcp__<serverName>__<rawName>,与 Claude Code 和 Codex 同形。转换规则(packages/mcp/mcp-client/src/tools.ts 第 96–102 行):
serverName\0rawName。为什么要哈希:截断会引入碰撞风险。假设 get_forecast_daily 与 get_forecast_daily_x 都被截到 64 字符,两者可能折叠成同一个名字。哈希保证不同身份绝不折叠。
为什么要是纯函数:公开名只由 (serverName, rawName) 决定,与连接顺序、重新同步、其他服务器全部无关。这直接带来断线重连后名字不变、KV cache 前缀得以保留的实际收益。
服务器工具清单会变,变了就要重新同步。同步分两阶段:
交换阶段的回滚写法(tools.ts 第 159–172 行):注册循环里每成功注册一个工具,就把它的注销函数存进一张表;任何一次注册抛出冲突(意味着有外来注册霸占了这台服务器的命名空间),catch 分支就把表里已注册的全部注销,一个不留,再记一条 error 日志。
源码注释的意图很直白:回滚是为了让模型看到的要么是完整的一个世代,要么什么都没有,绝不能是半套。
这不是代码整洁问题,是失败形态问题。模型看到一半工具,会以为另一半压根不存在,于是自己找一条绕路——而那条绕路很可能走不通,或者更糟,走通了但走到错误的地方。宁可让它看到空,它至少会停下来说一句「工具不见了」。
设计推论:凡是模型会感知到的资源状态,都应该保证「全有或全无」的原子性,中间态的代价由模型的错误推理来放大。
「调用失败」是可解释的,模型会重试或换参数;「工具不存在」是不可解释的,模型会以为自己记错了工具名,进而编造一个根本不存在的工具名——这是最坏的一种失败。
设计推论:断线时保持旧注册表的决策,本质上是在购买错误的可解释性。任何会让模型怀疑自身记忆的状态,都应该被显式避免。
MCP 协议里还有 resources(资源)和 prompts(提示词模板)两类能力,DSH 一概没接。README 的「已知限制与暂缓事项」写得坦白:
「只桥接 MCP 的工具能力:资源和提示词没有 harness 消费接口,暂缓实现。」
理由很清楚:运行时内部没有任何组件会消费一个外部 resource 或外部 prompt,先造桥墩没有意义。工具有明确消费方(Agent loop 的工具调用),所以先桥工具。
配套的另一处克制:图片、音频这类非文本结果做有损投影,在模型上下文里变成占位符,二进制载荷不进上下文。
这是本集最值得迁移的一条。协议支持什么是一回事,你的运行时里谁会消费是另一回事。中间那段没人走的桥,造出来就是负债——它不只是代码量,还是长期的维护面和测试面。
设计推论:实现协议时先问「消费方是谁」。答不上来就先不实现,等消费方出现再补。
Cordis 是 DSH 的插件框架,整个 harness 就是一棵 Cordis 插件树。Extensions 子系统让模型在会话里现写一个 Cordis 插件、当场跑起来。
packages/extensions/tool-cordis 注册)| 工具 | 作用 |
|---|---|
cordis_inspect | 查询当前运行时有哪些服务和接口可用 |
cordis_define | 提交插件源码 |
cordis_run | 启动插件 |
cordis_stop | 停止插件 |
cordis_undefine | 注销插件 |
先探测再提交是这套设计的核心——先问运行时「你能给我什么」,再根据答案写插件。这把模型从「猜接口名」这件最容易出错的事情上解放了出来。
node:vm 沙箱里跑逻辑。ctx.dynamicCordisRunner(packages/extensions/cordis-host-runner/src/index.ts 第 124 行起)统一管理。带 Browser 半的启动要走审批:cordis/request-run 事件把请求送到页面,用户点了允许才继续,还可勾选一并放行该插件的后续版本(runHostHalf 的 approveFutureVersions 参数)。
每个 Package 版本不可变,改代码就是追加新版本,不是原地覆盖。
这条约束与审批配合起来看,意图很清楚:版本不可变是为了让审批对象具体化——你批准的永远是某一版代码,而不是某个会变形的东西。否则「已批准」这个状态会立刻失去意义。
| Claude Code | Grok Build | DeepSeek Harness | |
|---|---|---|---|
| MCP 实现厚度 | 满配:六种传输(stdio、sse、sse-ide、http、ws、sdk)、七个配置来源层级、OAuth + 15 分钟缓存 | MCP 客户端与插件市场并存 | 薄:只桥 tools |
| 额外扩展点 | 无(MCP 是唯一官方扩展点) | 插件市场(集中审核分发) | 原生 Extensions(进程内) |
| 信任管理 | 分散:七层配置 + 权限规则(工具级/服务器级) | 集中:市场审核 | 拆分:桥外隔离,桥内审批 |
| 特色防御 | 工具描述截断到 2048 字符 | — | 世代回滚、哈希防碰撞 |
工具描述截断到 2048 字符,原因是观测到用 OpenAPI 自动生成的服务器会往描述里塞 15–60KB 的文档(services/mcp/client.ts 第 217–219 行注释)。
值得留意的是这条防御的性质:它不是拍脑袋加的上限,而是长在真实观测上的。
在设计你自己的外部能力接入方案时,逐条对照:
packages/mcp/mcp-client/ 与 packages/extensions/ 两个子系统,外加 Claude Code 与 Grok Build 的横向对照。源码核对日期为 2026-08-13,依据本地仓库 deepseek-harness-master。claude-code-sourcemap-main/study/chapters/08-mcp.md),不涉及运行时实测。假设 weather 服务器在模型刚拿到工具清单后崩溃,8 秒后被 supervisor 拉起,且这次工具清单多了一个 get_alerts。
mcp__weather__get_forecast 会得到什么? 一次调用失败,而不是工具不存在。前者模型会重试或换参数,后者可能诱发编造工具名。get_forecast 的公开名不变(纯函数,服务器名未变),get_alerts 作为新工具补进来。get_forecast,会冲突吗? 不会。各自有独立命名空间,公开名里带服务器名这一段,天然不重叠。若同一命名空间下真撞了,世代回滚会把整代注销并报错,不会留下一半。*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*