ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从巨型提示词到可插拔技能库:Agent技能体系的设计与实践

从巨型提示词到可插拔技能库:Agent技能体系的设计与实践 最近一段时间我大部分精力都花在一个叫 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(encodingutf-8)) skills[meta[name]] { meta: meta, body: parse_front_matter(manifest.read_text(encodingutf-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 发给一个不熟悉项目的同事问他某条典型请求会不会触发这个技能。如果同事的判断和你的预期一致模型大概率也没问题。这个办法帮我排掉了至少一半的命中率问题比跑十次测试用例都管用。技能库这件事没有太多玄学规范越细坑越少。
返回列表