本集对应课程: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 会话持久化 / 压缩 / 文件系统工具
验收标准:同一个操作从哪张脸进来,会话日志里落下的事件一字不差。
examples/ 目录下有三份现成的 cordis.yml:headless、JSON-RPC、ACP。三者大同小异——内核插件三份全有,差异集中在最上面几行:
| 入口 | 顶部多挂的插件 |
|---|---|
| JSON-RPC | sdk-jsonrpc-server |
| ACP | acp-demo 协议桥 + 沙箱策略 |
| headless | 无(进程本身就是入口) |
推论:加一张新面孔的成本 ≈ 写一个翻译插件 + 一份配置。翻译插件只做协议翻译,业务逻辑一行都不进它。
host-webserver:纯粹的 node:http 载体。文档明说它不属于 agent loop、不了解任何 harness 概念(docs/subsystems/web-server.zh.md)。frontend-static:认领回退席位,当 SPA 服务器。client-modules:用 tapIndex 往 index.html 注入启动清单 window.__DSH_BOOT__,浏览器端照单加载各插件的前端模块。连前端模块清单都不是写死的,而是各插件声明、框架启动时拼出来的——与服务端插件树同一个思路,只是长在浏览器那一侧。
api-remotes(身份解析)→ api-gateway(参数解码 + 方法调用)→ connection(独占 /api 路由的 RPC 信封)→ 落回同一个 webserver(docs/api-gateway.zh.md)。
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(...)。
examples/jsonrpc-agent/cordis.yml 第 1 行注释说明这是给捆绑运行时用的无人值守部署,第 2 行就是铁律原文:
stdout 保留给 JSON-RPC,不要加 console logger 或终端 UI。
examples/acp-agent/cordis.yml 同样声明:整棵树不挂 stdout 日志和 HMR。
这两种协议把 stdout 当传输线用。报文一个字节一个字节从这条线上过去,往上面混打一行日志,对端的解析器当场断线。
日志走 ctx.logger 另寻出路。协议入口的铁律:传输线只能跑协议,别的什么都别往上写。
凡是进程把某个输出流当成管道用,那个流就必须独占。谁都可以往里写的管道,等于没有管道。
划定管道时,第一件事应该是宣布它归谁独占,而不是先想着往里塞什么。
Web 与 HTTP API 这两张脸需要跨进程调用 Host 里的业务方法。DSH 没用现成框架,自研了 Typert。
只剩一个装饰器。在服务方法上标 @Remote('create'),构建时 Typert 分析 TypeScript 类型图,生成三样东西:
不需要手写路由表、参数转换、客户端 stub。改一处方法签名,重新构建后所有端的调用约定同步更新(docs/subsystems/typert.zh.md、packages/typert/generator/README.zh.md)。
| # | 问题 | Typert 的解法 |
|---|---|---|
| 1 | Host 与浏览器是两个独立的 TypeScript Program,同名 Cordis Context 的类型合并结果不同,一份 schema 喂不通两边 | 各自在构建时生成对应的 descriptor |
| 2 | 参数可能是 Agent 这样的活对象,不能被序列化过 wire | lookup 机制:agent 参数改写为 wire 字段 agentId,Gateway 收到后先解析回活对象再调方法 |
| 3 | 客户端 ctx.remote.goals 是随插件挂载卸载的活服务,最后一个方法撤回整个 namespace 跟着卸载 | 静态描述(如 OpenAPI)无法表达此生命周期 |
@Remote 标记的方法才是远程方法,未标记的既不进 Client 类型,也无法经 ctx.remote 调用。生成器遇到表达不了的类型时报错,与「拒绝解读」一脉相承——说不清楚的事宁可不做。
为什么这条重要:报错你立刻就知道;降级会让你以为类型是对的,然后在很远的地方炸掉。一个悄悄弱化的生成器,比一个会报错的生成器危险得多。
| Grok Build | Claude Code | DeepSeek Harness | |
|---|---|---|---|
| 主体面孔 | 桌面端 + CLI | 终端 CLI(TUI-first) | Web(Web-first) |
| ACP | 独立 Rust 实现 xai-acp-lib,stdin 行读取器 + 双向通道 + 网关收发器 | — | acp-demo 协议桥 + 沙箱策略 |
| 多面孔来源 | 主体 + 给编辑器的接口 | 同一 CLI 衍生(-p 模式、Agent SDK 再包一层) | 摊平成配置差异 |
| 需要 RPC 层吗 | 部分需要 | 不需要(一个进程一个 UI) | 需要(Typert) |
| 内核是否知悉入口 | — | — | 内核对入口一无所知 |
cordis.yml 加一个驱动器。两条路线没有对错,是成本结构不同。DSH 发布时甚至没有传统终端交互入口,发布讨论里最响的声音就是「我的 CLI 呢」——这是入口取舍上的产品决策:先把内核与面孔解耦的架构立住,缺哪张皮补哪张。
设计多入口系统时逐条对照:
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。cordis.yml 配置的交集,不代表任意历史版本的完整清单。crates/codegen/xai-acp-lib/ 的静态阅读;Claude Code 为其公开架构特征。不涉及运行时实测。假设要给 DSH 加一个聊天软件机器人入口:用户在群里 @机器人 说话,回复流回群里。
哪些东西不用写?
内核插件、会话持久化、压缩、工具——全部照抄现有 cordis.yml。这是「入口 = 配置」最直接的收益:要写的代码量与入口复杂度无关。
哪些东西必须写?
一个协议桥插件:把群消息翻成会话 prompt,把会话事件翻成群回复。就这一件。
这个桥有没有 stdout 互斥问题?
大概率没有——群消息不跑在 stdout 上。
那它的「传输线纪律」等价物是什么?
群平台的速率限制和消息长度限制。模型产出的事件是连续、细碎的,而群消息不能这么发(刷屏 + 被限流),所以事件流必须节流与合并。
这道题的价值在于:它逼你把一条具体纪律抽象成一般原则——不是记住「stdout 不能打日志」,而是先问「我的传输线是什么,它的约束是什么」。
*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*