上篇(《本地部署DSH两个月,152个会话、9个自研插件》)讲了我为什么把主力路径放在Skill+Python CLI:LLM当调度员、Python当工人、硬门禁当安全带。
这条路的核心资产是一份SKILL.md------DSH的Skill机制:一个目录放一份SKILL.md(给LLM的说明书)+一套脚本。目录里的脚本Python怎么写,网上一搜一大把;但SKILL.md怎么写,几乎没有靠谱的资料------而它恰恰决定了LLM是严格执行还是自由发挥。
我这9个skill的SKILL.md前后改了3轮才稳定下来。这篇把4个坑全部摊开:同一份说明书,README风格LLM跑偏六成,操作手册风格一次到位。
先建立核心认知:SKILL.md的读者不是你
| README | SKILL.md | |
|---|---|---|
| 读者 | 人(有上下文知识、会推理) | LLM(每次会话cold start) |
| 阅读方式 | 通读理解,融会贯通 | 按需匹配,照抄执行 |
| 写错了的后果 | 人看不懂会自己探索 | LLM会自信地乱拼命令 |
| 最佳文体 | 架构说明+设计思想 | 红线+命令模板+状态机+异常对照表 |
一句话:你给散文,它还你概率;你给模板,它才能给确定性。
我踩的4个坑,全部源于把SKILL.md当README写。
坑1:写成README------LLM自己拼命令
翻车现场
第一版货盘Skill的SKILL.md,我按README的习惯写:背景介绍两段、架构设计一节、模块说明一节,两页纸。命令只在一句话里"提了一嘴":解析用parse_huopan.py,可新建批次。
结果LLM执行时:
说明书里的正确命令:
python scripts/parse_huopan.py --new-batch --raw workspace/huopan_raw.json --url "<表链接>"
LLM实际拼出来的:
python parse_huopan.py --new # 参数名自己编的
python scripts/parse_huopan.py --create-batch xxx # 编的
python parse_huopan.py -n workspace/huopan_raw.json # 缩写也是它猜的
三种拼法,全错。每错一次它还很有礼貌地道歉,换一种拼法再错。
根因
给描述,LLM就生成;给模板,LLM才复制。 "可新建批次"在人脑里能翻译成--new-batch,是因为人读过源码或文档全篇;LLM没有这个上下文,它只能按参数命名习惯概率生成 --------new、--create-batch、-n在训练数据里都是高频写法,它赌哪个都不奇怪。
修复
命令全部写成可复制模板,占位符明确,注释讲清输出:
# 货盘表
python scripts/fetch_sheet.py "<货盘表链接>" -o workspace/huopan_raw.json
python scripts/parse_huopan.py --new-batch --raw workspace/huopan_raw.json --url "<货盘表链接>"
# ↑ 输出 batch_id(ACT-YYYYMMDD-XXX),记住它贯穿全程
# 提报表(用同一个批次号)
python scripts/fetch_sheet.py "<提报表链接>" -o workspace/tibao_raw.json
python scripts/parse_tibao.py --batch <批次号> --raw workspace/tibao_raw.json --url "<提报表链接>"
三个细节:
-
占位符用尖括号包裹中文 (
<批次号>)------LLM知道往哪代入 -
输出写在注释里 (
↑ 输出 batch_id)------LLM知道下一步要用什么变量 -
一条命令一个语义,不写"也可以用另一种方式"------多解法=多种出错机会
改完之后,命令拼错率肉眼可见地掉到接近零。
坑2:frontmatter没写触发条件------该加载时不加载
翻车现场
有个skill的frontmatter我最初写成:
---
name: zentao-helper
description: 禅道操作工具集,包含API封装和批量处理功能。
---
结果用户说"帮我把上周激活的bug批量关了"------skill根本没被加载。DSH的加载机制是按description匹配意图的,"禅道操作工具集"这个描述跟"批量关bug"的语义距离太远,LLM选择用通用能力硬干:自己现编禅道API调用,404了三次。
根因
**description不是"这是什么"的介绍,是"什么时候加载我"的触发器。**DSH的skill发现机制拿用户意图跟description做匹配------写得太抽象,匹配就是掷骰子。
修复
description三件套:能力+场景+触发关键词。看我重写后的(实际在用的版本):
---
name: zentao-bug-batch
description: 禅道bug批量处理助手:直连公司禅道,自动探测API模式,
支持按产品/状态/指派人查询bug、批量解决/关闭/激活/转派。
写操作强制DryRun预览+用户确认后才提交。
当需要批量处理禅道bug(批量关闭/解决/激活/转派)、按条件拉取bug清单、
或任何任务提到禅道/zentao bug操作时加载本技能。
---
最后那句"当需要......或任何任务提到禅道/zentao"就是给匹配机制喂的关键词------用户不管说"禅道"、"zentao"还是"bug批量关",都能命中。
多写一句"什么时候用我",比正文写得再好都有用。
坑3:状态机没写死------LLM跳状态直接想执行
翻车现场
货盘流程是个状态机:解析→建批次→构建计划→(可能要人工清零)→确认→执行。第一版SKILL.md里我用一句话带过:"构建计划后如无异常即可确认执行"。
翻车:批次状态明明是MATCHING(还有3个待人工匹配项),LLM看到计划生成成功,直接对用户说"已就绪,是否执行?"然后就要调execute。它不知道MATCHING意味着"禁止执行"------因为说明书里没写。
根因
流程有状态,但状态语义只存在于我的脑子里和代码里,不在说明书里。LLM没有"读代码理解流程"的义务------它只按说明书行事。
修复
把每个状态的含义+该做什么,逐个写死:
状态含义:
PAUSED 货盘套餐OMS查不到 → 提示用户改表重发,不可继续
MATCHING 有待匹配/机构错误/UNKNOWN → 按清零指令逐条处理,禁止执行
PENDING_CONFIRM 可确认执行 → 引导用户回复"确认执行 批次号:ACT-xxx"
EXECUTING 执行中 → 只能查进度,重复确认会被拒绝
配套的还有消息模板------LLM回给用户的每类话术都给模板:
解析摘要:批次 ACT-xxx:项目 N;匹配成功 a/待匹配 b;机构 n(错误 e);
执行表:<链接>;清零后回复"确认执行 批次号:ACT-xxx"
项目候选:无法匹配:项目名称A\n1. OMS项目A(P001)\n2. …\n请回复:项目名称A=1
好处有两个:用户每次看到的格式稳定(信任感);LLM不用自己组织话术(少一个发挥出错的环节)。
坑4:cwd没约定+依赖没声明------路径崩,环境被污染
翻车现场
两个子坑一次爆出来。
子坑A :SKILL.md里的命令是python scripts/fetch_sheet.py ...相对路径写法。LLM有一次在用户当前工作目录 (不是skill根目录)执行------scripts/找不到,它又开始了"探索":ls一下、find一下、然后把路径拼成绝对路径但拼错盘符。来回五轮。
子坑B :脚本import requests失败(那个环境没装),LLM的修复很勤快:现场pip install requests------装到了系统Python里,还顺手把一个有冲突的包给升级了。
根因
-
LLM的默认cwd是会话工作区,不是skill目录------相对路径的前提(在哪个目录执行)我从来没写出来
-
LLM遇到缺依赖的本能是修复(装包),它不知道你的Python环境是精心隔离的
crontab的老坑换了个宿主:cron的cwd是用户home,crontab里写相对路径必挂------一模一样的逻辑,只是主角从cron换成了LLM。
修复
两条写死在SKILL.md开头:
## 流程编排(所有命令在 skill 根目录执行)
## 环境
- 依赖已就绪(requests/pytest,装于 .venv),不要执行 pip install
- 测试:.venv/Scripts/python.exe -m pytest tests -q
依赖问题从"LLM现场修"变成"部署时人工一次装好"------环境的确定性靠流程保证,不靠LLM的判断。
一份合格的SKILL.md骨架(直接抄)
改了3轮之后,我的9个skill全部收敛到这个结构:
---
name: xxx-agent
description: 能力一句话+触发场景+关键词("当任务提到XX时加载本技能")
---
# 标题
## 红线(必须遵守,脚本层有硬门禁)
1. 写操作唯一入口+确认条件(编号列出,写明违反后果)
2. 凭据类红线("严禁在回复中出现密码")
3. 禁止静默猜测类红线
## 环境与凭据
- env优先级、依赖已就绪声明、工作目录约定
## 流程编排(所有命令在 skill 根目录执行)
### 1. 触发:xxx
可复制命令模板(占位符+输出注释)
### 2. 构建:xxx
命令模板+状态机含义逐条写死
## 消息模板
- 每类回话的标准格式
## 异常对照
| 场景 | 处理 |
|---|---|
| 常见报错 | 精确的处理动作 |
## 测试
- pytest命令
逐节说为什么:
| 节 | 作用 | 对应的坑 |
|---|---|---|
| frontmatter description | 触发加载 | 坑2 |
| 红线 | LLM行为约束(配合代码硬门禁双保险) | 坑3的前半 |
| 环境与凭据 | cwd+依赖+env | 坑4 |
| 流程编排 | 命令模板+状态机 | 坑1+坑3 |
| 消息模板 | 回话格式统一 | 坑3的后半 |
| 异常对照表 | LLM遇错查表不瞎猜 | 坑1的泛化 |
红线那节特别说一句 :红线是写给LLM的"劝",代码门禁是"锁"。SKILL.md里的红线负责让LLM知道 不该干什么(90%的时候管用),execute.py里的检查负责它不听话的时候也干不成(100%管用)。双保险,缺一不可------纯靠prompt红线,就是赌概率;纯靠代码门禁,LLM会经常撞墙浪费时间。门禁设计是下一篇的主题。
4坑速查表
| 坑 | 翻车 | 根因 | 修复 | 类比 |
|---|---|---|---|---|
| 写成README | LLM乱拼命令参数 | 给描述→概率生成 | 可复制命令模板+占位符+输出注释 | 给人API文档 vs 给人curl示例 |
| 没触发词 | 该加载时不加载 | description是触发器不是介绍 | 能力+场景+关键词三件套 | 函数docstring要写"何时调用" |
| 状态机没写死 | 跳过MATCHING想执行 | 状态语义不在说明书里 | 每个状态含义+该做/禁做什么逐条写死 | 把流程图翻译成文字 |
| cwd/依赖没约定 | 相对路径崩+现场pip install | cwd是会话目录;LLM本能修环境 | "在skill根目录执行"写死+依赖声明已就绪 | crontab的cwd坑 |
最后说句实话
写SKILL.md的挫败感很有欺骗性:你会觉得"LLM怎么这么笨,这都理解不了"。换个念头就好------不是它笨,是你给错了文体。 它要的不是理解,是匹配。
我的经验值:一份好的SKILL.md,改出来的过程跟写pytest fixture很像------第一版总是给"人"看的,反复跑挂几次之后,才被迫改成给"机器"看的。这份"被迫"的产物,稳定性高得惊人:9个skill里改到第3版的那份,现在的执行日志翻下来,命令拼错率为零。
下一篇讲这套架构最硬核的部分:硬门禁设计------prompt写"禁止写入"不管用,代码写exit(2)才管用。LLM静默猜错机构、自然语言"试试"变"执行"、MCP绕过审批、门禁本身没测试,4个坑全部有真实代码。