DeepSeek Harness 当前仍处于 Developer Preview,本文基于
0.1.0-rc.5。
最近 DeepSeek 开源了一个叫 DeepSeek Harness 的 Agent 框架。
看到 Harness 这个词,很多人的第一反应可能是:又一个封装大模型、注册几个 Tool、再套个聊天页面的框架。
我一开始也是这么想的。
但把源码拉下来、跑完构建,再顺着 packages/core、packages/bundle 和 apps/cli 看了一遍之后,我发现它真正想解决的不是"怎么调用模型",而是另一个更麻烦的问题:
当 Agent 开始拥有文件系统、Shell、权限、会话、子 Agent 和后台任务之后,怎么避免整个系统变成一团无法替换的胶水代码?
DeepSeek Harness 给出的答案是:把整个 Agent 产品拆成插件。
不是"支持插件",而是"一切皆插件"。
Agent Loop 也不是内核
传统 Agent 框架通常都有一段地位特殊的主循环:
text
接收消息
→ 调用模型
→ 解析 Tool Call
→ 执行工具
→ 把结果交还模型
→ 继续下一轮
模型、工具、历史记录和权限逻辑会逐渐堆进这段循环。刚开始很好理解,后面每增加一种能力,都要在主流程里加新的条件分支。
DeepSeek Harness 没有把 Agent Loop 放在不可替换的位置。
模型适配器、工具注册表、会话日志、权限策略、文件系统,甚至 Agent Loop 本身,都是挂载在 Cordis 上的插件。它们通过共享上下文 ctx 协作,而不是互相引用具体实现。
text
Cordis Context
├── ctx.sessions
├── ctx.tools
├── ctx.llm
├── ctx.agents
├── ctx.agentLoop
├── ctx.fs
├── ctx.subprocess
├── ctx.sandbox
└── ctx.jobs
这带来一个很直接的结果:新增能力时,默认动作不再是修改 Agent Loop,而是找到对应扩展点,挂载一个新插件。
例如:
- 新模型注册到
ctx.llm - 新工具注册到
ctx.tools - Shell 换成本地或沙箱 Provider
- 请求拦截使用
agent/*事件 - 工具审批使用
tools/*事件 - 后台任务注册到
ctx.jobs
只有现有扩展点真的无法表达需求时,才需要碰 Agent Loop。
Cordis 管的不只是依赖注入
Cordis 是 Harness 的底层插件框架。
一个最小插件只有几行:
ts
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('plugin loaded')
}
如果插件依赖工具注册表,需要显式声明:
ts
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(/* ... */)
}
Cordis 会等到 tools 服务可用后再加载插件。这不只是方便获取依赖,它同时解决了启动顺序和生命周期问题。
更关键的是副作用可逆。
ts
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
return () => clearInterval(timer)
})
}
插件卸载时,Cordis 会执行清理函数。事件监听、工具注册和通过上下文创建的能力也会随插件一起撤销。
这正是 Harness 能支持热更新和配置重组的基础。否则每次修改配置都重新加载插件,旧定时器、事件监听器和工具注册会不断残留。
Profile 不是配置文件,而是一套 Agent 产品
Harness 启动的不是某个固定应用,而是一个 Profile。
一个运行中的 Profile,本质上是一棵由多层配置组合出来的插件树:
text
Bundle 列表
→ Profile 自己的 cordis.patch.yml
→ $DSH_HOME/cordis.patch.yml
→ 命令行传入的 --patch
内置的 dsh-base 提供模型、工具、持久化、沙箱、审批和凭据等基础能力。
dsh-web-app 在它上面增加 Web 服务和前端界面;dsh-headless 则增加一次性任务执行器,不启动 Web 服务。
所以 Web 和 Headless 并不是两套独立实现,而是两种插件组合。
查看当前机器最终运行了什么,不需要猜:
bash
dsh --profile web --dump-config
这条命令会打印组合后的配置树。
需要注意的是,Patch 替换的是目标行的整个 config,不是深度合并。覆盖已有插件配置时,必须把需要保留的字段一起写回来。
真正值得研究的是 Capability Seam
Harness 的包很多,但它并不是单纯把大模块拆成小包。
一项需要替换实现的能力,通常会被拆成三种角色:
text
Service Definition
↓
Service Provider
↓
Consumer
以 Shell 为例:
- Definition 定义执行请求和结果
- Provider 决定在本机、远端还是沙箱里执行
- Consumer 把它包装成 Bash、PowerShell 或终端 Tool
Tool 不应该知道进程到底在哪里运行。
这使得文件系统与进程 Provider 切换到远程沙箱后,Bash、PTY 和 LSP 可以整体迁移,不需要为每个 Tool 单独维护远程版本。
当然,这种设计不是免费的。
它会带来更多包、更长的依赖图,以及更高的理解成本。如果某项能力只有一个实现,也不会被复用,更不需要隔离,那么硬拆 Definition、Provider 和 Consumer 只是在制造形式主义。
Harness 的做法更适合那些真正需要替换运行环境的能力。
会话日志才是系统的事实源
我认为 Harness 最重要的约束不是插件,而是这句话:
模型可见,即已记录。
Harness 不把一个可变的 messages[] 当成会话本身,而是维护仅追加的 SessionEvent 日志。
text
SessionEvent Log
├── 投影成模型历史
├── 渲染到 Web UI
├── 持久化到 JSONL 或 SQLite
├── 支持恢复与 Fork
└── 生成 Transcript 和遥测
这解决了 Agent 系统中一个很常见的问题:运行时给模型偷偷塞了上下文,但恢复会话时无法重建;或者页面显示了某个状态,模型却不知道它存在。
在 Harness 里,如果新内容会进入模型请求,就不能只在 agent/request 中临时拼接。它必须成为会话事件,并支持从日志重新投影。
代价是开发更严格,但换来的是可恢复、可回放和可审计。
一次 Turn 到底发生了什么
Harness 把一次模型请求加工具执行称为 Step,一次 Turn 可以包含多个 Step。
简化后的流程如下:
text
turn/start
→ 领取输入
→ agent/pre-step
→ 组装提示词与 Tool Schema
→ step/start
→ 从日志派生模型历史
→ agent/request
→ llm/stream
→ assistant/message
→ tool/call
→ tools/pre-execute
→ tools/execute
→ tools/post-execute
→ tool/result
→ step/end
→ turn/end
其中 agent/request、llm/stream 和 tools/* 的部分事件采用 Waterfall 机制。
监听器必须调用 next():
ts
ctx.on('tools/pre-execute', async (request, next) => {
validateRequest(request)
return next()
})
漏掉 next() 不是什么都没做,而是直接截断后续处理链。开发策略插件时,这一点很容易踩坑。
它适合什么项目
如果只是做一个调用模型、执行几个固定函数的小应用,DeepSeek Harness 可能太重。
它更适合下面这些场景:
- 同一套 Agent 需要 Web、Headless、ACP 等多种入口
- 文件系统和进程需要在本地、沙箱、远端之间切换
- 需要持久会话、恢复、Fork 和审计
- Tool 执行需要权限、审批和策略拦截
- 不同 Agent 需要不同能力集合
- 希望第三方扩展能力,但不想 Fork 主仓库
DeepSeek Harness 最值得借鉴的地方,并不是"用了插件架构"。
而是它把 Agent 的运行状态、能力边界和副作用生命周期都变成了可以组合、替换和验证的对象。
这比再封装一层模型 API 难得多,也更接近真正的 Agent 工程。