DeepSeek Harness 实战 01|同一条纪律,写在两个位置,模型只听一个原创
# 同一条纪律,写在两个位置,模型只听一个
给编码 Agent 写行为纪律时,很容易默认「写进人设就等于生效」。但在 DeepSeek Harness(下称 dsh)上做的一组对照实验说明,同一句话、同一个模型、同一个问题,只是换了个存放位置,模型的服从度就完全不同:写在 preset 人设里,模型不照做;原样挪到 AGENTS.md,立刻开始照做。
这篇讲清楚为什么会这样:dsh 的系统提示词是分层拼接的,人设与工作区指令走的是两条不同通道;顺带记三个在机制层面把人绊住的坑,其中一个让笔者当场下了个错误结论,最后是靠会话日志推翻的。
版本说明
本文基于 dsh 0.1.2-rc.1(developer preview)实测。上游明示会有破坏性变更,配置字段与插件划分可能随版本调整。文中所有机制均以 --dump-config 的实际输出和插件自带 README 为准,读到与本文不符时以你机器上的实机输出为准。
# 1. 一句纪律,两处存放,两种结果
被测的纪律只有一条,意思是「不要只给结论」:
每一个结论都必须来自你实际跑过的命令。贴出命令原文和它的真实输出,然后再给结论。不要只写结论,也不要用散文复述输出内容。
测试用的提问固定不变:
用一条命令报出本机 hostname 和当前登录用户,然后告诉我你现在的工作目录。
第一次,纪律写在 preset 的人设(persona)里。模型的回答:
结果如下:
- hostname: web-01
- 当前登录用户: deploy
我现在的工作目录是:/home/…/workspace
2
3
4
结论正确,但一行命令输出都没有——纪律里最核心的那句被无视了。
第二次,把同一段文字原封不动挪进 AGENTS.md,人设那边删掉。同一个问题:
结果如下:
```
web-01
deploy
```
- hostname:web-01
- 当前登录用户:deploy
2
3
4
5
6
7
8
9
输出块出现了。唯一的变量是这段文字存在哪里。
需要如实说明的是:第二次它贴了输出,但仍然没贴命令本身(要求是「命令原文和输出」两样)。所以这不是从 0 到 1,是从「完全不理」到「照做一半」。用的是一个中等能力的开源编码模型,对这类元指令的服从度本来就有限。但方向是明确的,且可复现。
# 2. 人设与工作区指令走的是两条通道
dsh 的系统提示词由一个注册表拼装:每个插件贡献若干「段落」(section),每段带一个数值 order,按升序拼接,同号时按名称的代码单元顺序排。
实测下来的分层是这样:
| order | 内容 | 谁配的 |
|---|---|---|
| −1000 | harness 固定开场白 | includeHarnessIdentity,可关 |
| 0 | 部署级 persona | 部署配置里的 persona 字段 |
| 0 | preset persona | preset 自己的人设行 |
| 之后 | runtime context 快照、各插件注册的段落(工具引导、计划模式等) | 各插件 |
第一个反直觉的点:preset 的人设不是「追加」在部署人设后面,而是注册了一个同名同 order 的段落,把部署那条整个遮蔽掉。所以你在 preset 里写人设,是在做替换,不是补充。
人设插件只有三个配置字段,值得记住的是后两个:
| 字段 | 默认 | 含义 |
|---|---|---|
text | 必填 | 人设正文,支持模板变量 |
complete | false | 开启后,这段人设成为唯一的系统提示词段落 |
includeRuntimeContext | true | 是否包含动态 runtime 上下文快照 |
complete: true 是个核弹开关:打开之后工具引导、计划模式提示这些段落全部消失,模型会突然不知道该怎么用工具。除非你在做极简组装,否则别碰。
第二个反直觉的点,也是本文开头那组对照的解释:AGENTS.md 根本不在上面这条链上。
它由一个独立的工作区指令插件加载,作为一条持久基线消息进入第一次请求,而不是提示词里的某一段。它兼容 CLAUDE.md 命名,默认预算 65536 字节,按「从宽泛到具体」叠加:用户全局文件 → 项目根 → 一路到当前工作目录,每层的候选文件都收。项目根的判定是从当前目录往上找最近的带 .git 的祖先目录,找不到就用当前目录本身。
超出预算时的淘汰顺序也是「先丢宽泛的、再截断最具体的」,并且会发出一条可见的预算通知,点名哪些文件被省略或截断——不会静默吞掉。
至于为什么这条通道更管用,笔者的推测是:人设只占 order 0 的一小段,后面还压着几千 token 的工具引导和上下文,稀释掉了;而基线消息是独立的一条,位置更靠近对话本身。这是推测,没有做进一步的对照实验证实,只有前面那组 A/B 的结果是确定的。
可以直接用的规则:人设只放身份和环境事实(「你运行在什么机器上」「node 是怎么装的」),工作纪律一律写 AGENTS.md。
# 3. 技能怎么被找到,怎么被加载
纪律解决「怎么做」,知识解决「知道什么」。dsh 的知识按需加载单元叫 skill。
# 准备:搞清它扫哪些目录
skill 由文件系统提供方扫描,六级根按 rank 排,小号赢同名冲突:
| rank | 位置 |
|---|---|
| 100 | <项目根>/.dsh/skills |
| 200 | <项目根>/.agents/skills |
| 300 | customSkillDirs(组装里显式配置的) |
| 400 | <dsh 家目录>/skills |
| 500 | <agents 家目录>/skills |
| 600 | bundledSkillDir(随包内置) |
「项目根」的判定和上一节一样,靠 .git。如果你的工作目录不是 git 仓库,前两级根本不存在——这是很多人第一次写完 skill 发现没生效的原因。
# 执行:写一个 skill
两种形态,都必须在被扫描根目录的顶层:
<root>/<name>/SKILL.md # 目录式,可以带 references/ scripts/ 等资源
<root>/<name>.md # 单文件式
2
不支持嵌套发现——**/SKILL.md 是刻意不扫的,放深一层就找不到。
文件以 YAML frontmatter 开头,name 和 description 必填:
---
name: storage-layout
description: 回答任何涉及磁盘、存储、空间占用、剩余容量、清理、扩容的问题之前,必须先加载本技能。它给出本机各挂载点的真实含义,以及哪些挂载点必须排除在可用空间统计之外。不加载就直接看 df 下结论会得出错误答案。
---
# 存储布局
(正文……)
2
3
4
5
6
7
8
三条约束值得记死:
name必须是 kebab-case,正则是^[a-z0-9]+(?:-[a-z0-9]+)*$。- frontmatter 缺失或字段非法,整个 skill 被静默丢弃,模型侧看不到任何诊断——它只会表现为「这个 skill 不存在」。
description上限 500 字符,模型只靠这一句决定要不要加载。
# 验证:确认它进了目录,且真的被加载
这是两件事,必须分开验。
先验「在不在目录里」,直接问:
不要跑任何命令。只列出你现在能用的所有 skill 名字,一行一个。
再验「有没有被加载」——这一步不能靠看回答,理由见下一节。
好消息是改 skill 不需要重启:被扫描的根目录有 watcher,新增、改名、删除或改 frontmatter 会在下一个模型步骤触发目录刷新;改正文更简单,每次加载都重读当前文件。但 references/、scripts/ 这些资源目录下的改动不触发刷新。
# 4. 三个把人绊住的坑
# 坑一:写在 preset 里的 skills 目录,默认扫不到
症状:在 preset 目录下建了 skills/,写好 SKILL.md,格式全对,模型的技能目录里就是没有它。
原因:组装里的技能提供方如果是裸挂一行、不带配置,它只扫默认根(也就是上表的 100/200/400/500),不包括 preset 自己的目录。rank 300 那一格默认是空的。
解药:在组装里显式把 preset 自己的 skills/ 配进 customSkillDirs。官方自带的一个 preset 就是这么做的,用一个 JS 表达式从 preset 自身的 base URL 推出绝对路径:
- id: skill-filesystem
name: '<技能文件系统提供方>'
config:
customSkillDirs:
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"
2
3
4
5
好处是路径跟着 preset 走——复制或搬移 preset 时技能一起走,不用改配置。
顺带一条判断分界:本机通用知识(存储布局、服务清单)放 <dsh 家目录>/skills,所有组装都看得到;只属于某个 preset 的知识才放 preset 自己的 skills/。
# 坑二:无头模式验不了 preset
症状:用一次性任务模式(headless)做验收,模型用中文回答了、工作目录也对,于是判断「人设生效了」。
原因:preset 系统不在核心 bundle 里。它由 Web 那一层的 bundle 挂载,headless bundle 没有它。所以无头模式下压根没有 preset 机制,跑的是部署级人设。
那次「验收通过」是假的:中文回答和贴输出全部来自 AGENTS.md(那条通道在两种模式下都在),preset 从头到尾没参与。这个错误结论挂了整整一轮,直到去翻会话日志才被推翻。
解药:先确认你的运行模式到底挂没挂 preset:
dsh --profile <profile 名> --dump-config | grep -n "agent-presets"
没有输出,就说明这个模式没有 preset 机制,别在这儿验人设。
看有输出的那一侧更能说明问题:agent-presets 出现在 Web 应用那个 bundle 的段落下,而不是核心 bundle 的段落下。也就是说 preset 系统是上层 bundle 带进来的能力,不是所有模式共有的地基——这比翻日志更直接,一条命令就能定性。
日志则给出事后可查的证据(见下一节):会话头会写着这一轮用的是哪个 preset,没有就是没有。
# 坑三:回答看起来对,但 skill 根本没被加载
症状:问一个存储相关的问题,模型给出的分析完全正确——该排除的挂载点排除了,结论也对。看起来 skill 生效了。
原因:翻日志发现整个会话只有一次工具调用,是 bash: df -h,技能加载工具一次都没调。那份正确答案是模型自己现推的,正确纯属运气。同一个问题换个措辞,它就会漏掉该排除的项。
解药:直接查会话日志的工具调用记录,不要从回答质量反推(见下一节)。
根因是 description 写法。第一版把触发条件放在了句子末尾:
本机的真实存储布局——各挂载点分别是什么……在回答任何关于磁盘的问题之前先加载。
模型只看这一句决定加载与否,而它读到的前半句像是一份「资料介绍」。改成把触发条件提到句首之后就正常了:
回答任何涉及磁盘、存储、空间占用、剩余容量、清理、扩容的问题之前,必须先加载本技能。它给出……
规则:description 要写成「什么时候该用我」,不是「我是什么」。
# 5. 会话日志是唯一可信的验收依据
上面三个坑里有两个是靠日志发现的,值得单独说。
dsh 把每个会话的逐条事件写成 zstd 压缩的 JSONL,按工作目录分桶存放:
<dsh 家目录>/sessions/<工作目录转义后的名字>/session-<id>/session.jsonl.zstd
第一行就是会话头,直接写着用的哪个 preset:
zstd -dc <会话目录>/session.jsonl.zstd | head -1
{"type":"session","id":"session-…","cwd":"…/workspace","delegationDepth":0,"agentPreset":"<preset 名>"}
agentPreset 为空就是没走 preset——坑二就是这么坐实的。把机器上积累的 11 个会话头一次性过一遍,结论没有例外:只有 3 个带 agentPreset 值,全部是 Web 会话;其余 8 个无头会话这个键根本不存在(用 Python 的 .get() 读出来是 None,不是空字符串)。
顺带一条读日志的注意事项:判定要用「键存不存在」,不要用「值等不等于空」——两者在无头会话上表现不同,后者会写出误判的脚本。
统计工具调用同样简单,这是判断 skill 到底有没有被加载的唯一硬证据:
zstd -dc <会话目录>/session.jsonl.zstd | python3 -c "
import sys, json
for line in sys.stdin:
try: e = json.loads(line)
except: continue
if e.get('type') == 'tool/call':
d = e.get('data', {})
print(d.get('name'), '|', json.dumps(d.get('arguments'), ensure_ascii=False)[:120])
"
2
3
4
5
6
7
8
9
预期输出(坑三那次的真实结果,只有一次 bash 调用,没有技能调用):
bash | {"command":"df -h","description":"查看所有挂载点的磁盘使用情况"}
还有一条相关事实:dsh 没有跨会话记忆系统。插件清单里没有任何 memory / knowledge / embedding 组件,连会话搜索索引都配成内存态、不落盘。它把「记忆」外包给文件——也就是 AGENTS.md 加上 skills。这是设计取向而非缺陷:知识必须是你能 cat、能进版本库、能 diff 的东西。代价是换个工作目录就什么都记不住。
正因为如此,这两样东西怎么写、写在哪,就不是细节而是这套系统的全部长期状态。
# 6. 可复用要点
- 人设放身份,纪律放工作区指令文件。 人设只占提示词里一小段且会被后续段落稀释;
AGENTS.md走独立的持久基线通道。同一句话换个位置,服从度就不一样。 description写触发条件,不写内容简介。 模型只靠这一句决定加不加载,触发条件必须在句首。这是 skill 生不生效的开关。- 验收看日志,不看回答。 回答正确不等于机制生效——模型完全可能靠现推给出对的答案。查会话日志里的工具调用记录,那是唯一硬证据。
- 先确认你的运行模式挂了哪些插件,再去验它。 不同 bundle 的插件集不同,在没有某机制的模式下验那个机制,只会得到假阳性。
--dump-config一条命令就能排除这类误判。
最后一条元规则:这四条里有三条,是在一个已经「验收通过」的结论被推翻之后才得到的。给 Agent 做调优时,把验证手段本身也当成需要验证的对象——否则你调的是自己的错觉。
# 7. 延伸阅读,以及一条查文档的实操建议
上游有公开文档站,中文版就是默认站点:DeepSeek Harness 参考手册 (opens new window)(系统提示词、skills、Agent 生命周期、工具执行流水线等子系统都在这一层下面)。项目仓库在 deepseek-ai/deepseek-harness (opens new window),MIT 许可。
但要提醒一句,这也是本文写作过程中真实踩到的:文档站给的是全局架构,插件级的配置字段它不一定收。本文用到的人设三字段(text / complete / includeRuntimeContext)和 skill 的六级扫描 rank 表,在站上对应的子系统页里都查不到——它们来自本地安装目录下每个插件自带的 README.zh.md。那些 README 中英双语、按插件分文件,比站上详细,排障时也比翻网站快。
所以顺序建议反过来:先读本地插件 README,再去文档站补全局图。另外该项目仍处于 developer preview,文档路径与配置字段都会随版本变动,读到与本文不符时以你机器上的实机输出为准。
下一篇:三个 Harness 的解剖:同一个文件名,三种加载语义——把 Claude Code、Hermes Agent 与 dsh 的骨架并排拆开,看工作区、记忆、skill 与「模式」在三家分别是什么。
🤖 Agent 可直接解析的元数据块(点击展开)
{
"_meta": {
"doc_version": "2026-09-08",
"article_id": "dsh-01-prompt-layers-and-skills",
"profile_context": "any",
"estimated_setup_time": "20min"
},
"quick_start": {
"step_1": "dsh --profile <profile> --dump-config | grep -n 'agent-presets'",
"step_2": "mkdir -p \"${DSH_HOME:-$HOME/.dsh}/skills/<skill-name>\"",
"step_3": "写 SKILL.md,frontmatter 必须有 name(kebab-case)与 description(<=500 字符,触发条件放句首)",
"step_4": "把工作纪律写进 \"${DSH_HOME:-$HOME/.dsh}/AGENTS.md\",不要写进 persona"
},
"safety_rules": [
"不要设置 persona 的 complete: true —— 会让工具引导等所有其他提示词段落消失",
"不要修改随包发行的 preset 目录 —— 升级会覆盖;要改就复制一份到用户 preset 根目录",
"改默认 preset 只对此后新建的会话生效,运行中的会话不受影响",
"developer preview 版本,升级前先备份 dsh 家目录(排除 node_modules 与日志文件)"
],
"verification": {
"check_preset_available": "dsh --profile <profile> --dump-config | grep -c 'agent-presets' # 0 表示该模式没有 preset 机制",
"check_skill_in_catalog": "向 agent 提问:不要跑任何命令,只列出你现在能用的所有 skill 名字",
"check_skill_actually_loaded": "zstd -dc <会话目录>/session.jsonl.zstd | grep '\"type\":\"tool/call\"' # 看有没有技能加载工具的调用",
"check_which_preset_ran": "zstd -dc <会话目录>/session.jsonl.zstd | head -1 # 读会话头的 agentPreset 字段"
}
}
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 使用指南:
- 当用户说「给 dsh 加一条工作纪律」→ 写入
AGENTS.md,不要写进 persona;然后跑一个真回合,用verification.check_which_preset_ran确认这一轮的组装符合预期,再看模型有没有照做 - 当用户说「技能没生效」→ 依次跑
check_skill_in_catalog(在不在目录里)和check_skill_actually_loaded(有没有被调用);两者是不同故障,前者查 frontmatter 与扫描根,后者查 description 写法 - 当用户说「人设不起作用」→ 先跑
check_preset_available与check_which_preset_ran,确认当前模式真的挂了 preset,再去改人设内容