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

多入口与 Typert:一个内核,五张面孔

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

同步字幕

章节导航(点击跳转)

0:00开场1:23
1:23入口只是一份配置1:54
3:17Web 与 HTTP 这两张脸1:50
5:07stdout 归协议的铁律1:21
6:28Typert 为什么要自研2:07
8:35三条固执的纪律1:16
9:52别家的面孔怎么长1:29
11:22设计第六张面孔1:21
12:44可带走的设计原则1:28
解读全文

多入口与 Typert:一个内核,五张面孔

本集对应课程:DeepSeek Harness · 持久化与基建 ·《多入口与 Typert:一个内核,五张面孔》
音频与本文配套,本文为文字版深度解读,可独立阅读。

一句话速览

Web、headless、ACP、SDK、HTTP 共享同一个内核。所谓「入口」,就是一份不同的 cordis.yml;加一张新面孔约等于写一个翻译插件加一份配置。


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

一个 Agent 内核要同时支持五种使用方式:

面孔用途形态
Web UI浏览器里的人机交互插件树上挂 webserver
headless无人值守脚本进程本身就是入口,不挂任何服务器
ACP给编辑器用的协议接口挂 ACP 协议桥 + 沙箱策略
Python SDK从 Python 调用拉起子进程,stdio 上说 JSON-RPC
HTTP API给外部系统调一条 api-remotes → api-gateway → connection 链

最朴素的办法是把业务逻辑复制五份,每个入口一份。复制的第一天很爽,第三个月开始出事:改一个会话字段要改五处,而且总有一处会漏。

DSH 的解法是把这件事反过来——内核对入口一无所知。


二、能力地图


五张面孔(皮)                    同一个内核(身)
─────────────────────────────────────────────
Web UI        → host-webserver + frontend-static + client-modules
headless      → (不挂服务器,进程即入口)
ACP           → acp-demo 协议桥 + 沙箱策略
Python SDK    → 拉起 dsh-jsonrpc-agent 子进程 + stdio JSON-RPC
HTTP API      → api-remotes → api-gateway → connection
                        ↓ 全部共用
        DeepSeek 适配器 / bash 执行器 / JSONL 会话持久化 / 压缩 / 文件系统工具

验收标准:同一个操作从哪张脸进来,会话日志里落下的事件一字不差。


三、入口只是一份配置

3.1 三份示例配置的对比

examples/ 目录下有三份现成的 cordis.yml:headless、JSON-RPC、ACP。三者大同小异——内核插件三份全有,差异集中在最上面几行:

入口顶部多挂的插件
JSON-RPCsdk-jsonrpc-server
ACPacp-demo 协议桥 + 沙箱策略
headless无(进程本身就是入口)

推论:加一张新面孔的成本 ≈ 写一个翻译插件 + 一份配置。翻译插件只做协议翻译,业务逻辑一行都不进它。

3.2 Web 面孔的皮厚一点,但仍是插件

  • host-webserver:纯粹的 node:http 载体。文档明说它不属于 agent loop、不了解任何 harness 概念(docs/subsystems/web-server.zh.md)。
  • frontend-static:认领回退席位,当 SPA 服务器。
  • client-modules:用 tapIndex 往 index.html 注入启动清单 window.__DSH_BOOT__,浏览器端照单加载各插件的前端模块。
连前端模块清单都不是写死的,而是各插件声明、框架启动时拼出来的——与服务端插件树同一个思路,只是长在浏览器那一侧。

3.3 HTTP API 是一条链

api-remotes(身份解析)→ api-gateway(参数解码 + 方法调用)→ connection(独占 /api 路由的 RPC 信封)→ 落回同一个 webserver(docs/api-gateway.zh.md)。

3.4 Python SDK 说明「皮」可以有多薄

pip install deepseek-harness-sdk 会连带装一个平台 wheel,里面是单文件可执行的 dsh-jsonrpc-agent。SDK 启动它当子进程,通过 DSH_CORDIS_CONFIG 注入默认组合,然后在 stdio 上说 JSON-RPC(python/sdk/README.zh.md)。

结论:Python SDK 与 JSON-RPC 入口是同一张面孔的两种穿法,Python 这层只是把协议包成了 harness.run(...)。


