1. DSH 到底是什么,Skill 又在补什么
很多人第一次接触 DeepSeek Harness(下面统一叫 DSH)的时候,都会有个错觉:既然模型本身已经能读懂代码、能写脚本、能解释报错,那还要这层东西做什么?我一开始也这么想,直到真的把一个稍微复杂的活交给它,才发现问题不在于“它会不会想”,而在于“它想完能不能落地”。DSH 的定位说白了就是给模型配上一双手和一套工具箱,而 Skill 就是这套工具箱里的每一件工具。
你可以把 DSH 理解成一个“编排层”:它负责把用户的意图拆成若干步,把每一步该调什么工具、传什么参数、拿到结果之后怎么接着往下走,这一整套流程管起来。而模型在这个过程里更像是一个现场指挥,负责判断和决策。这两者分工明确,缺一不可。模型再聪明,如果没法读你磁盘上的文件、没法执行一条命令看真实输出、没法记住上一轮聊到哪儿,那它就永远停留在“给建议”的层面,做不了“给结果”。
所以这篇文章想解决的问题很具体:一个刚把 DSH 跑起来的人,面对着 Skill 列表一脸茫然,不知道先装哪个、后装哪个、装完怎么验证、装错了会怎样。我按自己踩过的顺序,把五个真正“没装就没法用”的核心 Skill 拆开讲清楚,顺带把安装顺序背后的依赖逻辑、参数怎么算、出了问题怎么查,一并交代。适合两类人看:一是刚上手 DSH 想快速跑通一条完整链路的,二是已经装了一堆 Skill 但发现互相打架、想回头理一遍架构的。
1.1 从“会聊天”到“能上手干活”的落差在哪
我先讲一个特别典型的场景。你让模型帮你改一个配置文件里的端口号,它能给你一段改好的内容,甚至能把整段 YAML 贴出来。但接下来呢?它不知道这个文件在哪个目录、不知道你本地是不是已经有一份同名文件、不知道改完之后服务要不要重启、更不知道重启之后有没有起来。这四件事,一件都落不了地,整个任务就卡在“我建议你这样做”这一步。
这中间的落差可以拆成四层,每一层对应一类能力缺口。第一层是感知缺口:模型看不到你的工作区,不知道有哪些文件、内容长什么样。第二层是动作缺口:模型不能真的执行命令,所以它给的命令对不对、跑出来什么结果,全靠你手动验证。第三层是记忆缺口:上下文一长,前面的约定就淡了,同一个项目聊到第三天它可能忘了你定的命名规范。第四层是约束缺口:模型输出的格式经常“差不多但不完全对”,你要的是能直接喂给下一个程序的 JSON,它给你一段带解释文字的代码块。
这四层缺口,恰好对应我后面要讲的五个 Skill:前两个补感知和动作,第三个补记忆,第四个补约束,第五个把前面四个串成流水线。所以“必装 5 个”不是拍脑袋定的数字,而是这四层缺口加上一层编排,正好凑成一套最小可用组合。少装任何一个,你会发现某类任务就是做不顺,而且很难定位到底卡在哪。
我见过不少人一上来装十几个 Skill,结果每个都只跑通一次,之后全是环境冲突和权限报错。这跟装修一个道理:水电没通就先买家具,最后只能堆在客厅里。所以顺序这件事,真的比数量重要得多。
1.2 Skill 的运行机制:它凭什么能插进来就生效
要理解安装顺序,得先明白 Skill 在 DSH 里是怎么被“看见”和“调用”的。虽然不同版本的实现细节有差异,但主流做法大同小异,基本遵循这么一套机制:每个 Skill 是一个独立目录,里面至少有一份描述文件(说明这个 Skill 叫什么、干什么用、需要什么参数)和一份执行入口(真正干活的那段逻辑)。DSH 启动时会扫描指定目录,把描述文件读进来,拼成一份“工具清单”交给模型。
这里有个关键点很多人忽略:模型选择调用哪个 Skill,靠的是描述文件里的自然语言说明,而不是代码本身。也就是说,你写的 description 如果含糊其辞,模型就不知道该在什么时候用它。我见过一个自己写的 Skill,功能是把 Markdown 转成表格,但描述写的是“格式转换工具”,结果模型十次里有八次想不起来调它。后来改成“把 Markdown 表格转换为 CSV 文本,适用于导出数据场景”,命中率立刻不一样了。这件事说明,Skill 的描述文件本身就是提示词工程的一部分。
第二层机制是参数校验。DSH 在把调用请求转给 Skill 之前,一般会按描述文件里的参数定义做一次校验,类型不对、必填项缺失,直接拒掉,不会让错误的参数跑进执行逻辑里。这层设计的意义在于,它把“模型瞎编参数”这种常见问题挡在了外面。但反过来,如果你的参数定义写得太宽松,什么都用字符串,那校验就等于没做。
第三层是结果回灌。Skill 执行完,输出会被塞回对话上下文,模型基于这个输出决定下一步。这里就有个隐形成本:输出越长,占的上下文越多。一个只会返回“成功/失败”的 Skill 和一个返回三千字日志的 Skill,对后续推理的影响完全不是一个量级。这也是我后面会反复强调“输出要做裁剪”的原因。
理解了这三层,你就能明白为什么装 Skill 不是“装上就行”。描述写得准不准、参数定义严不严、输出裁得狠不狠,直接决定了这套东西是帮模型干活还是给模型添乱。
1.3 为什么必须讲究安装顺序
安装顺序的核心逻辑只有一句话:后装的 Skill 会依赖先装的 Skill 提供的能力。这不是框架强制的规定,而是功能上的自然依赖。举个最直白的例子:检索类 Skill 需要先把文件读进来、切块、建索引,而“读文件”这件事本身就是文件工作区 Skill 的活。你如果先装检索,那它在建索引的时候要么自己去实现一套文件读取,要么直接报错。
再比如任务编排 Skill,它的作用是“把一个大任务拆成子任务,分派给不同的执行单元”。那子任务执行单元凭什么去改文件、跑命令?还是得回头调前面装的那几个。所以编排类必须放在最后,它是消费者,不是生产者。
我把这个依赖关系画成一句话的安装链路:文件 → 命令 → 记忆 → 校验 → 编排。前两个是“手脚”,第三个是“笔记本”,第四个是“质检员”,第五个是“工头”。工头当然要最后到位,不然他指挥谁?
还有个更实际的理由:排查成本。你按这个顺序装,每装一个就能单独验证一次,出问题立刻知道是哪个环节。要是五个一起装完再测,报一个错你根本不知道该翻哪个目录的日志。我吃过这个亏,花了两个小时在一个编排 Skill 的配置文件里找问题,最后发现是文件工作区的根目录路径写错了,导致所有下游全部失效。从那以后我就老老实实一个一个装、一个一个验。
2. 装之前把地基打好:环境与目录约定
在动手装任何 Skill 之前,有三件事必须先定下来,否则后面会反复返工。这三件事分别是:运行环境够不够、目录怎么摆、权限边界划在哪儿。听起来像是废话,但我见过太多人跳过这步,装到第三个 Skill 的时候发现路径全乱、权限全开、日志找不到,最后只能推倒重来。
2.1 运行环境与依赖自查清单
DSH 本身对环境的胃口不算大,但它对“确定性”要求挺高。所谓确定性,就是同一个 Skill 在你这台机器上跑出来的结果,换一台机器也得一样。这就要求你把版本号都钉死,别用“最新版”这种模糊说法。
我的做法是先跑一遍自查,把结果记在一个env-report.md里,后面出问题直接对照。检查项大概是这些:运行时版本(比如 Python 3.11 或 Node 20,看你的 DSH 发行版要求)、包管理器版本、磁盘剩余空间(建索引会吃空间,建议留 5GB 以上)、可用内存(并发跑子任务时会翻倍)、以及是否有可用的本地端口段(有些 Skill 会起本地服务)。
磁盘这块我得多说一句。检索类 Skill 建索引的体积,大概是原始文本的 1.2 到 2 倍,取决于你存不存向量。一个 500MB 的代码仓库,索引做完可能占 800MB 到 1GB。如果你手上是几十个仓库一起挂进去,那磁盘消耗会很可观。我的习惯是先估算:索引体积 ≈ 原始文本体积 × 1.5,然后留一倍余量。这个系数不是精确值,但用来判断“够不够”很实用。
另外提醒一点,如果你打算在容器里跑,注意容器内的路径和宿主机的路径映射关系。文件工作区 Skill 的根目录如果指向容器内路径,而你的文件在宿主机上,那模型看到的就是一个空目录。这个问题我第一次遇到时排查了很久,因为日志里一切正常,只是“读不到文件”。后来养成习惯,装完第一个 Skill 第一件事就是让它列一下根目录的内容,确认看到的是对的东西。
2.2 目录结构与配置文件的最小约定
目录结构这事,我的建议是别用默认值。默认值通常是为了让所有人快速跑起来,所以它把所有东西都放在一个扁平目录里。等你装到第五个 Skill,那个目录里会有几十个文件夹,改配置全靠猜。
我用的结构大概是这样:
dsh-home/ ├── skills/ # 所有 Skill 的安装目录 │ ├── fs-workspace/ │ ├── shell-runner/ │ ├── memory-index/ │ ├── schema-guard/ │ └── task-orchestrator/ ├── workspace/ # 模型可读写的工作区根目录 │ ├── inbox/ # 待处理文件 │ └── outbox/ # 产出物 ├── data/ │ ├── index/ # 索引数据 │ └── logs/ # 各 Skill 的日志 └── config/ ├── dsh.yaml # 主配置 └── skills.yaml # Skill 注册与参数分成skills、workspace、data、config四块的好处是,备份和迁移特别省事。要换机器,skills和config打包带走,workspace挑需要的带走,data里除了索引之外基本可以丢。索引重建虽然费时间,但比重装环境可靠。
skills.yaml里我建议给每个 Skill 单独留一段,写清楚启用状态、根目录、超时时间、日志级别。这样出问题的时候,改一个字段就能定位,不用去翻 Skill 自己的内部配置。下面是一个可以照抄的骨架:
skills: fs_workspace: enabled: true root: ./workspace allow_write: true max_read_bytes: 262144 shell_runner: enabled: true cwd: ./workspace timeout_sec: 30 allowlist_profile: dev memory_index: enabled: true store: ./data/index chunk_tokens: 512 chunk_overlap: 64 schema_guard: enabled: true strict: true task_orchestrator: enabled: true max_parallel: 3 subtask_timeout_sec: 300max_read_bytes这个参数值得单独说一下。它的作用是限制单次读取的字节数,防止模型一次性把几 MB 的日志拉进上下文,把预算烧光。我设成 256KB,大约是 6 万到 8 万字符,已经够读一份完整的中等规模源码文件了。如果你的场景经常要读大文件,建议不要调大这个值,而是让模型配合检索 Skill 做分段读取——这样既不爆上下文,也能覆盖全文。
2.3 权限边界要先画好再装东西
这一节我要说得重一点:权限必须在装 Skill 之前定好,装完再收紧,成本高得多。原因很简单,Skill 一旦被模型调用过,你就不知道它到底在你系统里留下了什么。文件改了什么、命令执行过什么,事后追溯很麻烦。
我遵循的是“三个默认”原则。默认只读:文件工作区初始状态设为只读,等确认模型能正确读文件了,再打开写权限。默认白名单:命令执行 Skill 只放行一组预先写好的命令前缀,比如git status、ls、python -m pytest,其他一律拒绝。默认时限:任何命令执行都有超时,超时就杀,不给“再等等”的机会。
白名单这块,我建议按用途分档,别一股脑全开。下面这个分档方式是我在几个项目里用下来比较顺手的:
| 档位 | 放行内容 | 适用阶段 | 风险提示 |
|---|---|---|---|
| read-only | ls、cat、grep、git status、git diff | 初次验证 | 几乎无风险,建议起步用这档 |
| dev | 上述 +git add/commit、npm run build、pytest、python 脚本 | 日常开发 | 注意命令里的路径不能逃出工作区 |
| ops | 上述 + 服务重启、容器操作 | 慎用 | 建议只在隔离环境开 |
| full | 不设限 | 不建议 | 一旦误操作很难回滚 |
我自己的项目里,dev档是长期状态,ops档只在明确知道要做什么的时候临时开一下,做完立刻收回。这个“临时开”的习惯,帮我躲过至少两次误删。
还有一条经验:把危险命令做成硬拦截,不要靠模型自觉。比如rm -rf、git reset --hard、chmod 777这类,直接在 Skill 层拦掉,不管模型怎么请求都拒绝。这不是不信任模型,而是模型有时候会把“清理临时文件”理解成“删掉整个临时目录”,而你那个临时目录里可能还有别的东西。拦一下,让它换个方式做,成本几乎为零。
3. 五个必装核心 Skill 逐个拆解
下面这五个,是我认为构成“最小可用组合”的核心。每个我都会讲清楚它解决什么问题、关键参数怎么设、以及为什么它必须排在这个位置。
3.1 第一个:fs-workspace 文件工作区
这个 Skill 是整个体系的地基,它给模型提供的是“看见文件”和“修改文件”的能力。听起来简单,但细节很多。一个设计得好的文件工作区 Skill,至少要有这几个动作:列目录、读文件、写文件、按模式搜索、以及在写之前做存在性检查。
为什么它是第一个?因为后面四个全都依赖它。命令执行需要在某个目录下跑,检索需要读文件内容,校验需要把结果写回去,编排需要给子任务指定工作区。没有它,其他四个就是空中楼阁。
关键参数我前面提过root和max_read_bytes,这里再补两个。一个是allow_write,我建议分阶段开:先关着,跑通读取;再开,但只允许写到outbox子目录;最后才放开整个workspace。另一个是follow_symlink,默认关掉。软链接是个坑,模型顺着链接可能读到工作区之外的东西,你以为它只在workspace里活动,实际上它跑到别处去了。
读文件这块有个实操细节:不要一次性返回全文。我的做法是,读文件时先返回前若干行加总行数,模型如果判断需要更多,再按行号区间请求。这样一次读取的上下文占用能降一个数量级。具体实现上,读动作接受start_line和end_line两个可选参数,不传就返回开头 200 行。这个设计参考的是人翻代码的习惯——先看结构,再看细节。
写文件也有讲究。我要求写动作必须带一个expect_hash参数,也就是“我期望这个文件当前的内容哈希是 X”。如果实际哈希对不上,说明文件在你读完之后被别人改过,这次写入直接拒掉。这个机制叫乐观锁,成本很低,但能挡住“基于过期内容覆盖”这类事故。我加这个是因为有一次模型读了一个配置文件,中间我手动改了一行,模型再写回去的时候把那行覆盖掉了,排查了半天才发现是这么回事。
3.2 第二个:shell-runner 命令执行
有了读文件的能力,接下来最迫切的就是“跑一条命令看看结果”。这在调试场景里几乎是刚需——模型给出一段修复,你得知道它到底能不能跑通。shell-runner 就是干这个的。
它的核心设计难点不在执行,而在边界。执行一条命令容易,难的是怎么保证这条命令不会干出你不想让它干的事。我的方案是三层防护:命令白名单、工作目录锁定、超时强杀。
命令白名单我前面讲过按档位分,这里补充一点实现思路:不要用字符串包含来判断,那太容易被绕过(比如在命令里塞个分号接上另一条)。正确做法是把命令解析成结构,取第一个 token 作为可执行文件名,再检查它是否在白名单里。如果要更严一点,还可以对参数做模式匹配,比如只允许git后面跟status、diff、log这几个子命令。
工作目录锁定是指,命令执行时的当前目录必须落在workspace之内,而且命令里出现的路径参数不能包含..跳出。这一条我建议做成硬性校验,因为路径逃逸是最常见的越界方式。
超时这块,我给两档:普通命令 30 秒,长任务 300 秒。超过就杀掉进程树,不是只杀父进程。这点很重要,很多命令会 fork 子进程,只杀父进程的话子进程还在跑,端口还占着,你下次再执行就报端口冲突。杀进程树这个操作在不同系统上写法不一样,但思路是一致的:先拿到进程组 ID,再对整个组发终止信号。
输出处理也是个大问题。一条pytest跑下来,输出可能几千行,全塞回上下文直接把预算烧穿。我的做法是在 Skill 层做裁剪:只保留最后 N 行(比如 80 行),加上一个“共 X 行,已截断”的摘要。同时把完整输出写到data/logs/下,用一个 ID 索引,模型需要看细节的时候可以按 ID 再取。这样既保留了可追溯性,又不占上下文。
3.3 第三个:memory-index 检索与长期记忆
前两个 Skill 让模型能看能动手,但它们解决不了“记不住”的问题。一个项目聊上几天,上下文早就清了好几轮,你之前定的规范、约定的接口、踩过的坑,全靠人肉提醒。检索类 Skill 就是为了把这个外部化。
它的工作原理不复杂:把工作区里的文本切成块,每块算一个向量存起来,查询的时候把问题也转成向量,找最相似的几块返回。听起来是标准流程,但里面的参数选择对效果影响极大。
分块大小我定在512 tokens,重叠 64 tokens。为什么是 512?因为太小了语义不完整,一句话被切成两半,检索出来也没用;太大了噪声多,一块里混了好几个主题,匹配精度下降。512 大概是三四段话的量,对代码文件来说差不多是一个函数加注释。重叠 64 是为了避免关键句正好落在切口上,被切成两半。
检索数量我定top-k = 8,重排后取 5。先粗召回 8 条,再用一个轻量的重排模型打分,取前 5 条塞进上下文。为什么不直接取 5?因为向量召回的排序经常不准,多召回一点再重排,效果提升很明显。我实测过,重排这一步能把“答非所问”的比例降下来一大截。
| 参数 | 我的取值 | 调大的后果 | 调小的后果 |
|---|---|---|---|
| chunk_tokens | 512 | 单块噪声多,匹配变模糊 | 语义被切断,召回内容残缺 |
| chunk_overlap | 64 | 索引体积膨胀 | 切口处的关键信息丢失 |
| top_k | 8 | 重排开销上升 | 好结果可能没进候选 |
| final_n | 5 | 上下文占用增加 | 信息不足,回答变浅 |
还有一个容易被忽略的点:索引要能增量更新。项目一直在改,你不能每次改一个文件就全量重建。我的做法是记录每个文件的修改时间和内容哈希,定时扫描,只重建变化的那些。全量重建一个中等仓库大概要几分钟,增量通常几秒钟,差别很大。
我踩过的一个坑是:索引建好之后,文件改了但没更新索引,模型检索出来的是旧内容,然后基于旧内容给出建议,你怎么看都不对。后来我加了个规则,任何写文件动作之后,自动触发对应文件的索引更新。这样虽然多了一点开销,但省掉了大量“为什么它说的和实际不一样”的困惑。
3.4 第四个:schema-guard 结构化输出校验
前面三个 Skill 让模型能干活,但输出的东西经常“差一点”。你要 JSON,它给你一段带解释的 JSON;你要特定字段,它给你同义但不同名的字段。人看着没问题,程序一解析就崩。schema-guard 就是干这个的:定义好结构,不合规就重试或报错。
它的用法是,你在调用的时候附上一份结构定义(JSON Schema 是通用做法),Skill 拿到模型输出后校验,通过就返回,不通过就带着错误信息打回去让模型重试。重试次数我设成 2,超过就返回失败,不无限循环。为什么是 2?因为第一次失败通常是格式问题,第二次基本能改对;如果两次都不对,说明模型没理解要求,再试也是浪费。
结构定义这块,我的经验是别写太复杂。嵌套三层以上的结构,模型出错的概率会明显上升。如果确实需要复杂结构,拆成多次调用,每次产出一层,再用代码拼起来。这样虽然调用次数多了,但成功率反而更高。
还有一个技巧:在结构定义里给字段加description,用自然语言说明这个字段该填什么。这不是给程序看的,是给模型看的。我做过对比,同样一个结构,加了描述之后一次通过率能提高不少。本质上这还是提示词工程,只是换了个位置。
{ "type": "object", "required": ["file", "change_type", "summary"], "properties": { "file": { "type": "string", "description": "相对于工作区根目录的文件路径,不要用绝对路径" }, "change_type": { "type": "string", "enum": ["create", "modify", "delete"], "description": "本次变更的类型,只填这三个值之一" }, "summary": { "type": "string", "description": "一句话说明改了什么,不超过 50 字" } } }校验失败时的错误信息也要设计。别只说“校验失败”,要说清楚“哪个字段、期望什么、实际收到什么”。信息越具体,模型第二次改对的可能性越大。这一点我是从编译器报错里学的——好的报错能自己教会你怎么改。
3.5 第五个:task-orchestrator 任务编排
前面四个都是“单项能力”,第五个是把它们串起来的能力。它的作用是:接收一个大任务,拆成若干子任务,给每个子任务指定该用哪些 Skill、在什么范围内活动,然后并发或串行执行,最后汇总结果。
为什么放最后?因为它是个纯消费者,自己不产生任何基础能力。它做的每一件事,都是回头调前面四个。你如果先装它,它一启动就去找文件工作区,找不到就报错,你还得回头补装。
关键参数是max_parallel,我设成 3。为什么不设更高?因为并发数上去之后,资源竞争会变严重,而且多个子任务同时改文件容易冲突。3 这个数字是我的经验值:足够让独立的子任务并行跑起来,又不至于把机器压满。如果你的任务主要是读操作,可以调到 5;如果涉及写操作,建议降到 2 甚至 1。
子任务的隔离也很重要。每个子任务应该有自己的一块“活动范围”,比如各自一个子目录,或者至少是各自一组文件。这样即使两个子任务同时跑,也不会互相覆盖。我见过编排设计得不好的情况,两个子任务同时往同一个日志文件里写,最后日志内容交错,完全没法看。
汇总环节有个常见问题:子任务返回的结果格式不统一。有的返回文本,有的返回 JSON,有的返回文件路径。我的做法是在编排层定一个统一的结果结构,每个子任务执行完必须按这个结构返回,不符合的用 schema-guard 打回去重来。这样汇总的时候就不用写一堆分支判断了。
3.6 安装顺序与依赖关系总表
把五个 Skill 的依赖关系整理成一张表,装的时候对着看,能省很多来回。
| 顺序 | Skill | 提供能力 | 依赖谁 | 装完怎么验 |
|---|---|---|---|---|
| 1 | fs-workspace | 读写工作区文件 | 无 | 让它列一下根目录,看输出对不对 |
| 2 | shell-runner | 执行命令并取回输出 | fs-workspace | 跑一条ls或git status,看退出码和输出 |
| 3 | memory-index | 切块、建索引、检索 | fs-workspace | 建一次小索引,问一个只有文档里才有的问题 |
| 4 | schema-guard | 结构化输出校验与重试 | 无强依赖,建议在前三个之后 | 故意给一个缺字段的输出,看是否被打回 |
| 5 | task-orchestrator | 拆任务、分派、汇总 | 前四个全部 | 跑一个两步的小任务,看能否正确汇总 |
验证这一步千万别省。我给自己定的规矩是:每装完一个,必须跑通一个最小验证用例,再装下一个。这个习惯让我在后期排查问题时,能快速判断“是新装的这个有问题”还是“老的那个被影响了”。省下来的时间远超验证本身花的那几分钟。
4. 完整实操:从零到跑通一条真实链路
这一节我把前面讲的东西串起来,走一遍完整流程。假设的场景是:工作区里有一个小项目,需要改一处配置、跑一次测试、最后产出一份变更说明。
4.1 第一步到第三步:落地文件、跑通命令、建好索引
先装 fs-workspace。把目录建好,配置里的root指向workspace,allow_write先设成 false。启动 DSH,发一句“列出工作区根目录下的所有文件”。正常的话,你应该看到目录列表。如果看到的是空的,先检查路径,再检查权限,最后检查是不是在容器里跑但路径没映射对。
确认能读之后,打开写权限,但只放开outbox目录。发一句“在 outbox 下创建一个 test.md,写一行 hello”。然后去文件系统里确认,这个文件真的存在,内容也对。这一步通过,说明写入链路是通的。
接着装 shell-runner。配置里cwd指向workspace,白名单用read-only档。发一句“执行 git status,把输出告诉我”。如果工作区不是 git 仓库,换成ls -la也行。关键是看退出码和输出都能回来。有些实现只返回输出不返回退出码,那模型就没法判断命令是成功还是失败,这个一定要确认。
第三步装 memory-index。这一步最费时间,因为要建索引。我的建议是先用一个小目录试,比如只索引workspace/docs,里面有十几个 Markdown 文件。配置好chunk_tokens: 512、chunk_overlap: 64,触发一次全量建索引。建完之后问一个“只有文档里才写过”的细节问题,看它能不能答上来。如果答不上来,先检查分块是不是切碎了关键句,再检查 top-k 是不是太小。
这三步走完,你已经有了“看、动、记”三件套。很多人到这一步就觉得够了,其实还差得远——输出格式不对、任务拆不开,这两个问题会在稍微复杂一点的场景里立刻暴露。
4.2 关键参数怎么算出来的
参数这块我一直觉得不能照抄,得知道算法。拿上下文预算来说,假设你的模型上下文窗口是 128K tokens,你不能把它全用满。我的分配是这样的:系统提示和工具描述占 10%,检索结果占 25%,对话历史占 20%,工具返回占 15%,剩下的 30% 留给模型输出和推理。
这个比例不是拍脑袋,是按实际使用反推的。工具返回那一块经常被低估,一条命令的输出、一次文件读取的内容,加起来很容易超过预期。留 15% 是保守估计,如果你的场景里命令输出特别长,这个比例还得往上调。
再拿索引体积估算来说。前面提过系数 1.5,具体算法是:原始文本按平均 4 字符/token 折算成 token 数,再乘以每 token 的存储开销(存向量的话通常是 4 字节浮点 × 维度数)。假设维度是 768,那每个 token 大约 3KB。这个数字看着吓人,但实际索引通常只存块级向量,不是 token 级,所以真实体积要除以块大小。我一般直接用 1.5 倍原始体积做粗估,够用了。
超时时间也有算法。普通命令的超时,我取“同类命令在本机执行时间的 P95 再乘 3”。比如pytest跑全套通常 8 秒,那就设 30 秒(8×3 约 24,取整到 30)。乘 3 是为了容纳冷启动和机器负载波动。长任务的超时取 P95 乘 5,因为长任务的不确定性更大。
4.3 端到端演练:一次“改代码+跑测试+出报告”
现在五个 Skill 都装好了,走一遍完整流程。任务描述是:“把config/app.yaml里的端口从 8080 改成 9090,跑一次测试,如果通过就生成一份变更说明写到 outbox。”
第一步,编排层拆任务。它会拆成三个子任务:改配置、跑测试、写报告。第二个子任务依赖第一个的结果,第三个依赖第二个的结果,所以是串行。
第二步,改配置。文件工作区先读config/app.yaml,定位到端口那一行,写入新值。写的时候带上expect_hash,防止并发冲突。写完返回一个变更摘要。
第三步,跑测试。命令执行 Skill 在workspace下跑测试命令,超时 300 秒。如果退出码非零,把最后 80 行输出带回去,编排层看到失败,就终止后续子任务,返回“测试未通过”。
第四步,写报告。结构化校验 Skill 先让模型产出一个符合结构的结果对象,包含文件路径、变更类型、摘要。校验通过后,文件工作区把它写成 Markdown 落到outbox。
整个流程跑下来,正常情况下十几秒到几十秒。如果中间任何一步失败,编排层会带着具体的失败信息返回,你能直接看到是哪一步、什么原因。这个可观测性是我最看重的——任务失败不可怕,可怕的是失败了不知道卡在哪。
5. 高频故障与排查实录
下面这些是我在实际使用中反复撞上的问题,按出现频率排。
5.1 五类最容易撞上的问题
第一类:技能调用不触发。表现是模型明明该用某个 Skill,却自己硬答。原因通常是描述文件写得太抽象。解决办法是把描述改成“什么场景下用、输入是什么、输出是什么”三句话,越具体越好。
第二类:路径找不到。表现是文件读取返回空或者报错。九成是根目录配错,或者在容器里跑但没做路径映射。排查顺序是:先确认配置里的 root,再确认进程的工作目录,最后确认文件系统权限。
第三类:命令执行超时。表现是明明命令几秒就能跑完,却一直卡到超时。常见原因是命令在等标准输入,或者子进程没被正确回收。解决办法是执行时把标准输入重定向到空,以及杀进程树而不是只杀父进程。
第四类:检索结果不相关。表现是问 A 它答 B。先看分块是不是把关键内容切碎了,再看 top-k 是不是太小导致好结果没进候选,最后看索引是不是过期了。这三个原因按这个顺序查,命中率很高。
第五类:结构化输出反复失败。表现是模型连续两次都产不出合规结构。通常是结构定义太复杂,或者字段描述不清楚。把嵌套拆平、给每个字段加一句人话描述,基本能解决。
5.2 速查表
| 现象 | 最可能原因 | 快速验证 | 处理 |
|---|---|---|---|
| 该调 Skill 却没调 | 描述太模糊 | 看工具清单里的描述文字 | 改成场景化描述 |
| 读文件返回空 | 根目录配错 | 手动ls该目录 | 修正 root 或做路径映射 |
| 写入被拒 | 哈希不匹配 | 看文件是否被外部改过 | 重新读取后再写 |
| 命令卡住不动 | 等待标准输入 | 看进程状态 | 重定向标准输入为空 |
| 端口被占用 | 子进程未回收 | 查端口占用 | 杀进程树 |
| 检索答非所问 | 分块或 top-k 问题 | 看召回内容 | 调 chunk 或加大 top-k |
| 索引内容过期 | 未增量更新 | 对比文件与索引时间 | 触发重建 |
| 结构校验总失败 | 定义太复杂 | 看错误信息指向哪个字段 | 拆平结构、补描述 |
再补一个不太算故障但很容易被忽略的点:日志要看 Skill 层的,不要只看 DSH 主进程的。很多问题在主进程日志里只是一句“调用失败”,真正的错误堆栈在对应 Skill 自己的日志文件里。我现在的习惯是,出问题第一件事是tail一下相关 Skill 的日志,通常一眼就能看到根因。
6. 玩法拓展:五个 Skill 组合出来的进阶套路
装完这五个只是起点,真正有意思的是把它们组合起来。下面分享三个我自己常用的套路,以及怎么自己写一个最小的 Skill。
6.1 三种组合套路
套路一:批量文档整理。文件工作区扫描一个目录,检索 Skill 判断每份文档的主题,结构化校验保证输出格式一致,最后按主题分目录归档。这个套路我用在整理一堆散落的技术笔记上,几十份文档几分钟就跑完了。关键是分块参数要调小一点,因为笔记通常很短,块太大会把好几篇混在一起。
套路二:变更影响面分析。命令执行跑git diff拿到改动文件列表,检索 Skill 找出这些文件被哪些地方引用,编排层汇总成一份影响面报告。这个在改公共模块之前特别有用,能提前知道会波及多少地方。我一般会在报告里加上“引用次数最多的前五个文件”,优先看这几个。
套路三:日常巡检。定时触发,命令执行跑一组检查命令(磁盘、依赖版本、测试通过率),结构化校验保证每次输出结构一致,最后追加到一份历史记录里。这样对比不同时间点的数据,趋势一眼就能看出来。这个套路的重点是输出结构一定要固定,不然历史数据没法对比。
6.2 自己写一个最小 Skill 的骨架
如果你发现现有 Skill 都不太贴合你的场景,写一个其实不难。最小骨架大概是这样:
# skills/my-skill/main.py def run(params: dict) -> dict: target = params["target"] # 干活的地方,注意所有路径都要限制在工作区内 result = do_something(target) return { "ok": True, "summary": f"处理完成,共 {len(result)} 条", "detail_id": save_detail(result), # 细节写日志,只回摘要 }配套的描述文件是关键,写清楚三件事:什么时候用、参数是什么意思、返回什么。下面这个描述模板我用了很多次,效果不错:
name: my-skill description: > 当需要对某个目录下的文本做批量关键词统计时使用。 输入一个目录路径,返回出现频率最高的前 20 个词及其次数。 不适用于二进制文件,遇到会跳过并在结果里标注。 parameters: target: type: string description: 工作区内的相对目录路径,例如 docs/notes写自定义 Skill 有几条经验。第一,输出一定要裁剪,别把原始数据全塞回去,给个摘要加一个 detail_id,需要细节再取。第二,所有路径校验放在最前面,别等干到一半才发现路径越界。第三,错误信息要带上下文,别只抛一个异常,要说清楚在哪个文件、哪一步、期望什么。第四,先写验证用例再写实现,哪怕只是三行断言,也能帮你在改代码的时候知道有没有改坏。
最后说一个我自己的体会。这套东西装完之后,最容易犯的错是“贪多”——看到一个新 Skill 就想装,装完发现平时根本用不上,还占着工具清单的位置,让模型在选择时多了一层干扰。我后来的做法是,工具清单控制在十个以内,每个都必须说得出“上周用过几次”。说不出来的,就卸掉。工具的价值不在于多,而在于你闭着眼睛都知道什么时候该用它。