刚接触 Agent 开发的时候,我对skills这个词的理解还停留在"把常用的提示词存成模板"这个层面。真正让我改变想法的,是一个被几千行 system prompt 逼疯的下午。指令越堆越长,模型输出越来越飘,改一句话要全量重发一遍上下文,排查问题时根本分不清是哪条规则在起作用。
后来我把整个项目从"一个巨型 Prompt"重构成了十几个按需加载的技能包,效果立竿见影,token 消耗降了一截,输出稳定性也明显改善。这篇想认真聊聊我在这套机制上从拆解、编写、踩坑到编排的完整过程,适合正在折腾 AI Agent、做 LLM 应用落地,或者被超长 system prompt 折磨过的开发者参考。
1. 为什么我把几千行 Prompt 拆成了 Skills
1.1 上下文拼接的天花板
传统做法是把所有能力一股脑写进 system prompt。比如我的早期版本就是一个巨大的系统指令:先讲角色设定,再讲回复风格,然后是一大堆业务规则,最后还附上各种输出格式要求。这个方式在小规模场景下能用,但一旦业务复杂起来,问题会接二连三地冒出来。
首先是 token 成本。每轮对话都要带上全套指令,用户问一句"今天天气怎么样",模型也要先读完两千行的规则才知道自己该干嘛。其次是注意力稀释,指令条数越多,模型对单条规则的遵循程度就越差。我实测过,当 system prompt 超过 3000 字以后,靠后的规则经常被"忽略"或者只在某些措辞下才触发,非常不可控。
最难受的是维护。新业务要加规则,旧业务要调整逻辑,你只能在几百行的文本里小心地插入修改,一不小心就破坏了其他模块。这就像把几千条规章制度全贴在一个工位上,员工上班时每条都扫一眼,结果遇到具体场景时全靠临场发挥。
1.2 从"常驻内存"到"按需调取"
Skills 机制的核心思路完全不同:它不再要求模型在每轮对话里记住所有规则,而是把不同的能力拆成独立的"技能包",每个技能包有自己的名称、说明、指令文件和配套资源。模型在处理用户请求时,先根据对话内容判断这次需要哪个技能,再去读取对应的技能说明来执行。
理解这个概念的最好方式,是把模型比作一个有经验但需要查手册的员工。旧方案是让他把所有手册背在身上,随时翻阅;Skills 方案是告诉他"工具间里有二十本手册,每本封面都写了适用场景,遇到对应任务再去拿对应那本"。这样既不需要他把所有内容背下来,又能保证他拿起手册时看到的是完整、聚焦的操作指南。
这种机制对上下文窗口的利用效率提升非常明显。一个 500 字的技能说明,只有在该技能被触发时才进入上下文,其余时间完全不占空间。多个技能之间也不会互相干扰,因为每个技能的规则都是独立封装的。
1.3 和 Function Calling、MCP 的边界在哪里
很多人会问,Skills 和 Function Call(函数调用)、MCP(模型上下文协议)到底有什么区别?我的理解是:这三者解决的不是同一个层面的问题。
Function Calling 解决的是"模型如何决定调用外部函数"的问题,重点在结构化参数传递和结果返回;MCP 解决的是"模型如何标准化地访问外部工具和数据源"的协议问题,重点在打通工具生态;而 Skills 解决的是"模型如何获得完成一项任务所需的领域知识和操作规范"的问题,重点在知识的组织与加载。
用一个生活化的比喻:Function Calling 是"电话拨号",MCP 是"通信协议标准",Skills 是"岗位操作手册"。打生产系统里的 API 查询数据,靠的是前两者;知道"遇到订单异常时应该按什么步骤排查、排查完按什么格式汇报",靠的是 Skills。它们可以独立使用,也可以组合:Skill 里写出判断逻辑和操作流程,流程中需要查库存时再调用 MCP 工具。
我在实际项目里的分工是:知识密集的部分放进 Skills,操作密集的部分交给 Function Call / MCP。这样既能发挥各自优势,又不会把技能包变成一个大杂烩。
2. 一个技能包的解剖:目录结构、元信息和加载规则
2.1 标准目录布局与三种文件角色
技能包说白了就是一个目录,但目录里每个文件承担的角色完全不同。以我常用的布局为例:
skills/ └── log-anomaly-analysis/ ├── SKILL.md ├── resources/ │ ├── common_error_patterns.md │ └── example_logs.txt └── scripts/ └── parse_log.pySKILL.md是技能包的主文件,里面写清楚这个技能的触发条件、执行步骤、输出格式和注意事项。resources/目录放辅助性的参考材料,比如错误码对照表、规范文档、示例数据。scripts/目录放可执行的辅助脚本,用来处理那些模型不适合直接完成的高精度计算或结构化解析任务。
这三种文件的角色可以这么理解:SKILL.md告诉模型"你该怎么做",resources告诉模型"你有什么可参考的",scripts告诉模型"哪些事你不需要亲自做,交给工具就行"。三者互相配合,才能形成完整的闭环。
2.2 name 和 description 决定了这个包能不能被命中
技能包的元信息通常写在SKILL.md开头的 Frontmatter 区域。我见过不少人忽视这段内容,但实际上name和description这两个字段直接决定了模型在什么情况下会加载这个技能包。
一个标准的 Frontmatter 长这样:
--- name: log-anomaly-analysis description: 分析系统日志中的错误与异常,识别根因并给出处理建议。当用户提供日志片段、报错信息、或请求排查线上问题时使用。 ---name要简短、唯一、有辨识度。它相当于技能包的身份证,在多技能协作时被互相引用。description则是触发器的核心依据,它需要描述清楚三件事:技能是用来做什么的、什么输入会触发它、执行后能产出什么。空泛的描述会拉低命中率,比如"处理日志"这种写法和完全没写差别不大,因为模型无法判断"处理日志"到底是否匹配当前场景。
2.3 Resources 的引用姿势:相对路径与按需读取
资源文件不是越多越好,关键是让模型知道什么时候应该去读、读哪部分。我在SKILL.md里通常会用明确的引用语句来指向资源文件,比如"当需要判断错误码含义时,参考resources/common_error_patterns.md",而不是"错误规则请看 resources 目录"。
这里有个细节容易被忽略:模型按需引用的粒度可以做到比文件更细。如果某个资源文件本身就很大,建议在SKILL.md中用行号、章节号或关键词来引导,比如"参考resources/common_error_patterns.md中『数据库连接异常』一节",这样模型就只读取相关片段,避免把整套参考文档都塞进上下文。
我早期犯过的一个错误是把所有参考材料打包成一个大文件,结果模型每次执行技能时都把整个文件读一遍,消耗了大量 token 还引入了无关信息的干扰。后来我把参考材料拆成按场景划分的小文件,并在主文件中写清楚"何时引用哪个文件",问题立刻就解决了。
3. 实操:把"日志异常归因"写成 Skill
3.1 需求拆解:不是把旧 Prompt 换个壳
很多人写 Skill 时会犯一个起步错误:把原来的长篇 prompt 原封不动塞进SKILL.md,然后改一下文件名就完事。这样做的效果非常差,因为你只是把"上下文里的凌乱规则"变成了"技能包里的凌乱规则",核心问题没有解决。
正确的做法是先做需求拆解。以日志异常归因为例,我先把任务拆成四个部分:触发条件、分析步骤、输出模板、参考规则。触发条件回答"用户在什么场景下会用到这个能力";分析步骤回答"拿到日志后按什么顺序处理";输出模板回答"最终结论用什么格式呈现";参考规则回答"判断时需要依赖哪些领域知识"。
拆完之后你会发现,原来的 prompt 里大量内容是"角色设定"和"语气要求",这些其实不该出现在技能里。技能包应该聚焦于任务本身,角色和语气交给上层的主指令去控制,否则技能包就失去了可复用性。
3.2 SKILL.md 的编写与迭代
拆解完成后,我写出的SKILL.md大概长这样:
--- name: log-anomaly-analysis description: 分析系统日志中的错误与异常,识别根因并给出处理建议。当用户提供日志片段、报错信息、或请求排查线上问题时使用。 ---下面接正文:
# 目标 根据用户提供的日志信息,定位异常类型,分析可能原因,并给出可执行的处理建议。 # 分析步骤 1. 读取日志片段,提取时间戳、日志级别、服务名、业务关键字。 2. 对照 resources/common_error_patterns.md 中的已知错误模式。 3. 若命中已知模式,直接采用该模式的根因结论和处理建议。 4. 若未命中,根据日志上下文推断最可能的异常原因,并标注推断置信度。 # 输出格式 - 异常类型:一句话概括 - 触发日志:摘录关键日志行 - 可能原因:分条列出 - 处理建议:分条列出,按优先级排序 - 置信度:高 / 中 / 低 # 注意事项 - 不要猜测日志中没有依据的原因,宁可不给结论,也要标注证据不足。 - 当日志涉及敏感信息时,只保留必要字段,不输出完整堆栈。这份SKILL.md我只保留了三样东西:任务目标、处理步骤、输出约束。它不包含"你是一个专业的日志分析师"这种角色设定,也不包含大段的思维方式描述。模型的推理能力通过步骤引导来发挥,而不是靠情绪感化。
迭代过程中我发现,最初版本的分析步骤太线性,默认所有日志都会先查错误模式表。但实际使用中,用户有时提供的是完整日志文件,有时只是一行报错,步骤一的"提取时间戳"并不总是适用。后来我把步骤改成了带分支的描述,并强调根据日志类型灵活跳转,命中率明显提升。
3.3 配套资源与辅助脚本怎么配合
资源文件的价值在于提供模型无法凭空生成的确定性知识。比如运维场景下的错误码含义、数据库常见异常的分类、不同中间件的超时机制,这些内容靠模型训练数据里的印象来推断很容易出错,放进resources/反而靠谱。
以我的实战为例,common_error_patterns.md里会记录类似这样的条目:
## 数据库连接池打满 - 关键日志:Connection pool exhausted / waiting for connection timeout - 常见原因:连接未释放、突发流量、连接池配置过小 - 处理建议:先扩容连接池并重启服务,再排查慢查询和连接泄漏而scripts/parse_log.py则负责做模型不擅长的精确解析。比如从大日志文件中批量提取时间分布、统计高频错误码、过滤噪声行。模型的强项是综合判断和文本生成,弱项是精确的批量计算,把这两类工作分开,总体的执行效率和准确率都会更高。
技能包开发到后期,真正拉开差距的地方就在于:你愿不愿意把那些"模型可能知道但可能记错"的知识沉淀成资源文件,以及愿不愿意为高频场景写配套脚本。这一步做好了,技能包才从"提示词整理"升级成了"可复用资产"。
4. 加载背后的两段式逻辑与命中率优化
4.1 先判断、后读取:模型的两步动作
理解技能包的加载机制,最关键的是意识到模型执行的是"先判断、后读取"两步动作。第一步,模型根据当前对话内容,结合你注册的各个技能包的name和description,判断"这个任务是否需要某个技能";第二步,模型读取被命中的技能包主文件,拿到指令和资源引用信息,再按指令执行。
这个机制意味着:技能包只有被调用时,它的内容才会进入上下文。所以"描述写得是否清晰"直接影响第一步的判断是否准确。如果description写得模糊,模型可能该触发时不触发,或者任何时候都凑合触发某个技能包,反而造成副作用。
4.2 Description 写法直接影响命中率
我从大量实测中总结出来的经验是:description里要放"这个技能特有的动词和名词",而不是通用词汇。比如"分析"这个词太泛,任何任务都可以说自己在分析;"日志"这个词也不够,因为用户可能说的是"帮我看看这段报错"。更好的写法是把技能涉及的动作对象和典型场景都揉进去。
对比一下:
| 写法 | 效果 |
|---|---|
| 处理日志相关请求 | 太泛,命中率低,容易乱触发 |
| 当用户提供日志片段、报错信息或请求排查线上问题时使用,分析异常类型并给出根因结论与处理建议 | 命中准确,模型能清晰判断使用边界 |
另外要注意description里可以加入否定提示,比如"不适用于性能优化类问题"。这能帮助模型把边界划清楚,避免两件相近的任务互相抢占。
4.3 多个 Skill 冲突时的取舍标准
技能包数量上去之后,冲突是必然的。我遇到过两个技能包的description都覆盖了"日志分析"这个场景,结果模型有时候走 A 流程,有时候走 B 流程,输出格式都不统一。
后来我定了一个规则:如果两个技能的目标产出有明确区别,就保留单独的技能包,但在描述里互相加否定提示,比如 A 的描述末尾写"如需要安全检查请使用 skill-b"。如果两个技能的目标产出几乎一致,只是细节不同,就合并成一个技能包,用内部步骤来区分分支场景,而不是靠模型去猜用哪个。
还有一个容易忽略的点:技能包之间的命名空间冲突。如果 A 技能的资源文件和 B 技能的脚本重名,模型在多技能协作时可能读错路径。我的习惯是资源文件名带上技能前缀,比如log-anomaly-analysis-common-patterns.md,虽然名字变长了,但能肯定地避免跨技能包读错文件。
5. 实测中踩过的三个坑和完整排查链路
5.1 坑一:上下文污染导致输出跑偏
第一次上线技能包机制后,我遇到一个很奇怪的现象:用户让模型生成一封对外邮件,模型居然在结尾加上了"请不要在日志中输出敏感信息"这样的提示。我一度以为是主指令的问题,排查了很久才发现,原因是邮件生成技能在读取配套资源时,把"合规检查技能"的参考文档也一起加载进了上下文,导致模型把不属于当前任务的规则也当成了约束。
排查链路是这样的:先确认主指令没有被修改,再逐个检查技能包的SKILL.md引用指向,最后才发现是资源目录里放了一个通用文档,两个技能包都引用了它。问题是 A 技能引用它的本意只是取一小段信息,但由于没有限定章节,模型把整个文档读进去了。
修复方法很直接:把通用文档拆成按技能拆分的独立片段,并且在SKILL.md里精确到章节引用。从此我给自己定了一条规则:资源文件的引用永远要精确到章节或关键词范围,不允许出现"参考 resources 里相关文件"这种模糊指引。
5.2 坑二:技能互相调用形成循环
技能包之间是可以互相调用的,这本来是个高级玩法,但如果不加约束就会出问题。我踩过的一次循环是:用户要求做周报,A 技能负责整理数据,B 技能负责生成报告,结果 A 的说明里写了"生成报告的步骤请参考 B 技能",而 B 的说明里又写了"数据整理步骤请参考 A 技能"。模型在这个循环里来回跳跃,上下文被反复插入,最后输出的报告结构完全错乱。
排查链路首先是看模型日志中的调用序列,发现技能 A 和技能 B 在交替加载;然后打开两个技能包的主文件比对引用关系,确认是互相引用的死锁。
修复方式是三层:第一,在SKILL.md中用明确的"本技能不负责 XX"来打断循环;第二,在调用关系上保持单向性,即 A 可以引用 B,但 B 不能反向引用 A;第三,在每个技能包主文件顶部加一行"执行入口标记",标明该技能包只能由顶层入口触发,避免递归加载。经过这次教训,我之后设计技能协作时都会先画一遍调用关系图,确保是树状结构而不是环状结构。
5.3 坑三:相对路径和版本迭代的低级失误
第三个坑更隐蔽。某个技能包更新后,我把脚本文件从parse_log.py重命名成了log_parser.py,但是SKILL.md里的引用没有同步更新。结果模型加载技能包后去读脚本,读到的是一个不存在文件,然后开始"合理地推测"脚本逻辑,输出了一份看似正常但根本没有真正执行脚本的报告。
这里的关键教训是:模型和普通程序不一样,普通程序遇到文件不存在会抛异常,模型却会尝试用自圆其说的方式绕过错误。所以技能包里的任何引用都必须反复核验,尤其是文件名、路径、版本号这些细节。
给读者提供一个有效的排查方法:每次修改技能包后,都用一个固定测试用例跑一次完整流程,检查最终输出是否真正触碰到了所有引用的资源文件和脚本。不要相信一次成功的输出,要主动在测试用例中加入"容易走到分支路径"的输入,看看那些引用是否依然健全。
6. 把多个 Skill 编排成一条生产线
6.1 多 Skill 协作的编排示例
当技能包数量超过五个,它们之间的关系就不只是简单的"互不干扰"了,而是需要明确编排。拿我做的日报生成流程为例,它拆成了三个技能:>
Agent-Reach:生产级Agent的架构、并发与安全实践
做Agent-Reach这个项目,起因是一个很实在的痛点:市面上的Agent演示,绝大多数都停在“能聊天”这一步。你跟它聊得再流畅,一旦要它去查数据库、调第三方接口、操作内部系统、把任务闭环跑完,它就露馅了。关键词Agent听起…
AI应用开发安全:从Prompt注入到生产级纵深防御
1. 这不是“加个防火墙”就能搞定的事:AI应用开发安全的底层逻辑变了 “AI应用开发安全方案大全:从代码落地到生产级纵深防御”——这个标题里每个词都不是虚的。我带团队做过7个从0到1上线的AI应用,其中3个在灰度期就被发现存在提示注入、模…
游戏引擎对象与资源管理:从组件模式到ECS架构
1. 从“一个对象”到“一套系统”:游戏对象模型的设计演进聊到游戏对象,很多刚入行的朋友第一反应就是“类呗,写个 GameObject 类,里面有 Transform、Mesh、Material,再挂个脚本”。这种思路本身没错,但真正…
游戏引擎架构深度解析:对象生命周期与资源管理实战
写这篇游戏引擎架构深度解析的第四篇时,我一直在想一个很实际的问题:很多引擎初学者能熟练摆弄场景里的物体,却说不清一个游戏对象从创建到销毁经历了什么,更别说背后那套资源管理体系是怎么支撑起整个世界的。游戏对象和资源管理…
AI编码代理caveman实战:token消耗控制与代理层设计
1. 从“caveman”说起:一个AI编码代理的极简主义实验第一次看到“caveman”这个词被拿来命名一个AI coding agent,我脑子里蹦出来的画面是:一个裹着兽皮、举着石斧的原始人,蹲在终端前面敲代码。这个反差感本身就很有意思——我们…
caveman极简工作流:从终端编辑器到专注力回归
“caveman”这个词,我第一次看到时以为说的是游戏里的穴居人,直到我试用了一个也叫这个名的终端文本编辑器,才意识到它真正代表的是那种“回到最简单状态”的设计取向:界面近乎空白,功能全靠快捷键调用,没有…