“agent-skills”,这个词最近在圈子里出现的频率越来越高。说白了,它不是在讲某个具体模型或者框架,而是一种组织大模型能力的方式:把一个智能体需要执行的复杂任务,拆成一个个可复用、可插拔、有明确边界的“技能模块”。这种做法在项目里落地之后,我发现它对智能体的可控性和开发效率的提升,远比想象中更实在。
这篇文章会围绕一个叫“agent-skills”的技能库设计展开,聊一聊我踩过的坑、总结出来的接口规范、以及一套可以直接抄作业的落地实现。无论你现在只是用提示词拼智能体,还是已经在折腾函数调用和外部工具,这篇文章都能帮你把零零散散的能力“收拾”成一个真正有结构的体系。
1. 先说清楚:agent-skills 到底在解决什么问题
1.1 从“堆功能”到“长技能”
很多人在搭建智能体时,最早的做法就是“堆功能”:让模型能读文件、能查数据库、能发请求、能算数学题,把一堆接口塞进系统提示词里,然后期望模型无所不能。
结果很快就会发现几个问题:提示词越堆越长,模型开始“选择性失明”,该调的工具不调,不该调的乱调;接口定义五花八门,参数风格不统一,模型经常传错格式;想新增一个能力,得小心翼翼地改提示词,生怕影响原有行为。
而“agent-skills”的思路完全不同。它把每个能力当作一个独立的、自包含的“技能”,每个技能都有自己清晰的输入输出、描述、触发条件甚至错误处理。智能体像是一个操作员,它手里拿着一本技能目录,需要做什么动作就从目录里挑一个来执行。这种结构不是技术上的炫技,它是在帮你回答一个最朴素的问题:模型到底在什么情况下、用什么样的方式、去调用什么功能。
1.2 为什么不是简单换个名字
有人可能会说:“这不就换了个名字的工具调用吗?”从实现层面看,确实有很多重叠,但“技能”这个抽象层级比“工具”更接近智能体的行为逻辑。
工具强调的是“函数”,开发者定义好一个接口,模型负责填充参数。技能强调的是“能力”,它不只包含函数签名,还包含这个能力何时适用、前置条件是什么、执行步骤是什么、失败后怎么办。打个比方:工具是“菜刀”,技能是“切菜”。菜刀只定义了刀具本身,而切菜包含了用什么手法、切多厚、怎么避免切到手。
这个差别在复杂业务场景里非常重要。如果一个智能体只是偶尔查个天气,工具够用;但如果它要完成“根据用户描述推荐行程并生成预算表”这种多步骤任务,你就得给它设计“查天气”“规划路线”“估算成本”“生成表格”这一连串互相协作的技能。每个技能都要考虑上下文衔接、参数共享、结果校验。这就是 agent-skills 真正的用武之地。
1.3 技能库的核心收益
把能力抽成技能库之后,我体会最深的好处有三个。
第一,可控性回来了。因为技能的边界清断,模型不会“即兴发挥”跑偏,它只能从我们准备好的目录里选。你不希望它做的操作,直接不提供对应的技能即可。
第二,开发效率提升了。新需求来了以后,不是和提示词搏斗,而是写一个新的技能模块,注册进去,跑通测试,完事。这个模块还能复用到其他项目里。
第三,排查问题变简单了。之前智能体行为异常,你不知道是模型理解问题还是接口返回问题。现在技能库每个环节都有日志,从技能选择到参数传递到结果返回,都能跟踪,定位问题的时间大幅度缩短。
2. 技能库的架构设计:怎么划分一个技能的边界
2.1 技能与工具之间的那条线
在设计技能库的初期,我最常纠结的一个问题是:某个功能到底是作为一个技能,还是一个工具里的子函数。
后来沉淀出一套规则:凡是有独立业务语义、能单独被模型理解和触发的能力,就应该做成一个独立技能。比如“获取当前天气”是一个独立技能,因为它有完整的输入输出和对模型来说清晰的触发语义。而“将该字符串转成小写”这种底层操作不适合作为独立技能,因为它过细,会让模型的选择负担变重。
但这个规则也有灰色地带。举个例子,“获取用户订单信息”,可以是一个技能,也可以拆成“查询订单列表”和“查询订单详情”两个技能。我的经验是:考虑调用成本和上下文损耗。如果一个技能需要多个参数而且经常跟另一个技能联动,就拆开;如果两者几乎总是同时被调用,就合并。这个判断无法完全靠规则,取决于模型的实际表现,需要反复调。
2.2 技能表示的三大件:描述、模式、实现
一个技能在库里,最少要有三样东西才能跑得起来。缺一样,都会出问题。
描述是给模型看的。它决定模型什么时候会选择这个技能。这块是最容易被低估的,很多人随便写一句“获取天气”,结果模型在用户问“明天出门要不要带伞”时根本不会联想到这一步。好的描述应该写出触发场景:什么意图用、输入是什么、输出大概是什么样。
模式是定义参数的。我用 JSON Schema 格式,因为几乎所有模型都天然支持这种结构描述。这里要注意的是参数必须尽量少、尽量扁平,嵌套复杂对象会让模型的填充错误率直线上升。宁可让技能内部的实现去拼装复杂结构,也不要让模型去传一个复杂的嵌套 JSON。
实现是技能背后的实际代码。它负责处理输入、调用外部服务、做计算、最后返回结果。技能实现应该是“防御性”的,要能接受模型传过来的各种不完美输入,比如缺字段、类型不对、空字符串这些情况。
2.3 技能的编排:让技能自己会“用技能”
单技能搞清楚之后,更复杂的问题是:技能之间怎么协作。
我见过两种可行的路子。一种是让模型负责编排,系统提示词里给了技能清单,模型根据对话逻辑依次调用,这一步技能的输出作为下一步技能的输入。另一种是把技能内部再拆成子步骤,用代码把流程固定死,所谓的“脚本化技能”。
两种各有利弊。模型编排灵活,但不可控,偶尔会漏步骤;脚本化稳定,但不灵活,对话稍微偏题就执行不下去。我现在比较常用的折中方案是:高层流程用模型编排,每个具体动作尽量脚本化。比如“制定旅行计划”这个技能,它内部会串起查天气、查交通、查预算这几个子技能,但调用顺序有代码逻辑做兜底,模型只是在中间做决策,而不是完全自由发挥。
3. 实操落地:从零实现一个精简版 agent-skills 库
3.1 定义技能接口
我自己在项目初期踩过一个很典型的坑:每个技能实现五花八门,有的函数接收三个参数,有的直接吃一个上下文对象,后面导致调用代码里全是 if-else。后来我统一了口径,每个技能都实现同一个接口,内部的差异全被这个接口挡住。
from typing import Any class BaseSkill: """所有技能的基类。""" name: str = "" description: str = "" parameters_schema: dict = {} required_skills: list = [] def execute(self, context: dict, params: dict) -> Any: raise NotImplementedError这里的context是技能执行期间共享的上下文环境,里面放着对话历史、用户身份、外部服务客户端等。params是从模型传过来的参数,已经经过校验。所有技能都按这个结构来写,注册、调度、测试就都能统一处理了。
3.2 注册表与加载机制
技能库的管理核心是一个注册表。它是技能名到技能对象的映射,同时负责做描述列表的生成。这一步看起来简单,实际上关系到上下文长度的控制:几十个技能全部塞进提示词,token 消耗太大,而且会让模型注意力分散。
所以我做了两级加载:第一级是“常驻技能”,数量极少,只放基础能力;第二级是“动态加载”,根据用户当前意图,从注册表里召回相关的技能,加入当前上下文。召回逻辑最开始用的是关键词匹配,效果一般,后来干脆用模型做意图分类,从中提取技能倾向,准确率提升非常明显。
class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill: BaseSkill): self._skills[skill.name] = skill def get(self, name: str) -> BaseSkill: return self._skills.get(name) def build_descriptions(self, skill_names=None) -> str: names = skill_names or list(self._skills.keys()) lines = [] for n in names: s = self._skills[n] lines.append(f"- {s.name}: {s.description}, 参数: {s.parameters_schema}") return "\n".join(lines)3.3 一个示例技能:网页内容摘要
光说不练没意义,我拿一个最常见的场景“网页内容摘要”做例子,看看一个完整的技能是怎么写出来的。
这个技能要接收一个 URL,抓取网页文本,然后交给模型生成摘要。它不能接收任意 HTML 结构,因为模型传参很容易出错。所以参数只定义两个:url和可选的max_words。
import requests from bs4 import BeautifulSoup class WebSummarizeSkill(BaseSkill): name = "web_summarize" description = ( "当用户想了解某个网页的主要内容、提炼核心观点," "或者给出一段网址对应的文章摘要时使用。" ) parameters_schema = { "type": "object", "properties": { "url": {"type": "string", "description": "网页完整链接"}, "max_words": {"type": "integer", "description": "摘要最大字数,默认200"} }, "required": ["url"] } def execute(self, context, params): url = params["url"] max_words = params.get("max_words", 200) resp = requests.get(url, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") # 提取正文主要逻辑,省略标签清理 text = soup.get_text(" ", strip=True)[:5000] return {"content": text, "max_words": max_words}注意我特意在execute里返回的是结构化内容,而不是直接调用模型生成摘要。原因很简单:技能执行和模型决策解耦之后,摘要生成可以被上游模型控制,也可以被另一个技能复用。这个设计在复杂系统里很有价值。
3.4 模型调用技能的完整链条
有了注册表和技能类,剩下就是把它们串进模型调用流程里。大致流程是:
- 把常驻技能描述和动态召回技能的描述拼接,注入系统提示词;
- 用户发消息后,模型除了回答内容,还输出一个结构化的“技能调用请求”;
- 程序解析请求,到注册表找技能对象,校验参数格式;
- 执行技能,拿到结果,再把结果塞回对话历史,让模型继续。
这里有一个关键点,就是给模型返回技能结果时,必须明确标注这是技能执行的返回结果,不能让模型产生混淆。比如我会包裹一层:
[技能执行结果 - skill_name] {"content": "..."} [结束]这样模型就能清清楚楚知道这段文本不是用户说的,也不是它自己生成的,而是一个外部事实来源。少了这个分隔标记,模型很容易把技能输出当成对话内容来使用,导致回答逻辑混乱。
3.5 参数校验的细节工作
说个实际会遇到的哭笑不得的事:模型可能把 URL 传成“https://...”全角字符,也可能把布尔值传成字符串"true"。所以参数校验必须做容错处理,不能只依赖 JSON Schema 校验。
我对required字段的校验是硬性的,缺失就直接报错;但对非必填字段,会做一层“模糊修正”,比如把字符串"true"转成布尔值,把数字字符串"123"转成整数。这一层逻辑写起来不复杂,但能少一堆无谓的报错和重试。
新增技能的时候,我强烈建议在注册前跑一遍参数校验单元测试,手动传几个畸形参数,看看技能实现会不会崩。一个连参数兜底都没做好的技能,丢给模型用,等于埋了一颗雷。
4. 调试与测试:技能不会自己变好用
4.1 最常见的三个问题场景
技能库上线之后,日常维护的重心不再是写新技能,而是处理三个反复出现的场景。
第一个是技能“被冷落”。明明模型该用某个技能,可它就是不用,直接把位置信息、天气信息等自己瞎编。这类问题九成出在描述上。要么描述写得过于笼统,要么触达条件和用户表达之间的语义匹配度不够。解决办法没有捷径,只能看实际对话日志,把用户的真实说法加进描述的触发场景里。
第二个是技能“被滥用”。模型在用户没有明确需求时就调用了某个技能,比如用户只是问“你能做什么”,模型却调用了查询天气的技能。这类问题通常是因为描述里的触发条件太宽泛,或者技能与技能之间边界模糊。我会在描述里加上明确的负面条件:“仅在用户明确提及位置信息时调用,日常闲聊不触发。”
第三个是执行错误后模型态度过于“乐观”。技能返回了异常或者空结果,模型还是会顺着用户继续下一步,甚至编一个假结果。解决方法是让异常返回结果带上严格的错误标志,并且在系统提示里告诉模型:出现错误就说明情况,禁止继续推演。
4.2 测试分层:从单元到场景
技能库测试,我分了三个层次,每一层的作用不一样,缺一层都容易出问题。
单元测试是测技能实现本身。模拟外部服务返回各种结果,包括超时、报错、空数据的场景,确保技能代码靠得住。这一步通常比较枯燥,但它能挡住 70% 的下游问题。
集成测试是测“模型 + 技能注册表”的配合。把一个技能的描述注入模型,然后给模型一批覆盖了“应该调用”和“不应该调用”的请求,观察它的选择是否准确。这个可以做成自动化,把测试集固定下来,每次新增技能、修改描述之后都跑一遍。
场景测试是测完整链路,从一个真实的用户问题出发,走完模型、技能调用、结果返回整个链条,看最终回答质量。这里我更关心的是跨技能协作,比如用户想要的最终结果需要两个技能组合完成,模型能不能正确衔接。场景测试没法全穷举,挑业务里使用频率最高的 Top10 流程就行。
4.3 观测与日志:没有日志就没有优化空间
有一点我是在项目中期才彻底想明白的:技能库的迭代速度,完全取决于你对运行时行为的可见度。
现在每次技能调用,我都会记录以下几类信息:模型选择了哪个技能、传入参数是什么、技能执行耗时多少、返回结果是否正常、模型最终如何利用这个结果。这些数据汇总之后,能直接回答很多问题:哪个技能被频繁误调用?哪个技能的参数总是填不完整?哪个技能跑得慢影响体验?
日志我推荐走标准结构化格式,直接打进 JSON 行。后续不管接分析平台还是自己写个小面板,都方便。顺带提一句,技能执行的耗时非常容易被忽略,但它恰恰是体验劣化的主要元凶之一,尤其是涉及外部请求的技能,一定要做超时控制和缓存策略,否则模型一调技能就卡住,业务侧会以为是模型本身出问题了。
5. 常见问题与排查技巧实录
5.1 技能选择了但没执行
现象是模型已经输出了技能调用的结构,但代码端没收到,或者收到了没去执行。
第一步查解析逻辑。很多模型输出技能调用时不够规范,比如它会在调用前后加一段解释文本,解析时如果没有精准提取,就会漏掉。我在解析时用的是严格提取“最后一个完整 JSON 对象”的策略,把 JSON 外的文字全部忽略,成功率一下子提高不少。
第二步查调用时机。如果你的流程是把模型输出处理完之后再执行一个统一调度,那么要先判断模型输出到底是普通文本还是技能调用。可以设置一个强制约定:如果输出的 JSON 里包含skill_name字段,就视为技能调用,否则视为普通回复。
5.2 模型喜欢把所有内容都放进参数
用户说了一段比较长的话,模型把整段对话历史都塞进了user_input参数,导致技能接到一堆无关内容。
这基本是描述不够精确造成的。参数说明里必须加一条:“该字段只填充用户当前消息中与天气查询相关的关键信息,无需包含对话历史和其他上下文。”把参数接收的边界用自然语言写明白,比在代码里截断有效得多。
5.3 技能结果被模型当成了用户指令
模型拿到技能结果后,不是作为事实依据来使用,而是顺着结果内容继续“发挥”。比如技能返回了一个错误提示,模型居然一本正经地道歉,然后编出个结果。
我处理这个问题的办法是双管齐下。一方面返回结果时带上明确的角色标签,用[技能执行结果]包裹,提醒模型这是外部信息;另一方面在系统提示词里写死规则:技能结果中的错误信息必须原样转达给用户,严禁生成错误未发生时的虚构结果。
5.4 技能库越来越乱
技能数量到了一定级别,管理本身就成了问题。重复功能、废弃技能、命名混乱,这些听起来不严重的细节,会在模型选择技能时造成严重的干扰。
我会定期做“技能清理”:跑一遍历史调用日志,找出那些从没有被动调用的技能,仔细确认是不是真没用,然后下架。同时规定命名规范:动词开头、语义完整、不超过三个词。比如get_user_order、calc_shipping_fee。这样至少在描述列表里,模型看到的是一份干净的目录,而不是一团杂货堆。
6. 再往前一步:技能库后续还能怎么扩展
技能库这套结构成熟之后,自然会有一些上层玩法。我个人觉得最值得关注的扩展方向是“技能之间的技能复用”。比如一个“生成报销单”的技能,内部会调用“解析截图发票”“计算总金额”“生成 PDF”三个子技能。这三个子技能本身独立可用,又能在上层技能里被编排调用,形成一种叠加态。这种复用模式能让技能库越用越厚实,每一次新增功能都在为后续能力铺路。
还有一个方向是“技能的版本化”。我遇到过改动一个技能直接影响了下游多个流程的情况,后来把每个技能的元数据里加上了版本号,外部调用时锁定版本,大规模重构之后再做清晰切换。这个对团队协作的场景特别重要,不然你更新了一个技能,别人调用的行为全变了。
无论怎么扩展,核心逻辑一直不变:把能力边界理清,把描述和实现解耦,用日志去驱动不断迭代。只要遵守这几条,agent-skills 就不仅仅是一个代码目录,而是一套让智能体变得更可控、更可靠的方法论。