📖 从零读懂 DeepSeek Harness

章节制源码教程(非官方社区出品)——不背目录、不念文档,用「学习者问题 → 机制模型 → 真实源码证据」的方式,把 DSH 拆开讲清楚。

用户消息 user/message 一轮 turn(用户请求为边界) 模型选择动作 (一次 step) 工具执行 结果回填 循环直到完成 / 步数用尽 / 取消 会话日志(append-only,seq 递增) turn/start → user/message → step/start → assistant → tool → step/end → … → turn/end
轮(turn)内由多个「模型→工具」步(step)组成;每一步都按序写入会话日志。

1 Agent 循环——模型如何驱动工具

❓ 你发一条消息给 DSH,它内部发生了多少次模型调用?

答案是:往往不止一次。DSH 把「用户请求」切成一层层结构——轮(turn)步(step)。一次用户消息是一个 turn;turn 内部,模型选择动作 → 工具执行 → 结果回填 → 模型继续,这样一个「模型-工具」循环单元是一个 step。循环直到模型说完成、步数用尽或被取消。

这套结构不是内部实现细节,它被完整写进会话日志:每一次模型请求、工具调用、结果回填,都有对应的事件,带递增序号。下面的片段就是一次真实会话日志的前几行——你能直接看到 turn 与 step 的边界事件:

{"type":"turn/start","seq":0,"time":1784998084454,"data":{"turn":1,"trigger":{"kind":"message"}}}
{"type":"user/message","seq":1,"time":1784998084454,"data":{"content":[{"type":"text","text":"Reply with a one-sentence description of event sourcing, then stop."}],"source":{"kind":"user"}}}
{"type":"step/start","seq":2,"time":1784998084454,"data":{"turn":1,"step":1}}

📁 源码证据:apps/web/tests/snapshots/live-interactions/session.jsonl(真实录制)

为什么这样设计?因为「模型选择动作、运行时验证并执行、结果进入下一次请求」是 agent 系统的因果核心。日志里的事件顺序就是因果顺序——这也为第 4 章的「可追踪」埋下伏笔。

→ 下一步:模型每次请求都会带上「上下文」——它从哪来?→ 第 2 章

会话日志 唯一真相 全部事件 deriveMessages 从日志投影 模型历史 稳定前缀请求 前缀字节不变 KV 缓存整段命中 窗口逼近上限时:compaction 压缩 早期轮次消息 折叠为一条摘要事件 压缩本身也是日志事件(可回放)
上下文从日志投影而来,稳定前缀让 KV 缓存整段命中;逼近上限时用日志内摘要事件折叠早期轮次。

2 上下文与缓存——长会话为什么不爆

❓ 会话越长,模型看到的上下文越多,为什么不一会儿就超限?

DSH 有一条铁律:模型所见的一切都必须能从会话日志重建。上下文不是随便拼的——运行时从会话日志中投影(deriveMessages)出模型历史,保证回放与 UI 看到的完全一致。

在请求组装层,DSH 保持「稳定前缀」:只要前面的消息没变,请求的前缀字节就不变,模型侧的 KV 缓存就能整段命中——这是长会话能跑得动的真正原因,而不是简单地把旧消息丢掉。

当窗口确实逼近上限时,压缩(compaction)介入:把早期轮次折叠成摘要,用一次「日志内的摘要事件」替换掉原始消息,然后继续。压缩本身也是日志事件——它可回放、可审计。

📁 源码证据:docs/architecture(会话日志是模型上下文的来源;deriveMessages 从中投影模型历史)

→ 下一步:这些能力为什么都能被替换、被扩展?→ 第 3 章

ctx(服务容器) 插件注入 / 注册 / 事件 卸载 → 自动回收 模型适配器 是插件 工具注册表 是插件 UI 槽位(slot) tsconfig.client.json cordis.patch.yml bundle package.json 三面登记
没有特权内核:模型、工具、UI 都是插件;进入 Web 界面还需三面登记。

3 一切皆插件——没有特权内核

❓ 「插件」在 DSH 里到底是个什么东西?

DSH 构建在 Cordis 之上。一个插件就是一个实现服务的对象:通过 ctx 声明依赖(inject)、注册能力,而注册是可撤销的副作用——插件卸载时,它注册的一切都会被自动回收。没有特权内核:模型适配器是插件、工具注册表是插件、会话日志是插件,UI 也是插件。

Web 界面里的插件通过「槽位(slot)」组合。下面的代码是真实 UI 插件(goal 面板)的注册方式——注意它只声明自己渲染到哪个槽、提供哪些数据:

ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
  name: 'conversation.input.dock',
  id: 'goal', order: 0, locale: NS,
}, GoalBar))

