DeepSeek Harness 深度拆解:一个“Agent = Model + Harness”的插件化运行时到底怎么工作
这篇文章适合已经写过 Agent、Tool Calling、MCP、CLI 自动化或 Web Agent UI 的工程师。它不讲概念鸡汤,重点拆 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 | apps/ CLI 和浏览器入口 |
真正的产品代码在 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 到底发生了什么
apps/cli/src/bin.ts 是产品命令入口。它解析命令后分发到 profile 模式、plugin 管理模式或 dump-config 模式。
常用入口是:
1 | dsh web |
这里最重要的概念是 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 | bundle patches |
这带来一个很实用的工程结果:部署差异、用户覆盖、插件安装、模式切换,都可以表达为配置层,而不是 fork 一套入口代码。
packages/boot/app-boot 是这条链路的共享启动胶水。它负责加载 .env、解析 profile/bundle、挂载 cordis:include、等待插件激活、把 Loader 错误变成带产品前缀的失败信息,并让长驻 Web 进程能监听用户 patch 变化。
四、核心 Loop:Turn 和 Step 是整个 Agent 的心跳
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 | turn/start |
这里有两个关键抽象。
第一,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 | tools/pre-execute |
这条链路把几个问题拆清楚了:
pre-execute处理 allow / ask / deny,适合审批和策略。- guard 是单调拒绝,后续 listener 不能把它放回允许态。
tools/executewrapper 适合 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 | pnpm install |
默认 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 | DEEPSEEK_API_KEY=sk-... |
如果你要改 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 平台,我建议重点抄思想,不要先抄代码:
- 把 runtime facts 做成 append-only event stream。
- 把工具执行做成 pipeline,而不是函数表。
- 把模型 adapter 和 request recovery 拆开。
- 把权限和 sandbox 作为能力层,不要散落在工具里。
- 把 UI 渲染意图交给工具或 capability owner,而不是前端硬编码工具名。
- 把部署差异放进 profile/config,不要 fork app entry。
十三、我对这个仓库的评价
DeepSeek Harness 的代码量和包数量会让第一次阅读的人有点懵,但它的主线是清楚的:Cordis 负责插件化组合,core packages 负责 Agent spine,capability families 负责可替换能力,session log 负责事实账本,Web client/host 负责产品界面。
它不是一个“最容易上手”的项目,但它很适合回答一个更难的问题:如果 Agent 真的要在真实机器上长期运行、能恢复、能审计、能扩展、能让用户控制权限,工程上应该怎么分层?
这也是它最有干货的地方。
推荐阅读路径
如果你准备继续读源码,建议按这个顺序:
README.md:先知道产品怎么跑。docs/architecture.md:理解 profile、bundle、turn flow、session log、capability seam。docs/codebase-guide.zh.md:按代码路径建立地图。packages/core/agent-loop/README.md:看具体 loop 合同。packages/core/tools/README.md:看工具流水线。packages/bundle/base/cordis.patch.yml:看默认能力是怎么挂进去的。packages/host/README.md和packages/client/README.md:看 Web UI 分层。docs/cookbook/adding-a-tool.md、docs/cookbook/adding-an-llm-adapter.md:开始写扩展。
参考资料
- DeepSeek Harness 官方页面:https://deepseek.com/harness
- DeepSeek Harness GitHub 仓库:https://github.com/deepseek-ai/deepseek-harness
- Cordis 项目:https://github.com/cordiverse/cordis
- 本仓库架构文档:
docs/architecture.md - 本仓库代码库指南:
docs/codebase-guide.zh.md - 本仓库包索引:
packages/README.md