☰
从巨型提示词到可插拔技能库:Agent技能体系的设计与实践
2026/10/7 6:50:47 网站建设 项目流程

最近一段时间,我大部分精力都花在一个叫 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 的核心,我写周报技能的步骤是:

  1. 询问用户本周的主要工作方向,如果用户已提供则跳过。
  2. 将工作内容归类:业务进展、研发事项、问题与风险。
  3. 按周报模板输出,使用 Markdown 格式,每类至少两条。
  4. 不得编造未提供的数据,缺失信息留空并提示。

步骤要写成“怎么说、怎么做”,而不是“为什么这么做”。模型需要的是可执行的指令序列,不是给它讲道理。

第四步,给示例。这一步的效果被严重低估。我给周报技能配了一组示例,比如:

用户输入:这周做了订单模块重构,对接了新的支付网关,修复了三个线上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 messages

render_skill 做的事情是把技能正文里的占位符替换成从用户消息提取的信息。这里有两个教训:一是提取失败时不要猜,直接进入“追问用户”的步骤;二是不要把敏感信息完整打印进日志。技能正文注入之后,后续的模型调用都被限制在技能框架内执行,这保证了流程的确定性。

3.4 参数传递与上下文压缩

技能内部参数传递遵循“最小化”原则。技能模板里用 {order_id}、{date_range} 这类占位符,只在命中技能后做一次插值,不在多个技能之间共享变量状态。跨技能需要数据时,显式声明依赖,让上一个技能的输出作为下一个技能的输入,而不是偷偷塞进全局上下文。

上下文压缩是我的重点优化方向。技能正文注入本身是有成本的,一个技能动辄几百 token,多命中几个技能,上下文直接爆炸。我制定的预算规范如下:

技能类型建议 token 预算说明
查询类技能400-600步骤少、目标明确
流程处理类技能800-1200含多个步骤和条件分支
内容生成类技能1000-1500需要示例和输出模板

超出预算时,裁减顺序是:先砍背景知识,再砍示例,最后砍步骤。步骤是技能的骨架,不能轻易动。另外,历史对话裁剪只保留最近十轮和最终结论,那些“好的”“明白”之类的确认消息就是浪费时间。

4. 常见问题与排查技巧实录

4.1 技能互相打架:两个技能都想响应同一个请求

这是技能库最常见的翻车现场。用户问“我的订单怎么还没到”,订单查询技能说该我上,物流投诉技能也说该我上,两个技能描述重叠,模型随机选一个,结果经常选错。

排查顺序我总结成了一个清单:

  1. 技能索引里是否同时出现了两个描述?
  2. 两个技能的 description 是否包含完全相同的触发词?
  3. 是否设置了 priority 字段?
  4. 两个技能的核心职责是否本质上就是同一件事?

解决办法有两个方向。职责确实重叠的,直接合并成一个技能,比如“订单查询”和“物流查询”合并成“订单与物流查询”,内部步骤通过分支处理。职责不同但容易被混淆的,用 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 发给一个不熟悉项目的同事,问他某条典型请求会不会触发这个技能。如果同事的判断和你的预期一致,模型大概率也没问题。这个办法帮我排掉了至少一半的命中率问题,比跑十次测试用例都管用。技能库这件事没有太多玄学,规范越细,坑越少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询