DeepSeek Harness 项目代码与架构导读(小白版)
阅读对象:刚开始接触编程、TypeScript 或 AI Agent 的读者 分析对象:deepseek-ai/deepseek-harness: DeepSeek Harness: Everything is a Plugin.
仓库版本:提交
47f943859b(2026-08-13) 一句话结论:这是一个把“大模型、工具、会话、权限、网页界面”等零件按配置组装起来的 AI Agent 运行框架。
1. 它到底是干什么的?
普通的大模型只能“读文字、写文字”。如果想让它像编程助手一样读取文件、修改代码、运行命令、搜索网页、保存聊天记录,就需要在模型外面增加一套控制系统。
DeepSeek Harness(命令名是 dsh)就是这套控制系统。英文里的 Harness 原意类似“挽具、连接装置”,在软件中可以理解为:把模型和各种能力连接起来,并控制它们如何协作。
可以把整个项目想成一家可自由组装的餐厅:
-
大模型是“厨师的大脑”,负责判断下一步做什么;
-
Agent Loop 是“店长”,不断询问厨师并执行决定;
-
工具是“刀、锅、烤箱”,例如读文件、执行 Bash、搜索网页;
-
Session 是“订单和监控录像”,记录发生过的每件事;
-
Web UI 是“前台和菜单”;
-
Cordis 是“门店的插座标准和管理制度”,让各个零件能插拔、替换。
因此,这个项目不是 DeepSeek 模型本身,也不负责训练模型。它主要负责让模型能够在真实计算机环境里可靠地工作。
2. 先认识几个最重要的词
| 术语 | 小白解释 | 在项目中的作用 |
|---|---|---|
| LLM | 大语言模型,例如 DeepSeek 模型 | 思考并生成文本或工具调用 |
| Agent | 能反复思考、行动、观察结果的 AI 程序 | 接收用户任务,直到完成或停止 |
| Agent Loop | Agent 的循环控制器 | 调模型、执行工具、把结果再交给模型 |
| Tool | 模型可以调用的功能 | 读文件、改文件、运行命令、搜索网页等 |
| Plugin | 可安装、卸载、替换的功能模块 | 本项目几乎每项功能都是插件 |
| Service | 插件对外提供的一种能力 | 其他插件通过统一名字找到它 |
| Provider | 某种能力的具体实现 | 例如本地文件系统或 E2B 远程沙箱 |
| Consumer | 使用某种能力的一方 | 例如“读文件工具”使用文件系统服务 |
| Session | 一次持续的会话 | 保存用户、模型、工具调用等事件 |
| Event | “刚刚发生了一件事”的记录或通知 | 连接插件,并支持重放、持久化和界面更新 |
| Cordis | 项目底层的插件框架 | 管理插件、服务、依赖、事件和生命周期 |
| Profile | 一套运行方案 | 例如 web 或 headless |
| Bundle | 一组可复用的插件配置 | 多个 Bundle 叠成一个 Profile |
3. 项目的核心思想:“一切皆插件”
很多程序有一块很难替换的“核心代码”,其他功能围绕核心编写。这个项目走得更彻底:模型适配器、工具注册表、会话日志,甚至 Agent Loop 自己都是插件。
插件之间通常不会直接寻找某个具体类,而是通过 Cordis 的共享上下文 ctx 获取服务。例如:
-
ctx.llm:模型服务; -
ctx.tools:工具注册和执行服务; -
ctx.sessions:会话服务; -
ctx.agents:Agent 注册和管理服务; -
ctx.systemPrompt:系统提示词组装服务。
这像电脑的 USB 标准:键盘品牌可以变,但只要遵守 USB 接口,电脑就能使用。类似地,文件系统既可以由本机实现,也可以换成 E2B 沙箱实现;上层读文件工具通常不需要跟着重写。
项目把一项完整能力分为三个角色:
-
Service Definition:规定“能做什么”;
-
Service Provider:决定“具体怎么做”;
-
Consumer:把能力交给模型或其他模块使用。
这也是理解 packages/ 目录时最有用的规律。例如 Shell 能力并不只有一个包,而是拆成接口、本地实现、沙箱实现、模型工具等多个包。
4. 从高处看:整体架构
可以把它分成六层:
-
启动与装配层:读命令行和 YAML 配置,决定加载哪些插件;
-
交互层:Web、无界面命令行、ACP、JSON-RPC;
-
Agent 核心层:管理 Agent、回合、步骤、收件箱;
-
能力层:模型、工具、文件、Shell、搜索、子 Agent 等;
-
数据层:会话事件、JSONL/SQLite、投影和检索;
-
基础设施层:Cordis、工具函数、测试、构建、原生沙箱。
5. 用户发出一句话后,内部发生了什么?
这是理解项目最关键的一条主线。
假设用户输入:“请读一下 README.md 并总结。”
ToolsLLMPrompt 组装器Session 日志Agent Loop用户/界面ToolsLLMPrompt 组装器Session 日志Agent Loop用户/界面发送任务记录 turn/start记录 user/message取系统提示词和工具说明从事件日志还原历史消息请求模型判断下一步调用 read_file记录 tool/call校验参数、权限并执行工具返回 README 内容记录 tool/result把工具结果再次交给模型返回总结文字记录 assistant/message记录 step/end 和 turn/end界面根据事件更新显示
这里还要区分两个词:
-
Turn(回合):从用户本次输入开始,到系统认为本次任务暂时结束;
-
Step(步骤):一次模型请求,以及这次请求引出的工具调用。
一个 Turn 可以包含多个 Step。模型第一次可能决定读文件,第二次可能决定搜索,第三次才写出答案。
核心循环的实际实现主要在:
-
packages/core/agent-loop/src/agent.ts:ReactLoopAgent和回合推进; -
packages/core/agent-loop/src/index.ts:AgentLoop服务和 Agent 工厂; -
packages/core/agent/src/:Agent 接口、收件箱、状态和事件; -
packages/core/tools/src/index.ts:工具注册、限制、执行和结果处理; -
packages/core/session/src/:事件日志以及从日志还原消息。
6. 为什么 Session 事件日志如此重要?
该项目不是只保存最后一段聊天文字,而是采用“只追加事件”的日志思路。发生一件事,就往日志末尾加一条记录,例如:
turn/start user/message step/start assistant/chunk assistant/message tool/call tool/result step/end turn/end
这种设计的好处是:
-
程序崩溃后可以恢复;
-
Web 界面可以实时播放模型输出;
-
可以回看工具调用过程;
-
可以从旧会话分叉出新会话;
-
可以统计、搜索、导出和生成标题;
-
同一份日志既能重建模型上下文,也能重建界面。
项目有一条重要原则:只要模型看见过的信息,就必须能从 Session 日志中重新构造。 否则恢复会话后,模型看到的上下文就会与之前不同。
Session.deriveMessages() 可以理解为“把流水账整理成模型能读懂的聊天记录”。日志是事实来源,聊天消息只是从事实中计算出来的一种视图。
7. 工具系统不是简单地“调用一个函数”
模型给出的工具名和参数不能直接盲目执行,因为模型可能写错参数,也可能请求危险操作。工具调用要经过一条管线:
packages/core/tools 负责这套公共机制,具体工具散落在能力目录,例如:
-
packages/fs/tool-fs:文件工具; -
packages/shell/tool-bash、tool-pwsh:命令行工具; -
packages/web/tool-web:联网工具; -
packages/lsp/tool-lsp:语言服务器工具; -
packages/subagent/tool-subagent:委派子 Agent; -
packages/todo/tool-todo:待办事项; -
packages/interaction/tool-ask-user:向用户提问。
工具还带展示信息,让 Web UI 知道应该显示成普通卡片、终端输出还是文件差异。
8. LLM 层如何做到可替换?
packages/llm/llm 定义统一的模型语言,包括:
-
用户、助手、工具结果等消息类型;
-
文本、推理、图片、工具调用等内容块;
-
流式输出块;
-
Token 用量;
-
模型信息和上下文窗口;
-
错误及重试规则;
-
模型 Provider 的注册机制。
具体模型接入放在其他包中:
-
packages/llm/llm-deepseek:DeepSeek 原生适配器; -
packages/llm/llm-pi-ai:另一套模型后端适配; -
packages/llm/llm-retry:失败重试; -
packages/llm/token-meter:Token 计量。
Agent Loop 主要依赖统一的 ctx.llm,而不是把 DeepSeek API 请求写死在循环里。这使模型后端可以替换。
9. 项目是怎样启动的?
命令行入口是 apps/cli/src/bin.ts。它先解析参数,再根据模式动态加载相应模块。
常见命令:
npx @deepseek-ai/dsh web pnpm dsh web pnpm dsh --profile headless "任务内容"
启动过程可以简化为:
Profile、Bundle 和 Patch 的关系
项目没有在入口文件里写几百行 new XXX()。它把插件清单写在 YAML 配置层中。
-
dsh-base:共同地基,包含模型、工具、会话、权限、设置等; -
dsh-web-app:在地基上增加服务器和浏览器界面; -
dsh-headless:在地基上增加一次性任务执行器,不启动网页。
配置应用顺序大致是:
空配置 → Profile 中列出的 Bundle(按顺序) → Profile 自己的 cordis.patch.yml → 用户主目录的 patch → 命令行 --patch
越靠后的配置越有机会覆盖前面的配置。这有点像穿衣服:基础层、外套、个人修改,最后一层决定最终外观。
dsh --profile web --dump-config 可以打印机器最终会启动的插件树,是排查“究竟加载了什么”的重要命令。
10. Web 版为什么又分“前端”和“后端”?
浏览器不能随意读取本机文件或启动子进程,所以 Web 版必须分成两半:
-
packages/client:浏览器里的界面、对话视图、设置页、输入框等; -
packages/host:Web 服务器、静态文件、API 代理、目录选择器等; -
packages/api和packages/typert:定义并生成前后端通信所需的类型和 RPC 结构; -
packages/bundle/web-app/cordis.patch.yml:把 Web 所需插件装配到基础系统上。
前端不会直接操作 Agent 内部对象,而是通过通信层请求后端;后端把 Session 事件传给前端,前端再渲染成聊天界面。
11. 顶层目录地图
| 目录 | 里面是什么 | 新手是否需要马上读 |
|---|---|---|
apps/ |
真正的产品入口,目前重点是 CLI | 是,从这里看“程序如何开始” |
packages/ |
绝大多数产品功能,按领域拆成大量包 | 是,但只挑主线包读 |
docs/ |
架构、子系统、开发和使用文档 | 是,优先级很高 |
examples/ |
可运行的 YAML 装配示例 | 是,适合理解插件如何拼起来 |
vendor/ |
项目固定使用的 Cordis 等第三方源码副本 | 暂时不要深入 |
python/ |
Python SDK 和打包运行时 | 用 Python 接入时再读 |
native/ |
Landlock 等原生安全模块 | 除非研究沙箱,否则后读 |
scripts/ |
构建、校验、生成文档和发布脚本 | 开发项目时再读 |
website/ |
VitePress 文档网站 | 想改官网时读 |
assets/ |
图片等静态资源 | 无需重点阅读 |
.agents/ |
Agent 工作流、设计记录和历史说明 | 深入设计决策时很有价值 |
仓库在当前版本大约有 226 个 packages/ 工作区包、2319 个 .ts 文件、259 个 .tsx 文件。看到目录很多并不代表运行一次会把所有代码都执行;Profile 只会组装本次需要的插件。
12. packages/ 各大区域是做什么的?
下面不是逐包罗列,而是按新手容易理解的方式分组。
核心大脑
-
core/session:会话事件日志; -
core/system-prompt:组合系统提示词和工具说明; -
core/tools:工具注册与执行管线; -
core/agent:Agent 接口、状态、事件和收件箱; -
core/agent-loop:默认循环实现。
模型与上下文
-
llm/:模型接口、DeepSeek 适配、重试和计量; -
context/:时间、工作区说明等模型可见上下文; -
compaction/:上下文太长时压缩; -
preset/:为不同 Session 组合不同 Agent 能力。
计算机操作能力
-
fs/:文件系统; -
shell/:Bash / PowerShell; -
subprocess/:启动和管理子进程; -
terminal/:持续存在的终端; -
lsp/:连接语言服务器; -
sandbox/:限制进程访问范围; -
e2b/:远程 E2B 沙箱实验实现。
Agent 的扩展能力
-
skill/:发现和加载 Skill; -
subagent/:创建或委派子 Agent; -
jobs/:后台任务; -
workflow/:工作流; -
goal/、todo/、plan/、schedule/:目标、待办、计划和定时跟进; -
web/:网页搜索与抓取; -
mcp/:接入 MCP 工具。
数据与产品界面
-
session/:JSONL/SQLite 持久化、投影、标题、统计和遥测; -
session-query/:会话检索和导出; -
storage/:非 Session 数据存储; -
client/:浏览器端; -
host/:Node Web 后端; -
api/、typert/:前后端 RPC 和类型图; -
interaction/:审批、权限、命令和向用户提问。
装配与对外接口
-
bundle/:基础、Web、Headless 等配置层; -
boot/:通用启动逻辑; -
sdk/:TypeScript JSON-RPC SDK; -
acp/:Agent Client Protocol 服务; -
hooks/:连接 Claude Code / Codex 的 Hook; -
examples/:供顶层示例使用的演示包。
13. 代码中的几种常见写法
export
表示把类、函数或类型提供给别的文件使用。
interface 和 type
可以理解为数据的“说明书”。TypeScript 会在开发和构建时检查数据是否符合说明,但很多类型在程序运行时并不存在。
class XXX extends Service
说明这是一个 Cordis 服务类。它通常会占用一个 ctx.xxx 名称,供其他插件使用。
inject
表示插件依赖哪些服务。Cordis 会等依赖准备好再挂载插件,而不是完全依靠配置文件的书写顺序。
ctx.on(...)
注册事件监听器。当某件事发生时调用相应逻辑。
ctx.waterfall(...)
像洋葱中间件:多个监听器可以依次查看、修改或拦截一次操作。监听器调用 next() 才会继续交给下一层。
ctx.effect(...)
注册一个可以撤销的副作用。例如插件注册了工具,卸载插件时也应自动取消工具注册,避免留下“幽灵功能”。
cordis.patch.yml
不是业务代码,而是“本次应用要装哪些插件、每个插件用什么配置”的装配文件。
14. 建议的新手阅读路线
不要按文件名从 A 读到 Z。建议分五轮,每轮只回答一个问题。
第一轮:这个产品能做什么?
-
README.md -
docs/architecture.md -
docs/cordis-primer.md -
packages/README.md
目标:理解 Agent、插件、服务、事件、Provider。
第二轮:它如何启动?
-
apps/cli/src/bin.ts -
apps/cli/src/args.ts -
apps/cli/src/profile-boot.ts -
packages/boot/app-boot/src/index.ts -
packages/bundle/base/cordis.patch.yml -
packages/bundle/headless/cordis.patch.yml
目标:知道命令如何变成一棵插件树。第一次读 base 配置时只看每行的 id 和 name,先忽略复杂 config。
第三轮:Agent 如何工作?
-
docs/agent-lifecycle.md -
packages/core/agent/README.md -
packages/core/agent-loop/README.md -
packages/core/agent-loop/src/agent.ts
目标:追踪 Turn、Step、模型请求和工具结果。
第四轮:数据和能力如何接入?
-
packages/core/session/README.md -
packages/core/tools/README.md -
packages/llm/llm/README.md -
任选一个简单工具包,例如
packages/todo/tool-todo
目标:理解公共接口和具体实现为何分包。
第五轮:Web 如何显示这些内容?
-
packages/bundle/web-app/cordis.patch.yml -
packages/host/README.md -
packages/client/README.md -
packages/api/README.md
目标:理解浏览器、Node 后端和 Agent 之间的关系。
15. 调试和探索时最有用的方法
方法一:先看 README,再看 src/index.ts
这个仓库的包普遍有自己的 README。先看包要解决什么问题,再进入源码;直接读上千行的 index.ts 很容易迷路。
方法二:从服务名反向搜索
例如看见 ctx.tools,可以搜索:
rg "ctx\.tools|class ToolRuntime" packages
这能找到谁定义它、谁提供它、谁使用它。
方法三:沿事件名搜索
例如搜索 tool/call 或 agent/request,能同时看到事件的生产者和消费者,比只跟函数调用更适合这个事件驱动项目。
方法四:查看最终配置
dsh --profile web --dump-config
当某插件似乎没有生效时,先确认它是否真的被 Profile 加载、是否被后续 Patch 禁用或覆盖。
方法五:先运行 Headless
Headless 没有 Web、HTTP 和浏览器通信,更适合观察 Agent 主循环:
pnpm dsh --profile headless "请只回复 hello"
真实模型调用需要配置相应 API Key。
16. 这个架构的优点和代价
优点
-
可替换:模型、存储、文件系统、沙箱等可以换实现;
-
可组合:Web 和 Headless 共享同一套基础能力;
-
可扩展:新功能通常作为插件或事件监听器加入,不必改 Agent Loop;
-
可恢复:Session 事件日志支持恢复、分叉、重放和审计;
-
易做安全控制:工具执行经过统一管线,可插入权限和沙箱策略;
-
多界面共享:同一 Agent 能通过 Web、ACP 或 JSON-RPC 等方式使用。
代价
-
文件和包非常多:一个能力常拆成接口、Provider、Consumer;
-
控制流不直观:大量行为由事件和配置连接,不一定能顺着函数一路读完;
-
需要理解 Cordis:不懂
ctx、Service、inject、effect、waterfall 时会觉得“代码从哪里被调用”很神秘; -
配置也是程序的一部分:只读 TypeScript 而不读 YAML,会漏掉真正的装配关系;
-
仍在开发者预览期:仓库明确表示可能有破坏兼容性的变化。
这不是“过度拆分”就能简单概括的。项目选择用更多小包和明确接口,换取能力可替换、可测试和多种产品形态共享。但对初学者来说,确实需要先建立地图。
17. 最后用一段话串起来
当你执行 dsh web 时,CLI 读取 Web Profile,把基础 Bundle、Web Bundle 和个人 Patch 合并成插件清单。Cordis 根据依赖挂载这些插件,得到模型、工具、Session、Agent 和 Web 服务。浏览器把用户消息发给后端,Agent Loop 开启一个 Turn,把系统提示词、工具说明和 Session 历史整理后请求 LLM。模型可以直接回答,也可以要求调用工具。工具系统校验参数、检查策略、执行文件或命令操作,再把结果记入 Session 并交还模型。这个过程可以重复多个 Step,直到模型完成回答。所有关键过程都作为事件追加进 Session 日志,Web UI 根据这些事件实时显示内容,持久化插件则把它们保存到 JSONL 或 SQLite 中。
只要先牢牢记住四件事,后面的代码就不会再是一团乱麻:
-
Cordis 负责装插件和连服务;
-
Agent Loop 负责反复“问模型—执行工具—再问模型”;
-
Session 事件日志是历史事实的唯一来源;
-
YAML Profile/Bundle 决定本次运行究竟有哪些能力。
附录:关键入口速查
| 想了解的问题 | 首选文件 |
|---|---|
| 程序从哪里启动? | apps/cli/src/bin.ts |
| Profile 如何启动? | apps/cli/src/profile-boot.ts |
| Cordis 配置如何加载? | packages/boot/app-boot/src/index.ts |
| 基础插件有哪些? | packages/bundle/base/cordis.patch.yml |
| Web 增加了什么? | packages/bundle/web-app/cordis.patch.yml |
| 无界面模式增加了什么? | packages/bundle/headless/cordis.patch.yml |
| Agent 主循环在哪里? | packages/core/agent-loop/src/agent.ts |
| Agent 公共接口在哪里? | packages/core/agent/src/ |
| 工具如何注册和执行? | packages/core/tools/src/index.ts |
| 会话如何记录? | packages/core/session/src/ |
| 模型统一接口在哪里? | packages/llm/llm/src/ |
| DeepSeek 如何接入? | packages/llm/llm-deepseek/src/ |
| 前端在哪里? | packages/client/ |
| Web 后端在哪里? | packages/host/ |
本文是面向新手的架构地图,因此有意省略了 Typert 代码生成、Client 双面插件、Session projection、原生沙箱、发布流水线等高级实现细节。掌握上面的主线后,再按具体问题进入相应子系统,会比一次读完整个仓库高效得多。
更多推荐

所有评论(0)