四、stdout 归协议:一条硬纪律

4.1 原文证据

examples/jsonrpc-agent/cordis.yml 第 1 行注释说明这是给捆绑运行时用的无人值守部署,第 2 行就是铁律原文:

stdout 保留给 JSON-RPC,不要加 console logger 或终端 UI。

examples/acp-agent/cordis.yml 同样声明:整棵树不挂 stdout 日志和 HMR。

提示1:这不是洁癖,是正确性

这两种协议把 stdout 当传输线用。报文一个字节一个字节从这条线上过去,往上面混打一行日志,对端的解析器当场断线。

日志走 ctx.logger 另寻出路。协议入口的铁律:传输线只能跑协议,别的什么都别往上写。

提示2:可推广的抽象原则

凡是进程把某个输出流当成管道用,那个流就必须独占。谁都可以往里写的管道,等于没有管道。

划定管道时,第一件事应该是宣布它归谁独占,而不是先想着往里塞什么。


五、Typert:为什么要自研 RPC 层

Web 与 HTTP API 这两张脸需要跨进程调用 Host 里的业务方法。DSH 没用现成框架,自研了 Typert。

5.1 业务开发者要做的事

只剩一个装饰器。在服务方法上标 @Remote('create'),构建时 Typert 分析 TypeScript 类型图,生成三样东西:

  1. 校验参数的 Zod schema
  2. 描述调用的 descriptor
  3. 给浏览器端用的类型声明

不需要手写路由表、参数转换、客户端 stub。改一处方法签名,重新构建后所有端的调用约定同步更新(docs/subsystems/typert.zh.md、packages/typert/generator/README.zh.md)。

5.2 现成方案为什么不行的三个原因

#问题Typert 的解法
1Host 与浏览器是两个独立的 TypeScript Program,同名 Cordis Context 的类型合并结果不同,一份 schema 喂不通两边各自在构建时生成对应的 descriptor
2参数可能是 Agent 这样的活对象,不能被序列化过 wirelookup 机制:agent 参数改写为 wire 字段 agentId,Gateway 收到后先解析回活对象再调方法
3客户端 ctx.remote.goals 是随插件挂载卸载的活服务,最后一个方法撤回整个 namespace 跟着卸载静态描述(如 OpenAPI)无法表达此生命周期

5.3 三条固执的纪律

  1. descriptor 是本地反射信息,不上 wire。 Host 与客户端各自构建时生成彼此对应的 descriptor,请求里只发 endpoint 和具名参数;取消信号作为带外 carrier signal 注入,绝不混进业务参数。
  2. 方法不标记就不存在。 只有被 @Remote 标记的方法才是远程方法,未标记的既不进 Client 类型,也无法经 ctx.remote 调用。
  3. 遇到表达不了的类型投影直接报错。 绝不把源类型展平弱化蒙混过去。

提示3:会降级的工具比会拒绝的工具更危险

生成器遇到表达不了的类型时报错,与「拒绝解读」一脉相承——说不清楚的事宁可不做。

为什么这条重要:报错你立刻就知道;降级会让你以为类型是对的,然后在很远的地方炸掉。一个悄悄弱化的生成器,比一个会报错的生成器危险得多。


六、横向对比:别家的面孔怎么长

Grok BuildClaude CodeDeepSeek Harness
主体面孔桌面端 + CLI终端 CLI(TUI-first)Web(Web-first)
ACP独立 Rust 实现 xai-acp-lib,stdin 行读取器 + 双向通道 + 网关收发器—acp-demo 协议桥 + 沙箱策略
多面孔来源主体 + 给编辑器的接口同一 CLI 衍生(-p 模式、Agent SDK 再包一层)摊平成配置差异
需要 RPC 层吗部分需要不需要(一个进程一个 UI)需要(Typert)
内核是否知悉入口——内核对入口一无所知

成本结构差异

  • TUI-first 加 Web 面孔 → 要补一整层 RPC。
  • Web-first 加 CLI 面孔 → 理论上只是一份新的 cordis.yml 加一个驱动器。