📁 源码证据:packages/client/ui-goal/src/client/index.ts(真实注册代码)

把插件装进一个 profile(组合包)只需在 cordis.patch.yml 里加一行;一个插件包要进入 Web 界面,还需要在聚合 tsconfig 和 bundle 清单里登记——这三处缺一不可,是社区插件发布的「三面注册」。

→ 下一步:插件运行时把事件写进了同一个日志——日志到底是什么?→ 第 4 章

事件流(seq 严格递增,追加式) turn/start seq 0 user/message seq 1 step/start seq 2 assistant turn/end seq N 一份日志,四个角色 模型历史 · UI 回放 · 文本导出 · 遥测 JSONL 后端 文件 · 压缩 · 目录 fsync SQLite 后端 行存储 · 级联删除
追加式事件流是唯一真相;JSONL 与 SQLite 两种后端都能整份落盘,崩溃恢复由日志补齐边界。

4 可追踪:会话日志——唯一真相

❓ 怎么证明 DSH 每一步做了什么?

一切最终都落在追加式(append-only)会话日志里:事件按 seq 严格递增,turn/step 边界闭合,带 surfaceOp 标记哪些事件是用户可见的。这份日志同时服务四个角色:模型历史、UI 回放、文本导出、遥测——所以它是「唯一真相」。

日志不是内存态:JSONL 与 SQLite 两种持久化后端都可以把整份日志落盘。崩溃恢复也是日志的职责——一个未闭合的 turn 在下次加载时会被识别并补齐关闭事件,而不是丢数据。

这套设计也解释了为什么「删除会话」在 DSH 里是个严肃操作:删除 = 停掉 agent → 从工作区账目移除 → 物理删除持久化日志。社区插件 ui-side-tasks 的「用完即删、零残留」正是建立在这个语义上。

📁 源码证据:packages/session/session-persistence(delete 契约:coordinator + JSONL/SQLite 双后端)

→ 下一步:运行时能自我修改吗?→ 第 5 章

Agent 通过工具查看 运行时(插件/服务) 「写插件」也是 普通工具调用 挂载插件(运行中) 无需重启 卸载插件 注册的服务/工具/槽位自动回收 不留僵尸能力
extensions 包:模型把「写插件」当普通工具调用,挂载/卸载运行中完成,注册可撤销所以无残留。

5 运行时自我进化——agent 能改自己

❓ agent 能不能检查自己的插件,甚至挂载新的?

能,而且这是官方明确的能力方向。仓库的 extensions 包提供「运行时自我修改」:agent 可以实时检查自己的插件与服务清单,模型通过工具查看运行时,并把「写插件」当作普通工具调用来执行——挂载、卸载都在运行中完成,不需要重启。

因为「注册是可撤销副作用」,自我修改是安全的:卸载一个插件,它注册的服务、工具、槽位自动回收,不会留下僵尸能力。这也是插件生态敢放开手脚的基础。

📁 源码证据:packages/extensions(Agent runtime self-modification;设计见官方 Agent Notes)

→ 下一步:任务很长、中途重启怎么办?→ 第 6 章

会话日志(落盘) 持久化 进程重启不丢 resume 冷恢复 从日志重建会话 继续未完成的任务 fork 分支 从已完成轮次切出 子会话继承全部上下文 goal 多轮续跑 目标/轮次持久化 恢复后需人工授权 ui-side-tasks fork 语义的 UI 化(删除零残留)
持久化 + resume + fork + goal 四件套,让「重启不是终点」;侧边任务插件就是 fork 的 UI 化。

6 长任务续跑——重启不是终点

❓ 一个长任务跑到一半,进程重启了,还能继续吗?

可以。三件套配合:持久化让日志可恢复;resume 从持久化日志重建会话(冷恢复);goal 把「同一目标的多轮自动续跑」变成一等公民——目标、轮次、权限激活都持久化,恢复后需经人工授权才继续自动工作。

同一层的还有 fork:从一个会话的已完成轮次切出子会话,完整继承上下文(种子边界 seedLength 记录继承到哪里),父子互不干扰、可并行。我们的侧边任务插件正是 fork 语义的 UI 化。

至此六个问题闭环:循环(1)→ 上下文(2)→ 插件(3)→ 日志(4)→ 自进化(5)→ 续跑(6)。它们共用同一根主轴:会话日志是唯一真相,一切能力都是可替换的插件

📁 源码证据:packages/bundle/base/cordis.patch.yml(goal / goal-round-driver 行);packages/core/session(fork 种子语义)

本教程由非官方社区 MyDSH 编写。 想提意见、纠错、共建下一章?→ 去留言 →