最近一段时间,我一直在折腾AI Agent的可复用性。做过的Demo不算少:文档批量处理、信息抽取、报告生成……但真正的痛点不是“能不能跑通”,而是“换个场景就得重写”。提示词改了又改,工具函数越堆越多,每次新项目都像从零开始。后来我把思路整体换了一下,把Agent的能力抽成独立“技能”模块,才真正体会到什么叫“可复用超能力”。这套东西业内有个叫法——Agent Skills,本质上是一套模块化系统,让Agent具备某项完整能力时,不再靠散落的提示词和零散函数,而是以技能块为单位进行封装、注册、调度和复用。
这篇文章我会按照自己的实战踩坑路径来写,适合正在做Agent应用、却总觉得“每次都在重复造轮子”的开发者,也适合刚接触Agent、想找到一套更工程化组织方式的入门者。我会把为什么提示词复用不彻底、为什么工具函数不够用、Agent Skills到底封装了什么、具体怎么实现,以及我反复踩过的几个坑一次讲清楚。
1. 为什么Agent的能力总是“用完就丢”:从提示词复用谈起
1.1 提示词复用的天花板
大多数人做Agent的第一步是写提示词。提示词写得好不好,确实决定了Agent的初版表现。可问题也恰恰出在这里——提示词本质上是一段“面向大模型的指令文本”,它的复用能力非常有限。比如我需要让Agent处理PDF发票,第一次写提示词时包含了字段说明、格式约定、多种发票版式示例。这套提示词换到合同场景,即使字段相似,也得大改一遍;更麻烦的是,一旦提示词超过一定长度,模型对细节的关注会下降,排在后面的示例常常被“淡忘”。
这一现象背后是上下文注意力分配的物理限制。当前主流大模型对长上下文的处理是近因加权的,新内容比旧内容更占注意力;当提示词里塞满各种任务约束和历史示例时,真正关键的指令反而容易被稀释。所以提示词复用只能是低层次复用,它复用的是“话术”,不是“能力”。
有人会说自己维护一份“提示词库”,需要时复制粘贴到新上下文里。这确实省了一些字面功夫,但也只是省了打字时间。真正的问题是没有任何机制保证这份提示词对应的行为是稳定的:模型版本换一下、输入格式微变一下,行为就漂移了。而且提示词里没有“状态”概念,Agent不知道该技能上一次运行到什么阶段,也无法处理多轮任务中的临时结果。我在多个项目里验证下来,纯靠提示词复用的Agent,换场景后的成功率衰减非常明显。
1.2 工具函数的局限不止于状态
既然提示词不够用,很多人会转向函数调用,给Agent挂一堆工具函数。比如一个query_document接口、一个extract_invoice接口。函数调用让Agent可以落地操作,但说实话,工具函数和“技能”是两种东西。
工具函数是无状态的单点动作。它接收几个参数,返回一个结果,没有前置条件,没有内部流程,更不知道自己被调了几次、和什么任务关联。你的Agent要完成“读取合同、提取关键条款、校验金额、生成摘要”这一串动作时,工具函数本身并不知道该怎么串联,引导串联的责任又落回到提示词和主逻辑上。
更麻烦的是,工具函数没有正交性时很容易互相打架。驱动到一半发现还需要另外一个工具的结果,任务就断了,Agent只能在函数返回的错误信息里猜下一步。我见过很多项目把十几个工具函数全部塞进系统提示词里,结果模型每次只调用那两三个描述最长的函数,其余的都成了摆设。工具函数适合做原子能力,但它承接不了“完整任务”的封装。
1.3 Agent真正缺少的是“任务处理流程”
把前面两段放在一起看,结论就很清晰了:Agent要稳定完成一件事,缺的不是“一段话”也不是“一个函数”,而是“一套流程”。流程里有执行步骤、有中间状态、有异常处理、有成功判据。就像人学会开车不是记住一两句口诀,而是身体里形成了一套完整的操作流程,遇到什么路况怎么处置都有章法。
Agent Skills恰恰是冲着这个缺口来的。它把一类任务的处理流程封装成一个独立模块,这个模块对上暴露清晰的调用接口,对下包含具体的执行逻辑,同时保留必要的内部状态。这样Agent在运行时看到的是“我有哪些技能、各负责什么、怎么调用”,而不是一坨混杂的指令和函数列表。这是模块化复用和文本复用在本质上的区别。
2. Agent Skills把“能力”变成独立交付物:核心设计观察
2.1 一个技能模块的组成
我理解的Agent Skill,至少要有五部分:
- 接口说明(Skill Specification):技能名称、适用场景、输入参数、输出结构。这一部分是给Agent“看”的,必须写得足够清楚,让模型能判断何时该调用这个技能。
- 执行流程(Execution Logic):技能启动后的具体步骤,可能是调用外部API、解析文件、循环处理数据、调用子模型等。
- 状态管理(State Handling):技能执行过程中产生的中间数据放哪里,多轮任务下怎么保存和恢复。
- 边界控制(Boundary Control):技能内部出错时怎样处理,哪些错误要抛给主流程,哪些错误在技能内自行消化。
- 评估指标(Evaluation):技能完成后如何自检,判断结果是否符合预期。
这五部分不一定每条都要很重,但至少接口说明和执行流程必须有。没有接口说明,Agent不知道何时用;没有执行逻辑,技能就是个空壳。初期我实现时只做了前两项,后来发现没有状态和边界控制,技能一遇到多轮场景就断掉,所以后三项是长期运行才会暴露出必要性的部分。
2.2 技能与工具、提示词、记忆的分工
好多人问我:Agent系统里既有工具又有提示词,再塞一个Skills,是不是重复造轮子?其实它们的边界很清晰。我通常用一句话区分:提示词负责定义模型的行为偏好,工具负责提供原子的操作能力,记忆负责存取过往事实,而技能负责封装“完整任务的执行方案”。
打个比方,提示词像驾驶手册,它指导你文明驾驶;工具像方向盘、油门、刹车这些单独部件;记忆像你的驾驶经验。而技能就像“倒车入库”这个完整动作——你不需要在每次倒库时临时回忆每个部件的操作顺序,而是有一套已经固化的流程直接启用。这样Agent在复杂任务中的决策负担会大大下降,模型只需判断“当前该用哪个技能”,而不是判断“每一小步怎么做”。
2.3 可复用超能力的具象化
“可复用超能力”这个词不是营销话术。当技能被封成独立模块后,它在不同项目之间迁移的成本变得极低。我在一个文档处理Agent里写好了一个“PDF结构化抽取”技能,换到另一个完全不同的知识库问答Agent里时,只需要把技能模块带过去,注册一下,额外微调接口说明即可,效果几乎不变。
这种复用性放到团队里价值更大。不同成员可以各自维护自己擅长领域的技能包:有人维护办公文档技能包,有人维护数据分析技能包,有人维护网络数据获取技能包。Agent项目的组装变成“选技能包”而不是“写代码”。我已经明显感觉到,技能模块一旦积累到一定数量,开发新Agent的节奏完全是另一量级——从十几个工作日压缩到两三天,这还只是我目前积累技能不算多时的体验。
3. 落地一套Skill系统的四个关键机制
3.1 技能描述:让Agent在正确时机调用正确技能
技能描述是所有机制里最容易被低估的。很多人写技能描述时非常随意,写一句“处理文档”,结果Agent在遇到图片时也去调用,调用后又处理不了。我后来把技能描述当成接口文档来写,规定必须包含:触发场景、前置条件、输入输出格式、典型示例、禁用边界。
这里有个小技巧:描述中要写清“不适用的场景”。给模型明确负例反而能提升调用的准确率。比如一个“文档解析”技能描述里写明“本技能不支持图片内容,若输入为.jpg或.png请拒绝执行并说明原因”,Agent就会在图片任务上自动绕过这个技能。这个技巧我最早是从评测数据里发现的——那些带“不适用场景”字段的技能,误调用率大约降了一半。
3.2 技能注册表与版本管理
多个技能并存时,必须有一个注册表来管理技能的名单、版本和依赖关系。我用最简单的数据结构:一个字典,键是技能名,值是技能对象的元信息。注册表至少要维护技能名、描述、版本号、入口函数。版本管理不是可选项,因为技能内部的执行逻辑会迭代,而Agent正在使用的上下文里可能还是旧版本的行为描述,如果不注明版本,排查问题时非常头疼。
我踩过一个具体问题:某次把“表格提取”技能从v1升到v2,改了输出字段命名,但技能描述里忘改版本号。结果Agent按旧字段处理新结果,整整排查了两天。后来我强制规定,技能描述里第一行必须是版本号,主循环在调用前会检查版本一致性,不一致就直接报错而不是静默执行。这个强制约定救了我很多次。
3.3 技能槽:控制Agent当前可用上限
Agent一次任务能同时挂载的技能数量不是越多越好。曾经我把二十多个技能一次性全部注入系统提示词,模型直接“精神分裂”——每次调用决策都变慢,还会频繁调用不相关技能。后来我借鉴了插件系统的“技能槽”概念:为当前任务动态选择少量技能挂载,而不是把所有技能都暴露给Agent。
实现方式不复杂:先根据用户任务描述做一次预筛选,把可能相关的技能筛进技能槽,其余技能冻结。这样Agent上下文里永远只有3到6个可选技能,决策质量明显上升。这个预筛选可以用较小模型做,也可以用简单的关键词匹配,前者更灵活,后者更省钱省时。我目前常用的是混合方案:先关键词粗筛,再用小模型精排,效果已经够用。
3.4 上下文边界:技能内部状态与主对话隔离
技能执行过程中会产生大量中间过程:步骤日志、临时结果、错误堆栈。如果这些全部回灌到主对话,很快会把上下文撑爆,还会干扰Agent的核心判断。我的做法是给每个技能一个独立的执行上下文区,日志写进去,关键产出单独输出,错误信息按等级过滤后只把摘要返回给主对话。
这个“上下文隔离”机制听起来简单,但对稳定性提升非常大。之前我的技能执行日志和Agent思考过程都混在一起,模型经常被日志里的细节带偏,甚至把调试信息当成任务数据。隔离之后,Agent只需要接收“技能完成、产出如下”这种干净消息,判断准确率明显回升。上下文tokens也省了很多,长任务下不容易跑到窗口上限。
4. 从零搭建一个Skill系统的最小闭环
4.1 定义技能基类的设计
最小闭环里,我建议先定义一个非常轻的基类,不要一上来就上框架。可以用Python风格描述,核心就四个方法:check_precondition(检查前置条件)、execute(执行技能)、get_status(获取状态)、cleanup(清理资源)。
基类本身不实现任何具体逻辑,只定义协议。具体技能继承时只需要实现这些方法。这样做的好处是主循环对技能是统一调度的,不需要关心每个技能内部实现差异。
我给出一个参考结构:
class BaseSkill: name = "base" version = "0.0.1" description = "" def check_precondition(self, context) -> bool: raise NotImplementedError def execute(self, context): raise NotImplementedError def get_status(self): return {"state": self.state, "logs": self.logs[-20:]} def cleanup(self): self.state = "idle" self.logs = []这个基类足够应付大部分场景。如果你的Agent要支持多模态,可以再扩展输入输出协议,但最小闭环用这个够了。一个容易忽略的细节:get_status返回的日志要限制条数,否则长期运行的技能会内存膨胀。我一般只保留最近20条。
4.2 实现一个可复用的文档解析技能
以我写得最多的“文档解析”技能为例,它的执行流程分四步:接收文件路径、判断文件类型、提取文本和表格、返回结构化结果。每一步都有明确产出,任一步出错都在技能内部捕获并转成标准错误码。
这里关键点在于结构化结果的定义。我建议所有技能输出统一用JSON格式,并约定顶层字段。这样主循环和后续技能都可以按固定格式消费产出,不会出现“这个技能返回列表、那个技能返回字典”的混乱。
class DocParseSkill(BaseSkill): name = "doc_parse" version = "1.3.0" description = "解析常见办公文档,输出文本块与表格结构;不支持图片输入。" def check_precondition(self, context): return context.get("file_path") is not None def execute(self, context): file_path = context["file_path"] result = {"type": "doc_parse_result", "blocks": [], "tables": []} if file_path.endswith(".pdf"): result["blocks"] = self._extract_pdf(file_path) elif file_path.endswith(".docx"): result["blocks"] = self._extract_docx(file_path) else: raise SkillError("unsupported_file_type", "仅支持pdf/docx格式") return result这个技能写得非常简单,但非常实用。它满足前面说的两个核心条件:输入输出有明确协议,内部有真实计算逻辑。真正上线时,_extract_pdf内部可能是一大段复杂解析代码,但不影响主循环对它的统一调度。
4.3 把技能挂到Agent主循环上
技能定义好之后,主循环只做三件事:维护技能槽、检查前置条件、执行技能并把结果回写到上下文。我习惯用一个注册表字典,再在每轮循环里把技能槽内的技能描述拼接给模型,让模型返回技能调用指令。
主循环的骨架可以这样理解:模型先生成一个结构化决策,是直接回答还是调用技能。如果调用技能,主循环在注册表里找到对应技能对象、校验前置条件、执行、拿结果,再把结果追加到对话历史中继续下一轮。这个过程一直重复,直到模型认为任务完成。
def agent_loop(user_request, registry, skill_slots): messages = [{"role": "user", "content": user_request}] while True: decision = model.decide(messages, registry_index=skill_slots) if decision.action == "answer": return model.final_answer(messages) skill = registry[decision.skill_name] if not skill.check_precondition(decision.context): messages.append({"role": "system", "content": "技能前置条件不满足"}) continue result = skill.execute(decision.context) messages.append({"role": "system", "content": f"[技能产出] {result}"})这里我对decision结构做了简化,实际中你还需要包含参数校验和错误处理,但核心链路就是这个样子。值得提醒的是,技能调用指令最好让模型输出成JSON,而不是自然语言,否则主循环解析会很痛苦。
4.4 运行效果与关键验证
这个最小闭环跑通后,我用一个包含三个技能的场景做了验证:文档解析、表格校验、摘要生成。任务是把一份合同PDF解析出来,校验金额字段,最后生成一段摘要。整个过程Agent只调用了两次技能,其余判断由模型直接完成,任务成功率和手工编写流程时基本持平,但代码量减少了一半以上。
还有一个值得关注的量化指标:上下文tokens消耗。技能隔离后,主对话历史里不再有解析日志,同样任务的平均tokens比之前混杂日志的方案下降了约40%左右。这个数字在不同任务上会浮动,但趋势是一致的。另外,由于技能内部步骤是确定性代码,错误率大大低于让模型一步步“自由发挥”的方式。
5. 技能编排:粒度切分与组合原则
5.1 什么时候该切技能,什么时候该合并
技能粒度是初学最头疼的问题。切太细,Agent需要频繁在技能间切换,反而增加决策负担;切太粗,技能内部耦合一堆分支,几乎不可复用。我给自己的原则是:以“任务完整性”为切分单位,而不是以“操作步骤”为单位。
什么是任务完整性?就是“输入可描述、输出可验证、过程可以自包含”。比如“解析文档”是一个完整任务,“读取PDF的第一页”就不是任务,只是一个步骤。按这个标准切出来的技能,描述容易写清楚,Agent也容易判断何时调用。如果一个技能里出现好多“如果用户要……就……”的分支,说明粒度太粗了,应该拆分。
5.2 技能依赖的处理策略
技能之间有时会有依赖关系。比如数据分析技能依赖前一步的数据清洗成果。处理依赖有两种方式:显式依赖和隐式数据流。显式依赖就是技能A执行完后,把产物写入一个共享区,技能B执行前从共享区读取;隐式数据流则是直接在调用指令中把上一轮结果作为参数传过去。
我更推荐第二种,因为显式依赖容易出现“技能B没有检查数据是否就绪”的问题。隐式数据流要求每次调用技能时都明确传入完整上下文,这样每个技能可以被单独测试,复用时也不需要外部状态支撑。虽然有经验的工程师会觉得显式依赖更“干净”,但在Agent场景里,隐式数据流和主循环的配合天然更好。
5.3 一个组合案例:从文档到结构化数据再到可视化
我举一个实际做过的组合:让Agent读一份市场调研报告,提取表格数据,再生成图表。单用一个超大技能实现会非常脆弱,但拆成三个技能——文档解析、数据抽取、图表生成——之后,每个技能都很好实现,组合时也只靠Agent主循环调度。
组合的关键在于中间结果格式的一致性。文档解析技能输出“结构化文档块”,数据抽取技能消费这些块并输出“表格JSON”,图表生成技能再消费“表格JSON”。三个技能中间产物格式是前后对齐的。只要格式约定到位,任意一个技能都可以被替换成更优实现,而不影响另外两个。这也是我认为技能系统比单体式Agent更适合团队协作的根本原因。
6. 实战中反复踩到的坑与我的纠正
6.1 把提示词当技能,技能越写越虚
第一次设计技能时,我犯了一个典型错误:把一个包含大量提示词的文本包一层函数就称为技能。结果技能描述写了一长串,但执行逻辑基本没有,Agent调用后只是把原问题丢给大模型再生成一段文本。这种“技能”没有提供任何增量价值,行为比直接写提示词更不稳定。
纠正方法很简单:技能里必须有“确定性逻辑”或“外部计算”。哪怕只是文本格式转换、字段校验、API调用这类琐碎的事,只要它实际做了计算或IO,技能就有了不可替代性。纯粹靠大模型生成的步骤,不需要封技能,直接放在主提示词里就行。这个判断标准我后来一直沿用,帮我过滤掉了很多“伪技能”。
6.2 过度封装的代价
和上面那个坑相反,有的技能被封装得太狠,输入输出全是很抽象的协议,内部还包了多层适配。好处是通用性极强,坏处是调试时很难查清楚问题出在哪一层。我有一次为了调试一个“通用数据处理”技能,打了十几层日志才定位到一个字段映射错误。
我的建议是:技能封装的抽象程度要和它的复用频率匹配。一个只在一个项目里用一次的逻辑,不值得抽象;一个会被多个Agent重复调用的技能,才值得多花时间设计协议。不要为了“整洁”而过度设计,工程里最重要的永远是“能快速定位问题”。
6.3 缺少回归评测,技能悄悄退化
技能不是写好就一劳永逸。模型版本升级、底层API变化、输入数据格式漂移,都会让同一个技能表现退化。我曾经有个“实体抽取”技能,上线时准确率不错,三个月后准确率明显下滑,直到业务方反馈才发现。因为我从没给技能建立专用的评测集。
后来我养成了一个习惯:每个核心技能都要配一个20到50条样例的回归集,每次改动技能后跑一遍,对比前后结果的差异。这个习惯救了我好几次,有些看起来“优化了”的改动,实际上把输出格式破坏了,没有回归集根本发现不了。如果你没有时间给每个技能配回归集,至少给调用频率最高的那几个配。
6.4 技能冲突时的优先级判定
多个技能同时可选时,Agent可能选择错误的那个。比如同时有“文档解析”和“网页正文提取”,输入是一个URL时,Agent有概率去调用文档解析技能,因为描述里写了“处理文档”,但URL其实需要网页技能。
我的解决办法是在技能描述中增加“冲突提示”字段,并且在主循环里做一层规则优先:如果两个技能的前置条件同时满足,按描述里显式声明的优先级排序。这个排序本身不需要用大模型,用注册表里维护的优先级数字即可。一旦排好,Agent调用失误率会明显下降。
最后说一点个人体会。Agent Skills这套思路并不是什么全新的理论,它更像是把软件工程里的模块化、接口、封装理念移到了大模型应用层。真正让它发挥作用的不是某一个机制,而是“把能力当成可管理的资产”这个态度。技能模块写多了以后,再开发新Agent时,体会完全不一样——你不再是每次说服模型理解一个全新任务,而是在一堆已经验证过的能力基础上做组合。建议你从一个小技能开始积累,哪怕只是“解析一个文件”“格式化一段数据”,跑通之后自然会发现,值得沉淀的技能远比你想象的多。