前段时间我在重构一个基于大模型的智能客服系统,碰到了非常典型的问题:业务逻辑越加越多,Prompt 越来越长,每新增一个功能都要去动主流程代码,改完还会影响原有功能的稳定性。后来我把一套叫 agent-skills 的设计思路落地到项目里,才算是把问题从根上解决了。这篇文章想聊聊 agent-skills 到底在解决什么问题、一个技能库该怎么设计、以及我实际落地时踩过的那些坑。
先说清楚:agent-skills 不是某个特定框架,而是一套让智能体具备“可复用能力单元”的设计模式。它的核心思想是把 Agent 能做的各种操作,从纠缠在一起的主流程里拆出来,封装成一个个独立、可描述、可注册、可调用的“技能”。智能体本身只负责理解目标和做决策,具体怎么做,交给技能去执行。这就像你开车不需要每次从零思考怎么打方向盘、踩油门一样,技能就是 Agent 的肌肉记忆。
适合谁看?如果你正在做大模型相关的应用开发,尤其是做 Agent、工作流、自动化工具,或者被“Prompt 越来越长、逻辑越叠越乱”的问题折磨过,这篇内容可以直接参考。我会从设计思路讲到代码实现,再到排查实录,尽量把你落地时会遇到的问题一次说透。
1. 为什么 agent-skills 会成为智能体落地的关键拐点
1.1 单体 Agent 的三大通病
我最早做 Agent 时,也是典型的“单体式”写法:一个超长 System Prompt,里面塞满了角色设定、任务描述、工具列表、使用说明、约束条件,然后让模型自由发挥。刚开始功能少还没什么感觉,等接了五六个外部 API 之后,问题就全冒出来了。
第一个问题是 Prompt 长到一定程度,模型会出现“意图漂移”。你明明让它去做 A 任务,它非要先解释一遍 A 任务再顺带提一下 B 任务,甚至把 B 任务的工具描述也当作操作指令执行。不是模型变笨了,而是过长的上下文里,有效信息密度被大幅稀释,注意力被分散到无关描述上。我们实测过,当 System Prompt 超过 3000 token 时,工具调用的准确率会有明显下降,而且带业务色彩的描述越多,下降越严重。
第二个问题是工具调用逻辑和业务逻辑高度耦合。比如我需要一个“查询订单状态”的功能,代码里既要写如何调用订单 API,又要写如何解析结果、如何判断是否超时、如何生成用户可读的回复。这些逻辑一旦散落在主代码里,后续维护就是灾难。要么造成重复代码,要么改一处崩三处。更尴尬的是,你还很难复用,换个项目或者换个场景,这些逻辑又得重新复制一遍。
第三个问题是能力无法沉淀。团队里可能已经有人写好了 PDF 解析模块、有人写好了数据图表生成模块,但因为没有一个统一的技能接入标准,这些模块只能躺在各自的项目里。新启动的项目如果需要类似能力,往往选择重写而不是复用,因为“接进来要改的代码太多了,还不如自己写”。长期看,这是非常大的隐性成本。
这三个问题叠加起来,让我意识到:Agent 的核心问题不只是“怎么回答得更聪明”,而是“怎么组织它的行为能力”。这时候,技能化的思路就应运而生了。
1.2 技能化到底解决了什么
agent-skills 的出发点很朴素:把 Agent 能做的事,像乐高积木一样拆成一个个标准颗粒,需要哪个就拼哪个,不用的就不让它出现在上下文中。
这里的“技能”包括几个层次:有简单的函数级别技能,比如计算、格式转换、字符串处理;有工具调用类技能,比如查天气、查库存、发消息;也有复杂的组合类技能,比如“整理周报”,内部可能要调用数据查询、文本摘要、格式排版等多个步骤。但无论哪一层,它们都遵循同一套接口规范,Agent 不需要关心内部实现,只需要知道“这个技能叫什么、有什么用、需要什么参数”。
这个设计带来三个直接收益。第一,上下文可以被精确控制。需要用到哪些技能,就把对应描述注入到 Prompt 里,其他技能完全静默,这就从根源上缓解了 Prompt 过长带来的意图漂移。第二,技能变成了一等公民,可以独立编写、独立测试、独立发布。一个团队完全可以分成技能开发组和智能体业务组,两组之间只通过技能接口契约沟通,开发效率提升非常明显。第三,能力真正开始沉淀。每做一个新项目,你的技能库里都会多出几个新的通用技能,下一次同类需求直接从库里选就行,而不是从零开发。
我自己的体会是,技能化最大的价值不在于某一个技能的编写,而在于它帮你把“Agent 应用”和“Agent 能力”这两个概念彻底解耦了。应用会变、场景会变,但能力库可以越来越厚,这才是 agent-skills 长期价值的核心。恰恰是这个拐点,决定了 Agent 从实验室玩具走向工程化落地时,选哪条路可以走得更远。
2. 技能库设计:agent-skills 的核心抽象与调度逻辑
2.1 技能的接口契约长什么样
在动手写代码之前,得先定义一个清晰的技能接口契约。我见过很多失败的例子,就是因为技能接口没定义好,导致后面每加一个技能都要为它单独写适配代码。好的技能接口应该包含这几部分:技能名称、描述信息、参数 Schema、执行函数、依赖声明。
其中最重要的是描述信息。很多人会觉得执行函数才是核心,其实在 agent-skills 体系里,模型能不能正确调用你的技能,百分之六十以上取决于描述写得好不好。描述要让模型清楚三件事:这个技能是用来干什么的;什么时候应该使用它;使用时需要注意什么边界条件。我举个例子,下面这个技能描述就写得很糟糕:
技能:天气查询 描述:查询天气 参数:城市名模型看到这种描述,根本不知道它内部做了几天的预报、是否需要 GPS 坐标、支持哪些城市。结果就是要么不调用,要么拿着用户的一句话强行调用然后就报错。更好的写法是这样的:
技能:weather_query 描述:查询指定城市未来三天的天气预报信息。当用户询问天气、气温、降水、风力等情况时使用。支持国内主要城市,参数使用标准城市中文名称。如果用户未指定城市,必须询问用户后再调用。 参数: - 城市 (city_name, string, 必填) - 日期 (date, string, 可选, 格式YYYY-MM-DD,默认当天)这个描述把触发条件、支持范围、参数格式、边界情况全说清楚了,模型在调用时就不容易犯模糊错误。我建议在定义技能时,把描述模块当作一项正式工作来做,而不是随手写一句话。一个技能描述写得好不好,直接决定你的技能命中率。
除了描述之外,参数 Schema 也很重要。强烈建议使用 JSON Schema 格式来定义参数,这样既能给模型明确的参数结构,又能在运行时做完整的参数校验,避免模型输出一个缺少必填字段的调用请求。执行函数的部分只需要保证输入输出都是纯 JSON 即可,内部可以调用 HTTP API、读文件、操作数据库,但对外只暴露一个干净的 JSON 边界。
2.2 注册表与动态发现机制
有了技能接口之后,还要考虑“技能从哪来、模型怎么知道有哪些技能”。这一步我把它分成两个场景:静态场景和动态场景。
静态场景比较简单,项目里技能数量少,比如十几个以内,可以直接在代码启动时把所有技能加载到一个全局技能注册表中,然后把技能名称和描述拼进 System Prompt。这是最稳妥的做法,所有技能都会暴露给模型,调用路径最短,调试也直观,缺点是上下文会随着技能数增长而膨胀。
动态场景就复杂一些,适用于技能数量较大、或者技能本身是插件化动态加载的情况。这时候,你不能把全部技能描述都塞给模型,而是需要做两件事:一是技能注册表支持按需注册和查询,二是有一个路由层负责在模型决策前先根据用户意图去筛选候选技能。我采取过的做法是给每个技能维护一个标签列表,包含功能分类、适用场景、以及若干关键词。当用户消息进来时,先用一层轻量分类模型或简单的向量相似度检索,把相关性最高的一批技能提取出来,只把这些技能的描述注入到 Prompt 中的工具列表里。
还有一个容易被忽略的环节:动态发现后的“版本一致性”。技能是动态加载的,但模型生成调用请求时,参数结构必须与当前加载的版本完全一致。我曾经踩过一种坑,同一套技能代码,旧版本参数是city_name,新版本改成了city,但注册表缓存还在用旧版本描述。结果模型生成了新字段,运行时代码却去找旧字段,导致运行时参数校验失败。现在我的做法是,所有技能加载时强制校验版本号,任何变更必须同步更新注册表,不允许出现新旧字段混用,这个约定在多人协作时尤其重要。
2.3 上下文窗口与技能加载策略
技能数量一多,AI 上下文窗口就是必争之地。一个技能描述平均要占 100 到 200 token,如果注册表里有 50 个技能,光技能描述就要吃掉近 1 万 token,还没算上聊天历史和业务数据。这显然不可持续,所以必须制定加载策略。
我常用的策略分三层:常驻层、候选层、隐式层。常驻层是所有核心决策技能,无论如何都要出现在上下文中,这个集合尽量控制在 5 到 8 个。候选层是当前用户意图相关的技能,前面讲的动态检索就是这一层在做的事,数量控制在 3 到 5 个。隐式层指的是那些不需要模型记住、只需要运行时能调用的内部函数,比如日志记录、数据落库、格式清理,这些完全不进 Prompt,由主流程直接调用。
这样的分层设计,既保证了模型做决策时有足够的信息,又不至于让上下文被技能描述挤占。实测下来,同样的任务,分层加载比全量加载在响应 token 上平均节省了 30% 到 40%,而且因为输入噪声少了,调用的精准度反而更高。需要注意的是,常驻层的技能列表不是一成不变的,要根据业务发展趋势定期复盘。我自己每两周会拉一次模型调用日志,统计哪些技能是高频的、哪些几乎没有被调用过,然后调整分层策略。你可能会发现,有些你认为很重要的技能,实际上从来没有被触发过,反而是某些边缘技能高频使用,这时候就要果断调整常驻集合。
3. 实操记录:从零搭建一个可用的 agent-skills 技能库
3.1 基础架构与目录结构
前面理论聊得不少,这一节我直接分享一套可以照着落的代码结构。为了不依赖特定框架,我用纯 Python 做示例,但你完全可以用 TypeScript、Java 等语言复刻同一套思路。
我习惯的目录组织方式是:把技能库当作一个独立包来管理,每个技能独占一个模块文件,对外暴露一个统一的register(registry)方法。
agent_skills/ ├── core/ │ ├── registry.py # 技能注册表 │ ├── schema.py # 参数Schema定义与校验 │ └── skill.py # 技能的抽象基类 ├── skills/ │ ├── weather_query.py # 天气查询技能 │ ├── remind.py # 定时提醒技能 │ ├── report_weekly.py # 周报生成技能 │ └── __init__.py # 自动扫描注册 ├── loader.py # 技能加载器(分层策略) └── tests/核心抽象基类skill.py大致长这样:
# core/skill.py from abc import ABC, abstractmethod from typing import Any, Dict, List class BaseSkill(ABC): name: str # 唯一技能名 description: str # 给模型看的描述 tags: List[str] # 检索标签 version: str # 版本号 param_schema: Dict[str, Any] # JSON Schema @abstractmethod def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: """执行技能逻辑,以JSON格式返回结果""" pass其实就这么简单。每个技能只需要继承这个基类,完成execute方法的实现。注册表registry.py则维护一个字典,提供register_skill、get_skill、list_skills三个基础方法,再加一个从文件夹自动扫描加载的能力,方便新技能写完后直接放进skills/目录就被自动注册。
3.2 编写第一个技能模块
我拿一个实际用过的“定时提醒”技能当例子,完整走一遍。
场景需求是:用户可以让我在某个时间点提醒他做某件事。这个技能既要解析自然语言里的时间,又要处理提醒触发逻辑。在 agent-skills 的设计下,我只需要把“解析时间”和“执行提醒”封装在技能内部,模型负责把用户的自然语言对应到参数上。
# skills/remind.py from core.skill import BaseSkill class RemindSkill(BaseSkill): name = "remind" version = "1.0.0" description = "创建定时提醒任务。当用户要求在今天或未来某个时间提醒他时使用,参数task为提醒内容,remind_time为ISO 8601格式时间字符串。" tags = ["remind", "timer", "todo", "提醒"] param_schema = { "type": "object", "properties": { "task": {"type": "string", "description": "提醒内容"}, "remind_time": {"type": "string", "description": "ISO 8601格式时间,如2025-06-01T09:00:00"} }, "required": ["task", "remind_time"] } def execute(self, params): # 实际逻辑:写入提醒任务表,并启动定时检查 task, remind_time = params["task"], params["remind_time"] # 落库、创建调度任务等操作略 return {"status": "scheduled", "task": task, "remind_time": remind_time}写好这个模块后,在skills/__init__.py里加上一行导入,技能库启动时就会把它扫描进注册表。做完这些,模型就能在聊天中收到这样的工具定义,并根据用户的“下午三点提醒我给客户回电话”生成一个调用请求,把task设成“给客户回电话”,remind_time设成当天下午 15:00 的 ISO 时间。
这里有个实操细节:提醒时间一定要让模型输出标准格式时间,最好是 ISO 8601,而不是“下午三点”这种中文描述。因为模型生成的是字符串,后续代码解析越是标准化越不容易出错。如果条件允许,可以再加一层参数后处理,允许模型传相对时间(比如“一小时后”),然后在技能内部做转换。不过建议第一版先让模型输出标准时间,减少解析复杂度。
3.3 技能编排与组合调用
单技能写好之后,真正的复杂度在“多技能组合”。一个复杂的用户请求往往要调用多个技能才能完成。比如用户说“帮我整理一下本周的项目进展并发给团队”,这个需求至少涉及查询任务数据、生成摘要、查找团队成员邮箱、发送消息四个步骤。
最简单也最稳的组合方式,是自己在主流程里做硬编排:定义好步骤顺序,按顺序调用技能。这种方式的优点是逻辑透明、易于调试和兜底,缺点是每接入一个应用场景都要写一份编排代码。
更进阶的方式是利用模型做动态规划。把技能列表暴露给模型,让它自己决定调用顺序和参数传递。这种方式的灵活性高,但需要非常强的容错设计,因为模型可能跳步、可能传错参数、可能在一个技能失败后不知道如何恢复。我自己的实践是,在 agent-skills 体系里同时保留这两种方式,用技能描述里的一个requires字段来声明跨技能依赖,比如“周报生成”技能声明依赖“数据查询”技能,模型在规划时会优先找到依赖项并串联执行。
另外,组合调用场景下,技能之间的输出传递也要约定清楚。我规定所有技能执行结果必须返回结构化 JSON,并且至少要包含status和data两个字段。status标记成功、失败或部分成功,data内是具体的业务数据。这样主流程在判断下一步时,可以统一根据status做分支,而不是去解析千奇百怪的错误文本。
组合技能还有一个容易忽略的坑:幂等性。尤其在面对重复调用时,一个技能如果执行了两次,会不会产生两条重复的提醒、两封重复的邮件?如果有副作用,就要在技能定义里明确标注“非幂等,需要调用方保证只执行一次”,同时在上层做一个执行去重机制。我自己就在发送通知的技能上吃过亏,模型因为生成太慢重试了一次,结果用户收到两条一模一样的消息,从那以后所有写操作类技能我都要求做幂等控制。
4. 踩坑实录:agent-skills 的常见问题与排查方法
4.1 技能命名冲突与命名空间管理
技能数量从十几个增长到几十个的时候,第一个撞上的就是命名冲突。不同开发者可能各自写了“发送邮件”技能,一个叫send_email,另一个叫email_send,功能相近但实现不同。更严重的是同名技能互相覆盖,注册表里后加载的会把先加载的顶掉,而模型看到的还是同一个名字,生成的调用请求语义就完全跑偏了。
我建议从一开始就引入命名空间前缀,比如公司级技能统一用acme_前缀,项目级技能用proj_前缀,个人实验技能用exp_前缀。虽然命名会变得长一些,但换来的是全局唯一性和可追溯性。同时注册表里要做“重复注册检测”,一旦发现同名技能,直接报错并暴露两个冲突模块的信息,而不是静默覆盖。这个检测逻辑看起来不起眼,实战里救了我好几次。
还有一点:技能间如果有调用关系,引用名称一定要用全名,不要用简写,否则在分布式或插件化部署时很容易出现找不到依赖的问题。
4.2 上下文污染与记忆管理
技能执行过程中会产生中间结果,这些结果要不要放回对话上下文里,是个需要谨慎控制的问题。我之前遇到一个场景:技能 A 执行后返回了很长一份 JSON 数据,主流程没有做截断,直接把整个数据拼进了下一条模型输入里。结果模型在后续决策时被这些数据细节带偏,开始模仿 JSON 结构输出,生成的内容一团糟。
后来我定了一个规则:技能返回数据在注入上下文前,必须经过“提炼”处理。提炼可以是截断到指定长度、转换成自然语言摘要、或者提取核心字段。比如查询订单列表返回了 200 条记录,注入上下文前只保留前 5 条和总条数、热门品类等聚合指标,模型关心的数据足够做决策就好,不需要看到全部明细。这个规则让输出的稳定性提升非常明显。
记忆管理的另一面是历史消息里的技能调用记录。模型为了生成下一步动作,可能会去“回忆”前面某次的技能调用细节,但那些细节已经过时了。我的做法是在每次技能执行完成后,把执行结果和当时的用户意图合并成一条摘要记忆,放在单独的 memory 区,干净的上下文只保留当前轮次的必要信息。这样做的好处是既保留了技能执行的关键信息,又不会让历史缓存拖慢推理速度。
4.3 技能不命中:为什么模型就是不调用你的技能
这是整个 agent-skills 落地过程中最让人头疼的问题。明明技能已经注册了,模型却频繁不用,或者干脆直接根据自己的常识来回答。排查这个问题,我把经验总结成一个检查表,按顺序排查:
第一,技能描述是否精准匹配用户表述。如果你的技能描述里写的是“查询股票行情”,而用户的表达习惯是“看看今天股票涨了没”,模型可能意识不到这是同一件事。要让技能描述的语料风格尽量贴近用户的自然表达习惯,必要时从历史对话里抽取高频问法,充实到描述里。
第二,技能是否被加载进当前上下文。如果你实现了动态加载,但当前轮的候选技能筛选没有覆盖到目标技能,那模型再聪明也调不到。我遇到过很多次,问题不在模型,而在检索层的召回条件写窄了。排查时打印出每一轮实际注入的技能列表,一眼就能看出来。
第三,参数是否可以轻松从对话中提取。有时候模型想调用,但你要求的参数它很难从用户话里提取到,比如你要一个 user_id,但用户从头到尾只提了手机号,又没有相关映射信息,模型就只能放弃调用转而乱猜或者给一句“暂时无法处理”。解决方案是降低技能对信息的提取要求,或者增加一个默认值机制,无法确定的参数不要去死磕,允许技能内部再向用户追问。
第四,多个相似技能互相干扰。如果两个技能功能重叠,模型在决策时会产生犹豫,随机选择一个甚至来回切换。这就要从技能设计层面做整合,或者给每个技能增加更明确的适用边界描述。
我把排查时最常用的几个操作整理成一个速查表,方便大家遇到问题时按图索骥。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 模型完全不调用技能 | 技能描述质量差 / 技能未加载 | 检查加载日志,重写描述 |
| 模型调用了错误的技能 | 相似技能互相干扰 | 收敛技能边界,增加排他说明 |
| 模型生成了非法参数 | Schema 定义与实际调用不一致 | 用 JSON Schema 做运行时校验并反馈给模型 |
| 技能执行成功但模型没引用结果 | 返回结果未正确注入上下文 | 检查结果提炼逻辑,确认注入字段 |
| 调用超时 | 技能执行链路过长 | 拆分技能,增加超时兜底 |
这个表并不是面面俱到,但覆盖了绝大多数会直接导致业务异常的场景。每次排查完,我都会顺手给技能库补一条日志监控项,把相关指标加进监控看板,避免下一次踩同一个坑。
最后再分享一个我觉得对整个技能库长期迭代帮助最大的动作:建一个技能使用率的周报机制。每周回顾一次所有技能的调用次数、成功率、平均耗时和用户反馈,定期把不常用的技能下线或合并,把高频技能做重点优化。这个习惯坚持下来后,我的技能库不仅没有越来越臃肿,反而越来越精练,每次项目启动时的接入成本都大幅降低。如果你正在构建自己的 Agent 技能体系,我建议从第一天就把这个复盘机制设计进去,它带来的长期价值会远超你的预期。