两条路线没有对错,是成本结构不同。DSH 发布时甚至没有传统终端交互入口,发布讨论里最响的声音就是「我的 CLI 呢」——这是入口取舍上的产品决策:先把内核与面孔解耦的架构立住,缺哪张皮补哪张。


七、审查清单

设计多入口系统时逐条对照:

  • 同一个操作从不同入口进来,落下的事件是否一字不差?有差异就说明逻辑被分叉了。
  • 入口是不是退化成了「一份配置 + 一个翻译插件」?翻译插件里有没有混入业务逻辑?
  • 加一张新面孔的预估成本,是接近写一份配置,还是接近重写一遍系统?
  • 哪些输出流被当成了传输线?这些流有没有被显式独占?
  • 协议入口的配置里,有没有明令禁止往传输线打日志?这条约束写在哪、谁看得见?
  • 远程方法是不是显式声明的?有没有「碰巧长出来」的远程接口?
  • 活对象过 wire 时,是通过 id 间接引用,还是试图直接序列化?
  • 客户端的远程服务有没有生命周期?静态描述能不能表达它?
  • 代码生成器遇到表达不了的类型,是报错还是悄悄降级?
  • 你缺的是哪张皮?先想清楚这个,再决定内核该长成什么样。

八、约束说明

  1. 取材范围:examples/headless-agent/cordis.yml、examples/jsonrpc-agent/cordis.yml、examples/acp-agent/cordis.yml、docs/api-gateway.zh.md、docs/subsystems/typert.zh.md、docs/subsystems/web-server.zh.md、packages/typert/generator/README.zh.md、python/sdk/README.zh.md。源码核对日期 2026-08-13,依据本地仓库 deepseek-harness-master。
  2. 演示数据的性质:课程中的「五面孔控制台」交互演示为教学化模拟,报文与事件是虚构的示意 fixture,字段做了简化。本集所有结论均来自上述配置文件与文档,不依赖演示 fixture。
  3. 共用内核插件的口径:取三份 cordis.yml 配置的交集,不代表任意历史版本的完整清单。
  4. Typert 的必要性论证:仅针对 DSH 的 Web-first 架构成立。TUI-first 架构(如 Claude Code)不需要跨进程 RPC,也就不存在这个问题。
  5. 横向对比口径:Grok Build 依据 crates/codegen/xai-acp-lib/ 的静态阅读;Claude Code 为其公开架构特征。不涉及运行时实测。
  6. 本集不含题库内容:课程中的「设计第六张面孔」练习已改写为正文讲解,不在集页另行出题。

九、设计第六张面孔(练习推演)

假设要给 DSH 加一个聊天软件机器人入口:用户在群里 @机器人 说话,回复流回群里。

哪些东西不用写?

内核插件、会话持久化、压缩、工具——全部照抄现有 cordis.yml。这是「入口 = 配置」最直接的收益:要写的代码量与入口复杂度无关。

哪些东西必须写?

一个协议桥插件:把群消息翻成会话 prompt,把会话事件翻成群回复。就这一件。

这个桥有没有 stdout 互斥问题?

大概率没有——群消息不跑在 stdout 上。

那它的「传输线纪律」等价物是什么?

群平台的速率限制和消息长度限制。模型产出的事件是连续、细碎的,而群消息不能这么发(刷屏 + 被限流),所以事件流必须节流与合并。

这道题的价值在于:它逼你把一条具体纪律抽象成一般原则——不是记住「stdout 不能打日志」,而是先问「我的传输线是什么,它的约束是什么」。

十、可带走的设计原则

  1. 先立内核与面孔的边界,再谈支持几个入口。 判断标准:同一操作从不同入口进来,落下的事件是否一字不差。
  2. 入口应退化成一份配置加一个翻译插件。 加新面孔的成本应接近写一份配置,而非接近重写。
  3. 被当成传输线的输出流必须独占。 可推广到任何管道、队列、共享通道。
  4. 远程面要显式声明。 方法不标记就不存在,宁可让人多写一个装饰器,也不要让远程面碰巧长出来。
  5. 生成器遇到表达不了的东西要报错,不要悄悄降级。 会弱化的工具比会拒绝的工具危险得多。

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