最近几个月我一直在折腾智能体(Agent)应用,最深的感受是:真正决定一个智能体上限的,不是模型本身有多强,而是你往它身上挂了多少“skills”。所谓 skills,通俗讲就是一组可复用、可插拔、能按需调用的能力封装,里面包含指令、领域知识、工具配置和示例,而不是简单的一段提示词。它能解决很多团队都会遇到的问题——提示词越写越长、越改越乱、一改全崩。这篇文章我想把这套东西掰开揉碎讲清楚:skills 到底怎么设计、怎么落地、会遇到哪些坑。适合正在做智能体开发的人,也适合想用 AI 完成复杂任务的效率玩家,哪怕你完全没搭过后端,也能看懂核心思路。
1. 为什么“技能化”是智能体落地的关键
1.1 从“大而全的提示词”到“小而精的技能包”
很多团队最开始做智能体,习惯把所有规则、角色、知识、示例全部塞进一条提示词里。看起来简单,实际跑起来问题一堆。
首先是上下文长度越来越失控。业务知识一多,提示词动辄上千行,每次调用模型都要重新处理一遍这些内容,费用高、响应慢。其次是注意力稀释,模型要在大段文本里自己找重点,经常找错。最难受的是维护,今天改了一个规则,明天可能把另一个功能的回复风格带偏,因为所有逻辑都搅在一起,根本没有边界。
技能化就是把这些混在一起的逻辑拆成独立模块。每个技能只负责一件具体的事,比如“查订单状态”就是查订单状态,它不会关心退款政策。模型收到用户请求后,先判断这个问题该用哪个技能,再把对应技能加载进来执行。这就像你家里不会把所有工具焊在一把锤子上,而是备一个工具箱,拧螺丝用螺丝刀,敲钉子用锤子,各取所需。
我见过一个很典型的案例:某个客服智能体原本用一条大提示词同时处理订单查询、退改政策和物流信息,上线后准确率一直上不去。后来把它们拆成三个独立技能,每个技能只带自己的知识库和示例,两周后意图识别准确率从 76% 提到了 94%。原因很直接,模型不需要再纠结“这段文字到底是规则还是上下文”,它只需要针对当前场景做判断。
1.2 一套技能系统需要具备哪些核心能力
如果只是把提示词拆开存到不同文件里,那还不叫技能系统。一套真正能落地的 skills 体系,至少要包含下面几个能力:
- 技能注册与清单管理:系统要知道自己一共有哪些技能,每个技能的版本、名称、职责是什么。
- 输入输出定义:每个技能都要说清楚自己需要哪些参数,返回什么结构。这是后续做校验和稳定的基础。
- 触发匹配机制:模型或路由层要能从用户请求里判断“该不该调用某个技能”,以及多个技能候选时选哪个。
- 执行层:技能内部可以是提示词模板,也可以是一段代码、一个 API 调用,甚至一套完整的多步骤流程。
- 反馈与日志:每次调用有没有成功、参数抽成了什么样、耗时多少,都要能记下来,方便排查。
这里面的关键字段,我用表格整理一下,你在设计 manifest(技能的声明文件)时可以直接参考:
| 字段 | 作用 | 注意事项 |
|---|---|---|
| skill_id | 技能唯一标识 | 建议用“业务域_动作”格式,如 order_query |
| name | 技能名称 | 用户或模型可读,尽量短 |
| description | 功能描述 | 必须写清楚“何时用、何时不用” |
| parameters | 输入参数 Schema | 推荐 JSON Schema,字段要收敛 |
| execution | 执行逻辑 | 可以是 prompt、函数引用或工作流 |
| priority | 优先级 | 多个技能相似时决定顺序 |
| version | 版本号 | 升级和回滚都靠它 |
你可能已经注意到了,我把 description 和 parameters 放得很重。实际使用中,绝大多数技能调不准,问题都出在这两个字段上。尤其是 description,它是模型判断“要不要调用这个技能”的主要依据,写得模糊,模型就不敢调用,或者乱调用。我通常会在描述里明确写两句话:第一句是“当用户提到什么的时候使用”,第二句是“千万不要用于什么场景”。这两句话比你在后面堆十条规则都管用。
2. 设计技能包:从业务场景到可执行单元
2.1 先拆场景,再写指令
很多新人一上来就急着写技能内容,恨不得把大语言模型的提示词技巧都用上。我的建议是反过来,先别碰提示词,先拆场景。
具体做法很简单:找出你历史对话记录里高频出现的问题,比如过去三个月用户反复问的那些东西。把这些问题归归类,一类就是一个候选技能。然后为每个候选技能写一张“场景卡”,包括:触发条件、用户典型说法、必须提供的核心信息、期望的输出。这张场景卡先在真实对话里验证 2-3 天,再决定要不要做成正式技能。
举个例子,一个内容创作助手项目,最初我按“写文章”“改文案”“起标题”三个场景做了三个技能。跑了一周后,发现“写文章”这个技能太贪心,用户有时候只想要一个开头,有时候想要完整长文,有时候想要目录大纲。三个诉求混在一个技能里,模型经常给错格式。后来我把“写文章”拆成了“生成大纲”“生成章节初稿”“生成完整文章”三个技能,每个技能只关心一种输出,立刻稳定下来。
拆场景的时候有个容易忽略的细节:你不仅要拆“用户说了什么”,还要拆“用户没说但隐含了什么”。比如用户说“帮我看看这个订单”,可能隐含了“查物流”的意思。这时候就需要技能支持从上下文推断参数,而不是硬等用户把所有信息说全。
2.2 输入输出的结构化设计
技能参数设计得越随便,模型抽参就越飘。我见过最离谱的情况,是模型把用户输入的日期“明天”直接传给了接口,接口收到一个中文字符串直接崩溃。要解决这个问题,必须给每个技能定义严格的输入结构。
我一般直接用 JSON Schema 来定义。下面是一个订单查询技能的 parameters 示例:
{ "name": "query_order", "description": "查询用户的订单状态。当用户提到订单、物流、发货时使用,不要用于退款和售后。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,支持模糊匹配" }, "customer_id": { "type": "string", "description": "用户唯一标识" }, "query_time": { "type": "string", "description": "查询时间范围,格式为 YYYY-MM-DD", "default": "最近7天" } }, "required": ["order_id"] } }看到没,我在 description 里写清了格式,在 query_time 里给了默认值。这就能让模型在用户没说时间范围时自动补上“最近7天”,而不是留空让下游报错。required 字段一定要用起来,该要求必填的参数就要求必填,别怕模型抽不出来,宁可让它反问用户,也比传错值强。
输出结构也一样。建议每个技能都定义好返回字段,比如 status、data、message。这样前端、日志和后置校验都能统一处理。我在项目里还会让技能执行结果经过一次“后处理层”,把模型输出里明显不符合 schema 的内容拦截下来,比如把“查询成功”这种无意义内容重新变成结构化字段,而不是直接透传给用户。
2.3 把领域知识塞进技能包
技能不只是“告诉模型要做什么”,还要给模型足够的上下文。我习惯在每个技能包里放四块内容:角色设定、知识要点、执行流程、示例。
角色设定不是那种“你是一个博学的助手”的废话,而是“你是订单查询专家,只负责返回订单状态,不回答价格争议问题”。知识要点是当前技能专属的规则,比如订单状态的枚举值、退款原因的分类、不同渠道的响应差异。执行流程是模型拿到请求后先做什么、再做什么,比如“先解析 order_id,如果缺失则追问,再查询,再按固定模板输出”。
示例部分是我最看重的。不要只放正例,一定要放反例。正例告诉模型“这种情况该调用”,反例告诉模型“这种情况千万不要调用”。比如订单查询技能里,可以加一个反例:“用户问‘我的退款怎么还没到’,不要调用 query_order,应该调用 refund_status。”这样模型就不会在退款场景里误触订单接口。
有人会问,知识量特别大的时候怎么办?比如一个技能涉及几千条产品规则。我的答案是不要全塞进技能提示词里,而是把长知识库放到向量检索或倒排索引中,技能本身只留检索的入口。这样技能包瘦身,模型聚焦,还能避免提示词超长带来的费用和延迟问题。这个思路相当于你给技能配了一个“外接大脑”,用到的时候再取,而不是把所有书都塞在口袋里。
3. 从零搭建一套轻量级 skills 管理系统
3.1 技术选型与目录结构
很多人一听“技能系统”就想去上一个微服务框架,项目管理、权限中心全上,最后维护成本比业务代码还高。我的建议是从最小可用结构开始,先用一个目录 + 多个 JSON 文件把技能管起来,跑通了再考虑要不要加中心化存储。
我自己搭建过一套轻量级方案,基于 Python 后端做路由,技能本体用目录和 JSON manifest 描述。目录结构大概长这样:
skills/ ├── manifest.json ├── order_query/ │ ├── skill.json │ └── prompt.md ├── refund_status/ │ ├── skill.json │ └── prompt.md └── common/ └── few_shot_examples.json每个技能目录里,skill.json 是声明文件,专门给路由层读;prompt.md 是给模型看的实际内容。为什么拆成两个文件?因为路由层只需要技能名、描述、参数这些元信息,不需要读整份提示词。拆开后,路由层加载轻,模型生成时再读完整内容,各司其职。
manifest.json 是全局索引,记录当前有哪几个技能、各版本号、是否启用。很多团队会忽略这个索引文件,直接在代码里硬编码技能列表,结果新增一个技能还要改路由代码。用索引文件的好处是,技能上线和下线都只是改一个 JSON 字段,不用重新部署。
3.2 技能注册与调用的核心流程
技能系统最核心的是两条流程:注册流程和调用流程。
注册流程在服务启动时执行:扫描 skills 目录 -> 解析每个 skill.json -> 校验字段是否完整 -> 注册到内存路由表。校验这一步非常关键,如果发现 description 为空,我建议直接报错,而不是让这个半成品技能上线害人。
调用流程可以拆成五步:
- 接收用户请求,提取对话上下文和当前任务。
- 意图匹配:把请求和所有技能的 description 做匹配,选出候选技能。
- 参数抽取:按照选中技能的 Schema,让模型抽取出结构化参数。
- 执行技能:可能是调用一段代码、拼好 prompt 再问一次模型,或者调用外部 API。
- 结果返回:把执行结果统一包装,再送回对话流程。
简化后的 Python 逻辑大概长这样:
import json from pathlib import Path SKILLS_DIR = Path("./skills") def load_skills(): skills = {} for skill_dir in SKILLS_DIR.iterdir(): manifest_path = skill_dir / "skill.json" if manifest_path.exists(): with open(manifest_path, "r", encoding="utf-8") as f: skill = json.load(f) skills[skill["skill_id"]] = skill return skills def find_skill(skills, user_input): # 这一步通常会调用模型做意图识别 # 简化:通过关键词和描述匹配 for skill in skills.values(): for keyword in skill.get("keywords", []): if keyword in user_input: return skill return None def call_skill(skill, params): # 执行技能:这里可以是代码逻辑,也可以是提示词模板 prompt = load_prompt(skill["skill_id"]) return run_model(prompt, params)这不是完整生产代码,但骨架够用了。你可以看到 find_skill 的结果完全依赖技能描述和质量,所以前面我把 description 的重要性强调那么多次,不是没道理的。
3.3 如何处理技能依赖与版本升级
技能不是写一次就完事了,业务一变,技能就得改。但改技能和改代码一样,最怕改坏线上行为。所以我建议每个技能都要有 version 字段,同时支持依赖声明。
比如一个技能用到了另一个技能产出的中间结果,那就不能随意调整上游技能输出格式,否则下游会崩。我会在 skill.json 里加一个 depends_on 字段,启动时做依赖检查,确保所有引用都存在,而不是等到线上调用时才报错。
版本升级时,我采用类似“灰度发布”的思路:同一技能保留多个版本目录,路由表里 version 字段控制当前默认版本。先切少量流量到新版本,观察日志有没有异常,再逐步放量。如果新版本出事,把 version 字段回退就行,整个过程不用重新部署服务,只要切换配置。
这里还有一个很容易踩的坑:升级技能时只改了你认为需要改的那部分,但模型可能在另一个没有任何改动的场景里行为变了。这不是模型玄学,而是因为技能描述里某句话的措辞变化,影响了整体语义匹配。所以每次升级后,一定要用回归测试集跑一遍全部历史用例,而不是只测当前改的这个场景。
4. 实测数据与效果对比
4.1 三组实验:有技能 vs 无技能
为了验证技能化到底有没有用,我在某个智能客服项目里做了一组对比实验。测试集是 500 条真实用户问题,覆盖订单查询、物流、退款、售后、常见咨询五个业务域。我对比了三组方案:纯大提示词、单技能、多技能路由。
结果如下:
| 指标 | 纯大提示词 | 单技能 | 多技能路由 |
|---|---|---|---|
| 意图识别准确率 | 76.4% | 86.2% | 94.8% |
| 参数抽取完整率 | 82.1% | 90.3% | 96.5% |
| 平均响应时间 | 3.8s | 2.9s | 2.1s |
| 一次需求变更平均耗时 | 2小时 | 1小时 | 15分钟 |
单技能方案是只把内容做了一个大技能包,没有拆业务域,所以提升有限。多技能路由的提升最明显,而且不只是准确率,维护效率的变化更让人惊喜。之前改一个业务规则要在几百行提示词里找位置,现在只需要打开对应技能目录,改动几十行,也不怕影响其他业务。
这个结果并不意外。模型在上下文变短、目标单一的情况下,判断和抽取的稳定性都会有显著提升。你给模型太多无关信息,它就会“分心”;你把无关信息隔离开,它就只盯着眼前这一件事。
4.2 真实场景中的调用记录
光看指标不够,我再看一条真实调用记录。用户输入是:“我要查一下昨天订单到哪里了。”
多技能路由收到后,先匹配到 query_order 技能,然后抽取参数:order_id 没有直接从这句里出现,但系统从会话上下文中找到了之前用户留下的订单号“ORD20240701”,query_time 被解析为“最近7天”。最终技能返回结果:
{ "status": "success", "data": { "order_id": "ORD20240701", "tracking": "已到本市分拨中心,预计明天送达" } }整个过程模型只做了一次意图判断和参数抽取,真正返回给用户的文案由技能模板拼装完成,所以稳定。我还专门测过冷启动场景:一个新技能刚上线时没有配 few-shot 示例,召回率只有 67%,补了三条正例和两条反例后,召回率直接到 93%。这印证了我前面说的,示例不是可有可无,它是技能数据的一部分。
5. 常见问题与排查技巧实录
5.1 模型总是不触发技能
最典型的症状是,用户明显在问订单问题,模型却当普通聊天回复了。排查思路从三个方向入手。
第一,看技能描述是不是写得太宽泛或太窄。太宽泛会导致模型不敢确认,太窄会导致漏匹配。我自己的标准是:描述里必须出现至少两个用户可能会用的动词和名词,比如“查询”“订单”“物流”。第二,看示例里是不是只有正例没有反例。模型需要知道“什么时候不用”,否则它会在边界场景犹豫。第三,检查匹配阈值。有些框架默认阈值是 0.6,如果用户表达比较口语化,置信度可能上不去。我习惯把阈值下调到 0.35,同时靠反例来兜底误触。
5.2 参数抽取乱如麻
参数抽取错误是另一个高频问题。比如用户说“帮我查下上个星期的订单”,模型把“上个星期”抽成了字符串“上个星期”,而接口要的是具体日期。
解决思路是把格式要求写进参数描述,比如“时间统一为 YYYY-MM-DD”,同时在 Schema 里加正则约束或枚举值。如果模型还是抽错,就要在技能执行前加一道二次校验,发现参数不合法就要求模型重新生成,而不是直接送到接口。实在不行,就把这个常见表达写进示例,让模型模仿示例里的抽取方式。
5.3 技能包升级后行为漂移
我在项目里遇到过一件特别诡异的事:一个技能从 v1 升级到 v2 后,原本好好的订单号提取突然全乱了。我检查 diff,发现自己只是改了一下角色设定里的措辞,没有动参数定义,但模型就是被新措辞带偏了。
这就是提示词改动的耦合效应。哪怕只是增加一句“你是一名资深客服”,也可能让模型更倾向于用“更贴心”的表达方式,进而影响输出格式。所以我后来给每个技能配置了一个 golden set,也就是一组历史全部跑过的用例,包含输入、期望输出、期望参数。每次升级后,先跑一遍 golden set,再决定放不放量。这比靠肉眼验证省心太多。
5.4 技能冲突与优先级
技能一多,冲突就会出现。最典型的一个例子:我有两个技能,一个负责火车票退票,一个负责机票退票,用户说“我要退票”,模型不知道该选谁。
解决办法是给技能加 priority 字段,同时强化 description 里的互斥条件。比如火车票退票技能里写“仅当用户提到高铁、火车、12306 时使用”,机票退票技能里写“仅当用户提到航班、机票、航空公司时使用”,两个描述都带上“不要用于”的边界。这样一来,大多数模糊请求会被路由到优先级更高的技能,或者让模型先追问,而不是瞎猜。
6. 一点个人体会
这篇文章写到这里,核心的内容都讲完了。最后聊几句我自己的真实感受。我第一次做技能系统的时候,以为写几个好看的提示词模板就够了,后来才发现,技能库跟代码库一样需要认真维护——要有版本、要有测试、要有文档。没有测试集的技能库,升级一次就像是蒙眼换零件,早晚会出事。
另外一个小技巧:一定要重视“模型没调用技能”的日志。很多时候问题的信号就藏在这些被跳过的调用里。我会定期把这类日志拉出来看,分析是描述不清晰、示例不足,还是意图识别阈值太高,比反复调提示词有效得多。
我后续还打算把自己手上的技能库做成一个共享仓库,让不同项目复用同一套 skills,并结合不同的业务数据做配置化接入。这件事大概率会踩更多坑,但我已经做好准备,把技能系统的设计当成一个长期资产来打磨。