加载 Skills:扫描、渐进披露与触发判定,让经验按需上桌
把可复用的经验做成技能包:实现一个技能加载器,启动时只读摘要、命中触发条件时才读全文、需要时才执行附带脚本,并用真实数字说明这套渐进披露到底省下了多少上下文。
今日目标
- 能实现一个技能扫描与元信息解析器,并说清目录约定与校验点
- 能实现渐进披露的三个阶段,并用 token 数量化它的收益
- 能设计触发判定,避免技能过度触发或该触发时不触发
昨天接的是别人的工具,今天接的是别人的经验。做完之后回到页面顶部把三条目标勾掉。
小白版讲解
老师傅把经验写成一页手艺卡
车间里有个老师傅,手上一堆别人不知道的规矩:这台机器开机要先空转两分钟、那种料下刀要慢。他要退休了,于是把这些规矩写成一沓卡片,一件事一张,钉在车间墙上。新人不用背下整沓卡片,也不该背——只要在碰到那件事的时候知道墙上有一张写着它,走过去翻开看一眼就够了。
前十五天我们给 mca 加的是能力:会读文件、会改文件、会跑命令、会用别人的工具。今天给它的是经验:这个仓库的除法为什么必须在除数为零时抛错、这里的测试该怎么跑、发版前先做哪一步。这些东西模型不可能知道,它们不是通用知识,是你们团队某年某月吵过一架之后定下来的。
那为什么不直接写进系统提示词?因为墙上的卡片会越来越多。一个团队攒上三十张手艺卡很正常,每张一千来字,全塞进系统提示就是三万字——每一次请求都带着,哪怕这一轮用户只是问了句「这个函数干嘛的」。钱是一方面,更糟的是注意力:面前铺着三十张不相干的卡片时,模型对真正相关那一张的敏感度是下降的。
所以今天真正要解决的不是「怎么加载一份文档」,而是怎么在需要它的时候才加载它,同时还得让模型知道有这么一张卡片存在。这两件事看起来矛盾——不加载它怎么知道有它——而这正是渐进披露要回答的。
技能与工具的分工
工具是能力,技能是流程。 工具让 Agent 能做一件它本来做不到的事——没有 run_command 它就是跑不了测试,这不是知不知道的问题。技能让 Agent 把它已经能做的事做对——它当然会跑测试,但不知道这个仓库要跑哪条命令、输出里先看哪一行、什么情况算没跑成。
这条分工有一个干净的实现体现:今天的代码一行都没进工具注册表。昨天接 MCP 时我们往注册表塞了五个远端工具,今天一个都没塞。技能不占工具清单的位置,模型也不会「调用」一个技能——它只是在合适的时候,发现有一段文字出现在了自己面前。
两个直接后果都值得记住。
好处:技能没有数量上限,工具有。 工具表随每次请求发出去,多一个工具就多一份 schema 常驻在提示前缀里,几十个之后模型选错工具的概率会肉眼可见地上升。技能的常驻成本只是一行描述,全文根本不常驻。
代价:技能不能保证被执行。 工具调用有确定语义——模型发出调用我们就真的执行,参数不对还会被挡回来。技能只是一段文字,模型可以读了不照做,而你没有任何机制拦得住。所以一条必须被执行的规矩就不该只写成技能:mca 里「写文件之前要拍快照」是写在代码里的(D14),不在手艺卡上。手艺卡管判断,代码管纪律。
顺带和昨天对照:MCP 解决「工具在别人那儿」,技能解决「经验在别人那儿」;前者要接线、协议与信任边界,后者只要一段文字加一套加载规则。三者的完整分工在 Skills 课 第六天 有对照表,本课不重讲。
目录约定与元信息校验
技能包的形状很简单:一个目录,里面一个 SKILL.md,可以再带几个附件。
skills/
calc-conventions/
SKILL.md
release-notes/
SKILL.md
scripts/bump-version.mjsSKILL.md 的头部是一段 YAML frontmatter,规范要求的必需字段只有两个:name 与 description。本实验另加了四个本实现的扩展字段(领域、优先级、触发词、路径),它们不属于规范,只是我们自己的加载器认得——不说清的话你会以为那是标准的一部分,换个客户端就懵了。
---
name: calc-conventions
description: "calc 模块的数值约定。改 src/calc.js 或讨论除零行为时按它来。"
domain: calc
priority: 20
triggers: [divide, 除零, 除数为 0, 除法]
paths:
- src/calc.js
- test/calc.test.js
---解析这段头部,第一个决定是不引 YAML 库。除了零依赖纪律,还有一条更实际的理由:完整 YAML 的表达力远超技能元信息需要的那一点点,多出来的全是风险面——锚点、合并键、以及那个著名的隐式类型转换(写成 no 的字符串会变成布尔假),每一条都会造成「同一份 SKILL.md 在两个客户端里行为不同」。所以我们实现的是一个明确的子集,只认四种形状:裸标量、带引号的标量、行内数组、缩进的多行数组。红线只有一条:看不懂的行要报错,绝不猜。 猜的代价是作者写错了却没人告诉他,技能安静地不触发——这是技能系统里最难查的一类故障。
第二个决定藏在一行看起来该是一行的代码里:怎么把头部和正文切开。直觉写法是按 --- 分割取中间那段,而那是错的——Markdown 正文里 --- 是合法的分隔线,稍长一点的文档就会有一条。按分隔符切会把正文切成碎片,表现是「技能全文莫名其妙少了一半」,而且小文件上一切正常。正确做法是按行扫描:第一行必须是 ---,然后找其后第一条单独成行的 ---。
export function splitFrontmatter(text: string): { head: string; body: string } {
const lines = text.replace(/^/, '').split('\n')
if ((lines[0] ?? '').trim() !== FENCE) {
throw new FrontmatterError('文件开头第一行必须是 ---')
}
// 从第二行起找第一条单独成行的 ---。按分隔符切会把正文里的分隔线也当成边界
const end = lines.findIndex((line, index) => index > 0 && line.trim() === FENCE)
if (end === -1) throw new FrontmatterError('frontmatter 没有结束的 --- 那一行')
return {
head: lines.slice(1, end).join('\n'),
body: lines.slice(end + 1).join('\n').replace(/^\n+/, ''),
}
}FENCE = "---"
def split_frontmatter(text: str) -> tuple[str, str]:
lines = text.lstrip("").split("\n")
if not lines or lines[0].strip() != FENCE:
raise FrontmatterError("文件开头第一行必须是 ---")
# next + 生成器:找到第一条就停,找不到给默认值,不用先把整张表算出来
end = next((i for i, line in enumerate(lines[1:], 1) if line.strip() == FENCE), None)
if end is None:
raise FrontmatterError("frontmatter 没有结束的 --- 那一行")
return "\n".join(lines[1:end]), "\n".join(lines[end + 1 :]).lstrip("\n")校验这一层的口径是跳过并说清原因,既不静默跳过也不抛到最外面:前者的后果是作者以为自己写好了,后者的后果是一个技能包写坏了整个 mca 起不来。这和昨天对掉线服务端的处理是同一条纪律:一个零件坏了,最坏的后果应该是少一个零件。 实验里故意放了一个缺 description 的包,启动时会被跳过并打印是哪个目录、缺哪个字段、为什么这个字段不能少。
三阶段加载
现在回到开头那个矛盾:不加载它怎么知道有它。渐进披露的答案是把「知道有它」和「知道它写了什么」拆成两件事,再加上第三件。
Mermaid 源码
flowchart TD
A[启动时扫描 skills 目录] --> B[阶段一 摘要常驻<br/>每个技能一行 名字加用途]
B --> C{这一句话命中了谁}
C -->|一条都没命中| D[阶段二注入 0 字符]
C -->|命中若干条| E[阶段二 全文按需<br/>按分数装 过两道闸]
E --> F[阶段三 附件再按需<br/>只给命令行 不读脚本]
F --> G[模型决定跑不跑<br/>跑就是一次工具调用]三个阶段的判据各不相同,这是最该记清楚的一点,因为判据决定了成本的形状:
| 阶段 | 判据 | 成本形状 |
|---|---|---|
| 一 摘要常驻 | 这台机器上装了什么 | 每次请求都付,与用户说什么无关 |
| 二 全文按需 | 这一句话命中了什么 | 按轮付,没命中就是零 |
| 三 附件按需 | 模型自己决定要用 | 根本不是注入,是一次工具调用 |
阶段一是每个技能一行,内容就是它的 description。这一行是模型判断「要不要用它」的全部依据,所以描述该写成「什么时候用我」而不是「我是什么」——怎么写在 Skills 课 第二天 讲过,本课只用。摘要拼在系统提示末尾:它的判据与用户说什么无关,天然是常驻成本。
阶段二把命中那几条的正文注入成一条消息。两个实现细节:角色写 user 不写 system——它是本轮的资料不是长期规则,写成 system 会在后面每一轮被当成同等权重的指令(这条和第八天的引用注入一致);它排在引用注入之前,因为两者的关系是「先给规矩,再给材料」——模型读到 src/calc.js 内容时,那份「除零要抛错」的约定应该已经在眼前了。
阶段三是附件。release-notes 那个技能带了一个升版本号的脚本,注入全文时我们只给一行命令加一句说明,脚本内容一个字都不读。这就是阶段三的定义:脚本是拿来跑的,不是拿来读给模型听的。 一个三百行的脚本塞进上下文,模型很可能「读懂之后自己照着写一遍」,那比直接跑它糟得多。而且要给能直接跑的命令行不能光给路径——只给路径的话模型多半先读一遍,阶段三就退化回阶段二了。
还要澄清一个常见误解:渐进披露省的是上下文,不是磁盘 IO。 扫描时整份 SKILL.md 其实都读出来了,因为拿 frontmatter 就得读文件头。读一份本地文件是微秒级的事,把同样这段文字塞进每一次请求才是要按 token 反复付钱、还占窗口的事。分清这两件事,才不会写出「为了省 token 去做懒加载文件句柄」这种没意义的优化。
触发判定:两类误判的代价并不对称
判定用三类条件,按可信度从高到低:显式点名(一百分)、路径命中(十分一条)、关键词命中(三分一条)。分数只用来排序与打破平局,优先级字段绝不参与「有没有命中」的判断——让它参与的话,一个优先级很高的技能会在一句完全不相干的话里上桌。
真正值得想清楚的是误判。过度触发与漏触发的代价完全不对称。 过度触发花掉一点上下文,而且看得见——注入报账里会多一行,你当场就知道它上桌了。漏触发则是模型按通用知识回答,答得像模像样,而没有任何东西提示你有一份约定没被读到,你只有在代码评审时才会发现它又把除零写成了返回无穷大。
所以默认姿态应该是「宁可多上一张」,真正的刹车放在预算那一层而不是判定这一层。这和直觉相反:多数人第一反应是把触发词写严,结果技能常年不触发,最后得出「这套东西没用」的结论。
有一个坑几乎人人都踩:英文触发词必须按词边界匹配。 一个写着 test 的触发词,在不加词边界时会被 latest、contest 命中,这是误报最主要的来源。而中文只能按子串——没有空格分词,词边界无从谈起。这条分界不能交给技能作者自己选,因为他写触发词的时候脑子里想的是「我这个技能是干嘛的」,不是「这个词会不会出现在别的句子里」。
export function hasWord(text: string, word: string): boolean {
const target = word.trim()
if (target === '') return false
const haystack = text.toLowerCase()
const needle = target.toLowerCase()
// 含非 ASCII 的按子串:中文没有空格分词,词边界无从谈起
if (/[^\x20-\x7e]/.test(target)) return haystack.includes(needle)
// 词边界只认「两侧不是字母数字」,所以 node --test 这种带空格的词也能匹配
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return new RegExp(`(^|[^a-z0-9])${escaped}($|[^a-z0-9])`).test(haystack)
}import re
def has_word(text: str, word: str) -> bool:
target = word.strip()
if not target:
return False
haystack, needle = text.lower(), target.lower()
if not target.isascii(): # 中文按子串,没有词边界可言
return needle in haystack
# re.escape 之后再拼,省得触发词里的 - . 被当成元字符
pattern = rf"(^|[^a-z0-9]){re.escape(needle)}($|[^a-z0-9])"
return re.search(pattern, haystack) is not None判定完还要把命中原因打出来,这不是装饰:漏触发时用户唯一的依据就是这几行——到底是触发词写窄了、路径写错了,还是这条技能压根没装上。
技能会互相冲突:同时命中两条怎么办
实验里故意放了两份互相矛盾的 calc 约定:当前那份说除数为零要抛错,归档那份说返回无穷大。触发词有重叠,于是一句「修一下 divide 的除零问题」会把两份都命中。
这时候两份都装,结果不是模型知道得更多,而是它随机挑一份,并且你看不出它挑了哪份。技能冲突的本质不是重复而是矛盾,所以两道闸都必须有:
第一道,领域互斥。 让技能作者显式声明自己属于哪个领域,同一领域只装分数最高的那一条。这比指望模型自己调和两份矛盾的规则靠谱得多——它调和不了,它只会挑一个。
第二道,总字符预算。 装不下的不装,而且必须把原因说出来:静默丢弃会让人以为技能写错了,实际是预算没给够,这两件事的修法完全相反。
for (const hit of hits) {
// 第一道闸:同领域只留分最高的那条。两份矛盾的约定同时上桌,模型只会随机挑一份
const winner = domains.get(hit.skill.domain)
if (winner) {
decisions.push({ hit, loaded: false, why: `与 ${winner} 同属 ${hit.skill.domain} 领域,被它挡下` })
continue
}
const block = await renderSkill(hit.skill)
// 第二道闸:预算。没装的原因必须说出来,否则会被当成「技能写错了」
if (used + block.length > budget) {
decisions.push({ hit, loaded: false, why: `全文 ${block.length} 字符,预算只剩 ${budget - used}` })
continue
}
domains.set(hit.skill.domain, hit.skill.id)
used += block.length
blocks.push(block)
decisions.push({ hit, loaded: true, why: `展开全文 ${block.length} 字符` })
}for hit in hits: # hits 已按分数降序
winner = domains.get(hit.skill.domain)
if winner: # 第一道闸:同领域只留分最高的那条
decisions.append(Decision(hit, False, f"与 {winner} 同属 {hit.skill.domain} 领域,被它挡下"))
continue
block = await render_skill(hit.skill)
if used + len(block) > budget: # 第二道闸:预算,且没装的原因要说出来
decisions.append(Decision(hit, False, f"全文 {len(block)} 字符,预算只剩 {budget - used}"))
continue
domains[hit.skill.domain] = hit.skill.id
used += len(block)
blocks.append(block)
decisions.append(Decision(hit, True, f"展开全文 {len(block)} 字符"))省下多少:把三种加载方式摆在一起
一个省不下东西的机制不值得留着,而你只有把数并排放才看得出来。下面这组数字来自本实验的自检,四个技能包全在仓库里,离线可复现。先说清一条:这里的 token 是估算值,用的是 第十二天 那对默认系数(今天全程离线,没有真实用量可以校准)。同一台机器上连跑两次结果完全一样,但它不等于任何一家模型的真实分词结果;精确计数与校准那一天已经讲过,这里只用它的估算器。
| 加载方式 | 这一轮要付的 token | 相对全量 |
|---|---|---|
| 全量加载(四份全文一次性进系统提示) | 1399 | 基准 |
| 渐进加载,一句话命中两条 | 1050(常驻 177 加本轮 873) | 省 24.9% |
| 渐进加载,这一句什么也没命中 | 177(只有常驻摘要) | 省 87.3% |
两个结论藏在这张表里。
第一,收益的大头在「没命中」的那些轮次上。 真实会话里绝大多数轮次和任何一张手艺卡都不相干——用户在问某个函数干嘛的、在让它读一段日志。这些轮次省下的是 87%,而命中的那一轮只省 25%。渐进披露不是让每一轮都变便宜,是让不相干的轮次不付钱。
第二,这套账会随技能数量变得更好看。 全量加载的成本随技能数线性增长,阶段一也线性但系数小一个量级(一行描述对一页正文),而阶段二只与「这一句命中了几条」有关,和总数无关。四份时省 25%,三十份时命中的那一轮仍然只装两三份——那才是这套设计真正的用武之地。
最后回到开头那个分工:实验跑完你会看到工具清单从头到尾是八个,一个都没多,而模型改出来的代码逐字符合了团队的约定——这就是经验与能力的区别。
源码导读
动手实验
实验目录里随仓库分发了五个技能包:两份互相矛盾的 calc 约定、一份测试手册、一份带附件脚本的发版流程,以及一个故意缺 description 的坏包。五个练习点全是「直觉写法在小数据上一切正常」的坑:按分隔符切 frontmatter、缺字段照样装上、把脚本正文读进上下文、触发判定只认关键词且不管词边界、命中几条装几条。起点代码原样跑是十四项里过七项。
- 把切 frontmatter 改成按行扫描,看第一项从红变绿——失败原因正是正文里那条 Markdown 分隔线。
- 补上四条元信息校验,看那个坏包被跳过,并且打印出缺的是哪个字段。
- 把附件从「读正文」改成「只给一行命令加一句说明」,看脚本里的函数名不再出现在注入文本里。
- 给触发判定补上路径条件与英文词边界,看命中原因里出现路径那一类,且
test不再被latest命中。 - 补上领域互斥与预算两道闸,跑
MOCK=1 SELFTEST=1 pnpm start看到十四项全过,再用 README 里那几条管道命令看三种加载方式各花多少。
验收看五条勾:自检十四项全过;坏掉的技能包被跳过且说清了缺什么;两份矛盾的 calc 约定只上桌分高的那一份;附件只出现命令行而脚本正文没进上下文;一句什么也没命中的话,阶段二注入零字符。
面试题
今天三道题,考的是渐进披露的实现判断与技能的边界,不是「Skills 是什么」:
- 渐进披露具体分几个阶段?每个阶段的判据是什么?
- 技能和工具的边界在哪?同一件事你怎么选?
- 技能的触发条件怎么设计?过度触发和漏触发哪个更难查?
完整题干、分析过程与答题要点见本课面试题库的第十六天。第三题最有区分度——多数人会答「触发词要写准」,能说出「两类误判的代价不对称、所以默认姿态该是宁可多上一张」并解释原因的人很少。
检查清单与明日预告
- 能说清技能与工具的分工,以及为什么今天的代码一行都没进工具注册表
- 能说出「技能不能保证被执行」这条代价,以及哪些规矩不该只写成技能
- 能解释为什么不引完整 YAML 库,以及自己写的子集红线是什么
- 能说出按分隔符切 frontmatter 为什么一定会出事,以及它为什么只在长文档上暴露
- 能说清三个阶段各自的判据,以及判据如何决定了成本的形状
- 能解释渐进披露省的是上下文而不是磁盘 IO
- 能说出阶段二的注入为什么用 user 角色而不是 system
- 能解释阶段三为什么要给命令行而不是只给路径
- 能说清过度触发与漏触发的代价为什么不对称,刹车该放在哪一层
- 能说出英文触发词要按词边界匹配、中文只能按子串,以及这条为什么不能让作者自己选
- 能解释技能冲突的本质是矛盾而不是重复,两道闸各挡住什么
明天是 D17《子 Agent 与并行:独立上下文、工具白名单、worktree 隔离与结果汇总》。这两天的对照值得先想一想:今天是把一段文字在合适的时候放到模型面前,明天是把一整个模型派出去干活——同样是「按需」,代价却差了两个数量级。
面试题库
技能的渐进披露具体分几个阶段?每个阶段的判据是什么?How many stages does progressive disclosure for skills have, and what triggers each one?
国内高频海外高频基础#progressive-disclosure#skills分析过程 · 先想清楚再作答
- 这题在考「你是不是自己实现过一个加载器」。只用过的人会答「先读摘要再读全文」,实现过的人会先说每个阶段的判据不同,因为判据决定了成本的形状。
- 怎么拆:三个阶段各说一遍「什么时候发生、注入什么、成本怎么算」,最后点一句三者的判据互不相同。
- 阶段一是摘要常驻:判据是「这台机器上装了什么」,与用户说什么无关,所以它拼在系统提示里、每一次请求都付钱。内容只有名字加一行 description,这一行是模型判断要不要用它的全部依据。
- 阶段二是全文按需:判据是「这一句话命中了什么」,所以按轮计费,没命中就是零。注入的消息角色应该是 user 不是 system——它是本轮的资料不是长期规则,写成 system 会让它在后面每一轮都被当成同等权重的指令。
- 阶段三是附件按需:判据是「模型自己决定要用」,所以它根本不是注入而是一次工具调用。注入全文时只给能直接跑的命令行加一句说明,脚本内容一个字都不读——只给路径的话模型会先读一遍,阶段三就退化回阶段二了。
- 一条容易说错的:渐进披露省的是上下文不是磁盘 IO。扫描时整份文件其实都读出来了,因为要拿 frontmatter 就得读文件头;省的是「把这段文字塞进每一次请求」那部分成本。
- 可预期的追问:收益大头在哪些轮次上;技能数量增长时这套账怎么变;阶段二的注入要不要进会话历史。
How to reason about it · think before answering
- This tests whether you have implemented a loader yourself. People who only used one answer "summary first, then full text"; people who built one start from the fact that each stage has a different trigger, because the trigger determines the shape of the cost.
- How to break it down - describe each stage as when it happens, what it injects, and how it is billed, then point out that the three triggers are unrelated to each other.
- Stage one is the resident summary. Its trigger is what is installed on this machine, independent of what the user says, so it lives in the system prompt and is paid for on every request. It carries only the name and a one-line description, and that line is the model's entire basis for deciding whether the skill is relevant.
- Stage two is the body on demand. Its trigger is what this particular sentence matched, so it is billed per turn and costs nothing when nothing matches. The injected message should have the user role, not system - it is material for this turn, not a standing rule, and a system role would make it carry equal weight on every later turn.
- Stage three is attachments on demand. Its trigger is the model deciding to use one, so it is not an injection at all but a tool call. The body should list a directly runnable command line plus one sentence of explanation and never the script's contents - given only a path, the model will read the script first and stage three collapses back into stage two.
- Easy to get wrong - progressive disclosure saves context, not disk IO. Scanning actually reads the whole file, because reaching the frontmatter means reading the head of it; what is saved is the cost of shipping that text with every request.
- Likely follow-ups - which turns carry most of the savings; how the arithmetic changes as skill count grows; whether the stage-two injection should persist in conversation history.
答题要点
- 三个阶段:摘要常驻、全文按需、附件按需,判据分别是「装了什么」「这句命中了什么」「模型决定要用」
- 阶段一每技能一行,拼进系统提示,每次请求都付;它是模型判断相关性的全部依据
- 阶段二按轮计费,没命中就是零;注入用 user 角色而不是 system
- 阶段三不是注入而是一次工具调用,只给命令行不给脚本内容
- 省的是上下文不是磁盘 IO;收益大头在「这一轮什么也没命中」的那些轮次上
Key points
- Three stages - resident summary, body on demand, attachment on demand - triggered by what is installed, what this sentence matched, and what the model decides to run
- Stage one is one line per skill in the system prompt, paid on every request; it is the model's entire basis for judging relevance
- Stage two is billed per turn and costs nothing when nothing matches; inject it as a user message, not a system one
- Stage three is a tool call rather than an injection - hand over a command line, never the script body
- What is saved is context, not disk IO; most of the saving comes from turns that match nothing at all
技能和工具的边界在哪?同一件事你怎么决定做成技能还是做成工具?Where is the line between a skill and a tool, and how do you decide which one a given capability should be?
国内高频海外高频进阶#skills-vs-tools#agent-design分析过程 · 先想清楚再作答
- 这题在考架构判断。答「技能是文档、工具是函数」只说到了形式,说不到代价;能说出「技能不能保证被执行」的人明显是做过取舍的。
- 怎么拆:先给一句判据,再各说一条好处与一条代价,最后给一个必须做成工具的反例。
- 判据一句话:工具是能力,技能是流程。工具让 Agent 做到它本来做不到的事(没有跑命令的工具它就是跑不了测试),技能让它把已经能做的事做对(它会跑测试,但不知道这个仓库跑哪条命令、输出先看哪一行)。
- 技能的好处是不占工具清单的位置:工具表要随每次请求发出去,几十个工具之后选错工具的概率会明显上升;技能的常驻成本只有一行描述,全文根本不常驻,所以技能数量几乎没有上限。
- 技能的代价是不能保证被执行:工具调用有确定语义,模型发出调用就真的会执行、参数不对还会被挡回来;技能只是一段文字,模型可以读了不照做,而你没有任何机制拦得住。
- 所以反例很清楚:一条必须被执行的纪律不该只写成技能。写文件前拍快照、危险命令要审批、结果超长要截断,这些都得写进代码。手艺卡管的是判断,代码管的是纪律。
- 还有一类中间态值得提:技能带的可执行脚本。它形式上是技能的附件,实际执行时走的是工具调用那条路,等于用技能承载「什么时候该跑」,用工具承载「跑起来」。
- 可预期的追问:技能里写的规矩和系统提示里写的规矩冲突了听谁的;要不要给技能加一个「强制执行」标记;技能的描述文本算不算不可信输入。
How to reason about it · think before answering
- This is an architecture judgment question. Saying "a skill is a document and a tool is a function" covers the form but not the cost; candidates who can articulate that a skill cannot be guaranteed to execute have clearly made the trade-off in practice.
- How to break it down - give one criterion, then one benefit and one cost for each, then a counter-example that must be a tool.
- The criterion in one line - a tool is a capability, a skill is a procedure. A tool lets the agent do something it otherwise cannot (without a shell tool it simply cannot run tests); a skill lets it do correctly what it already can (it knows how to run tests, but not which command this repo uses or which line of output to read first).
- The benefit of a skill is that it does not occupy a slot in the tool list. The tool table ships with every request, and past a few dozen tools the odds of picking the wrong one rise noticeably. A skill's resident cost is one line of description and its body is not resident at all, so skill count is practically unbounded.
- The cost of a skill is that execution is not guaranteed. A tool call has defined semantics - the model emits it, we really run it, and bad arguments are pushed back. A skill is just text; the model may read it and not comply, and nothing you own can stop that.
- So the counter-example is clear - a rule that must be enforced should not live only in a skill. Snapshot before writing, approval before dangerous commands, truncation of oversized results: all of those belong in code. The card governs judgment, the code governs discipline.
- Worth mentioning a middle case - executable scripts bundled with a skill. Formally they are attachments of the skill, but running one goes through the tool-call path, so the skill carries "when to run it" while the tool carries "running it".
- Likely follow-ups - what wins when a skill contradicts the system prompt; whether skills deserve a mandatory flag; whether skill description text counts as untrusted input.
答题要点
- 工具是能力、技能是流程:前者让它做到本来做不到的事,后者让它把已经能做的事做对
- 技能不占工具清单的位置,数量几乎没有上限;工具表随每次请求发出,多了会让模型选错
- 技能的代价是不能保证被执行,模型可以读了不照做,没有任何机制拦得住
- 必须被执行的纪律要写进代码:拍快照、审批、截断都不该只写成技能
- 技能附带的可执行脚本是中间态:技能管「什么时候跑」,工具调用管「跑起来」
Key points
- A tool is a capability and a skill is a procedure - one enables what was impossible, the other makes the already-possible correct
- Skills take no slot in the tool list and scale almost without limit; the tool table ships on every request and a bloated one causes mis-selection
- The cost of a skill is that execution is not guaranteed - the model may read it and ignore it, and nothing stops that
- Rules that must be enforced belong in code - snapshots, approval gates and truncation should never be skills alone
- Bundled executable scripts are the middle case - the skill says when to run, the tool call does the running
技能的触发条件怎么设计?过度触发和漏触发哪个更难查?How would you design skill trigger conditions, and which is harder to diagnose - over-triggering or under-triggering?
国内高频海外高频深入#trigger-design#false-positives分析过程 · 先想清楚再作答
- 这题的题眼是「哪个更难查」。多数人会答「触发词要写准」,那是在回避取舍;真正的答案是两类误判的代价不对称,所以默认姿态本身就该是偏向某一边的。
- 怎么拆:先答那个更难查的,再由它推出设计姿态,最后才说具体的条件类型与实现坑。
- 漏触发更难查,而且难得不是一个量级。过度触发的代价是花掉一点上下文,而且看得见——注入报账里会多出一行,你当场就知道它上桌了。漏触发是模型按通用知识回答,答得像模像样,而没有任何东西提示你有一份约定没被读到;你只有在代码评审时才会发现它又把除零写成了返回无穷大。
- 由此推出设计姿态:判定层宁可多上一张,真正的刹车放在预算层。这和多数人的直觉相反——第一反应是把触发词写严,结果技能常年不触发,最后得出「这套东西没用」的结论。
- 条件类型按可信度分三类:用户显式点名最可信、路径命中次之(很难误报)、关键词最容易误报。分数只用来排序与打破平局,作者声明的优先级绝不该参与「有没有命中」,否则一个高优先级技能会在完全不相干的话里上桌。
- 实现上有一个人人都踩的坑:英文触发词必须按词边界匹配,否则 test 会被 latest、contest 命中;中文只能按子串,因为没有空格分词。而且这条分界不能交给技能作者自己选——他写触发词时想的是「我这个技能是干嘛的」,不是「这个词会不会出现在别的句子里」。
- 最后是可观测性:命中原因必须打出来。漏触发时用户唯一的依据就是那几行——到底是词写窄了、路径写错了,还是这条技能压根没装上。没有这几行,这套机制就没法调。
- 可预期的追问:同时命中两条互相矛盾的技能怎么办;要不要让模型自己决定加载哪一条;触发判定能不能交给一次小模型调用。
How to reason about it · think before answering
- The crux is "which is harder to diagnose". Most people answer "write precise trigger words", which dodges the trade-off. The real answer is that the two failure modes have asymmetric costs, so the default stance should itself lean one way.
- How to break it down - answer the harder one first, derive the design stance from it, and only then discuss condition types and implementation traps.
- Under-triggering is harder, and not by a small margin. Over-triggering costs a little context and is visible - the injection report gains a line and you know immediately. Under-triggering means the model answers from generic knowledge, plausibly, with nothing anywhere hinting that a convention went unread; you find out at code review when it once again returns infinity on divide-by-zero.
- That yields the stance - be generous at the matching layer and put the real brake in the budget layer. This is the opposite of most people's instinct, which is to tighten trigger words until the skill never fires, ending in the conclusion that the whole mechanism is useless.
- Condition types rank by trustworthiness - an explicit mention by the user is the most reliable, a path match is next and rarely false-positives, and keywords are the most error-prone. Scores should only order and break ties; an author-declared priority must never decide whether something matched, or a high-priority skill will surface in a completely unrelated sentence.
- One implementation trap catches everyone - ASCII trigger words must match on word boundaries, or "test" fires on "latest" and "contest"; Chinese can only match as a substring because there is no whitespace tokenization. And that distinction must not be left to the skill author, who is thinking about what the skill does rather than where the word might otherwise appear.
- Finally observability - match reasons must be printed. When something fails to trigger, those lines are the user's only evidence for whether the word was too narrow, the path was wrong, or the skill never loaded at all. Without them the mechanism cannot be tuned.
- Likely follow-ups - what to do when two contradictory skills both match; whether the model should choose which to load; whether matching could be delegated to a small model call.
答题要点
- 漏触发更难查:过度触发在注入报账里看得见,漏触发没有任何提示,只能在评审时发现
- 由此定姿态:判定层宁可多上一张,刹车放在预算层而不是判定层
- 三类条件按可信度排:显式点名、路径命中、关键词;作者声明的优先级只打破平局,不决定是否命中
- 英文触发词按词边界匹配(否则 test 命中 latest),中文按子串;这条不能交给技能作者自己选
- 命中原因必须打出来,否则漏触发时无从判断是词写窄了、路径写错了还是技能没装上
Key points
- Under-triggering is harder - over-triggering shows up in the injection report, while a missed trigger leaves no trace and only surfaces at review
- Hence the stance - be generous when matching and put the brake in the budget layer instead
- Rank conditions by trustworthiness - explicit mention, path match, keyword; an author's priority only breaks ties and never decides a match
- Match ASCII trigger words on word boundaries (otherwise "test" fires on "latest") and Chinese as substrings; this must not be left to the skill author
- Always print match reasons, or a missed trigger gives no way to tell a narrow word from a wrong path from a skill that never loaded