DeepSeek Harness 深度评测 · Ep4DEEP DIVE
领域 Agent 与框架
DeepSeek Harness Ep 4:我改了一行类型,整个协议跟着变了
视频版:B站 公开投稿中无对应视频(已核对全部 16 个投稿) 视频总览 →
本期要点POINTS
本期要点
关键实测数据DATA
关键实测数据
锐Owl · 解构系列 · DeepSeek Harness(Ep 4/5) 素材:2026-08-25 clone(HEAD 0d8b7510)源码逐层拆解 + 真机复现,全部论断带文件路径/行号
开篇:协议由编译器生成,不是人手写的文档
上一期拆完热卸载,我们留下一个问题:一个 agent 框架,宿主和客户端是两个独立进程——它们之间靠什么对齐?靠一份「协议」:你传什么参数、我回什么结构、哪个字段允许空、哪个字段必须填。
问题在于:协议是双份的——服务端一份,客户端一份。手写,就是人工填报关单:出口商填一版,进口商填一版,稍微改个货品规格,两边忘了同步,货到了海关直接扣下。
dsh 的答案是:协议不手写,从类型自动生成。这一期拆 Typert——类型图生成器:一份 TS 类型定义,自动吐出 zod 校验规则、跨进程调用契约、客户端签名三份产物。改一行类型,三处全部自动同步。
一、类型即契约:一份真相源,三份自动产物
1.1 生成链路:入口 → 分析器 → 发射器 → loader → registry
Typert 的完整链路(packages/typert/)分五步:
- 入口
WorkspaceTypertGenerator(generator/src/workspace.ts:20):discover()扫描工作区包(workspace.ts:33-38),generate()= 分析 + 逐 face 发射(workspace.ts:46-66)。 - 分析器
WorkspaceAnalyzer(analyzer.ts,3113 行):模块头 JSDoc 明言 "Programs, symbols, and syntax nodes remain extraction-only implementation details"(analyzer.ts:1-5)——只抽取,不改写。对外只暴露编译器无关的 FaceModel/PackageModel/InvocationModel。 - 发射器
FaceModelEmitter(emitter.ts:88):emit()(emitter.ts:104-128)产出 js+dts。js =export const TYPERT = {package, face, schemas, invocations, model}(emitter.ts:200-215),schema 全投影成 zod v4 代码。host face 且invocations.length > 0时另发./remote子路径:export const TYPERT_REMOTE = {package, descriptors}(emitter.ts:245-272)。 - 构建期接线
typertPlugin()(generator/src/tsdown-plugin.ts:46):tsdown 构建时自动运行,写产物到 lib/;validateExport强制 package.json exports 声明./typert(workspace.ts:68-126)。 - 运行时注册 typert-loader(
loader/src/index.ts:42-44):按exports['./typert']解析包(loader:62-72)→validateTypertManifest逐字段校验(loader:83-142)→ctx.typert.register(manifest)(loader:383)持 disposer,条目卸载即撤回(loader:369-376)。注册表TypertRegistry(registry/src/service.ts)对 endpoint(<namespace>/<method>)与 id 双去重(service.ts:120-135)。
一句话:分析器只读、发射器投影、loader 注册、registry 去重——每一步都是纯函数式的推导,没有一步手写。
1.2 三份产物各管什么
| 产物 | 文件 | 职责 | 比喻 |
|---|---|---|---|
| zod schema | lib/typert.host.js |
运行时校验:每个字段怎么查 | 海关查验规则 |
| TYPERT_REMOTE | lib/typert.remote-client.js |
跨进程契约:endpoint/参数/返回值 | 报关单 |
| 客户端签名 | lib/typert.remote-client.d.ts |
客户端调用签名 | 收货单 |
← 表格可左右滑动查看
二、TYPERT_REMOTE 边界:能/不能,画得比文档清楚
2.1 能:公开实例方法 + 装饰器
host face 的公开实例方法,加 @Remote / @RemoteScope('key') 装饰器(protocol/src/index.ts:168-216;analyzer.ts:1215-1260 静态识别)即可上线;宿主必须继承 TypertRemoteService 或 bindTypertRemote(this, key)(analyzer.ts:963, 1141-1153)。名称约束:namespace/method 段必须匹配 /^[A-Za-z0-9_$.-]+$/(protocol/src/index.ts:10-19)。
2.2 不能:表达不了运行时校验的,全拒
- 参数:必须 identifier 绑定,禁 rest 参数、默认值、显式 this(analyzer.ts:1004-1008);取消只能用全局
AbortSignal且必须是末参(analyzer.ts:1015-1018);泛型 Remote 方法拒绝(analyzer.ts:992)。 - Zod 投影白名单(emitter.ts:600-642, 644-688, 783-798):keyword/literal/union/intersection/array/tuple/object/reference + 标准类型仅
Array/ReadonlyArray/Record/Date;operator/indexed-access/conditional/infer/mapped/template-literal/type-query/import-type/predicate/function/constructor/this 全 fail(emitter.ts:629-640);enum 无 Zod 投影(emitter.ts:585-587);动态 computed 成员、method 成员、多个 index signature 均拒绝(emitter.ts:743-764)。 - RemoteScope:context key 必须在
TypertContextMap声明(analyzer.ts:1070);scope wire 必须选中唯一 lookup 参数且类型一致(loader:242-247)。
这条边界的工程含义:协议是运行时校验的投影,不是 TypeScript 的全集。表达不了校验的类型进不了协议——宁可拒绝,不搞降级。这就是 Typert 敢说「生成即一致」的前提。
三、真机演示:改一行,三层变化
用仓库官方测试夹具(generator/tests/fixtures/remote-model/,专测 @Remote 机制,与真实插件同构)复现:GoalService 带 @Remote create() 和 @RemoteScope('agent') rename()(fixtures/remote-model/packages/remote/src/index.ts:11-26)。
改动:CreateGoalResult 加一行必填字段 status: string(types.ts:9)。
第 1 层 · 编译器拦截
直接重跑生成——analyze 阶段被 TS2741 拦下:
TypertAnalysisError: typert(host): packages/remote/src/index.ts:19:5:
TypeScript TS2741: Property 'status' is missing in type '{ ref: string; }'
but required in type 'CreateGoalResult'.
实现不跟上,协议根本生成不出来。一致性第一道闸在编译期,不在运行时。
第 2 层 · wire 契约自动重印
补实现后重跑,TYPERT_REMOTE 描述符(remote.js)的 result schema 自动更新:
const _fixture_remote_goals_create_result$schema = z.object({
'ref': z.string().readonly(),
+ 'status': z.string().readonly(), ← 自动新增
})
没碰任何协议文件,报关单自动重印。
第 3 层 · 客户端契约零改动自动同步
生成的客户端签名是类型引用而非内联投影:
create: (agentId: AgentId, request: CreateGoalRequest, signal?: AbortSignal)
=> Promise<RemoteResult<CreateGoalResult>> // d.ts:11 — 引类型名,不复制字段
d.ts 文本零变化,但引用的类型语义已变——客户端进程重新编译,类型检查器自动强制它处理新字段。改一个文件,两个进程的契约全部就位。
四、对工程实践的启示
- 类型即契约,收益在「漂移变编译错误」:手写协议的问题不是写得慢,是两边会漂移。把协议变成类型推导,漂移在编译期就被拦下,而不是上线后线上事故。
- 生成器的信用来自边界死守:Typert 能投影的才进协议,进不了的宁可拒绝——这个「不搞降级」的纪律,是「生成即一致」这句话的信用基础。
- 术语坑值得单独说:bundle 配置机制里是 cordis.patch.yml(补丁列表),不是 cordis.yml(示例里可运行的配置树),两套东西(ep3 拆热卸载时踩过)。
- 插件装上即生效:包导出
./typert后 loader 自动扫描注册,零额外配置——这正是下一期要实测的「装上即生效」。
结论
改一行类型,协议、校验、客户端契约三处自动就位——不是框架偷偷改协议,是类型系统把一致性变成了推导,把漂移变成了编译错误。接口维护从人工苦力变成自动生成,而这一切的前提,是生成器对边界的死守。
下一期:把它装起来实测——插件装上即生效,到底多快?边界会不会在真实插件上翻车?
来源表
| 论断 | 证据位置 |
|---|---|
| 生成入口 discover/generate | packages/typert/generator/src/workspace.ts:20,33-38,46-66 |
| 分析器 extraction-only | packages/typert/generator/src/analyzer.ts:1-5(3113 行) |
| 发射器 TYPERT 产物 | packages/typert/generator/src/emitter.ts:88,104-128,200-215,245-272 |
| zod v4 投影 | emitter.ts:600-642,644-688,783-798 |
| 投影拒绝清单 | emitter.ts:629-640(operator/indexed-access/conditional… 全 fail) |
| enum 无投影 | emitter.ts:585-587 |
| 构建期接线/validateExport | generator/src/tsdown-plugin.ts:46;workspace.ts:68-126 |
| loader 运行时注册/校验/撤回 | packages/typert/loader/src/index.ts:42-44,62-72,83-142,369-376,383 |
| registry 双去重 | packages/typert/registry/src/service.ts:67-69,120-135 |
| @Remote 装饰器 | packages/typert/protocol/src/index.ts:168-216 |
| 参数约束(rest/默认值/AbortSignal 末参) | analyzer.ts:1004-1018 |
| RemoteScope 约束 | analyzer.ts:1070;loader:242-247 |
| 演示夹具 GoalService | generator/tests/fixtures/remote-model/packages/remote/src/index.ts:11-26 |
| 演示 diff(TS2741 / schema 自动新增 / 类型引用) | 真机复现,见 ep12 视频第 4 章 |
← 表格可左右滑动查看
来源:DeepSeek Harness 源码(HEAD 0d8b7510),锐Owl 实机拆解复现。
信号来源SIGNALS
信号来源
这一期从哪条雷达长出来(雷达 → 产出的双向可追溯):
node_links/radar_items.intent 的 feeds 指向关系当前为 0(M2 首轮回填统一先落 standalone,待人工指认)——回填后此处显示「雷达 YYYY-Www 条目 ③ → 已成为本期」。
来源表SOURCES