前阵子我接手了一个客服场景的Agent项目,一开始的写法很朴素:把所有业务规则、角色人设、工具调用说明、话术模板全部塞进一段超长System Prompt里。前几次效果还行,但业务一变,整段Prompt就得重新擦写,而且每次改动都提心吊胆,因为根本说不清是改了哪句话才让效果变好或者变坏。后来我把这套系统推倒重来,换成了以“技能(skills)”为核心的架构,这就是agent-skills这个项目的由来。
如果你也在做Agent,大概率遇到过同样的问题:模型不是不聪明,而是我们把所有东西都混成一锅粥,导致它不知道该在什么时机做什么。agent-skills的思路很简单——把Agent能执行的每一个动作、每一项知识调用,都封装成一个独立的、可描述的、可测试的“技能”,让模型像“挑工具”一样去选择合适的技能来完成目标。这篇文章我就从为什么需要技能化、技能体系怎么设计、如何从零搭建、怎么调试避坑、怎么扩展复用这五条线展开,都是实际跑过之后的经验总结,适合正在搭建业务型Agent的开发者、甲方项目经理,以及想系统化理解Agent工作方式的产品同学。
1. 为什么Agent需要“技能”而不是“指令”
1.1 一段让我推翻重来的经历
最早那个客服Agent,我给它写了一个“全能提示词”:你是一个客服助手,你要热情、专业,遇到退款问题要查看订单、遇到物流延迟要安抚用户、遇到库存不足要推荐替代品……每个规则我都写得很清楚,但是模型的表现始终忽上忽下。有一次业务方要求把退货窗口从7天改成15天,我改了四个关联段落,结果用户问“能不能退”时,模型又开始背诵旧的退货政策。
问题出在哪?不是因为模型笨,而是因为“指令”本质上是一次性的、模糊的、上下文耦合的。模型没有一套可以独立选择、独立调用、独立验证的动作单元,它只能靠对整段文本的理解力去猜该做什么。猜对了是运气,猜不对才是常态。
1.2 技能与指令的核心区别
“技能”和“指令”听起来差不多,但设计逻辑差别很大。我用一张表格来说明:
| 维度 | 传统指令 | 技能化 |
|---|---|---|
| 存在形式 | 散落在提示词中的文字描述 | 独立的代码模块/配置单元 |
| 触发方式 | 模型根据上下文“领悟” | 模型通过技能描述主动选择调用 |
| 可测试性 | 几乎无法单独测试 | 可为每个技能写独立用例 |
| 可复用性 | 换个场景基本作废 | 可跨项目打包复用 |
| 可扩展性 | 新增规则需重写大段提示词 | 新增技能不影响既有能力 |
技能化的本质,是把“大模型即时推理”和“确定性执行”分开。模型负责理解用户意图、做判断、选技能;技能负责把决策落到可执行的动作上。这样既保留了模型的灵活度,又获得了确定性系统的稳定性。
1.3 技能化的直接收益
技能化之后,我在这个客服项目里立刻看到了三个变化。第一是可测试:每个技能都能单独喂测试数据验证,比如“查询订单状态”这个技能,我可以准备20条不同表述的用户问题,看模型能否正确选择并解析出参数。第二是可组合:一个“处理退货”技能可以内部依次调用“查询订单”“校验退货资格”“生成退货单”三个子技能,逻辑清晰。第三是可追溯:每次调用技能都有日志,模型为什么选了这个技能、参数是什么、结果对不对,一目了然,出了事故排障也要快得多。
一句话总结:技能化就是给Agent建一本“菜单”,模型不用背诵整个菜谱,只需要根据现状选菜下单。
2. 一套技能体系应该包含的核心模块
技能化不是简单地把函数列出名字,真正能跑起来、能维护的技能体系,至少要包含四个核心模块:技能描述、技能实现、技能注册与发现、技能评估与沙箱。少了任何一个,后面都会踩大坑。
2.1 技能描述:Agent怎么知道该调用你
技能描述(Manifest)是技能体系里最容易做糊弄的部分,但它恰恰是最关键的。模型是靠描述做“工具选择”的,一句话描述或者一份结构化的Schema,直接决定了模型在正确场景下能不能想起这个技能。一个合格描述要包含:技能名称、一句话说明、适用场景列举、输入参数定义、输出结构约束。
我通常用一个统一的YAML结构来定义技能描述:
name: check_return_eligibility description: 校验订单是否满足退货条件,适用于用户咨询退货资格、退货期限、订单是否在可退范围内。 params: order_id: type: string required: true description: 用户订单号,一般由用户在对话中提供 apply_date: type: string required: false description: 申请日期,默认取当天 returns: eligible: type: boolean description: 是否可退 reason: type: string description: 不可退时的原因说明这份描述里的“适用场景列举”非常重要,它相当于给模型划定了这个技能的使用边界。很多团队只写一句话“校验退货资格”,模型在遇到退货期限、退货费用、特殊商品等衍生问题时就会犹豫,或者干脆瞎选别的技能。
2.2 技能实现层:真正执行动作的代码
技能描述是“说明书”,技能实现层才是“执行的手”。实现层就是普通的Python函数,遵循一个约定:输入是参数对象,输出是结构化JSON。函数内部可以做任何事——查数据库、调第三方接口、算折扣、组装话术,但对上层只暴露沙箱化的入参和出参。
def check_return_eligibility(order_id: str, apply_date: str) -> dict: order = get_order(order_id) if not order: return {"eligible": False, "reason": "订单不存在"} days = (parse_date(apply_date) - order.paid_at).days if days > order.max_return_days: return {"eligible": False, "reason": f"已超过退货期限{order.max_return_days}天"} return {"eligible": True, "reason": ""}这里有个容易被忽略的原则:函数签名就是技能边界。不要在技能函数里做超出描述范围的事情,比如“校验退货资格”的技能就不要顺手把退款金额算了,那应该让另一个技能去做。边界越干净,模型越好选择,测试也越好写。
2.3 技能注册与发现机制
技能不是写了一个个函数就完事了,还需要一套注册机制让系统知道“我有哪些技能可用”。注册表负责技能描述的统一维护、版本管理、参数校验规则登记和启用/停用开关。运行时发现机制则负责在每一轮对话中,把当前可用的技能描述列表打包喂给模型,供其选择。
我习惯的做法是写一个轻量加载器,启动时扫描技能目录,建立名称到实现函数的映射:
def load_skills(skills_dir: str): skills = {} for manifest_file in Path(skills_dir).glob("**/skill.yaml"): manifest = yaml.safe_load(manifest_file.read_text()) module_path = manifest.pop("module") func = import_module(module_path).execute skills[manifest["name"]] = Skill(manifest=manifest, executor=func) return skills注册表是技能的“户口簿”,让每个技能都有归属、有版本、有状态。没有注册表的技能库,跑不了多久就会变成一屋子找不到东西的仓库。
2.4 技能评估与沙箱
最后一个核心模块是评估与沙箱,这是很多项目草草略过的部分。每个技能除了实现代码,还要配一份测试用例文件,覆盖典型场景、边界场景和异常场景。比如“校验退货资格”的测试用例至少要有:订单不存在、已过退货期、特殊商品不可退、正常可退等。测试的重点不仅是功能正确性,也包括“模型是否能在正确场景选择这个技能”的选择正确率。
沙箱的概念则更严格:技能运行时必须有独立的执行环境,不能直接改生产数据,不能访问无关系统。落地做法有两种,轻量级的是在函数层做权限拦截和参数校验,重量级的是把技能部署到独立容器。我一般建议先做函数层沙箱,等技能数量多了再升级到容器隔离。
3. 从零搭建一套Agent技能库的实操步骤
有了整体认知,下面讲讲怎么从一个业务需求出发,亲手搭出一套技能库。别急着写代码,先做场景拆解,再做技能定义,最后才到编码和注册。
3.1 先定义你自己的技能边界
拿到一个业务场景,第一件事不是敲键盘,而是把所有高频动作列出来。我以一个典型的“订单售后”场景为例,先列出潜在动作:
| 业务动作 | 是否拆成技能 | 理由 |
|---|---|---|
| 查看用户订单信息 | 是 | 查询类,频率极高,参数清晰 |
| 校验退货资格 | 是 | 规则独立且经常变更 |
| 生成退货单 | 是 | 写操作,需要严格校验 |
| 计算退款金额 | 是 | 涉及多种计价规则 |
| 修改收货地址 | 是 | 独立动作,可单独复用 |
| 发送安抚话术 | 否 | 属于生成式回复,让模型直接写 |
| 转人工客服 | 是 | 有明确条件的分流动作 |
拆分的粒度判断标准很简单:这个动作是否可以被单独描述清楚?是否需要独立的参数和返回结构?是否可能在多个流程中被复用?如果答案都是“是”,就值得拆成技能。反之,如果它只是另一个技能里的一小步,那先不拆,等它被复用两次以上再拆。
3.2 写好一份让模型“不犹豫”的技能描述
技能描述写得好不好,直接看两个指标:在正确场景下模型能否稳定选中;在相似场景下模型能否准确排除。我见过最典型的问题是把描述写得太泛,例如“该技能处理所有与订单相关的问题”,这种描述等于没有描述,因为它把本来就该由其他技能分担的场景全部模糊化了。
好的写法是“正向条件 + 负向条件”结合。举个退款相关的描述对比:
- 差:查询订单信息。适用所有需要了解订单的时候。
- 好:查询订单基础状态信息。适用于用户询问订单物流、金额、商品清单、下单时间。如果用户询问退货资格、退款金额,请改用check_return_eligibility、calc_refund_amount技能。
加了负向条件之后,模型的选择准确率会显著提升。原因很好理解:LLM做工具选择本质上是一个文本匹配任务,你给它更多的“不要调用”信号,它就不会在相似场景里纠结。
3.3 技能注册表与自动发现
有了技能定义,接下来需要一套注册与发现机制,让Agent运行时知道该轮对话可以调用哪些技能。我倾向于用一份集中式注册表,再配合自动扫描目录。集中式注册表的好处是业务方可以直观看到当前有哪些技能、每个技能启没启用、版本是多少。
我的注册表长这样:
skills: - name: check_return_eligibility enabled: true version: 1.2.0 module: order_skills.check_return - name: calc_refund_amount enabled: true version: 1.0.3 module: order_skills.calc_refund运行时的发现机制则是:每一轮对话,系统根据当前业务上下文从注册表里选出候选技能集合,再把候选技能的描述合并进用户的本次请求,等待模型选择调用。注意不要每次都把全部技能喂给模型——技能数量超过20个时,模型的选择准确率会明显下降,所以需要一层“粗筛”,先按业务场景缩小候选范围。
3.4 冷启动时的人工模拟测试
技能库搭好初期,大概率还没有真实业务流量,这时候不能干等,可以用“模拟器客户端”把技能调用链完整跑一遍。模拟器的套路是:准备一批模拟对话记录,每一轮都让模型走完整的“意图识别→选技能→传参→执行→生成回复”链路,然后将技能选择结果与人工标注结果比对。
我自己常用的做法是写一个简单的回放脚本,喂入用户消息和当时的候选技能列表,记录模型选中的技能名称,再跟标注结果比对,自动产出选择准确率。这一步能帮你在上线前发现大量“技能边界不清晰”“描述太相似导致混淆”的问题,成本远低于上线后修事故。
4. 让技能真正可靠:调试策略与避坑经验
4.1 最常见的坑:模型选了一个“看起来对”但语义不符的技能
技能化之后,最常遇到的故障不是代码报错,而是“技能选择错误”——模型调用了一个名称语义沾边、但实际不该用的技能。比如用户问“能退多少钱”,模型去调“查询订单信息”,虽然拿到了订单,却没有算退款金额,导致最后答非所问。
我排查这类问题的第一步,是把用户原话和当时的技能候选列表一起打出来,看模型是怎么“误解”的。多数情况下,问题出在技能描述里缺少“可替换选项的对比信号”。解决手段就是在那份“好描述”里加负向条件:如果用户询问退款金额,请调用calc_refund_amount,不要调用本技能。
另一个太容易犯的错误是两个技能描述高度相似,比如“查询订单金额”和“计算退款金额”,表面看都跟钱有关,但一个是读数据,一个是按规则计算。我会专门做一张“技能冲突矩阵”,把语义相近的技能两两列出,写明区别点,再把这些区别点各写进各自的描述中。这方法笨,但非常有效。
4.2 参数校验与安全边界
模型传参不可信,这是一个铁律。模型可能会把“去年”解析成奇怪的年月日,也可能会漏传订单号,甚至会把用户的一句牢骚当参数传进技能。因此,每个技能入口都需要做严格的参数校验,不能直接把模型返回的参数透传给数据库或接口。
我用JSON Schema做统一校验,这比手写一堆if判断要规范得多:
from jsonschema import validate schema = { "type": "object", "properties": { "order_id": {"type": "string", "minLength": 8}, "apply_date": {"type": "string", "format": "date"} }, "required": ["order_id"] } validate(instance=params, schema=schema)校验不通过时,技能系统应该返回一个明确错误,提示模型“参数不完整,需要补充订单号”,让模型有机会进行澄清追问。这比直接把异常抛出去要好,因为对话场景里,一次合格的追问就能化解掉参数缺失问题。
4.3 技能的可观测性:每一次调用都要留下痕迹
技能体系的迭代完全依赖数据,没有调用日志就没有优化依据。我要求每个技能调用都必须记录:技能名称、入参、出参、执行时长、调用的模型名称和版本、token消耗、用户原始消息、模型选择该技能时的推理理由(如果可获取)。这些数据沉淀下来,是后续做评估、做回归测试、做描述优化的原料库。
我的日志采用统一结构:
log_entry = { "skill": "check_return_eligibility", "params": params, "result": result, "latency_ms": 218, "model": "cloud-llm-v2", "user_msg": "我上月买的东西还能退吗", }没有这套日志,当你发现某个技能选择率下降时,会完全无从下手。有了日志,你可以定位是描述歧义、是参数质量问题、还是模型更新导致的选择偏好变化。
4.4 技能回归测试与版本管理
技能改了一版描述或者改了一段逻辑,有没有可能影响其他技能?答案是可能,而且经常是悄无声息的影响。所以我给技能库配了一套轻量回归测试:每次改动后,跑一遍历史标注集,看技能选择准确率是否下降、执行成功率是否变化。这个回归集不用很大,几百条覆盖主要场景的记录就足够发现退化。
版本管理上,我遵循一条原则:技能描述和技能代码一起打版本,不允许“代码升级了、描述没更新”或反过来。因为技能描述是模型选择它的依据,如果描述与实现不一致,模型就会照着旧说明书用新工具,出问题只是时间问题。
5. 技能复用的实战扩展
5.1 从一个项目到多个项目:技能打包与分发
技能化的另一个隐藏红利是跨项目复用。我在客服项目里沉淀的“订单查询”“资格校验”等技能,到了另一个售前咨询项目里,直接打包成独立技能包接入使用,省去了大量重复开发。
打包的思路是:技能目录是一个独立Python包,内含技能描述文件、实现模块、测试用例、README说明。分发则通过私有制品库(如内部pip源或镜像仓库)进行,各项目按需安装,运行时通过注册表注册即可。关键是“技能包版本”和“依赖的Agent框架版本”要绑定,避免接口不一致。
5.2 把技能组合成新的能力
技能除了单个调用,还可以编排成更复杂的工作流。比如一个“退货全流程处理”高级技能,内部依次调用“查订单”“校验资格”“算退款金额”“生成退货单”“发通知”五个子技能。这种组合编排可以在代码层实现,用简单管线即可:
def process_return(order_id: str, apply_date: str) -> dict: order = check_return_eligibility(order_id, apply_date) if not order["eligible"]: return {"status": "rejected", "reason": order["reason"]} refund = calc_refund_amount(order_id) ticket = create_return_order(order_id, refund["amount"]) notify_customer(order_id, ticket["no"]) return {"status": "done", "refund": refund["amount"]}组合技能的收益是让Agent平台的能力从“能执行单一动作”升级为“能完成完整业务需求”,同时仍然保留每个子技能的独立复用性。模型只需要决定调用“退货全流程处理”这一高层技能,细节由编排层接管,既降低了误召风险,也减少了token开销。
5.3 给技能加“自我改进”钩子
把每次技能调用的反馈数据收集起来,还能形成一条自我改进链路。最实用的做法是给每个技能定义“成功标准”:查询类技能看“模型选择正确率”,写操作类技能看“业务执行成功率和用户后续反馈”。每天用这些指标生成报表,发现哪个技能突然变差,就把它策略性地从候选列表里摘除,修复后再放回,而不是让它在线上持续带病运行。
更进一步的“自动改进”方案是在历史反馈数据上定期重新评估技能描述——用新的描述版本在旧数据集上做离线选择测试,指标提升才允许上线。这个流程目前我还没有做到全自动,但手工版本已经能带来明显的稳定效果:逻辑上更清晰,兜底策略更可靠。
写在最后,我最直观的感受是:模型永远会更新,但技能库是你在模型之上沉淀下来的最有价值的稳定资产。把业务的确定性部分用技能固定住,把开放式的判断交给模型,这种分工已经成了我做Agent项目的基本盘。最后分享一个小技巧:每写一个新技能,先写一份它“在什么场景下不该被调用”的说明,再写正常描述;先划清禁区,再扩大领地,这个技能在复杂场景里才能扛得住考验。