← 返回文章列表

如何为自己的工程项目设计并实现 DSH 预设

目标读者:已经用 dsh 或同类 CLI agent 做过真实项目,但每次开新会话都要重新解释一遍项目约定的人。
前置知识:知道 agent preset 大概是“给某个会话换一套工具和提示词”,不需要读过 dsh 源码。
示例来源:本文机制部分以一个真实开源预设为例,机器相关路径已全部占位符化。
本文核查时间:2026-09-13。

一、先问一个问题:AGENTS.md 为什么不够

大多数人给项目配 agent 的第一步,是在仓库根目录放一个 AGENTS.md(或者 CLAUDE.md)。这当然是必要的,但它有三个绕不过去的失效点。

第一,环境事实只能靠“读”。 假设你的项目横跨两套环境:计算和数据脚本在 Linux 侧,某个商业仿真软件只能装在 Windows 侧。你在 AGENTS.md 里写清楚了这件事,agent 也读到了。然后它要跑一个批处理脚本——它仍然可能在错误的一侧执行,因为“读到”和“被约束”是两件事。

第二,纪律只是建议。 你写了“参数必须来自文献,不许编造”。这句话对模型是个提醒,不是一道门。工具还是那些工具,该写的时候它照样能写。这类约定在压力下(用户催得急、上下文很长)最先被丢掉。

第三,也是最少被注意的一点:常驻成本。 AGENTS.md 要么每轮都在上下文里占位置,要么每次重新读一遍。项目约定越细致,这份成本越高,而其中大部分内容——比如“批量跑工况的七个步骤”——在大多数轮次里根本用不上。

这三点的共同解法,是把约定从“文件里的文字”升级成“会话装配的一部分”。dsh 里承载这件事的东西叫 agent preset(预设)

需要先破除一个误解:预设不是一份更长的提示词。 一个预设是一个目录,里面是一个 composition 文件,它会决定这个会话装配哪些插件——也就是有哪些工具、有哪些 prompt 段、能加载哪些技能。把约定写进 persona 只是其中一种手段,而且往往不是最有效的那种。

二、判断基准:host 平面与 agent 平面

在动手写预设之前,必须先建立一个判断基准,否则后面每一个决定都会靠感觉。dsh 的运行时分成两个平面:

host 平面持有:

  • 注册表本身——toolssystemPromptagentssessions
  • 跨越会话的东西——持久化、会话查询、存储、设置、凭据
  • 沙箱与审批栈、模型路由
  • 子代理注册表及其 spawn / fork 后端

agent 预设持有的是一个会话向这些注册表贡献的东西:

  • 它自己的工具插件
  • 它的 persona 与 prompt 段
  • 它的 compaction 策略

判定规则只有一条,但它足够锐利:

有 agent 平面之外的消费者,就必须留在 host 平面。

拿子代理注册表来检验这条规则。它看起来非常“和 agent 相关”,所以很自然会想把它塞进预设里。但事实是:host 侧的 api-proxy 要跨会话查询它(比如网页端要列出某个会话起了哪些子代理)。一旦把它搬进预设,会同时坏掉两件事——host 那一行会永远等一个没人提供的服务;而第二个会话挂载同一个预设时会在注册时撞名,因为一个 provider 名字只能注册一次。

所以正确的分工是:预设贡献子代理的“工具”,注册表和它的后端留在 host。

这个区分带来一个很实用的推论:当你写 agent.cordis.yml 时,你在写的不是“配置”,而是“这个会话装配哪些插件”。注册表里的东西不是你的,你只是在往里面加行。

三、素材提取:从一个真实工作区说起

讲了原则,来看一个真实的例子。假设你有一个科研仿真工作区,形状大致是这样:

