拆解nanobot Skills:智能体领域知识按需注入与触发判断
1. 先把 Skills 这个词拆开看:它到底解决了什么麻烦
写这个系列写到第五篇,后台被问得最多的一句是:为什么我的智能体什么都能聊两句,但一碰到具体业务就开始胡说八道?这个问题我太熟了,前面几篇里我们聊过消息路由、聊过工具调用链路、聊过上下文装配,那些东西解决的是"骨架"问题——消息怎么进来、怎么分发、怎么返回。但骨架搭好之后你会发现,一个能跑起来的智能体和一个能干活儿的智能体之间,隔着一条很宽的沟。
这条沟的名字叫领域知识。
举个特别常见的场景。你给智能体接了一堆文件读写工具,它确实能读文件、能写文件、能执行命令,个体能力看着挺全。可你让它"帮我整理一下这批日志里的异常",它大概会开始瞎编一套流程:先读这个目录、再 grep 那个关键词、然后输出一段格式一点都不稳定的东西。它不知道你们团队的日志规范是什么,不知道异常判定的阈值该怎么定,不知道输出必须走哪张表。这些不在模型的通用知识里,也不该硬塞进系统提示词——塞进去会让上下文变得又长又臭,而且改一次就得重启一次。
Skills 系统就是在填这条沟。用一句话概括:它把"某个岗位在某个场景下该怎么做"这件事,从提示词里的散装文字,变成了一份有名字、可加载、可组合、可复用、按需出现的能力包。你写一份 skill,里面声明"我是处理日志分析的,我推荐的流程是这个,我常用的脚本是这个,我判异常看这几个字段",模型在真正接到日志分析任务的时候,才会把这份内容装进上下文,平时它就安安静静躺着,一点都不占预算。
所以别把 Skills 想得太玄。它的本质是上下文的选择性注入机制,加上一层文件系统的约定。前者决定"什么时候该加载",后者决定"加载什么、怎么写"。这两件事想清楚了,剩下全是工程细节。
这一篇我会带着你把 nanobot 里这套机制的源码走一遍:它怎么发现 skill、怎么描述一个 skill、怎么在对话过程中决定要不要加载、加载之后知识是怎么进到模型可见区的、以及我在实际写 skill 的时候踩过哪些坑。如果你正在给自己的智能体做垂直能力沉淀,这篇应该能省你不少来回试的时间。
2. Skill 的定义形态:一份带元信息的目录约定
2.1 为什么用目录 + 文件,而不是塞进一个大配置
先说架构选型上最容易被忽略的一个决定:nanobot 里一个 skill 不是一条数据库记录,也不是一个 JSON 数组里的元素,而是一个目录,目录里至少有一个描述文件,剩下的空间随你放任何资产。
这个设计我第一次看的时候觉得有点笨——为什么不做成一个大 YAML 文件,把几十个 skill 全写进去,读取一次搞定?后来自己动手写了一段时间才明白,目录形态带来的好处几乎全在工程侧:
第一个好处是资产不受限。一个skill 目录里可以同时放说明文档、放脚本、放模板文件、放示例输入输出、放校验用的 schema。这些资产不需要序列化进一个配置结构里,它们就是普通文件,skill 加载器只需要知道"这个目录归我管"就行。要是把 skill 做成配置项,你想带个 30 行的 Python 脚本进去,就得先面对"脚本怎么转义进 YAML"这种无聊问题。
第二个好处是边界天然清晰。目录即边界,一个目录里所有的东西属于同一个 skill,永远不会出现两个 skill 的资产互相污染的情况。删除一个 skill 就是删目录,复制一个 skill 就是复制目录,迁移、打包、版本对比全都是文件系统原生支持的操作。
第三个好处是可读性。一个新人进到仓库里,看着skills/下面一片目录名,每个目录点开有说明有示例,学习成本比读一份几千行的中心化配置低得多。
代价当然也有:你需要一个扫描器。目录形态意味着加载器得自己去遍历、去解析、去校验,还得处理"这个目录看着像 skill 但缺描述文件"这种脏数据。这部分代码在源码里占的篇幅不小,后面我们会细看。
2.2 描述文件里到底有哪几样东西是必需的
抛开具体字段名,一个 skill 的描述文件在语义上必须回答四个问题,缺一个都会在实际使用中出问题。
| 语义问题 | 干什么用的 | 缺失后的后果 |
|---|---|---|
| 我叫什么、我是干什么的 | 出现在可加载列表里,让模型知道有这么个东西 | 模型永远发现不了它 |
| 什么情况下该用我 | 触发判断的依据 | 要么从不加载,要么乱加载 |
| 加载后给你什么内容 | 真正注入上下文的主体 | 加载了等于没加载 |
| 我需要什么前置条件 | 校验能不能用、缺什么 | 环境不满足时静默失败 |
第一项是身份。名字要短、要唯一、要能被人一眼读懂。我见过有人把 skill 名字写成"数据处理与分析能力增强模块",这名字在列表里出现时几乎没有任何筛选价值。叫log-triage或者release-notes才是有信息量的命名。
第二项是触发条件,这是整个系统里最难写好的部分,也是后面第 4 节的重点。简单说,它需要一段足够具体的自然语言描述,告诉模型"在什么任务面前你应该想起我"。注意是任务特征,不是关键词列表。写关键词列表看着精确,实际极其脆弱,用户换个说法就匹配不上了。
第三项是正文。这里有个很重要的取舍:正文是可以很长的,因为它是按需加载的,不是常驻的。但长不等于啰嗦,好的 skill 正文应该像一份写给聪明新人的 SOP——假定读者能力够,但不知道你们的约定,所以只讲约定和判断标准,不讲通用常识。
第四项是前置条件。这一项最多人偷懒,但它决定了系统在异常路径上是优雅还是难看。
2.3 目录结构的实际样子
落到文件上,一个典型的 skill 目录长这样:
skills/ log-triage/ SKILL.md # 描述 + 正文主体 scripts/ parse_logs.py # 可被引用的脚本 templates/ report.md # 输出模板 release-notes/ SKILL.md examples/ before.md after.mdSKILL.md是入口。它的结构是"头部元信息 + 正文",头部用分隔符包起来,正文就是普通的 Markdown。这个形态很关键——它意味着元信息和正文在同一个文件里,但解析时能分开。加载列表的时候只读头部,需要真正注入的时候才取正文,这样即使有几十个 skill,扫描阶段的开销也很小。
我个人的习惯是把头部控制在十行左右,只写名字、一句话描述、触发描述、以及需要的外部工具声明。剩下的全部放正文。头部越短,扫描越快,也越不容易写错格式。
3. 扫描与注册:一次启动期动作背后的三个坑
3.1 加载器在启动时到底做完了哪些事
源码里这套逻辑的位置通常在应用初始化的早期阶段,跟工具注册挨着。它做的事情按顺序是:定位 skills 根目录、遍历子目录、对每个子目录尝试读取入口文件、解析头部、校验必需字段、把结果登记到一个注册表里。
我把它拆成四个阶段方便对照源码读:
阶段一,定位根目录。这里通常支持多个来源,比如内置目录加用户自定义目录。多来源带来一个必然问题:重名怎么办。常见处理是后加载的覆盖先加载的,或者用户目录优先级高于内置目录。不管选哪种,一定要在日志里打出来,否则你调试"我明明改了 skill 怎么没生效"的时候会怀疑人生。
阶段二,遍历与初筛。遍历时只认特定文件名,其他文件一律跳过。同时要跳过隐藏目录、跳过明显的缓存目录。这一步最容易被忽略的是遍历深度——只扫一层还是允许嵌套。我的建议是只扫一层,理由很简单:skill 应该扁平化管理,嵌套会让人找不到东西,也会让命名空间变得难以推理。
阶段三,解析头部。这一步是纯文本处理,用分隔符切出元信息区,然后逐行解析。这里不需要完整的 YAML 解析器,手写一个简单的键值解析通常就够,好处是容错更强——某个 skill 头部写歪了一行,不至于让整个加载阶段炸掉。源码在这里的处理思路是"单个 skill 失败不影响整体",逐个 try,失败的记一条警告然后继续下一个。
阶段四,登记注册。注册表一般是个字典,键是 skill 名,值是包含元信息、路径、正文懒加载闭包的记录。注意正文在这里通常不读,只记路径。这就是懒加载的落点:扫描阶段只碰头部,磁盘 IO 的量级是每个 skill 一次小文件读取。
3.2 第一个坑:坏文件不该拖垮整个注册流程
这条我踩过。早期我写的扫描逻辑是"任何一个 skill 解析失败就抛异常",结果有一次一个小伙伴提交的 skill 头部少了一个冒号,整个应用起不来。这在团队协作场景里是灾难——你要么回滚,要么临时删掉别人的目录。
正确的处理方式是把"加载失败"当成一种正常状态。注册表里可以额外维护一份"加载失败列表",把失败的目录名和失败原因记下来。这份列表有用得很:
- 开发期可以把它直接打到启动日志里,一眼看到自己写错了;
- 可以做一个自检用的接口,把失败列表暴露出来,方便排查;
- 如果哪天实现了热加载,失败列表就是重试的依据。
提示:判断一个 skill 是否必需字段齐全时,宁可严格一点。名字为空、描述为空、正文为空,这三种情况都应该直接判失败。宁可让作者改,也别让一个残缺的 skill 混进列表里误导模型。
3.3 第二个坑:名字冲突的静默覆盖
多来源目录最容易出的事故就是同名覆盖。这事的恶劣之处在于它不会报错。你的自定义目录里有个log-triage,内置目录里也有个log-triage,谁的生效取决于加载顺序,而加载顺序往往写在一个你半年都不会看的地方。
我的处理习惯是:加载时检测到重名,打印一条明确的警告,写明两个来源的路径和最终生效的一方。如果项目对稳定性要求高,甚至可以配置成"重名直接判定为错误,拒绝启动",逼着大家把名字区分开。听起来很严,但比线上出现"改了不生效"要友好得多。
另外建议给 skill 命名定一条规范:统一小写、用连字符分词、不超过三个词。这样重名的概率会低很多,列表排出来也整齐。
3.4 第三个坑:扫描时机与热更新的边界
启动期扫描是最简单的做法,代价是改了 skill 必须重启。这对写 skill 的人很不友好——你在调触发描述的时候,可能要改十几次。
比较务实的折中是:启动期全量扫描,运行期提供一次手动刷新入口。刷新做全量重扫,不要试图做增量,增量监听在文件系统这一层会引入一堆平台差异,投入产出比很低。刷新的时候注意两点:正在进行的会话不要被打断,新会话使用新注册表;旧注册表里没有被删掉的 skill 记录可以保留引用,避免正在执行的流程拿不到正文。
如果你的场景是纯本地单人使用,甚至可以让每次会话开始前重扫一遍,反正 skill 数量有限,头部读取又便宜。团队共享部署就别这么干了,几十个人同时触发重扫,磁盘会很难受。
4. 触发判断:Skill 系统里真正决定成败的一环
4.1 两种触发路线的取舍
触发这件事,架构上有两条路可走,nanobot 里两条都有痕迹,但重心在第一条。
路线一:模型自主判断。系统把当前所有可用 skill 的"名字 + 一句话描述 + 触发描述"作为一个清单交给模型,模型在看到用户请求时,自己决定要不要加载某个 skill。加载动作本身是一个特殊工具调用,参数就是 skill 名字。
路线二:代码侧规则匹配。用关键词、正则、或者向量相似度在代码里预先筛出候选,只把候选交给模型。
两条路线的差异非常大:
| 维度 | 模型自主判断 | 代码规则匹配 |
|---|---|---|
| 语义理解能力 | 强,能处理换说法、跨语言、隐含意图 | 弱,规则覆盖不到的表述直接漏 |
| 可控性 | 较弱,可能该加载不加载 | 强,命中即加载 |
| 成本 | 每次请求都要带一份 skill 清单 | 清单只在命中时带 |
| 维护负担 | 落在触发描述的文字质量上 | 落在规则表上,越加越乱 |
| 误触发 | 会有,但通常能自我纠正 | 关键词撞车时会很离谱 |
我的实际体会是:主线用模型自主判断,代码规则只作为兜底或强制开关。原因是 skill 数量一旦上到十几个,规则表就彻底失控了——你会在里面看到一堆互相重叠的关键词,谁都说不清某条规则为什么存在。而模型的语义判断在"用户说的这段话是不是在要这类帮助"这个问题上,准确率比手写规则高得多。
4.2 触发描述到底该怎么写才算合格
这是我最想强调的一段,因为它几乎全靠写作者的手感,没有任何工具能替你做好。触发描述写坏了,skill 就等于不存在。
反面写法,我看着就头大:
- "用于处理数据相关任务"——太泛,任何带数据两个字的请求都可能命中,模型会被误导。
- "日志分析、日志解析、日志处理、日志清洗"——纯关键词堆砌,换个说法就完蛋。
- "当用户需要帮助时使用"——等于没说。
正面写法要包含三样东西:任务形态、典型请求的样子、以及不该用它的边界。
举个例子,一个日志排查 skill 的触发描述我会这么写:
当用户提供了一段或一批运行日志,希望定位异常原因、统计错误分布、或者还原某次请求的执行链路时使用。典型请求如"这批日志里为什么有大量超时""帮我看看这次失败发生了什么""统计一下各类错误的占比"。不适用于:纯代码审查、性能压测方案设计、监控告警规则配置。
这段描述里的每一句都在干活。第一句定义任务形态。第二句给出典型请求的样子,这等于给了模型几个锚点,让它能在语义空间里做模糊匹配。第三句划边界,这一句最容易漏,但它能挡掉相当一部分误触发——模型看到"不适用于代码审查"就不会在你让它看代码的时候莫名其妙加载日志 skill。
提示:写典型请求的时候,尽量用真实用户会说的话,别用你自己总结的术语。你可以翻一翻历史对话记录,把真实问法抄两三句进去,效果比编的好得多。
4.3 清单的呈现方式与上下文成本
模型自主判断的代价是每次请求都要带一份 skill 清单。这里有笔账要算。
假设你有 20 个 skill,每个的名字加描述平均 60 个字。清单就是 1200 字左右,折算成 token 大概几百。这听起来不多,但它是每一轮对话都要带的,多轮对话里会被反复计入。如果你的应用有很长的系统提示词,再加一份清单,前置开销就不小了。
优化手段有三个层次:
第一层,控制描述长度。清单里只放精简版描述,大概一到两句话,把详细触发描述留给"候选确认"阶段。这个做法在源码里体现为两级描述字段——一级用于列表,一级用于深度判断。实际写的时候,一句话描述要卡在 50 字以内,写不下就说明这个 skill 的职责不够聚焦。
第二层,分组或分类。skill 多了以后按场景分组,清单里只给模型看组名和组内 skill 数量,模型先选组再选 skill。这能显著压缩清单体积,代价是多一次判断往返。skill 数量在 10 个以内的时候不需要这么做,上了 20 个就值得考虑。
第三层,代码侧预筛。用一层很轻的规则先缩小范围,只把相关的几个 skill 放进清单。这本质上把两条路线结合起来了,适合 skill 数量特别大、且场景区分很明显的项目。
我的建议是:从第一层开始,别提前上第二第三层。大部分项目的 skill 数量根本到不了需要分组的规模,过早优化只会让结构变复杂。
4.4 加载之后发生了什么:知识是怎么进到模型可见区的
这一步是很多人搞不清的地方。加载 skill 不是"给模型装了个插件",它做的事情非常朴素:把 skill 正文的内容,作为一次工具调用的返回值,塞进对话历史里。
理解这一点很重要,因为它是后续所有行为的根源:
第一,正文进了对话历史,就意味着它占上下文,而且会一直在那儿。加载一个两万字的 skill,你后面每一轮都在为它付 token。所以 skill 正文该多长?我的经验是控制在两三千字以内,超过这个量级就该考虑拆分,或者把大段参考资料放到文件里,让模型按需读取。
第二,正文进了对话历史,就意味着它和用户的其他输入是平级的。模型不会因为它是 skill 就无条件服从。所以正文里写"你必须严格按以下格式输出"是有意义的,它确实能提高遵从度,但不是绝对的。
第三,同一个 skill 被加载两次会占两份空间。源码里通常会在加载时做个判断,已经加载过的 skill 不再重复注入,只返回一句"该 skill 已加载"。这个判断看似小事,但如果你不做,模型在复杂任务里反复加载同一个 skill 的情况非常常见,上下文很快就爆了。
第四,正文里可以引用同目录下的其他文件,但引用方式需要明确约定。常见做法是给模型一个"读取 skill 资产"的路径约定,让它自己用文件读取工具去拿。这样脚本和模板就不必全塞进正文,正文只放"什么时候该用哪个脚本"的判断逻辑。
4.5 卸载与"忘记":被忽略的一半
大多数实现只做加载不做卸载,nanobot 里我也没看到特别强的卸载机制。这个取舍是合理的——卸载意味着从对话历史里抹掉内容,而抹掉历史会影响上下文一致性。但它的后果是:一个会话里如果先后加载了四五个 skill,上下文就被这些正文塞得满满当当。
实际应对办法不是卸载,而是控制加载的粒度。具体有三条:
- 触发描述写得准,减少无关加载;
- 正文写得短,单次加载占用小;
- 给模型一条明确的提示,告诉它"skill 内容只是参考规范,不必在回复中复述"。这一条能显著减少模型把 skill 正文抄一遍的坏习惯。
我在一个长会话项目里试过一条补充提示:加载 skill 后,如果它真的用上了,就在回复末尾标注用了哪个 skill 的哪条规范。这个做法有两个好处,一是方便我事后追溯到底哪个 skill 有用,二是模型为了能标出来,会真的去读正文而不是扫一眼就过。
5. 正文怎么写:把老师傅的判断力塞进两千字
5.1 一份好正文的四个组成部分
这是纯写作方法论的部分,但它的重要性不低于前面任何一节。因为源码是固定的,而正文是每个使用者都要自己写的,写得好坏直接决定这套系统在你手里有没有用。
我把一份合格的 skill 正文拆成四块,按重要性排序:
第一块,判断标准。这是最有价值的部分,也是最难写的部分。它回答"面对这类任务,怎么算做对了,怎么算做错了"。比如日志排查 skill 里,判断标准可能是:先确认时间窗口,再确认错误类型分布,再看是否有集中出现的错误码,最后才去追单条链路。这个顺序就是老师傅的判断力,新人不知道,模型也不知道。
第二块,操作流程。按步骤写,但别写成流水账。每一步说清楚"做什么"和"为什么这么做"。为什么比做什么重要,因为模型遇到流程覆盖不到的情况时,理解意图才能自己变通。
第三块,输出约定。格式、字段、口径、必填项。这一块最容易被模型忽略,所以要写得硬一点,用"必须""不得"这种词。如果格式复杂,配一个模板文件比用文字描述有效得多。
第四块,常见错误。把你自己或团队踩过的坑列出来,三五条即可。这一块的性价比特别高,因为模型犯的错往往就是新人犯的错,你写下来它就少犯。
四块之外的东西,比如背景介绍、术语表、行业概述,尽量别放。那些内容模型大概都知道,放了纯占地方。
5.2 用"反面例子"提升遵从度的实操
有个技巧我用了很久,效果比正面描述好:在正文里给一两组对照示例。
比如输出格式,与其描述"请按三段式输出,第一段结论、第二段证据、第三段建议",不如直接给一个正例一个反例:
错误输出: 这个日志里有很多错误,可能是网络问题,建议检查一下。 正确输出: 结论:本次失败集中在 14:02-14:05,共 37 条,全部为下游调用超时。 证据:错误码统一为 TIMEOUT,超时阈值 3000ms,同期下游服务响应 P99 升至 8.2s。 建议:优先核对下游服务在该时段的实例数与负载,其次复核超时阈值是否过紧。对照示例的说服力远超形容词。模型看到这个对比,输出质量的提升是肉眼可见的。而且这个技巧成本极低——你只要从历史记录里挑一对真实的好输出坏输出贴进去。
5.3 脚本与模板资产怎么被引用
skill 目录里放脚本的价值在于:让模型不必每次现编代码。日志解析、格式转换、字段抽取这类操作,逻辑固定但细节繁琐,每次让模型手写既慢又容易出错。放一个参数化好的脚本,正文里写清楚"什么时候调用它、参数怎么传、输出长什么样",稳定性立刻上一个台阶。
引用方式我建议写成显式的三段:适用场景、调用方式、预期输出。
- 适用场景:一句话说明这个脚本干什么、什么时候别用。
- 调用方式:命令模板,参数用占位符表示,并说明每个参数从哪儿来。
- 预期输出:贴一小段真实输出片段。这一条特别关键,模型拿到输出后需要判断是否成功,如果你不告诉它成功长什么样,它会自己瞎猜。
模板文件同理。输出模板比文字描述格式可靠得多,因为模板本身就是格式。我经常在 skill 里放一个report.md,正文里写"输出必须严格遵循 templates/report.md 的结构,不得增删章节"。这句话加上模板文件,格式稳定性比写三百字格式说明强。
5.4 正文长度的自我约束
再说一次长度问题,因为这是最常见的失控点。
我给自己定的规矩是:单份正文超过三千字,必须拆。拆的方式有两种,看情况选。
按阶段拆:一个 skill 只覆盖任务的一个阶段,比如"日志初筛"和"根因定位"分成两个 skill。好处是加载粒度更细,只在需要时才加载对应阶段。坏处是模型得自己判断该加载哪个,触发描述要写得更精确。
按深度拆:主 skill 只放高频路径的判断标准和流程,把低频的边界情况、特殊场景放到同目录的参考文件里,正文里写"遇到 X 情况时读取 references/edge-cases.md"。好处是主路径轻,坏处是多一次文件读取。
我倾向按阶段拆,因为阶段划分对使用者更直观,而且天然符合"什么时候需要什么知识"的加载逻辑。
6. 我踩过的几个坑,以及对应的处理办法
6.1 触发描述写得太宽,导致每轮都在加载
最开始我写的一个 skill,触发描述是"当用户提出任何与项目相关的问题时使用"。想法很美好,觉得这是万能助手。实际结果是:几乎每轮对话都加载,上下文瞬间被占掉一大块,而且因为它的正文里塞了很多通用规范,模型的回复变得越来越模板化,什么话都要套一遍流程。
改法只有一个:把触发描述收窄到具体任务形态。我后来把它拆成了三个 skill,各自负责一个明确的任务类型,加载频率立刻降下来,回复质量反而上去了。
这件事让我形成一个判断:skill 的触发描述越宽,实际价值越低。宽意味着它什么都想管,什么都管不好,还占资源。窄才能专业。
6.2 正文里写了"视情况而定",模型就真的随便定
早期我在正文里留了不少灰区,比如"如果数据量较大,考虑分批处理"。"较大"是哪一级?模型不知道,我也不好意思追问,于是它有时候分有时候不分,行为完全不稳定。
后来我学乖了,所有判断都要给出可执行的阈值或判据。上面那句改成:"单次输入超过 5000 行时分批,每批 1000 行,批间保留 10 行重叠避免边界切断。"这一改,行为立刻就稳了。
所以写 skill 正文的时候要做一次自查:凡是出现了"适当""酌情""视情况""较大""较多"这类词的地方,都必须换成具体数字或具体判据。这不是对模型不信任,而是你自己在写 SOP 的时候本来就该定清楚的事,之前一直含糊着而已。
6.3 多个 skill 同时加载时的指令打架
skill 一多,冲突就来了。两个 skill 都定义了输出格式,模型会怎么办?实测下来它会倾向于最后一个加载的,或者干脆混着来,输出一个四不像。
这个问题的根源在正文里没有处理优先级。我的处理办法是在每个 skill 正文开头加一小段声明:
本 skill 与输出格式相关的约定,优先级低于用户在当前会话中的明确要求,高于其他 skill 的默认格式约定。
这句话有实际效果。它给了模型一个明确的排序规则,冲突时它知道该听谁的。不同的 skill 用不同的优先级声明,就能构造出一个简单的优先级体系。当然,如果你的 skill 之间存在大面积冲突,说明职责划分本身有问题,该拆的时候就得拆。
6.4 改了 skill 却不生效的三次排查
这是纯粹的运维问题,但我估计每个用这套系统的人都碰到过。我整理了一个简单的排查顺序,按命中率从高到低排:
| 排查项 | 具体怎么查 | 我的命中次数 |
|---|---|---|
| 文件没保存到扫描目录 | 确认路径在 skills 根目录下,不是同级的兄弟目录 | 多 |
| 头部格式错导致解析失败 | 看启动日志里的加载失败列表 | 多 |
| 名字冲突被覆盖 | 检查同名 skill 加载了几次、哪次生效 | 中 |
| 会话复用了旧注册表 | 重启或触发手动刷新 | 中 |
| 触发描述与当前请求语义不匹配 | 换一种说法试试,或者直接指定加载 | 少 |
| 正文加载了但被上下文截断 | 检查是否 skill 太长把前面内容挤掉 | 少 |
这张表我贴在备忘里,每次出问题从第一行开始往下走,基本两分钟能定位。
要补一句关于最后一项的:上下文截断这种事很难直接观察到,它的表现是"skill 好像加载了但模型没用上"。我的判断技巧是让模型复述一下 skill 里的某条具体规范,如果它复述不出来,多半是内容被截断或者压根没进上下文。
6.5 别把 skill 当数据库用
有一类误用值得单独提:把本该查表的数据写进 skill 正文。比如产品名称对照表、几十个字段的口径定义、几百条错误码含义。这些内容确实是领域知识,但它们会变动,而 skill 正文是相对静态的。
正确做法是正文里写"口径以 references/fields.md 为准,遇到不认识的字段先读该文件",把对照表放到文件里。这样改数据不用改 skill,也不用重启服务,而且正文体积可控。skill 应该承载的是判断和流程,不是数据。这条界线想清楚,能避免大量后期维护麻烦。
6.6 一个关于评估的小习惯
最后说个我坚持下来的小做法:每写或改完一个 skill,用三到五条真实请求去试,看它是否按预期触发、加载后输出是否达标。我会把这几条请求和期望结果记在一个eval.md里放在同目录下,下次改的时候直接照着重跑。
这套办法土得很,但它解决了一个真问题:改触发描述很容易改出新毛病,比如原来能触发的现在不触发了。有这份小清单在,回归测试就变成一分钟的事,而不是凭感觉赌。
7. 顺着这套设计还能往前走多远
把触发判断、正文加载、资产引用这三块吃透之后,你会发现这套机制的可扩展空间比想象中大一截,因为它的核心抽象只有两条:按需注入文本和按需引用文件。这两条几乎能支撑起所有"给智能体补充领域能力"的需求。
一个很自然的方向是能力的组合。既然 skill 正文里可以引用文件,那它同样可以写"本任务需要先加载 X skill 和 Y skill"。这就形成了 skill 之间的依赖关系。实现上不需要什么复杂引擎,正文里写一句就行。我试过用这种方式串一个"发布流程":准备阶段一个 skill、校验阶段一个、发布阶段一个,后一个的正文里写明前置依赖。跑下来很顺,代价是模型偶尔会忘记按顺序加载,所以依赖那一句要写得足够醒目。
另一个方向是面向团队的 skill 规范。skill 数量上到十几个之后,最大的问题不是没得用,而是乱用。这时候需要一层治理:命名规范、目录归属、每个 skill 的负责人、以及一份全量的索引。索引不必做成自动生成的复杂东西,一个 md 文件手动维护列表加一句话说明就够,反而比自动生成的好用,因为写索引的人会顺手做一次职责梳理。
还有一个我觉得挺有意思的方向:把 skill 当成团队的隐性知识沉淀容器。一个团队里最值钱的东西往往不在文档里,而在几个老人的判断习惯里——遇到这个报错先看哪、这个指标不对劲意味着什么、这种客户投诉该怎么回。这些东西以前只能靠带人慢慢传,现在有个地方可以写下来,写下来之后智能体也能用上,一举两得。我在一个项目里推过这件事,一开始大家不愿意写,觉得麻烦,后来发现写下来的东西自己下次也能查,参与度就上来了。写 skill 的过程其实就是被迫把话说清楚的过程,这个副产品本身就值回票价。
至于性能层面,这套机制的成本结构其实相当健康:静默期零开销,扫描期开销固定在头部读取,加载期开销等于正文长度,而且加载是可控的、有意识的。比起把所有领域知识塞进系统提示词的暴力做法,它的上下文利用率高了一个量级。真要上规模,需要处理的只有清单体积这一件事,前面第 4.3 节说的分层手段足够用。
我个人在几个项目里用下来的感受是:这套东西的收益跟你的领域有多"非标"强相关。场景越标准化、模型本来就越懂,skill 的边际价值越低;场景越依赖你所在团队的具体约定和判断习惯,skill 的价值越高。所以别为了用而用,先问一句:这件事的通用做法模型是不是已经会了?如果会,那就别写 skill,写多了只是给上下文添负担。真正值得写下来的,是那种"外人看着一头雾水、老手一秒钟知道该怎么办"的东西。这类东西不多,但一旦写对,效果立竿见影。