最近一段时间,我大部分精力都花在一个叫 agent-skills 的项目上。名字听起来挺玄乎,其实就是把 AI Agent 的能力拆成一个个可以复用、可以插拔的技能模块:一个技能对应一类任务,包含触发条件、执行步骤、示例和关联工具,Agent 遇到对应场景时自动加载并执行。这个词最近在 AI 应用圈里热度很高,但不少朋友还在用一坨巨型系统提示词硬扛。这篇文章我想把搭建这套技能体系时的设计取舍、踩过的坑、以及实测有效的规范一次讲清楚,适合正在做 Agent 产品、或准备把 prompt 工程升级成可维护技能库的同学参考。
1. 项目背景与整体设计思路
1.1 一个最朴素的问题:Agent 的能力到底应该存在哪
做 Agent 应用,我最早的习惯和大多数人一样:所有指令全部塞进 system prompt。角色设定、回答语气、业务流程、甚至某个工具的使用说明,全写在一大段提示词里。刚开始还行,三十行以内模型都能听话,效果也说得过去。
但业务一复杂就崩了。比如做一个电商客服 Agent,要同时处理订单查询、退款、物流投诉、优惠券规则解释四类任务。全塞进 system prompt 之后,prompt 轻松破三千字,维护变成噩梦:改退款流程的时候,订单查询的行为也开始抽风;加一个新功能,老场景的命中率掉一半。更麻烦的是,你根本说不清楚问题出在哪一段指令,只能整段回滚反复试。
做久了你会发现一个关键事实:Agent 的能力不只是“知识”,还有“流程”。知识可以扔进 RAG 让模型检索,但流程、步骤、约束这类东西必须结构化。agent-skills 就是冲着这个痛点去的——把每个任务对应的完整流程封装成一个独立技能,模型按场景加载、按步骤执行,而不是在一条看不见头尾的巨型 prompt 里盲人摸象。
1.2 技能、系统提示词和工具调用到底怎么分工
很多人第一次听到 agent-skills,第一反应是:这不就是 function calling 吗?或者直接说,多写几个 prompt 模板不就完了?我一开始也这么想,实际做下来才发现三者解决的问题完全不同。
| 维度 | 系统提示词 | 工具调用 Function Calling | 技能 Skill |
|---|---|---|---|
| 作用范围 | 全局角色、语气、总原则 | 单次确定性操作 | 一类任务的完整流程 |
| 是否结构化 | 无结构,纯文本 | 有结构化参数 Schema | 结构 + 文本结合 |
| 可复用性 | 差,绑定具体 Agent | 一般,工具可复用但无流程 | 强,整个流程包复用 |
| 维护方式 | 整段修改,牵一发动全身 | 独立函数,相对独立 | 独立目录 + 版本管理 |
举个生活化的例子方便理解:系统提示词是公司的规章制度,规定大家上班什么态度、穿什么衣服;工具调用是行政窗口,你要盖章就找它,一次一办;技能则是“新员工入职流程”的整套 SOP,包含该找谁签字、走什么审批、最后发什么工牌,是一套完整动作的组合。
这三者不是替代关系,而是配合关系。系统提示词负责“你是谁”,工具调用负责“怎么调接口”,技能负责“这类事情从头到尾怎么做”。技能在中间起的是编排作用,可以串联多个工具调用,也可以完全不碰工具,只约束模型的推理步骤和输出格式。
1.3 技能库的三个设计目标
动手之前,我先给自己定下了三个目标,后面所有设计决策都围绕这三个点展开。
第一是可复用。同一个“订单查询”技能,既可以用在对外客服机器人上,也可以挂在内网员工订单答疑助手里。技能一旦写成独立模块,换 Agent 只是换一套注册配置,不用重新写逻辑。
第二是可维护。改一个技能不碰其他技能,这是单体 prompt 完全做不到的。技能之间通过依赖声明来解耦,比如退款技能声明“需要订单查询技能提供订单状态”,但两者不互相写死逻辑。
第三是可观测。每个技能有名字和版本,每次 Agent 调用完,系统能记录“本次使用了哪个技能、跑了几步、花了多少 token”。有了这份日志,你才能回答“为什么这个请求处理得不好”这种核心问题。没有观测能力的技能库,本质上还是黑盒。
当然还有第四个隐藏目标:性能。技能不是越多越好,注入要克制,这个后面专门讲。
2. 核心架构与技能定义规范
2.1 一个技能模块的标准结构
先把最核心的东西放上来——一个技能在磁盘上长什么样。这是我从多个项目里迭代出来最舒服的目录结构:
skills/ order_query/ SKILL.md examples/ flow_1.txt flow_2.txt prompts/ init.md scripts/ query.py refund_process/ SKILL.md examples/ refund_ok.txt refund_reject.txt每个技能一个独立目录,目录名就是技能名。SKILL.md 是技能的唯一入口,机器读它做索引,模型读它执行步骤。examples 目录放少样本示例,prompts 目录放可选的详细步骤文本,scripts 目录放需要执行的代码。
SKILL.md 的格式我用“YAML front matter + Markdown 正文”的组合,机器可读的部分和人工阅读的部分分开。一个典型的技能大概长这样:
--- name: order_query description: 查询订单状态与物流信息。用户提到订单号、物流、发货、到货时间时使用。 negative: 不处理退款,不处理商品质量投诉。 version: 1.2.0 priority: 10 steps: - 引导用户提供订单号 - 调用 order.query 工具获取订单状态 - 按输出模板汇总:订单号、当前状态、预计到达时间 budget_tokens: 600 requires: tools: [order.query] ---每个字段都是踩坑踩出来的。description 前两行必须写触发场景和领域名词,这是后面技能选择和索引注入的基础;negative 字段专门写“这个技能不干什么”,用来防止多个技能互相抢响应;priority 处理技能冲突;budget_tokens 控制这个技能正文注入时最多允许占多少 token,防止长技能把上下文撑爆。
2.2 注册机制:技能不是放进目录就能用
技能目录建好了,接下来最关键的一步是注册。很多新手直接写一个循环,把每个 SKILL.md 的内容全部读出来拼进 system prompt,然后发现一次对话要带几千 token 的技能文本,效果没提升,成本先翻倍。
正确的做法是:启动时扫描技能目录,只解析 front matter 构建索引,技能正文留到命中后再加载。注册逻辑非常简单,示意代码如下:
import pathlib import re def parse_front_matter(text: str) -> dict: m = re.match(r"^---\n(.*?)\n---\n(.*)$", text, re.DOTALL) if not m: return {} # 简易解析,生产环境务必用 yaml.safe_load meta = {} for line in m.group(1).splitlines(): if ":" in line: key, value = line.split(":", 1) meta[key.strip()] = value.strip() return meta def load_skills(skills_dir: pathlib.Path) -> dict: skills = {} for manifest in skills_dir.glob("*/SKILL.md"): meta = parse_front_matter(manifest.read_text(encoding="utf-8")) skills[meta["name"]] = { "meta": meta, "body": parse_front_matter(manifest.read_text(encoding="utf-8")).pop("body", ""), "path": manifest.parent, } return skills代码里我故意用了正则解析 YAML,这只是为了让大家理解原理,生产环境千万别这么干,老老实实用 PyYAML。注册机制的核心思想是“索引与正文分离”:系统启动时只解析 front matter,构建一个轻量级的技能索引字典,正文留到技能被选中之后再读取注入,这样无论技能数量怎么涨,基线开销都稳定。
2.3 技能选择策略:让 Agent 自己找到对的技能
技能注册好之后,下一个核心问题是:Agent 怎么知道当前请求应该用哪个技能。我试过两条路,分别说一下实测感受。
第一条路是所有技能描述直接拼进 system prompt。做法是把每个技能的 description 和 negative 字段汇总成一个“技能索引区”,放在系统提示词的开头。这里有个铁律:索引区只放描述,绝对不放完整步骤和示例,控制在这个区块不超过十条。每条 description 的前 80 个字符要写清楚触发场景和领域名词,因为模型读索引基本就是前几行说了算。
实际效果大概是这样的索引块:
【可用技能索引】 - order_query:查询订单状态、物流、发货时间。不处理退款。 - refund_process:处理退款申请、退款进度。不处理订单查询。 - coupon_check:查询优惠券规则、使用范围、叠加条件。不涉及价格计算。第二条路是 embedding 召回。把用户消息向量化,召回 top-3 技能。这条路我早期试过,后来放弃了。主要问题是召回的技能之间容易出现重叠,模型看不到完整的技能集合,反而没法做排除判断;而且额外引入一套向量检索逻辑,排查问题时多了变量。在技能数量不超过几十个的前提下,直接注入索引比向量检索更稳定。
技能数量超过一百个就不一样了,那时候需要分层索引:先按领域粗分类,命中领域后再看细分技能。但在那之前,保持简单。
3. 实操过程:从零到一套可用技能库
3.1 目录规划与命名规范
动手搭技能库的第一步不是写 SKILL.md,而是规划目录。我吃过亏,一开始随手建了几十个技能,目录全是平铺,技能名也是随心所欲。半年后再看,根本不知道某个技能是给谁用的。
命名上,我的规范是 snake_case 加动词开头,比如 query_order、refund_process、generate_report。禁止出现 general_helper、utils、common 这种模糊名字,这类技能最后都会变成垃圾场,什么东西都往里塞。
技能数量超过二十个时,必须按领域分子目录:
skills/ sales/ query_order/ cancel_order/ after_sales/ refund_process/ exchange_process/ marketing/ coupon_check/ campaign_query/命名和目录结构不只是为了人看着舒服,它直接影响模型读索引的效率。技能名本身就带着场景信息,模型扫描索引时更快锁定目标。我实测过,把技能从“处理订单相关问题”这种抽象描述改成“query_order”之后,命中率提升了不少,因为触发词和功能一目了然。
3.2 写第一个技能:把会做的事翻译成 SKILL.md
下面用一个非电商的通用场景——周报生成,完整走一遍写技能的流程,大家可以照着抄方法。
第一步,划边界。周报就是周报,别什么都往里塞。所以我单独建了日报技能、月报技能,避免一个技能处理多种周期。边界不清是技能冲突的第一大来源。
第二步,写触发词。description 里明确写“用户要求生成周报、本周汇报、本周工作总结时使用”。这里要注意:不要写抽象的情感词,要写用户可能原样说出口的话。用户说“帮我写个周报”,如果你的 description 里只有“阶段性总结”,模型大概率匹配不上。
第三步,拆步骤。这是整个 SKILL.md 的核心,我写周报技能的步骤是:
- 询问用户本周的主要工作方向,如果用户已提供则跳过。
- 将工作内容归类:业务进展、研发事项、问题与风险。
- 按周报模板输出,使用 Markdown 格式,每类至少两条。
- 不得编造未提供的数据,缺失信息留空并提示。
步骤要写成“怎么说、怎么做”,而不是“为什么这么做”。模型需要的是可执行的指令序列,不是给它讲道理。
第四步,给示例。这一步的效果被严重低估。我给周报技能配了一组示例,比如:
用户输入:这周做了订单模块重构,对接了新的支付网关,修复了三个线上bug。 模型输出: ## 本周工作 ### 业务进展 - 完成订单模块重构 ### 研发事项 - 对接新支付网关 ### 问题与风险 - 修复三个线上bug,暂无遗留风险示例比一千字的说明文字有效得多。模型照着示例的格式填空,输出质量立刻上了一个档次。
第五步,定输出模板。模板直接写在步骤里,比如“必须输出 Markdown 格式、三级标题分类”,这种局部指令比在系统提示词里重复约束有效得多,因为它的作用范围只在这个技能内,冲突面小。
3.3 让技能真正生效:加载、注入与调用链路
写好了技能,接下来是运行时链路。从用户消息到技能生效,完整流程是五步:意图接收、索引匹配、正文加载、变量插值、按步执行。
第一步,把技能索引区拼进 system prompt;第二步,Agent 根据用户消息选择命中的技能名;第三步,从技能库里把对应技能的完整 SKILL.md 读出来;第四步,把用户消息里的变量填进步骤模板,比如订单号、日期范围;第五步,Agent 按 steps 逐条执行,需要调工具时走函数调用。
核心代码可以浓缩成这样一个函数:
def build_prompt(user_message: str, index_block: str, skill_registry: dict) -> list[dict]: selected = select_skill(user_message, index_block) messages = [{"role": "system", "content": f"你是电商客服助手。技能索引如下:\n{index_block}"}] if selected: skill = skill_registry[selected] messages.append({"role": "system", "content": render_skill(skill, user_message)}) messages.append({"role": "user", "content": user_message}) return messagesrender_skill 做的事情是把技能正文里的占位符替换成从用户消息提取的信息。这里有两个教训:一是提取失败时不要猜,直接进入“追问用户”的步骤;二是不要把敏感信息完整打印进日志。技能正文注入之后,后续的模型调用都被限制在技能框架内执行,这保证了流程的确定性。
3.4 参数传递与上下文压缩
技能内部参数传递遵循“最小化”原则。技能模板里用 {order_id}、{date_range} 这类占位符,只在命中技能后做一次插值,不在多个技能之间共享变量状态。跨技能需要数据时,显式声明依赖,让上一个技能的输出作为下一个技能的输入,而不是偷偷塞进全局上下文。
上下文压缩是我的重点优化方向。技能正文注入本身是有成本的,一个技能动辄几百 token,多命中几个技能,上下文直接爆炸。我制定的预算规范如下:
| 技能类型 | 建议 token 预算 | 说明 |
|---|---|---|
| 查询类技能 | 400-600 | 步骤少、目标明确 |
| 流程处理类技能 | 800-1200 | 含多个步骤和条件分支 |
| 内容生成类技能 | 1000-1500 | 需要示例和输出模板 |
超出预算时,裁减顺序是:先砍背景知识,再砍示例,最后砍步骤。步骤是技能的骨架,不能轻易动。另外,历史对话裁剪只保留最近十轮和最终结论,那些“好的”“明白”之类的确认消息就是浪费时间。
4. 常见问题与排查技巧实录
4.1 技能互相打架:两个技能都想响应同一个请求
这是技能库最常见的翻车现场。用户问“我的订单怎么还没到”,订单查询技能说该我上,物流投诉技能也说该我上,两个技能描述重叠,模型随机选一个,结果经常选错。
排查顺序我总结成了一个清单:
- 技能索引里是否同时出现了两个描述?
- 两个技能的 description 是否包含完全相同的触发词?
- 是否设置了 priority 字段?
- 两个技能的核心职责是否本质上就是同一件事?
解决办法有两个方向。职责确实重叠的,直接合并成一个技能,比如“订单查询”和“物流查询”合并成“订单与物流查询”,内部步骤通过分支处理。职责不同但容易被混淆的,用 negative 字段把边界写清楚,加上 priority 设置优先级。
我还有一个偷懒但有效的小技巧:当两个技能描述怎么改都还是冲突时,直接把两个 description 并排打印出来,让模型解释它们的差异,然后照着模型给出的差异去改写。模型帮你定位问题,这个操作实测效率极高。
4.2 模型“看不见”技能:索引注入但行为不变
另一个高频问题:技能索引明明放进 system prompt 了,模型行为却完全没变,就像技能不存在一样。我总结过三个原因。
第一,description 写得太抽象。比如写“处理用户体验问题”,模型根本不知道具体对应什么用户话术。改成动词加名词的写法:“查询快递单号对应的物流轨迹”,模型才能建立映射。
第二,索引区位置太靠后。如果 system prompt 前面已经堆了一千字的角色设定,技能索引被淹没在长文本里,模型注意力根本到不了那里。我把索引区移到 system prompt 最前面,问题立刻缓解。
第三,缺少使用约束。纯粹把索引放进去不够,必须加一句话:“当且仅当用户请求与某技能场景匹配时使用该技能,否则按默认能力回答。”这句话像触发器,能把模型的技能选择行为从泛泛而谈变成显式决策。
验收方法很简单:注入索引后,单独向模型发一句“你现在有哪些技能可用”,看它能否把技能名复述出来。复述不出来,基本就是索引注入方式有问题。
4.3 Token 开销失控:技能越加越多,响应越来越贵
技能库做到中期,自然会出现“什么任务都配个技能”的冲动,然后 token 开销开始失控。早期我犯过一个典型错误:为了让模型表现更好,把所有技能的完整正文注入到每一次对话里,结果一次请求消耗的 token 翻了三倍,模型在多余的指令之间互相干扰,效果反而变差了。
正确的成本控制策略是三层:索引层永远只注入描述,这是固定小开销;正文层只在技能命中后注入;执行层用完正文立即释放,不在后面的对话里长期保留。
步骤数量也要设上限。一个技能的 steps 超过六条,就要考虑拆成子技能,或者把细节放到 prompts 目录下的独立文件,按需加载。技能正文不是越详细越好,模型读不完反而会忽略关键步骤。
我做过一次基准测试:技能库从二十个技能扩大到四十个,因为坚持索引和正文分离,单次请求的平均 token 消耗只上涨了约 15%,主要来自索引区变长。这个涨幅完全可接受。
4.4 技能库的版本管理与回归测试
技能是代码,代码就要有版本管理。每个技能目录用 git 管理,SKILL.md 开头有 version 字段,每次修改都要在 commit message 里写清楚改动原因,比如“增加对预售订单的处理分支”。
更重要的是回归测试。我维护了一个场景集,用一个 JSON 文件记录二十条代表性请求和期望命中的技能名:
[ { "input": "我这个订单为什么还没发货", "expected_skill": "query_order" }, { "input": "刚买的衣服不合身,想退了", "expected_skill": "refund_process" } ]每次技能库变更后跑一遍这个场景集,统计技能命中率。我给自己定的标准是命中率不能低于 95%,低于就要回去改 description。这套机制看起来笨拙,但它是技能库长期健康的保证,没有回归测试的技能库早晚会在某次改动后悄悄崩掉。
5. 实测效果与个人心得
5.1 不同模型对技能体系的敏感度差异
同一套技能库,换一个底层模型,表现可能天差地别。我把这个项目在不同模型上跑过一轮,几个结论比较有参考价值。
旗舰级闭源大模型对长索引和 negative 语义的理解能力很强,索引可以放到十五条左右,negative 字段能真正起到排除作用。开源中等规模模型对长描述的敏感度明显下降,description 要压缩成一句话加三个触发词,negative 尽量改写成正面引导,比如“只在处理退款时使用”,而不是“不处理订单查询”。再小一点的模型连索引注入都吃不消,更适合用规则预筛,先把请求分类,再决定要不要上技能。
这意味着技能库的 description 要适配模型能力,而不是一套描写走天下。我最终的做法是给 description 准备了精简版和完整版两个字段,注入索引时根据模型型号选择版本。
5.2 从技能库到技能体系:后续还能怎么扩展
agent-skills 做到现在,我已经把它当成一个可持续演进的体系,而不是一次性交付的代码。几个明确的扩展方向值得说一下。
技能市场。把技能打包成可分享的模块,带上版本号、作者、适用场景描述,团队之间直接拉取复用。同一个公司内多个业务线共用一套核心技能,能省掉大量重复开发。
效果度量。给每个技能记录命中率、执行成功率、平均 token 成本三个指标,按周汇总,发现某个技能命中率高但执行成功率低,就去检查步骤设计是否合理。
技能联动。一个技能显式调用另一个技能,比如退款技能在处理“订单已发货需要拦截”时自动调用物流变更技能。这需要增加依赖字段和调用链追踪,复杂度上升不少,但换来的是更复杂的任务编排能力。
自动化技能生成。把历史会话聚类,找出高频任务模式,自动生成技能草稿,人工审核后并入技能库。这个方向我试过轮廓,效果还不稳定,但方向是对的,能大幅降低技能库的维护成本。
最后再分享一个小技巧:每写完一个技能,做一次“一句话测试”——把技能的 description 发给一个不熟悉项目的同事,问他某条典型请求会不会触发这个技能。如果同事的判断和你的预期一致,模型大概率也没问题。这个办法帮我排掉了至少一半的命中率问题,比跑十次测试用例都管用。技能库这件事没有太多玄学,规范越细,坑越少。