DeepSeek Harness 深度拆解:一个“Agent = Model + Harness”的插件化运行时到底怎么工作
JotaroRD

DeepSeek Harness 深度拆解:一个“Agent = Model + Harness”的插件化运行时到底怎么工作

这篇文章适合已经写过 Agent、Tool Calling、MCP、CLI 自动化或 Web Agent UI 的工程师。它不讲概念鸡汤,重点拆 DeepSeek Harness 这个仓库的运行模型、源码结构、扩展方式和真实使用姿势。

DeepSeek Harness 总架构

先给结论

DeepSeek Harness 不是一个“把 DeepSeek API 包一层”的 SDK,也不是只跑 prompt 的 demo。它更像一个完整的 Agent 操作系统:模型适配器、工具注册表、文件系统、shell、权限审批、会话日志、持久化、Web UI、子 Agent、工作流、设置页、插件管理,全都被拆成 Cordis 插件,再由 profile 和 bundle 组合成一个可运行产品。

官方落地页用一句话概括它的定位:Agent = Model + Harness。这里的 Model 负责生成下一步意图,Harness 负责给模型提供可控、可观测、可恢复的运行环境。换成人话就是:模型不是直接“裸奔”在你的机器上,而是在一个有工具边界、会话日志、权限策略、UI 投影和插件生命周期的 runtime 里行动。

这个仓库最值得学的不是某一个工具怎么写,而是它把 Agent 产品拆成了几条清晰的工程主线:

  • 启动不是写死入口,而是 profile + bundle + patch 组合 Cordis 插件树。
  • 核心 loop 只管 turn/step 生命周期,不把工具、权限、UI、持久化塞进自己内部。
  • 模型可见内容必须能从 session log 重建,所以恢复、fork、回放、UI transcript 都有同一份事实来源。
  • 工具不是简单函数调用,而是一条带审批、guard、wrapper、post policy、canonical output、UI render intent 的流水线。
  • Web UI 不是“读内存状态”,而是从 host remote、session events、tool presentation 和 client slots 投影界面。

一、为什么 Agent 需要 Harness,而不只是模型和工具

很多早期 Agent 项目会长成这样:一个 while loop 调模型,拿到 tool call,执行函数,把结果塞回 messages,再继续调模型。这个模型对 demo 足够,但一旦进入真实产品,就会遇到一堆硬问题:

  • 工具调用失败后怎么恢复,重试归谁管?
  • 用户取消时,已经开始的 shell、subprocess、tool body 怎么收敛?
  • 哪些文件能读,哪些文件能写,策略在哪里执行?
  • 工具结果太大时,是截断、spill,还是仍然塞进上下文?
  • Web UI 刷新后,怎么恢复流式输出、工具卡片、子任务状态?
  • 同一个会话如何 resume、fork、生成标题、查询历史?
  • 新模型、新工具、新 UI 面板、新权限策略,应该改核心 loop 还是挂插件?

DeepSeek Harness 的答案是:把 Agent 拆成运行时能力,而不是把一切写进一个 loop。Loop 只负责驱动步骤;能力通过服务和事件接入;持久事实进入 session log;可替换部分都变成插件。

这就是它和普通 wrapper 最大的差别。

二、源码总览:先看分层,不要先钻细节

仓库根目录的结构非常有信息量:

1
2
3
4
5
6
7
8
9
apps/       CLI 和浏览器入口
packages/ 一方包,@deepseek-ai/dsh-*,核心产品都在这里
docs/ 架构、子系统、cookbook、生成目录
website/ VitePress 文档站
scripts/ 校验、生成器、发布与仓库约束
vendor/ 固定版本 Cordis 源码副本
native/ Node addon sandbox 后端
python/ Python SDK
.agents/ Agent Notes 与项目专用工作流