<PROJECT_NAME>/
├── <MICRO_SIM>/        # 微观仿真:分子动力学,颗粒碰撞与团聚
├── <MACRO_SIM>/        # 宏观仿真:离散相模型,气固两相流沉积
│   ├── udf/            # 边界条件源码
│   ├── profile/        # 由微观结果生成的参数表
│   ├── scripts/        # 构建 / 批跑 / 聚合脚本
│   └── runs/           # 各类工况目录
├── <REPORT_DATA>/      # 过程汇报(LaTeX)
├── <PAPER>/            # 最终论文(LaTeX)
├── <VENV>/             # Python 虚拟环境
├── task_plan.md        # 当前阶段计划
├── findings.md         # 本轮发现
└── progress.md         # 跨会话进度日志

这类工作区有几个稳定的特征:环境分裂(计算在 Linux 侧、商业软件在 Windows 侧)、参数有严格的文献来源要求、报告有固定结构、而且它已经有一批约定载体了——AGENTS.mdCLAUDE.md、几个项目技能、以及那三个 .md 续接文件。

问题不是缺约定,而是这些约定散落在不同介质里,各自有各自的生效条件。所以第一步不是写代码,而是把已有约定归位

这张表是全文最有复用价值的部分,值得直接照搬到自己项目上:

项目里的东西性质落到预设的哪里理由
研究对象、领域术语、物理模型事实背景persona每轮都需要,必须常驻
哪台机器跑什么、路径怎么对应环境事实persona(工具路由约定)同上
参数不许编造、编译必须通过纪律persona + 硬门控需要“拒绝”的部分下沉到工具行
批量跑工况的操作步骤流程知识skills按需加载,不常驻
能读不能写、能跑不能装能力边界agent.cordis.yml 工具行只有工具行能真正约束
开工先读什么、收工回写什么续接约定persona属于行为习惯

从这张表里能抽出两条设计原则。它们基本能解释后面所有的取舍:

  1. 能落到工具行或 skill 的,不要写进 persona。 persona 是每轮都付的 token 成本,skill 是按需加载的。
  2. 需要“拒绝”的用工具行,只需要“提醒”的用 persona。 软引导会被无视,硬门控不会。

先把这两条记住,再往下看会更顺。

四、最小可运行预设

预设放在这里:

