1. 从"agent-skills"这个标题说起:一个被低估的工程化命题
第一次看到"agent-skills"这个标题的时候,我脑子里蹦出来的不是某个具体框架,而是一类很典型的问题:当我们在做智能体(Agent)相关项目时,真正卡住进度的往往不是模型能力本身,而是"技能"这一层的组织方式。模型能推理、能生成,但它要真正完成一件端到端的事,中间需要一连串可复用、可组合、可验证的能力单元,这些单元怎么定义、怎么注册、怎么调度、怎么保证稳定输出,才是工程上最磨人的部分。
"agent-skills"这个标题,本质上指向的就是智能体的技能体系设计。它不是一个单点技术,而是一套围绕"技能"展开的工程结构:技能如何抽象成统一接口、如何被智能体按需发现和调用、如何在多轮任务里保持上下文一致、如何对技能执行结果做校验和兜底。这套东西做得好,智能体就像一个熟练工,知道什么场景该用什么工具;做得不好,就是一个只会聊天的模型,遇到稍微复杂点的任务就开始胡编乱造。
这篇文章适合几类人看:一是正在做智能体应用、被"工具调用不稳定"折磨过的开发者;二是想把现有脚本、API、内部系统包装成智能体可调用能力的工程同学;三是对智能体架构感兴趣、想搞清楚"技能层"到底该怎么设计的技术负责人。我会从整体设计思路讲到具体实现细节,再到踩过的坑和排查方法,尽量把"agent-skills"这件事讲透,让你看完能直接对照自己的项目动手改。
需要先说明一点:下面涉及的具体实现,是基于我在多个智能体项目里反复验证过的常见实践做的合理补全,不是某个特定框架的官方文档。不同技术栈细节会有差异,但核心思路是通用的。
2. 技能体系的整体设计与选型思路
2.1 为什么"技能"要单独抽一层
很多人做智能体,第一反应是把所有能力都塞进一个巨大的提示词里,告诉模型"你可以做A、可以做B、可以做C"。刚开始跑demo没问题,一旦技能数量超过十个,问题就全出来了:提示词越来越长,模型开始漏掉某些技能;技能之间的边界模糊,模型经常选错;某个技能改了参数,整个提示词都要跟着动,维护成本爆炸。
把技能单独抽一层,核心目的是解耦。技能的定义、注册、调度、执行、校验各自独立,模型只负责"决策用哪个技能、传什么参数",具体怎么执行、执行失败怎么办,交给技能层去处理。这样做的好处很直接:技能可以独立开发、独立测试、独立上线;模型侧只需要一份精简的技能清单;出问题时能快速定位是"模型选错了"还是"技能执行挂了"。
我试过两种极端方案。一种是把技能描述写得极其详细,恨不得把每个参数的含义都塞进提示词,结果模型反而抓不住重点,选择准确率下降。另一种是只给技能名和一句话描述,模型经常猜错参数格式。实测下来,比较稳的做法是:技能描述控制在"做什么+关键参数+返回什么"三要素,参数细节通过结构化的schema单独传给模型,而不是混在自然语言描述里。
2.2 技能抽象的三个层次
技能抽象我一般分三层来看,这个分层直接决定了后面代码怎么组织。
第一层是原子技能,就是最小可执行单元,比如"查询某条记录""发送一条消息""计算两个数的和"。原子技能的特点是输入输出明确、无副作用或副作用可控、执行时间短。这一层最容易被复用,也最需要严格约束。
第二层是组合技能,由多个原子技能按固定流程编排而成,比如"根据用户问题查询数据并生成摘要"。组合技能的价值在于把常见流程固化下来,减少模型在多步任务里的决策次数。模型决策次数越少,出错概率越低,这是我踩了很多坑之后最深的体会。
第三层是技能编排器,负责在运行时根据任务动态选择技能、处理技能之间的依赖、做失败重试和降级。这一层不是技能本身,而是调度逻辑,但它决定了整个技能体系的上限。
三层之间的关系可以用一个简单类比理解:原子技能是零件,组合技能是预装好的模块,编排器是装配线。零件要标准化,模块要覆盖高频场景,装配线要能灵活应对非标需求。
2.3 技能注册与发现的机制选择
技能怎么被智能体发现,有两种主流做法:静态注册和动态发现。
静态注册就是在启动时把所有技能加载进一个注册表,模型每次决策时拿到的是完整技能清单。优点是简单、可控、调试方便;缺点是技能多了之后清单膨胀,模型选择压力大。动态发现是根据当前任务上下文,先做一轮技能召回,只把相关技能给模型。优点是精准、可扩展;缺点是多了一层召回逻辑,召回不准会直接导致任务失败。
我的建议是:技能数量在二十个以内,用静态注册就够了,别过度设计。超过二十个,再考虑动态发现,而且召回逻辑要做得足够简单可靠,比如基于关键词匹配或向量相似度,不要一上来就搞复杂的多级路由。我见过一个项目,技能召回用了三层模型级联,结果召回本身成了最大的不稳定源,得不偿失。
2.4 技能描述格式的取舍
技能描述格式直接决定模型能不能正确调用。我试过纯自然语言描述、JSON Schema描述、以及两者混合三种方式。
纯自然语言最灵活,但模型对参数格式的理解经常跑偏,尤其是嵌套结构。纯JSON Schema最严谨,但模型对schema的理解能力参差不齐,有时候会忽略schema里的约束。混合方式是我目前最推荐的:用一句话自然语言说明技能用途,用JSON Schema定义参数结构,两者配合。
具体来说,一个技能描述包含这几个字段:技能名(英文、下划线分隔、语义明确)、用途描述(一句话,动词开头)、参数schema(类型、是否必填、默认值、取值范围)、返回结构说明、以及可选的示例。示例这个字段特别有用,给一两个典型调用示例,模型的选择准确率能明显提升。
3. 核心细节解析与实操要点
3.1 技能接口的统一约定
技能接口不统一,是后期维护的噩梦。我定过一套约定,后来在几个项目里都沿用了,效果不错。
每个技能必须实现一个统一入口,输入是一个字典(或对象),输出是一个固定结构的结果对象。结果对象至少包含三个字段:状态(成功/失败/部分成功)、数据(成功时的返回内容)、错误信息(失败时的原因)。这个约定看起来简单,但它强制所有技能在失败时也要给出结构化信息,而不是抛异常了事。编排器拿到结构化错误,才能决定是重试、降级还是上报。
参数校验必须在技能入口处做,不能指望模型传对。模型传参出错是常态,不是异常。我一般会在入口处做三层校验:类型校验、必填校验、取值范围校验。任何一层不过,直接返回结构化错误,并且错误信息要足够具体,比如"参数start_date格式应为YYYY-MM-DD,实际收到2024/01/01",这样模型在下一轮有机会自我修正。
注意:错误信息不要暴露内部实现细节,比如堆栈、数据库表名、内部服务地址。这些信息对模型修正参数没有帮助,反而可能被写进最终输出,造成信息泄露。
3.2 参数设计的几个反直觉经验
参数设计有几个坑,我是踩过之后才明白的。
第一个坑是参数太多。一个技能如果有七八个参数,模型传错的概率会急剧上升。我的经验是单个技能参数控制在五个以内,超过五个就考虑拆成多个技能,或者把部分参数设成有合理默认值的可选参数。
第二个坑是参数名太抽象。比如用"mode""type""flag"这种名字,模型根本猜不出该传什么。参数名要具体,比如"query_type"就比"type"好,"output_format"就比"format"好。名字里带上语义,模型的理解成本会低很多。
第三个坑是枚举值没有明确列出。如果某个参数只能取几个固定值,一定要在schema里用enum列出来,不要只在描述里提一句。模型对enum的遵循度远高于自然语言描述。
第四个坑是日期、金额这类格式敏感的参数没有给示例。我一般会在schema的description里直接写一个示例值,比如"格式:2024-01-15",模型照着抄的概率很高。
3.3 技能执行的超时与重试策略
技能执行必须设超时,这是铁律。没有超时的技能调用,一旦底层服务卡住,整个智能体就挂在那里,用户体验极差。
超时时间怎么定,我的经验是按技能类型分档:纯本地计算类,超时设1到2秒;单次网络请求类,超时设5到10秒;涉及多个下游服务的组合技能,超时设15到30秒。这个分档不是拍脑袋,是根据P99耗时往上留一定余量定的。你可以先跑一段时间收集耗时分布,再反过来调整超时值。
重试策略要区分错误类型。网络抖动、下游限流这类瞬时错误,可以重试,一般重试一到两次,每次间隔做指数退避。参数错误、权限不足这类逻辑错误,重试没有意义,直接返回失败。我见过有项目对所有错误都无脑重试三次,结果参数错误被重试三次,白白浪费时间和资源,还让用户多等了好几秒。
提示:重试一定要设总时长上限,不能因为重试把整体响应时间拖到不可接受。比如单次超时5秒、重试2次,最坏情况就是15秒,这个上限要在设计时就明确。
3.4 技能结果的校验与兜底
技能返回结果不能直接信,尤其是涉及外部数据源的技能。我一般会做两层校验:结构校验和业务校验。
结构校验检查返回数据是否符合预期结构,字段是否齐全、类型是否正确。这一层可以用schema校验工具自动做。业务校验检查数据在业务上是否合理,比如查询用户信息返回的年龄是负数,结构上没问题,业务上明显不对。业务校验需要针对每个技能单独写规则,没法完全自动化,但关键技能一定要有。
兜底策略分几种:返回缓存数据、返回默认值、降级到简化版技能、直接告知用户当前不可用。选哪种取决于业务场景。查询类技能可以返回缓存或默认值,操作类技能一般只能降级或告知不可用,因为操作类技能失败后重试可能造成重复操作。
4. 实操过程与核心环节实现
4.1 技能注册表的搭建
技能注册表是整个体系的地基。我用Python写过一个简化版,核心结构就是一个字典,key是技能名,value是技能对象,包含描述、schema、执行函数。
class SkillRegistry: def __init__(self): self._skills = {} def register(self, name, description, schema, handler): if name in self._skills: raise ValueError(f"技能 {name} 已注册") self._skills[name] = { "name": name, "description": description, "schema": schema, "handler": handler, } def get(self, name): return self._skills.get(name) def list_all(self): return [ { "name": s["name"], "description": s["description"], "schema": s["schema"], } for s in self._skills.values() ]这个结构看起来简单,但有几个设计点值得说。注册时检查重名,是为了避免技能被意外覆盖,这种bug很难查。list_all只返回描述和schema,不返回handler,是为了让给模型看的清单和执行用的对象分离,避免把函数对象序列化进提示词。
注册时机我一般放在应用启动阶段,所有技能在启动时一次性注册完。这样启动阶段就能发现重名、schema格式错误等问题,而不是等到运行时才暴露。
4.2 技能调用的完整链路
一次技能调用的完整链路,我拆成六步:模型决策、参数解析、参数校验、技能执行、结果校验、结果回传。
模型决策阶段,把技能清单和用户输入一起给模型,让模型输出"用哪个技能、传什么参数"。这一步的输出格式要严格约束,我一般要求模型输出JSON,包含skill_name和arguments两个字段。
参数解析阶段,把模型输出的JSON解析成结构化对象。这一步要处理模型输出格式不规范的情况,比如多打了markdown代码块标记、JSON里有注释、字符串没加引号。我一般会先做一轮清洗,再解析,解析失败就返回错误让模型重试。
参数校验阶段,按schema校验参数。校验不通过,返回具体错误,让模型修正。
技能执行阶段,调用handler,带超时控制。执行结果统一包装成前面说的结果对象。
结果校验阶段,做结构和业务校验。校验不通过,走兜底策略。
结果回传阶段,把结果转成模型能理解的格式,回传给模型,让模型生成最终回复。
这六步里,最容易出问题的是参数解析和结果校验。参数解析的问题在于模型输出格式不稳定,结果校验的问题在于业务规则容易漏。我的做法是给这两步都加详细日志,出问题时能快速定位。
4.3 一个完整技能的实现示例
拿一个"查询订单状态"的技能举例,完整实现大概是这样。
def query_order_status(order_id: str) -> dict: # 参数校验 if not order_id or not isinstance(order_id, str): return { "status": "failed", "data": None, "error": "order_id必须是非空字符串" } if len(order_id) > 64: return { "status": "failed", "data": None, "error": "order_id长度不能超过64" } # 执行查询(带超时) try: result = call_order_service(order_id, timeout=5) except TimeoutError: return { "status": "failed", "data": None, "error": "查询订单超时,请稍后重试" } except ServiceError as e: return { "status": "failed", "data": None, "error": f"订单服务异常:{e.code}" } # 结果校验 if result is None: return { "status": "failed", "data": None, "error": "订单不存在" } if "status" not in result: return { "status": "failed", "data": None, "error": "订单数据格式异常" } return { "status": "success", "data": { "order_id": order_id, "order_status": result["status"], "updated_at": result.get("updated_at") }, "error": None }这个实现里,参数校验、超时控制、异常处理、结果校验都齐了。看起来啰嗦,但正是这些啰嗦的检查,让技能在真实环境里足够稳。我见过太多技能实现只写了happy path,一遇到异常就整个链路崩掉。
对应的schema大概是这样:
{ "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,格式如ORD20240115001" } }, "required": ["order_id"] }schema里的description带了示例格式,模型照着传的概率会高很多。
4.4 多技能编排的实现要点
单个技能跑通之后,下一步是多技能编排。编排的核心是处理技能之间的依赖和数据传递。
我一般用两种编排方式:固定流程编排和动态流程编排。固定流程编排适合高频、稳定的场景,比如"查订单-查物流-生成回复"这种固定三步,直接写死流程,模型只负责触发。动态流程编排适合不确定步骤数的场景,比如"根据用户问题决定查哪些数据",由模型在每一步决定下一步用哪个技能。
固定流程编排实现简单,用代码串起来就行,关键是每一步的失败处理要明确。动态流程编排复杂得多,需要维护一个执行状态,记录已经执行了哪些技能、拿到了哪些数据、还剩哪些目标没完成。我一般会给动态编排设一个最大步数限制,比如10步,超过就强制结束,避免模型陷入死循环。
数据传递是编排里的另一个难点。技能A的输出要传给技能B,中间要做字段映射。我一般用一个共享的上下文对象,所有技能的输出都写进上下文,技能B从上下文里取需要的数据。这样技能之间不直接依赖,耦合度低。
5. 常见问题与排查技巧实录
5.1 模型选错技能怎么办
模型选错技能是最常见的问题,排查思路分三步。
先看技能描述是否清晰。如果两个技能描述有重叠,模型很容易混。解决办法是把描述改得更具体,突出差异点。比如"查询用户信息"和"查询用户订单",描述里要明确前者返回用户基本资料,后者返回订单列表。
再看技能数量是否过多。技能超过二十个之后,模型选择准确率会下降。解决办法是做技能分组,先让模型选组,再选组内技能。或者做技能召回,只给模型相关技能。
最后看是否有示例。给每个技能配一两个调用示例,模型的选择准确率能提升不少。示例要覆盖容易混淆的场景,让模型看到差异。
5.2 参数传错怎么系统性解决
参数传错没法完全避免,但可以系统性降低概率。
第一,schema要写全。类型、必填、枚举、格式示例,一个都别省。第二,错误信息要具体。不要只说"参数错误",要说清楚哪个参数、错在哪、期望什么格式。第三,给模型自我修正的机会。参数校验失败后,把错误信息回传给模型,让它重新生成参数,一般一到两轮就能修正。
我做过一个统计,加了具体错误信息之后,参数错误的自修正成功率从不到三成提升到七成以上。这个投入产出比非常高。
5.3 技能执行超时怎么定位
超时问题定位,先看是哪个技能超时,再看是技能本身慢还是下游慢。
技能本身慢,一般是实现里有性能问题,比如循环里做了网络请求、没加缓存、数据量太大。下游慢,一般是依赖的服务响应慢,需要联系下游排查,或者在技能层加缓存和降级。
我一般会在技能执行处打点,记录开始时间、结束时间、耗时。耗时数据积累一段时间后,能看出哪些技能是慢点,优先优化。
5.4 结果不符合预期怎么排查
结果不符合预期,分两种情况:技能返回的数据不对,或者模型对数据的解读不对。
技能返回数据不对,查技能实现和下游数据源。模型解读不对,查回传给模型的格式是否清晰。我遇到过把原始JSON直接回传给模型,模型解读出错的情况。后来改成把关键字段提取出来,用自然语言描述回传,模型解读准确率明显提升。
排查这类问题,日志要打全。技能输入、技能输出、回传给模型的内容、模型最终输出,这四样都要记,出问题时能完整还原链路。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| 模型选错技能 | 描述重叠、技能过多、缺示例 | 检查技能描述差异度 | 细化描述、分组召回、补示例 |
| 参数传错 | schema不全、错误信息模糊 | 检查schema完整性 | 补全schema、具体化错误信息 |
| 技能超时 | 实现慢、下游慢 | 看耗时打点 | 优化实现、加缓存、降级 |
| 结果不符预期 | 数据错、解读错 | 看四样日志 | 修数据源、改回传格式 |
| 多步任务中断 | 状态丢失、步数超限 | 看执行状态记录 | 持久化状态、调整步数上限 |
| 重复执行 | 重试策略不当 | 看重试日志 | 区分错误类型、加幂等 |
5.6 几个独家避坑技巧
第一个技巧:技能上线前一定要做"对抗测试"。故意传错参数、故意让下游超时、故意返回异常数据,看技能能不能正确处理。我见过太多技能在正常流程下没问题,一遇到异常就崩。
第二个技巧:给技能加调用频率限制。某些技能可能被模型在短时间内反复调用,比如模型陷入循环。加个频率限制,超过阈值就拒绝,能避免资源被耗尽。
第三个技巧:技能版本管理。技能改了之后,老版本不要立刻删,保留一段时间。万一新版本有问题,能快速回滚。我一般用技能名加版本号的方式管理,比如query_order_status_v2。
第四个技巧:定期review技能使用数据。哪些技能从没被调用过,哪些技能调用失败率高,哪些技能参数经常传错,这些数据能指导你优化技能体系。没被调用的技能考虑下线,失败率高的技能重点排查,参数常错的技能改schema。
6. 技能体系的扩展与演进方向
技能体系跑通之后,往哪个方向演进,我分享几个实际用过的方向。
第一个方向是技能的组合化。把高频的多步流程固化成组合技能,减少模型决策次数。比如"查订单并生成物流回复"这种固定流程,做成一个组合技能,模型只需要调用一次。组合技能越多,模型决策压力越小,整体稳定性越高。
第二个方向是技能的自动化测试。给每个技能写单元测试,覆盖正常流程和异常流程。技能改动后自动跑测试,避免改出问题。测试用例可以从实际调用日志里提取,把真实场景变成测试用例。
第三个方向是技能的可观测性。给技能调用加详细的指标采集,包括调用次数、成功率、耗时分布、错误类型分布。这些指标能帮你快速发现异常,也能指导优化方向。
第四个方向是技能的权限控制。不是所有技能都该对所有场景开放。敏感操作类技能要加权限校验,确保只有授权场景能调用。权限控制做在技能层,比做在模型层可靠得多。
第五个方向是技能的跨项目复用。把通用技能抽出来,做成独立的技能库,多个项目共享。这样新项目启动时,基础技能直接拿来用,不用重复开发。技能库要有清晰的版本管理和文档,否则复用会变成负担。
我个人在实际操作中的体会是,技能体系这件事,前期多花时间把接口约定、schema规范、错误处理这些基础打好,后期能省下大量排查和重构的时间。反过来,前期图快省掉这些,后期每加一个技能都要还债。这个投入产出比,做过的人都懂。
最后再分享一个小技巧:技能描述里的动词要统一。要么都用"查询""创建""更新"这种标准动词,要么都用业务动词,不要混着来。动词统一之后,模型对技能用途的理解会更一致,选择准确率也会更稳定。这个细节很小,但实测有效。