DeepSeek Harness 深度评测 · Ep4DEEP DIVE

Ep 4 DeepSeek Harness 深度评测 连载中

领域 Agent 与框架

DeepSeek Harness Ep 4:我改了一行类型,整个协议跟着变了

2026-08-28 阅读约 15 分钟 正文约 6093 字

视频版: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/)分五步:

  1. 入口 WorkspaceTypertGenerator(generator/src/workspace.ts:20):discover() 扫描工作区包(workspace.ts:33-38),generate() = 分析 + 逐 face 发射(workspace.ts:46-66)。
  2. 分析器 WorkspaceAnalyzer(analyzer.ts,3113 行):模块头 JSDoc 明言 "Programs, symbols, and syntax nodes remain extraction-only implementation details"(analyzer.ts:1-5)——只抽取,不改写。对外只暴露编译器无关的 FaceModel/PackageModel/InvocationModel。
  3. 发射器 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)。
  4. 构建期接线 typertPlugin()(generator/src/tsdown-plugin.ts:46):tsdown 构建时自动运行,写产物到 lib/;validateExport 强制 package.json exports 声明 ./typert(workspace.ts:68-126)。
  5. 运行时注册 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 文本零变化,但引用的类型语义已变——客户端进程重新编译,类型检查器自动强制它处理新字段。改一个文件,两个进程的契约全部就位。


四、对工程实践的启示

  1. 类型即契约,收益在「漂移变编译错误」:手写协议的问题不是写得慢,是两边会漂移。把协议变成类型推导,漂移在编译期就被拦下,而不是上线后线上事故。
  2. 生成器的信用来自边界死守:Typert 能投影的才进协议,进不了的宁可拒绝——这个「不搞降级」的纪律,是「生成即一致」这句话的信用基础。
  3. 术语坑值得单独说:bundle 配置机制里是 cordis.patch.yml(补丁列表),不是 cordis.yml(示例里可运行的配置树),两套东西(ep3 拆热卸载时踩过)。
  4. 插件装上即生效:包导出 ./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

来源表

该篇正文里没有可解析的「来源」小节(早期篇目未写文末来源表)——不补造。