📖 从零读懂 DeepSeek Harness
章节制源码教程(非官方社区出品)——不背目录、不念文档,用「学习者问题 → 机制模型 → 真实源码证据」的方式,把 DSH 拆开讲清楚。
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 章
2 上下文与缓存——长会话为什么不爆
❓ 会话越长,模型看到的上下文越多,为什么不一会儿就超限?
DSH 有一条铁律:模型所见的一切都必须能从会话日志重建。上下文不是随便拼的——运行时从会话日志中投影(deriveMessages)出模型历史,保证回放与 UI 看到的完全一致。
在请求组装层,DSH 保持「稳定前缀」:只要前面的消息没变,请求的前缀字节就不变,模型侧的 KV 缓存就能整段命中——这是长会话能跑得动的真正原因,而不是简单地把旧消息丢掉。
当窗口确实逼近上限时,压缩(compaction)介入:把早期轮次折叠成摘要,用一次「日志内的摘要事件」替换掉原始消息,然后继续。压缩本身也是日志事件——它可回放、可审计。
📁 源码证据:docs/architecture(会话日志是模型上下文的来源;deriveMessages 从中投影模型历史)
→ 下一步:这些能力为什么都能被替换、被扩展?→ 第 3 章
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 章
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 章
5 运行时自我进化——agent 能改自己
❓ agent 能不能检查自己的插件,甚至挂载新的?
能,而且这是官方明确的能力方向。仓库的 extensions 包提供「运行时自我修改」:agent 可以实时检查自己的插件与服务清单,模型通过工具查看运行时,并把「写插件」当作普通工具调用来执行——挂载、卸载都在运行中完成,不需要重启。
因为「注册是可撤销副作用」,自我修改是安全的:卸载一个插件,它注册的服务、工具、槽位自动回收,不会留下僵尸能力。这也是插件生态敢放开手脚的基础。
📁 源码证据:packages/extensions(Agent runtime self-modification;设计见官方 Agent Notes)
→ 下一步:任务很长、中途重启怎么办?→ 第 6 章
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 编写。 想提意见、纠错、共建下一章?→ 去留言 →