DeepSeek Harness 实战 02|三个 Harness 的解剖:同一个文件名,三种加载语义原创
# 三个 Harness 的解剖:同一个文件名,三种加载语义
把一份写好的 AGENTS.md 原样丢进三个不同的编码 harness,会得到三种结果:一个把它和上级目录的同类文件逐层合并后全给模型;一个只在没找到更高优先级的同类文件时才读它,否则整份文件像不存在一样;还有一个会读,但带字节预算,超了就按规则截断。三者都不会报错。
这篇把 Claude Code、Hermes Agent、DeepSeek Harness(下称 dsh)三者的骨架拆开对照,回答四个每次换工具都要重问一遍的问题:工作区是什么、长期记忆存在哪、skill 存在哪、所谓「XX 模式」到底是什么。结论不是「谁更好」——它们的单位根本不同,一个的单位是一次工作,一个是一个人格,一个是一份组装。
版本说明
dsh 部分基于 0.1.2-rc.1(developer preview)实测;Hermes Agent 部分的机制以其 Python 源码为准(本文引用的判定逻辑在 agent/prompt_builder.py 与 agent/system_prompt.py);Claude Code 部分以其配置约定与本地运行痕迹为准。三者都在活跃迭代,读到与本文不符时以你机器上的实机输出为准。
# 1. 上下文文件:一个名字,三套判定规则
先把开头那件事说透,因为它是最容易静默出错的地方。
Claude Code 是分层合并。 用户级、项目级、以及子目录里的 CLAUDE.md 沿目录树叠加,都会进上下文。写在项目根的约定对整个仓库生效,子目录里再补充细节——加法模型,符合直觉。
Hermes Agent 是「首个命中即停」,而且只取一种类型。 它的上下文文件发现按优先级排:
| 优先级 | 文件 | 收集范围 |
|---|---|---|
| 1 | .hermes.md / HERMES.md | 向上走到 git 根 |
| 2 | AGENTS.md / agents.md | git 根 → cwd 的合并链 |
| 3 | CLAUDE.md / claude.md | 仅当前目录 |
| 4 | .cursorrules / .cursor/rules/*.mdc | 仅当前目录 |
源码里那句注释写得很直白:first found wins — only ONE project context type is loaded。也就是说,一个目录里同时放着 HERMES.md 和 CLAUDE.md,后者一个字都不会进上下文。这不是 bug,是刻意的单一权威源设计,但如果你带着 Claude Code 的直觉过来,会以为两份都生效。
身份文件是独立通道:SOUL.md 不参与上面的竞争,只要存在就一定进入身份槽位。它还与提示词缓存绑定——改了 SOUL.md,缓存前缀就对不上,系统会重新做一次真实文件 I/O 重建,而不是拿旧的凑合。
dsh 是带预算的合并链。 从宽泛到具体叠加(用户全局 → 项目根 → 一路到当前目录),默认预算 65536 字节,超出时先丢宽泛的、再截断最具体的,并且会发一条可见的预算通知点名哪些文件被省略或截断——这一点比另外两家友好,它不静默吞。
可以直接用的规则:换 harness 的第一件事不是搬文件,是搞清楚它对同名文件的收集范围和竞争规则。三者分别是「合并」「独占」「合并 + 预算」。
# 2. 工作区是什么:三种单位
这是所有差异的根。
Claude Code —— 单位是一次工作。 启动目录就是工作区,没有注册、没有对象,cd 到哪儿就是哪儿,项目边界靠 git 仓库自然划出。会话关掉就结束,除非显式恢复。是 shell 语义。
Hermes Agent —— 单位是一个人格。 工作区由 profile 定义,每个 profile 自带人设、凭据、记忆和目标主机。更关键的是工作区可以不在本机:终端后端可以配成 SSH、Docker 或远程沙箱,agent 的命令直接落在远端。是 profile 语义。
dsh —— 单位是一份组装。 CLI 下同 Claude Code;Web 界面里 workspace 是个持久化对象(有 id、标题、创建时间,会话挂靠其下)。是 IDE 语义。
有一条配置层面的观察,未实测,但值得在部署时留意:dsh 的沙箱写入根取的是服务进程自己的当前目录,而不是会话界面上显示的那个 workspace。用 systemd 之类的方式托管服务时,如果单元里的工作目录设成了家目录,那么会话的实际可写范围就是整个家目录,与界面显示的不是一回事。CLI 模式没这个问题——那里两者天然重合。
# 3. 长期记忆存在哪里
三者差距最大的一处。
| Claude Code | Hermes Agent | dsh | |
|---|---|---|---|
| 跨会话记忆 | 文件式记忆目录,一事一文件 + MEMORY.md 索引 | memories/ + 索引,且记忆后端是可换的 provider 插件 | 没有 |
| 常驻上下文 | CLAUDE.md 分层收集 | SOUL.md 身份槽 + 上节那套上下文文件 | AGENTS.md,65536 字节预算 |
dsh 这一栏不是没写完,是取向。它的插件清单里没有任何 memory / knowledge / embedding 组件,连会话搜索索引都配成内存态且从不打开落盘文件。它把「记忆」外包给两样东西:常驻的 AGENTS.md,加上按需加载的 skills。
这么做的收益是知识必须是你能 cat、能进版本库、能 diff 的东西,没有一个你看不见的向量库替你决定该记住什么。代价也很直接:换个工作目录就什么都不记得。
顺带一个真实场景的判断:想把一批现成的 markdown 知识库直接喂给 dsh,两条路都堵着。当 skill 根挂进去——不行,那些文件没有 frontmatter,会被静默丢弃;塞进 AGENTS.md——也不行,体量远超预算,且绝大部分与当下问题无关,纯浪费上下文。正解是写一个索引型 skill:目录里只放一句描述,模型判断相关才加载,加载后拿到的是「知识库在哪、有哪些页、怎么检索」。dsh 有 bash 和文件工具,读文件是它的强项——这正是渐进式披露该有的用法。
# 4. skill 存在哪里
三者都有「按需加载的知识/能力单元」,但寻址方式不同。
- Claude Code:用户级与项目级两处 skills 目录,另有子 agent 定义(人设 + 工具白名单)作为另一种复用单元。
- Hermes Agent:全局与 profile 两级 skills 目录,profile 级的只对该人格可见。
- dsh:六级扫描根,小号赢同名冲突:
| rank | 位置 |
|---|---|
| 100 | <项目根>/.dsh/skills |
| 200 | <项目根>/.agents/skills |
| 300 | customSkillDirs(组装里显式配置) |
| 400 | <dsh 家目录>/skills |
| 500 | <agents 家目录>/skills |
| 600 | 随包内置 |
这张表有个坑值得单独记:前两级依赖「项目根」,而项目根是从当前目录往上找最近带 .git 的祖先。如果你的工作目录不是 git 仓库,rank 100/200 根本不存在——很多人第一次写完 skill 发现没生效就是这个原因。另外 rank 300 默认是空的,preset 自带的 skills/ 必须在组装里显式配 customSkillDirs 才会被扫到。
三家共同的一条:skill 的 description 是给调度器看的,不是给人看的。模型只靠这一句决定要不要加载,所以它必须写成「什么时候该用我」,而不是「我是什么」。
# 5. 「XX 模式」到底是什么
dsh 界面上的「标准模式」「某某模式」,本体就是 preset,显示的是 preset 配置里的 name 字段。
一个 preset = 一个目录 = 一份「这个会话装哪些插件」的清单。 目录里的组装文件逐行列出这次会话要挂的东西:人设、有哪些工具、加载哪些提示词段落、扫哪些技能目录。所以切换模式不是换个语气,是换掉这个会话的能力集。随包发行的几个 preset 差别就很具体:标准那份是全套(shell、文件读写、搜索、技能、计划模式、子 agent);有一份专门用来改造 harness 自己,去掉了技能相关的挂载、换上改组装用的工具;极简那份则把人设换成一句话并开启独占模式,其他提示词段落全部消失。
三条容易误解的性质:
- 它是会话级的,不是全局的。 一个进程里可以同时跑着几个不同模式的会话,各装各的。这也是为什么 preset 里的服务必须放进隔离域,否则两个 preset 会撞车。
- 选定后基本锁死。 只有在会话还没产出任何内容时才能切;之后固定,因为中途换工具会留下新组装执行不了的历史记录。推论:改了默认 preset,只对此后新建的会话生效,正在跑的不受影响。
- 它的权限等于它挂的插件的权限。 上游文档把「能写 preset」与「有 shell 访问权」列为同级——因为 preset 就是一份组装,能挂 bash 就是能跑命令。所以随包发行的那份不要改(升级会覆盖),自建的写到用户级 preset 目录下。
和另外两家怎么对应:它不是 Hermes 的 profile——profile 带着凭据、记忆、目标主机,是个长期人格;preset 只管这一次会话装什么零件,没有记忆。它更接近 Claude Code 的子 agent 定义(给定人设 + 工具白名单),但粒度更粗:它决定的是整个主会话的装配,而不是派出去的分身。
一句话:profile 是「谁」,preset 是「带了哪些家伙」。
# 6. 会话存在哪里——这决定了你能不能验证
调优 agent 时最重要的一条纪律是「验收看日志、不看回答」,那么日志在哪、什么格式,就直接决定了这条纪律可不可执行。
| 存储形态 | 能不能全文检索 | |
|---|---|---|
| Claude Code | 每个项目一个目录(目录名由工作目录路径转义而来),会话是其中的 .jsonl,逐条事件 | 靠外部工具 grep |
| Hermes Agent | SQLite 会话库,且建了 FTS5 虚拟表(含 trigram 分词表) | 内建全文检索 |
| dsh | session.jsonl.zstd,按工作目录分桶 | 索引不落盘,只能解压后 grep |
两个实操结论:
- Claude Code 与 dsh 都是按工作目录分桶的。换个目录,历史在物理上就是另一个文件夹——这既是隔离的好处,也是「昨天那段对话找不到了」的常见原因。
- 三者都能拿到逐条事件,所以「模型到底调了哪个工具」这个问题在三家都是可查的。回答正确不等于机制生效,看工具调用记录才是硬证据。
# 7. 总表
| 维度 | Claude Code | Hermes Agent | dsh |
|---|---|---|---|
| 单位 | 一次工作 | 一个人格 | 一份组装 |
| 工作区 | 启动目录 | profile 定义,可在远端主机 | CLI 同左;Web 里是持久化对象 |
| 上下文文件 | 分层合并 | 首个命中即停,只取一种类型 | 合并链 + 字节预算 |
| 跨会话记忆 | 文件式记忆目录 | memories/ + 可换后端 provider | 无 |
| 按需知识 | 两级 skills 目录 + 子 agent | 全局与 profile 两级 skills | 六级扫描根,小号优先 |
| 会话存储 | 按工作目录分桶的 .jsonl | SQLite + FTS5 | 按工作目录分桶的 .jsonl.zstd |
| 扩展方式 | 配置 + 外部协议 | 注册工具后还须列入工具集 | 改 YAML 组装,不改代码 |
| 主动性 | 被动,靠外部调度 | 内建定时调度 | 被动 |
「扩展方式」那行 Hermes 的写法值得展开一句:工具文件里调用注册函数就会被自动发现,但要它的名字出现在工具集定义里才真正暴露给模型。两步,少做一步的表现是「工具明明写了却查不到」——这与 dsh 那个「preset 的 skills 目录不配 customSkillDirs 就扫不到」是同一类故障:注册与暴露是两件事。
# 8. 迁移时最容易踩的三个坑与边界
坑一:次优先级的上下文文件静默失效。 从分层合并的 harness 搬到独占式加载的 harness,同一个目录里的 CLAUDE.md 会因为旁边存在一个更高优先级的同类文件而一个字都不进上下文。没有警告,表现是「约定写了但它不遵守」。迁移时先合并成单一权威源,不要指望两份都生效。
坑二:注册不等于暴露。 三家都有第二步——工具集清单、customSkillDirs、白名单。第一步做完、第二步漏掉的表现完全一致:能力"明明写了"却查不到,且不报错。排查顺序应该是先确认在不在目录/清单里,再确认有没有被调用,这是两个不同的故障。
坑三:分桶存储让历史"消失"。 按工作目录分桶的两家,换个目录就是另一个物理文件夹。表现是「昨天那段对话找不到了」,实际上文件还在,只是不在当前桶里。养成记录工作目录的习惯,比事后翻磁盘便宜。
一条边界要说清:本文只比较骨架——工作区定义、上下文加载、知识存放、会话存储、扩展方式。不比较模型能力,也不比较价格与配额,那两项随时会变,且与 harness 的选择基本正交:同一个 harness 换个模型,编码水平就完全不同。看到这张表得出「谁更强」的结论,那不是本文的意思。
# 9. 可复用要点
- 换 harness 先问三个问题:同名上下文文件怎么收集(合并还是独占)、跨会话记忆存不存在、skill 从哪些根扫。这三条决定了你的既有资产能不能平移。
- 「注册」与「暴露」通常是两步。 三家都有各自的第二步(工具集、
customSkillDirs、白名单),漏掉的表现都是静默失效而非报错。 - 没有记忆系统不等于不能积累知识,只是知识必须显式地放进常驻文件或按需 skill;反过来,有记忆系统也不代表你能不管它。
- 验收永远看会话日志里的工具调用记录。 三家的日志形态不同,但都能拿到逐条事件——这是判断「机制真的生效了」的唯一硬证据。
# 10. 延伸阅读
dsh 的公开文档站有中文版:DeepSeek Harness 参考手册 (opens new window),仓库在 deepseek-ai/deepseek-harness (opens new window)。要提醒的是,文档站给的是全局架构,插件级的配置字段以本地安装目录里各插件自带的 README.zh.md 为准——本文用到的几张字段表在站上都查不到。
本站另有两个系列可以对照着读:Claude Code 系列讲的是单机编码 agent 的用法与护栏,Hermes Agent 系列讲的是多人格常驻平台的运维。
下一篇是一份学习笔记:读 12 篇 README 拿下 dsh 的五个核心心智模型,把 dsh 这一侧的骨架单独讲透——本文第 1、2 节里那些「为什么是这样」的答案,多数能在那五个模型里找到根。再往后计划把「插件」这个词在三家里的不同含义讲清楚:dsh 的插件树与按 id 覆盖、Hermes 的工具注册与插件目录、Claude Code 的 hooks 与外部协议,各自的调用时机与失败模式都不一样。
🤖 Agent 可直接解析的元数据块(点击展开)
{
"_meta": {
"doc_version": "2026-09-08",
"article_id": "dsh-02-three-harnesses-anatomy",
"profile_context": "any",
"estimated_setup_time": "15min"
},
"quick_start": {
"step_1": "确认目标 harness 的上下文文件收集规则:是分层合并、首个命中即停,还是带预算的合并链",
"step_2": "确认它有没有跨会话记忆;没有就把长期知识拆成『常驻上下文文件』与『按需 skill』两层",
"step_3": "确认 skill 的扫描根清单,特别是依赖 git 仓库判定的那几级",
"step_4": "确认『注册』之外是否还有第二步暴露动作(工具集 / customSkillDirs / 白名单)"
},
"safety_rules": [
"不要假设同名上下文文件在不同 harness 里语义相同 —— 独占式加载会让次优先级文件完全不生效",
"不要用『回答是否正确』判断机制是否生效,一律查会话日志里的工具调用记录",
"不要修改随包发行的组装或 preset 目录 —— 升级会覆盖,自建的放用户级目录",
"托管服务时确认沙箱写入根与界面显示的工作区是否一致"
],
"verification": {
"check_context_file_loaded": "在会话里直接问:你当前上下文里有哪些项目约定文件?逐个报文件名",
"check_skill_in_catalog": "提问:不要跑任何命令,只列出你现在能用的所有 skill 名字",
"check_tool_actually_called": "解析会话日志中的工具调用事件,确认目标工具/技能真的被调用过",
"check_sandbox_root": "对照服务托管单元里的工作目录设置与会话界面显示的工作区路径"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
AI Agent 使用指南:
- 当用户说「把这套约定从一个 harness 搬到另一个」→ 先跑
quick_start.step_1,确认收集规则差异,再决定文件放哪一层 - 当用户说「skill/工具写了但不生效」→ 依次查扫描根(是否依赖 git 仓库)与第二步暴露动作,两者是不同故障
- 当用户说「它记不住上次说的话」→ 先确认该 harness 有没有跨会话记忆;没有就改用常驻上下文文件加按需 skill 的两层结构