${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/

有两个容易踩的规则,先说清楚:

  • 只扫一级子目录。 预设目录不能嵌套,<id> 直接就是那一层目录名。
  • <id> 必须匹配 [a-z0-9][a-z0-9-]* 因为它会变成目录名,所以不能以连字符开头。

一个预设目录里最少要有两个文件。

preset.yml:给人看的元数据

name: 科研计算预设
description: "面向粉体/多相流仿真的科研 agent:中文 persona、双环境工具路由、科研写作纪律与跨会话续接。"
order: 0

这个文件的作用只有一个——在预设选单里显示名字和描述。不写它,你的预设会以裸目录名出现在列表里。 order 只对随部署发布的预设有意义,自己写的预设可以不填。

agent.cordis.yml:一组插件行

这是真正起作用的部分。一个能跑的最小版本大概长这样:

# persona:这个会话的身份与约定
- id: persona
  name: '@deepseek-ai/dsh-persona'
  config:
    prefix: |-
      你是粉体/多相流仿真科研助手,服务于 <PROJECT_NAME> 项目。
      交流一律使用简体中文;公式与技术名词保持英文。
      当前模型为 {{model}},工作目录为 {{cwd}}。

# 工具:文件读写与检索
- id: tool-fs
  name: '@deepseek-ai/dsh-tool-fs'

- id: tool-fs-search
  name: '@deepseek-ai/dsh-tool-fs-search'

# 技能:按需加载的流程知识
- id: skill-filesystem
  name: '@deepseek-ai/dsh-skill-filesystem'

- id: tool-skill
  name: '@deepseek-ai/dsh-tool-skill'

注意 {{model}}{{cwd}} —— 这两个由 dsh 在运行时解析,保持原样,不要替换

生效条件

预设的挂载时机有两个特点,都值得记住:

  • 只有“还没产出任何内容”的会话能切换预设。 一旦会话有了消息或工具调用,composition 就固定了——因为中途换工具会让历史里出现新装配无法执行的工具调用。所以改完预设,要新开一个会话才看得到效果。
  • 一个预设的改动会不会产生新一代,只看 composition 文件本身。 系统记录的是 agent.cordis.yml 的时间戳(mtime)与大小;文件一动,下一个新会话就会挂载新一代。而已经在跑的会话会一直留在它当初加入的那一代。

第二条有个重要后果,第九章会专门讲。

五、写 persona:把约定写成约束

persona 是最容易写坏的地方。典型症状是把它写成项目 README 的副本——洋洋洒洒几千字,其中大部分内容在大多数轮次里都用不到。

一个经过验证的结构是按下面的顺序组织,这和开源示例里的真实顺序一致:

  1. 身份
  2. 语言约定
  3. 领域背景
  4. 工具路由
  5. 写作纪律
  6. 续接工作流

关键在于每一段都要可执行。看一个对照:

反例(说了等于没说):

注意 Fluent 装在 Windows 上,脚本要在 Linux 上跑。

正例(给出事实、路径对应、以及理由):

工具路由约定:
- 分子动力学 / Python 数据分析 / LaTeX 编译在 Linux 侧(<WSL_DISTRO>):
  经 git bash 执行 `wsl -d <WSL_DISTRO> -- bash -lc '...'`,
  涉及 Python 时先激活 <WSL_PROJECT_ROOT>/<VENV> 再运行。
- 商业仿真软件在 Windows 侧(<ANSYS_INSTALL>):批跑走 run_batch.ps1。
- 路径对应:原生 <WSL_PROJECT_ROOT> ↔ UNC \\wsl.localhost\<WSL_DISTRO>\...
  ↔ 符号链接 <WIN_PROJECT_LINK>。重 IO(dump、.trn、批量小文件)
  必须在原生目录内跑;9P 路径(UNC / 符号链接)只做轻量读写。

差别不只是“更详细”。正例做了三件反例没做的事:

  1. 给出了三侧路径的对应关系,agent 才能在任意一侧拿到路径后自己换算。
  2. 给了理由(“重 IO 必须留在原生目录”)。这一点常被忽略,但很重要——跨文件系统访问的性能损失是数量级的,agent 不知道原因,就无法在边界情况下做判断。比如它要决定“这个几百 MB 的结果文件该写哪儿”时,有理由可依。
  3. 明确了降级路径(“9P 路径只做轻量读写”),而不是一刀切禁止。一刀切的规则在遇到“确实需要从 Windows 侧读一个小文件”时会逼 agent 绕路。

同理,写作纪律也要写成可检查的形式,而不是形容词:

科研写作纪律:
- 报告结构:时间线 / 问题发现 / 参数空间 / 敏感性分析 / 物理机制 /
  工程意义 / 后续计划。
- 图表遵循项目 plot_style.py 统一出版风格。
- 不写无依据的机理解释;数值改动需说明来源。

最后一段续接工作流,本质是把“行为习惯”编码进去:

跨会话续接:
- 开工前先读 task_plan.md / findings.md / progress.md,
  确认当前阶段与未决项后再行动;完成后按项目约定回写。

这条的价值在长周期项目里会持续放大。新会话不必从零重建上下文,而是先读三个文件——它们本身是跨会话的长期记忆。

六、能力边界用工具行表达

persona 解决“知道该怎么做”,工具行解决“能做什么”。后者是硬的。

平台条件

dsh 的 composition 支持用 JS 表达式做条件行:

- id: tool-bash
  name: '@deepseek-ai/dsh-tool-bash'
  disabled: !!js process.platform === 'win32'

- id: tool-pwsh
  name: '@deepseek-ai/dsh-tool-pwsh'
  disabled: !!js process.platform !== 'win32'

这两行是互斥的:Windows 上只装配 PowerShell,其他平台只装配 bash。忘了写平台条件的后果是:工具在错误的平台上被注册,然后在运行时报错——而不是在装配时被挡下来。

isolate realm:服务行必须放进隔离域

如果你要给预设挂一个服务(而不是一个工具),有一条硬规则:

服务行必须放进一个带 isolate 的 group,否则它会发布到 root realm。

root realm 是进程级的。后果是第二个挂载同一预设的会话会直接撞名失败。isolate 的取值为 true 时,表示“每个会话一个私有实例”,这是绝大多数情况的正确选择;只有当这个服务确实要跨会话共享时,才用共享标签把它们池化。

一个真实的例子:Windows 上要真正的 Git Bash

dsh 在 Windows 上的默认 shell 是 PowerShell。如果你的项目脚本全是 bash(很常见),就需要一个私有的 shell 服务:

- id: gitbash-shell
  name: cordis:group
  group: true
  isolate:
    shell: true
  config:
    - id: gitbash-executor
      name: ./gitbash-executor.mjs?v=50
      disabled: !!js process.platform !== 'win32'
      config: {}

    - id: tool-bash
      name: '@deepseek-ai/dsh-tool-bash'
      disabled: !!js process.platform !== 'win32'

这个 group 干的事是:提供一个 entry-local 的私有 shell 服务(它用 Git Bash 执行命令),然后让同组的 tool-bash 去消费它。于是 Windows 会话里的 bash 就真的是 bash。

这里必须说清楚它的边界,因为这是个诚实性问题。 它不绕过沙箱。MSYS 在受限令牌下可能启动失败,此时正确的做法是按正常流程走一次审批升级,而不是去篡改沙箱策略。把这个边界写进注释,比假装它没有更专业——因为下一个维护者会需要知道这一点。

七、让流程渐进披露:四阶段硬门控

前面几节是大部分预设都会用到的部分。这一节讲的是更进阶的机制,也是一个真实科研预设里最有技术含量的原创贡献。

动机

把全部工具一次性交给模型,有两个问题:

  1. 注意力税。 工具清单本身占上下文。一个几十个工具的面板,意味着每一轮都要为它们付 token,而其中大部分在当轮用不到。
  2. 模型会“大跃进”。 如果 write 从一开始就可见,模型倾向于跳过理解直接开写。这不是模型不听话,而是你给了它这条路

四个阶段

解法是给会话划分阶段,每个阶段只解锁一部分工具:

阶段名称解锁的工具
0了解/对齐read glob grep web_search ask_user_question
1拟合方案todo_write exit_plan_mode
2开发write edit str_replace_editor
3验证bash pwsh read_image job_list job_output job_kill

这个划分的逻辑是:先能看,再能想,然后能改,最后能跑。阶段的推进由完成信号驱动——比如阶段 1 在 todo_writeexit_plan_mode 被调用后自动进入阶段 2。这一点是有意设计的:如果靠模型自己调推进工具,它很可能会跳阶段。

硬门控是怎么实现的

这是最容易出 bug 的地方。核心调用是:

toolsSvc.restrict({ allow })

它有三个必须理解的语义。

第一,restrict 是交集语义,不是替换。

如果你设了一次 restrict,又设第二次,两次的交集才是结果。这意味着必须显式释放上一次的 restrict,否则工具面会越缩越小,最后卡死。做法是每个会话记住自己的 disposer,设新的之前先释放旧的:

const prev = sharedLift.get(sid)
if (prev) { try { prev() } catch {} ; sharedLift.delete(sid) }
// ... 然后再 restrict
const disposer = toolsSvc.restrict({ allow })
if (sid && disposer) sharedLift.set(sid, disposer)

第二,disposer 必须跨 generation 共享。

如果预设改了 composition 文件产生新一代,新一代的插件实例是新的。它必须能提起旧一代设下的 restrict,否则就继承了旧限制却无权释放——同样是越缩越小。做法是用 globalThis 上一个 Symbol.for(...) 命名的 Map 作为跨代的共享点。

第三,平台缺失的工具名会让整个 restrict 调用抛错。

假设你的 allow 列表里写了一个当前平台不存在的工具名(比如 Windows 上没有 bash),restrict 会直接抛异常。后果不是“那一个工具没解锁”,而是“整个阶段门控失效、退化成全量工具面”——一个静默的降级,很难发现。

所以正确的顺序是三重过滤:

const allow = [...allowed].filter(
  (t) => GLOBAL_SAFE.includes(t)            // 1. 本预设声明的工具全集
      && (known === null || known.has(t))    // 2. 当前运行时真实可限制的名字
      && !(memoryMuted(agent.session) && isMemoryTool(t))  // 3. 其它开关
)
if (allow.length === 0) return

并且要把 restrict 包在 try/catch 里,失败时保留全量目录而不是让会话瘫痪——同时打日志,别让它静默。

只暴露当前档

一个容易被忽略的细节:解锁窗口应该只包含当前阶段,而不是“当前 + 预放几档”。

export function windowFor(stage) { return Math.min(stage + 1, STAGES.length) }

为什么不做预放?因为一旦模型知道后面还有工具,它就会惦记。实测的后果是:模型会试图“提前”完成当前阶段以解锁下一档,产生大跃进,反而破坏了这个机制本来要保证的东西。只暴露当前档,模型就只能在当前阶段内把事情做好。

二级披露:拒绝调用,但允许查阅

硬门控带来一个副作用:模型想知道“有没有某个工具”都做不到。这会让它在需要时无从判断该向用户请求什么。解法是给两个元工具常驻:

  • tools_catalog:列出全部工具,但标注未解锁的状态
  • tools_help:查单个工具的说明

关键设计是:这两个工具可以查阅未解锁的工具,但不能调用它们。 拒绝的信息还要写清楚归属:

解锁阶段: 3(当前调用会被拒绝;详情可提前查阅。
若你判断它应属于当前阶段,请指出工具分配问题(调整 STAGES),
而不是寻求绕过)

最后这半句是有意写的。它在引导模型:遇到门控时的正确反应是报告设计问题,而不是想办法绕过。 这一句本身就是一条防绕过的护栏。

常驻指引

阶段的“目标说明”要写进 prompt 段,而不是只在阶段切换时说一次。原因很实际:上下文压缩随时可能把早先的说明丢掉,而常驻的 prompt 段经压缩不丢。

设计哲学

把这套机制说成“限制模型”是不准确的。它真正做的是把“该想清楚再做”从一个建议变成一种结构:阶段 0 只能读,于是模型不得不先把问题看清楚;阶段 2 才有写权限,于是“想清楚”这件事在物理上先于“动手”。这比在 persona 里写“请先充分理解需求”有效得多。

八、验证:怎么知道预设真的装配了

写完预设,最需要避免的就是“看起来能用”。

观察当前状态

会话内可以直接查路由状态:当前处在哪个阶段、哪些工具可调、presentation 模式是什么。这是最快的自检手段。

源码级回归自测

一个很实用的做法是写一个独立的 selftest 脚本,对插件源码做字符串断言。它不启动运行时,只读文件:

const src = readFileSync(join(here, 'router-bootstrap-v34.mjs'), 'utf8')
const cfg = readFileSync(join(here, 'agent.cordis.yml'), 'utf8')
const fails = []
const check = (name, ok) => { if (!ok) fails.push(name) }

// 引导文案必须完整拼接
check('guide-unlock-order', tail.includes("+ 'Unlock order:'"))

// restrict 的 per-session disposer 必须在场
check('restrictLift-declared', src.includes('const restrictLift = new Map()'))

// 失败不能静默
check('restrict-not-silent',
  src.includes("console.error('[router-bootstrap] applyStageRestrict failed:'"))

// 配置必须指向新一代(?v= 已递增)
check('config-v24-or-newer', /router-bootstrap-v34\.mjs\?v=(2[4-9]|[3-9]\d+)/.test(cfg))

这种自测的价值和边界都要说清楚。 它守的是“机制不被误删”,而不是“功能正确”。它挡不住逻辑错误,但能挡住一次重构中不小心删掉 disposer、或者忘记递增 ?v= 这类高发事故——而这些恰恰是最难在生产中发现、又最容易在编辑中引入的问题。

真实挂载才是最终判据

字符串断言全过,也不代表预设能挂载。真正的判据是新开一个会话,看它是否正常装配。失败时的排查顺序是固定的三步:

  1. composition 能不能解析?—— YAML 语法、行是否是合法的 {id, name, config} 结构
  2. 行指定的模块能不能加载?—— 路径对不对、?v= 指向的文件在不在
  3. 有没有哪一行在等一个 composition 从没提供的服务?—— 这是最难查的一类,通常意味着你把一个 host 平面的服务误放进了预设

第 3 类错误会在会话创建时失败并回滚,报错会列出所有失败的行(包括 group 内部的),所以看到报错时重点看它点名了哪些行。

九、迭代循环,和一个必踩的坑

日常改动分两类,生效方式不同。

改 persona 或工具行 —— 直接编辑 agent.cordis.yml,新会话生效。因为 generation 的指纹就是这个文件的时间戳与大小。

改插件 .mjs —— 必须递增 ?v=N

- id: router-bootstrap
  name: ./router-bootstrap-v34.mjs?v=91   # 改完代码把它改成 92

这里是全文最值得记住的一个坑。原因在于:

generation 只以 composition 文件的指纹为准。旁挂的 .mjs 改了,agent.cordis.yml 的 mtime 没变,指纹就不变,于是新会话仍挂在原来那一代上,ESM 模块缓存继续返回旧代码。

症状是“我明明改了,怎么没生效”,而且不报任何错。递增 ?v=N 让 composition 文件的文本变了,指纹变化触发新一代,新 URL 也没有缓存命中,改动立刻生效。

这个机制还有一条容易忽略的性质:已运行的会话会保持旧代。 这既是优点(正在跑的长任务不会因为你的编辑而中途变样),也是排查时的陷阱(你在当前会话里永远验证不了刚改的插件代码)。

改动前的备份

预设目录在用户主目录下,不在项目仓库里,不受项目 git 保护。建议改动前整体复制一份带日期后缀的副本。这比“记得改了什么”可靠得多。

十、脱敏与开源:把“我的项目”变成“任何同类项目”

预设一旦跑顺,很自然会想开源出去。但直接从本地目录复制粘贴是不行的——它塞满了你的机器事实。这一步需要分三层处理,而且这三层不能混为一谈。

第一层:机器事实 → 占位符

占位符含义示例
<PROJECT_NAME>项目名MyThesis
<WSL_DISTRO>发行版名Ubuntu
<WSL_PROJECT_ROOT>Linux 侧项目根(绝对路径)/home/you/MyThesis
<WIN_PROJECT_LINK>Windows 侧符号链接路径C:\MyThesis
<ANSYS_INSTALL>商业软件安装路径D:\ANSYS2024R2

务必注意:{{model}}{{cwd}} 不在这个表里——它们由 dsh 运行时解析,替换掉就等于破坏了功能。

上表五项是 persona 真正依赖的环境事实。第三节那张目录树里出现的 <MICRO_SIM><VENV> 等是子目录名的占位,属于叙述方便,不必进 persona——判断标准是:agent 是否需要知道它才能正确行动。它需要在哪个环境跑什么命令,这是必须的;某个子目录叫什么,读了就知道,不必常驻。

第二层:研究对象 → 通用化

这一层是语义脱敏,不是字符串替换。具体的研究对象名、未发表的数值、结论性表述都要泛化成机制描述。比如把具体的物料名称写成“粉体颗粒”,把具体的数值结果去掉,保留“存在临界速度幂律关系”这样的机制表述。

第三层:个人标识 → 移除

用户名、家目录绝对路径、内网 git 地址、私有软件安装根目录。这一层最简单,但也最容易漏——因为它们常常藏在脚本注释和示例命令里,而不在显眼位置。

为什么不能只做字符串替换? 因为把路径变量收敛成占位符的过程,会强迫你回答一个问题:这个预设到底依赖哪几个环境事实? 你可能会发现某个“环境依赖”其实是历史遗留、根本不必写进 persona;也可能发现两个占位符其实是同一个东西。这一步本身就是设计工作,不是收尾工作。

版权与归属

如果预设构建在别人的工作之上(很常见——你多半是从某个上游 preset 改的),就有一个 NOTICE 文件要写清楚。参考资料里给了一个真实例子,它的结构值得照搬:

  1. 逐项列出上游来源:项目地址、版权人、许可证
  2. 明确列出哪些文件是派生的——不要含糊地说“部分代码来自上游”
  3. 明确划分原创部分与派生部分
  4. 商标与背书免责:说明这是一个社区产物,未获官方背书

第 3 点最容易做错。一个 composition 文件往往同时包含派生的插件行和原创的 persona——两部分的归属不同。把它整体算作“原创”是不准确的,整体算作“派生”又抹掉了你的工作。老老实实写清楚“该文件的哪些内容是原创”,是既准确又体面的做法。

文档要跟着改

开源前值得通读一遍 README,把这几个高发问题检查掉:

  • 克隆命令里的仓库名和实际仓库名是否一致
  • 引用的辅助脚本、模板文件是否真实存在
  • 前置依赖是否指向真实存在的包
  • README 的归属声明是否和 NOTICE 一致

这几条听起来琐碎,但它们是读者对你的项目的第一印象,而且最容易在改名、重构后失效

十一、常见坑清单

按“症状 → 原因 → 处理”整理,可以直接当排查表用:

症状原因处理
预设不出现在列表里目录嵌套了预设只扫一级子目录,<id> 就是那一层的目录名
列表里显示成裸目录名没写 preset.yml补上 namedescription
改了 .mjs 但没生效忘了递增 ?v=N递增版本号,新开会话验证
改了 persona 但没生效在已有会话里验证只有空会话能切换预设,必须新开会话
第二个会话挂载失败服务行没进 isolate realm把服务行放进带 isolate 的 group
设置完 restrict 后工具越来越少restrict 是交集语义记住 disposer,设新的前先释放旧的
阶段门控毫无效果allow 里有平台不存在的工具名,整个调用抛错降级三重过滤 + try/catch + 失败时打日志
约束被模型无视用 persona 做了本该工具行做的硬门控需要“拒绝”的改用工具行
persona 越写越长该下沉到 skill 的内容没下沉流程类知识移到 skill,按需加载
升级 dsh 后预设改动丢失直接改了随部署发布的预设只改用户目录下的副本,不要动随部署发布的
换了机器预设就废路径硬编码占位符化,明确列出环境依赖
工具在错误平台被注册忘了 disabled 平台条件!!js process.platform 做互斥

十二、回到最初那个问题

开头那三个失效点,现在应该都有对应的解法了:

  • 环境事实靠“读” → 写进 persona 的工具路由约定,给出三侧路径对应和理由
  • 纪律只是建议 → 需要拒绝的部分下沉到工具行,用 restrict 做硬门控
  • 常驻成本 → 流程知识移到 skill,用阶段的渐进披露控制工具面

如果只记一件事,那应该是第二章那条判定规则:有 agent 平面之外的消费者,就必须留在 host 平面。 它决定了你写的每一行到底该放在哪,也是“预设能跑”和“预设一挂第二个会话就崩”之间的分界线。

预设真正值钱的地方,不在于它写得多详细,而在于它把哪些东西从“文字”变成了“结构”。一份写在 AGENTS.md 里、需要模型自觉遵守的约定,和一个写进工具行、模型绕不过去的约束,效力完全不同。前者是请求,后者才是工程。

参考资料

✓ 文章链接已复制到剪贴板