真正的产品代码在 packages/*/*,当前这个仓库有 200 多个一方包。不要被数量吓到,它们不是随意散开的。比较合理的读法是按“能力家族”看:

类型 代表目录 你应该看什么
核心主干 packages/core/* session、system prompt、tools、agent、agent-loop
模型能力 packages/llm/* 抽象 LLM service、DeepSeek provider、retry、token meter
执行能力 packages/fs/*、shell/*、subprocess/*、terminal/*、sandbox/* 文件、shell、PTY、进程与沙箱如何拆开
状态能力 packages/session/*、session-query/*、attachment/*、spill/* 持久化、投影、查询、大结果 spill
协作能力 interaction/*、goal/*、plan/*、feedback/*、jobs/* 审批、计划模式、目标、反馈、后台任务
外部界面 host/*、client/*、api/*、typert/*、sdk/*、acp/*、hooks/* Web、RPC、SDK、自动化协议
组合与发布 bundle/*、boot/*、preset/* profile、bundle、patch、启动装配

读这个仓库最容易踩的坑是:直接打开 agent-loop 想找全部业务逻辑。实际上它故意很克制。新行为通常不该塞进 loop,而应该挂在 agent/*、tools/*、session/event、ctx.tools、ctx.systemPrompt、ctx.fs 等扩展点上。

三、启动链路:dsh web 到底发生了什么

Profile 启动链路

apps/cli/src/bin.ts 是产品命令入口。它解析命令后分发到 profile 模式、plugin 管理模式或 dump-config 模式。

常用入口是:

1
2
3
4
dsh web
dsh --profile headless "帮我总结这个仓库"
dsh --profile web --dump-config
dsh plugin --profile web add <some-plugin>

这里最重要的概念是 profile 和 bundle。

Profile 是用户机器上的一套运行组合,位于 $DSH_HOME/profiles/<name>,默认 $DSH_HOME 是 ~/.dsh。Profile 里有一个 package.json 声明 bundle 列表,还有一个 cordis.patch.yml 放用户自己的 patch。

Bundle 是产品提供的配置层。内置的关键 bundle 有:

  • dsh-base:模型 provider、默认模型选择、工具、持久化、文件系统、shell、sandbox、权限、设置、凭据、telemetry 等基础能力。
  • dsh-web-app:HTTP server、API proxy、workspace、storage、projection cache、静态前端、浏览器 client plugins、Web 启动参数。
  • dsh-headless:一次性任务 runner,不启动 Web 界面。

启动时,系统不是手写 new 一堆对象,而是从空 entry list 开始按顺序应用:

1
2
3
4
5
bundle patches
→ profile cordis.patch.yml
→ $DSH_HOME/cordis.patch.yml
→ --patch overlays
→ Cordis Loader 激活插件树

这带来一个很实用的工程结果:部署差异、用户覆盖、插件安装、模式切换,都可以表达为配置层,而不是 fork 一套入口代码。

packages/boot/app-boot 是这条链路的共享启动胶水。它负责加载 .env、解析 profile/bundle、挂载 cordis:include、等待插件激活、把 Loader 错误变成带产品前缀的失败信息,并让长驻 Web 进程能监听用户 patch 变化。

四、核心 Loop:Turn 和 Step 是整个 Agent 的心跳

Turn/Step 流程

DeepSeek Harness 的核心执行路径集中在几个包:

  • packages/core/agent:Agent 接口、live registry、inbox、状态事件。
  • packages/core/session:append-only session log 和内存 session store。
  • packages/core/system-prompt:prompt section 和 tool schema 组装。
  • packages/core/tools:工具注册表和执行流水线。
  • packages/core/agent-loop:具体 loop driver。

它的生命周期可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
turn/start
claim inbox input
assemble prompt + tool schemas
agent/pre-step
step/start
user/message
derive model history from session log
agent/request
llm/stream
assistant/chunk*
assistant/message
tool/call*
tools pipeline
tool/result*
step/end
decide next step or stop
turn/end

这里有两个关键抽象。

第一,Turn 是用户可感知的一轮工作。它可以包含多个 Step,因为模型可能先调用工具,再看工具结果,再继续调用模型。

第二,Step 是一次模型请求加它产生的工具调用。Step 里的 prompt 来自三部分:系统 prompt sections、当前 agent 可见的 tool schemas、由 session log 派生出的历史 messages。

这解释了为什么 DeepSeek Harness 特别强调“model-visible means logged”。只要某个信息会进入模型请求,它就必须能从 session log 重建。否则 resume、fork、回放、UI transcript、调试审计都会出现“当时模型到底看到了什么”的黑洞。

这个设计看起来麻烦,但它是 Agent 产品从 demo 走向可维护系统的分水岭。

五、Session Log:不是聊天记录,而是运行时事实账本

很多人把 transcript 理解成 messages 数组。DeepSeek Harness 不是这么做的。它的 session log 是 append-only event stream,里面既有用户消息、assistant chunk、assistant message、tool call、tool result,也有 turn/step 边界和子系统事件。

为什么不直接存最终 messages?

因为 Agent 运行时不是纯聊天。它有流式输出、工具卡片、取消、未派发工具调用、后台任务、子 Agent、projection cache、fork boundary、session title、telemetry。最终 messages 只是模型下一次请求需要的一种投影,不是全部事实。

因此 session log 承担几件事:

  • 恢复会话:resume 时从持久事件恢复状态。
  • 派生模型历史:deriveMessages() 从日志生成 provider messages。
  • UI 回放:Web UI 可以重建 token stream、工具卡片和生命周期。
  • Fork:从某个边界生成 child session。
  • 查询:session-query 能对历史进行 bounded read、lineage、关系和全文检索。
  • 审计:模型看到过什么,工具返回过什么,都能追。

这也是为什么你新增模型可见上下文时,不能只在内存里塞一段 prompt。你要问:这段东西 reload 后还能被重建吗?如果不能,通常就要新增 session event 和 projection。

六、工具系统:它不是函数表,而是安全执行流水线

工具执行流水线

工具在 packages/core/tools 里注册。一个工具定义不只是 name + function,而包括:

  • 名称和描述。
  • JSON 参数 schema。
  • canonical output schema。
  • output renderer。
  • executor。
  • 可选 timeout 元数据。
  • 可选并发安全分类器。
  • 可选 UI presentation intent。
  • 可选 finalizeContent。

一次工具调用会经过:

1
2
3
4
5
6
7
8
tools/pre-execute
→ registered guards
→ tools/execute wrappers
→ tool body
→ tools/post-execute
→ finalizeContent
→ tools/result
→ durable tool/result

这条链路把几个问题拆清楚了:

  • pre-execute 处理 allow / ask / deny,适合审批和策略。
  • guard 是单调拒绝,后续 listener 不能把它放回允许态。
  • tools/execute wrapper 适合 timeout、metrics、retry 一类 around-dispatch 逻辑。
  • tool body 只负责自己的业务执行。
  • post-execute 可以替换 content、替换 canonical value、block 或附加上下文。
  • finalizeContent 是工具定义拥有的最后内容不变量。
  • durable tool/result 进入 session log,供模型历史和 UI replay 使用。

这套设计避免了一个常见灾难:权限、超时、展示、日志、业务函数互相缠在一起。工具作者写工具,策略插件管策略,UI 通过 render intent 渲染,loop 只认标准化结果。

Native Mode 与 Code Mode

DeepSeek Harness 的工具呈现还有一个很有意思的设计:工具可以用 native function calling 暴露,也可以用 Code Mode 暴露。

Native Mode 就是模型直接看到每个工具 schema。

Code Mode 则暴露一个保留工具 run_code,同时给模型生成当前可见工具集合的 SDK。模型写一段 TypeScript 或 Python 风格代码,在代码里调用 typed tool bindings。每个 binding 调用仍然回到同一条工具流水线;但只有程序打印和返回值进入模型上下文,中间工具结果不会全部塞回 messages。

这个设计的价值是:当任务需要多次读文件、过滤、聚合、排序、比较时,模型可以把中间计算留在代码里,而不是把每一步工具结果都污染上下文。

七、Capability Seam:为什么换一个 provider 不该改所有工具

仓库文档里有一个术语叫 capability seam,可以理解为一条可替换能力线。完整的 seam 至少包含三类角色:

  • Service Definition:声明接口。
  • Service Provider:实现接口。
  • Consumer:使用接口,常常是模型可见工具。

以文件系统为例:

  • packages/fs/fs 声明 filesystem service。
  • packages/fs/fs-local 提供本地实现。
  • packages/fs/fs-sandbox 应用沙箱/策略。
  • packages/fs/tool-fs、tool-fs-search 把能力变成模型工具。

Shell、subprocess、terminal、web search/fetch、subagent、workflow、storage、credentials、settings 也都类似。

这样做有一个很硬的工程收益:Consumer 不绑定具体 provider。你可以把本地 subprocess 换成远程 sandbox,把 shell 背后的执行世界换掉,把 web search provider 换掉,而不需要每个工具都分叉一份。

这也是 DeepSeek Harness 比“工具函数集合”更像运行时的地方。

八、文件系统、Shell、Sandbox:Agent 真正危险的地方

Agent 一旦能读写文件、执行 shell,就从“聊天应用”变成“本机自动化程序”。这部分如果设计粗糙,风险会非常高。

DeepSeek Harness 把相关能力拆成几层:

  • fs:文件系统抽象。
  • subprocess:进程树能力。
  • shell:Bash / PowerShell 执行能力。
  • terminal:持久 PTY 能力。
  • sandbox:进程隔离。
  • interaction:用户审批和权限 preset。
  • guard:loop hygiene、工具超时等策略。

POSIX 上默认挂 Bash 栈;Windows 上挂 PowerShell 栈和 Windows ACL sandbox 路径。两套栈注册同一个 shell service key,所以 profile patch 必须保证只启用一个执行家族。

这里最值得借鉴的是:它没有把 shell 工具写成“直接 spawn”。工具只是 Consumer;真正的执行、权限、sandbox、文件效果策略分别由不同 provider 和 plugin 组合出来。

九、Web UI:不是壳子,是运行时投影

Web 相关代码分两半:

  • packages/host/*:HTTP route、API proxy、frontend static、workspace directory picker、plugin inventory。
  • packages/client/*:browser shell、connection、runtime、React renderer、slots、conversation UI、sidebar、settings、tool cards、workflow views。

Web UI 不应该直接知道某个工具内部怎么跑。工具自己提供 presentation intent,session log 提供 durable events,host 通过 API gateway 和 Typert 生成的 remote contracts 暴露服务,client plugins 注册 slot 并渲染状态。

所以你在 UI 里看到的不是“当前内存里某个 loop 对象的状态”,而是从事件、projection、remote service、tool presentation 中组合出的界面。这个思路对做 Agent IDE、运维面板、协作界面都很有参考价值。

十、扩展 DeepSeek Harness:该改哪里

如果你想在这个仓库里加能力,先不要打开 loop 开写。更合理的定位方式是:

目标 应该接入哪里
新模型 provider 注册到 ctx.llm
新模型可见工具 ctx.tools.register()
新 prompt 上下文 ctx.systemPrompt 或 agent-scoped prompt section
新文件/进程后端 对应 Service Definition 的 provider
新权限策略 tools/pre-execute、guard、approval
新 UI 面板 client slot + host remote
新会话持久事实 扩展 SessionEventMap 并实现 projection
新 profile 默认能力 bundle patch 或 preset
新子 Agent 类型 subagent provider

这套规则背后的思想是:核心 loop 是产品 spine,不是业务垃圾桶。新行为应该挂在文档化扩展点上。

十一、从源码跑起来

源码运行最短路径:

1
2
3
pnpm install
pnpm run build
pnpm dsh web

默认 Web 地址是:

1
http://127.0.0.1:3080

如果要指定端口:

1
pnpm dsh web --port 8080

跑一个 headless 任务:

1
pnpm dsh --profile headless "总结这个仓库的核心架构"

看最终组合出来的 Cordis 配置:

1
pnpm dsh --profile web --dump-config

配置 DeepSeek API key:

1
2
DEEPSEEK_API_KEY=sk-...
DEEPSEEK_BASE_URL=https://... # 可选

如果你要改 profile,先 dump-config 找 row id,再在 cordis.patch.yml 里按 id 覆盖。注意 patch 是替换整个 config,不是 deep merge,保留字段也要重写。

十二、这套架构适合学什么

DeepSeek Harness 不是最小 Agent 框架,它反而是一个适合学习“复杂 Agent 产品如何拆工程边界”的样本。它对下面这些问题给出了比较完整的答案:

  • Agent loop 和工具执行怎样解耦。
  • 模型可见上下文如何做到可重建。
  • 工具权限、审批、超时、展示如何分层。
  • 本地 shell/文件系统能力如何放进策略和沙箱里。
  • Web UI 如何从 session log 和 tool presentation 投影。
  • 子 Agent、workflow、jobs 这些长任务如何作为能力接入。
  • 配置和插件如何支持不同运行 profile。

如果你正在做自己的 Agent 平台,我建议重点抄思想,不要先抄代码:

  1. 把 runtime facts 做成 append-only event stream。
  2. 把工具执行做成 pipeline,而不是函数表。
  3. 把模型 adapter 和 request recovery 拆开。
  4. 把权限和 sandbox 作为能力层,不要散落在工具里。
  5. 把 UI 渲染意图交给工具或 capability owner,而不是前端硬编码工具名。
  6. 把部署差异放进 profile/config,不要 fork app entry。

十三、我对这个仓库的评价

DeepSeek Harness 的代码量和包数量会让第一次阅读的人有点懵,但它的主线是清楚的:Cordis 负责插件化组合,core packages 负责 Agent spine,capability families 负责可替换能力,session log 负责事实账本,Web client/host 负责产品界面。

它不是一个“最容易上手”的项目,但它很适合回答一个更难的问题:如果 Agent 真的要在真实机器上长期运行、能恢复、能审计、能扩展、能让用户控制权限,工程上应该怎么分层?

这也是它最有干货的地方。

推荐阅读路径

如果你准备继续读源码,建议按这个顺序:

  1. README.md:先知道产品怎么跑。
  2. docs/architecture.md:理解 profile、bundle、turn flow、session log、capability seam。
  3. docs/codebase-guide.zh.md:按代码路径建立地图。
  4. packages/core/agent-loop/README.md:看具体 loop 合同。
  5. packages/core/tools/README.md:看工具流水线。
  6. packages/bundle/base/cordis.patch.yml:看默认能力是怎么挂进去的。
  7. packages/host/README.md 和 packages/client/README.md:看 Web UI 分层。
  8. docs/cookbook/adding-a-tool.md、docs/cookbook/adding-an-llm-adapter.md:开始写扩展。

参考资料