最近在带一个自动化任务系统的时候,被问得最多的一个问题就是:你的 agent 是怎么学会干活的?说实话,这个问题没有标准答案,但我自己的实践路径非常明确——把所有可复用的能力拆成独立、可注册、可调度的 skill 模块,而不是把指令全部堆给模型。这套思路就是我今天想聊的 agent-skills 架构。
如果你也在做大模型智能体、自动化工作流或者工具调度类系统,这篇文章应该能帮你少走很多弯路。我会从设计动机讲起,逐步拆到接口定义、注册机制、调度策略、容错手段,再附上我真实跑过的一些问题和排查记录。内容偏实践,但我会尽量把背后的为什么也讲清楚,方便你拿去调整和落地。
1. 为什么技能化是智能体落地的关键一步
1.1 单体式 agent 为什么会卡住
先说我踩的第一个坑。最早我做的 agent 是典型的单体式结构:一份超长的 system prompt,里面塞满了工具说明、规则、格式化要求和少量示例,然后让模型自己在一次对话里完成推理和调用。小规模演示还可以,真正接到业务任务时,问题集中爆发在几个点上。
第一是上下文长度失控。工具说明越多,prompt 越长,留给实际任务内容的余量就越少。第二是修改成本高,今天要加一个新工具,明天要改一个工具的返回格式,每次都要动整个提示词,而且改完经常影响到其它工具的稳定性。第三是模型选择困难,工具一多,模型在推理时经常误解指令,明明应该调用 A 工具,偏偏去调 B,或者隐含参数没补齐,直接调失败。第四是复用性很差,项目之间的工具逻辑大量重复,但全都硬编码在不同 agent 的提示词里,维护起来相当痛苦。
后来我换了一个思路:把“让模型知道有什么工具”和“让系统真正执行工具”分开。前者只需要极简的索引信息,后者由系统侧的调度层负责。这个转变看似简单,却让整个系统的稳定性和扩展性都上了一个台阶。
1.2 Skill 到底是什么
在我的设计里,skill 不是一段 prompt,也不是一个函数,而是“可被 agent 调用的原子能力单元”。它内部至少包含以下几个部分:
- 技能的元信息,包括名称、用途描述、参数说明、适用场景。
- 技能的执行逻辑,也就是真正干活的代码。
- 技能的输入输出协议,定义参数结构和返回结构。
- 技能的验证与失败处理,包括超时、异常、重试策略。
你可以把它理解成一个“带说明书的小工具”。agent 的核心模型并不需要理解技能的每一行代码,它只需要理解说明书,然后决策“该不该用、怎么传参”,真正的执行交给系统。
这个设计带来的好处是显而易见的:技能可以独立开发、独立测试、独立上线;技能的说明和实现分离,模型感知的只是轻量摘要;技能库可以横向膨胀,而不必每加一个能力就重写一轮 prompt。
1.3 与工具调用、插件体系的区别
很多人会问,skill 和 tool、plugin、function calling 有什么区别。凭我自己的使用体验,区分点主要在于抽象层级和生命周期管理。工具调用通常是一个单次函数接口,而 skill 更像是一个“封装好的完整流程”,它可以内部串联多个 API、做前置校验、做结果格式化,甚至调用其它 skill。插件体系则更多指一组扩展包的打包方式,而 skill 是更细粒度的能力单元。
所以我在设计时干脆把层级定成:skill 是最小调度单位,plugin 是技能的集合包,agent 是运行技能的载体。这个分层在后续扩展中非常省心,新增业务能力时只需要新增一个 skill,并用一个 plugin 聚合同类技能,agent 本身基本不用动。
2. 技能接口与注册机制的设计细节
2.1 接口定义:一份能直接抄的 Schema
技能接口是整个体系的基座,设计不好后面全是坑。我用过 JSON Schema 和 Python dataclass 两种方式,最终在团队内部统一成一种类似 OpenAPI 的轻量描述。下面是一个参考样例:
{ "skill_name": "fetch_web_content", "description": "获取指定网址的正文内容,适合用于信息采集和资料整理场景", "version": "1.2.0", "author": "agent-core-team", "parameters": { "type": "object", "properties": { "url": { "type": "string", "description": "目标网页的完整链接,必须包含协议头", "required": true }, "timeout_seconds": { "type": "integer", "description": "请求超时时间,默认 10 秒", "default": 10, "minimum": 1, "maximum": 60 } } }, "returns": { "type": "object", "properties": { "status": {"type": "string"}, "content": {"type": "string"}, "content_length": {"type": "integer"} } } }这个设计里的核心要点是:描述字段必须写清楚“使用场景边界”和“常见的失败条件”。我在实践里发现,模型非常依赖描述来决策,描述写得含糊,调用准确率直接掉一截。比如“获取网址正文”和“获取网页正文,需要登录或需要渲染的页面会失败,适合静态内容页”,后者虽然长一点,但能让模型在决策阶段就排除错误选项。
参数定义里,我建议把 required 和 default 写明确。模型在生成参数时往往不会主动补全可选字段,如果你指望默认值兜底,就必须在 schema 里显式声明。还有一个小心得:给参数加 maximum 和 minimum 能有效防止模型输入极端值,避免下游服务被打爆。
2.2 注册机制:技能库不能只是文件夹
技能写完之后,需要一个注册中心来统一管理。我这里说的注册中心不是简单地把文件扔到一个目录,而是要让每个技能在运行时可以被索引、校验、版本化。我的做法是分三层:
- 文件层:每个 skill 是一个目录,包含 skill.py、schema.json、README.md 和 test/。
- 注册表:系统启动时扫描目录,把每个 skill 的元信息写入内存中的注册表,同时做 schema 合法性校验。
- 索引层:把注册表里的元信息做向量化,用于后续语义检索。
注册表的代码核心其实不长,关键是校验逻辑。我要求每个技能必须有版本号、描述、参数 schema、返回值 schema,缺一个就不能注册。这看起来繁琐,但在排查线上问题时太有用了,因为只要注册成功,就说明技能的“外壳”符合标准,问题大概率出在内部执行逻辑。
关于索引层的向量化,我在早期版本偷懒没做,只用了关键词匹配。结果就是模型问“抓取一下这个页面”的时候,系统匹配不到 fetch_web_content,因为关键词对不上。换成向量检索之后,技能召回率明显提升。这部分后文会展开讲。
2.3 技能调用协议:输入校验与统一返回
所有 skill 执行统一包装成这样一个流程:接收参数 dict,经过 schema 校验器校验,再进入执行函数,最后由统一的返回结构包一层。这样做的理由很简单:agent 的调度层不需要为每个 skill 单独写适配代码,而模型侧也只需要理解一种返回格式。
我用的返回结构是:
{ "code": 0, "message": "success", "data": {}, "elapsed_ms": 231, "trace_id": "abc-123" }code 为 0 表示成功,非 0 表示各类失败。失败时 message 尽量写清楚原因,比如“url 请求超时”“参数缺失: url”,这样调度层可以原样把错误回传给模型,让模型决定是换参数重试还是换别的 skill。trace_id 我强烈建议加,尤其在多个 skill 串联的场景里,没有 trace_id 你几乎没法定位是哪个环节出了问题。
3. 调度层如何决定调哪个技能
3.1 技能选择的两个阶段
调度是整个 agent-skills 体系里最关键的环节。我的实现分两个阶段:召回和选择。
召回阶段的目标是把候选技能从几十上百个缩到五六个。方法是对用户的当前意图做向量化,然后和技能描述向量做相似度检索。这里可以适当用上关键词增强,把 skill 名称和标签的精确匹配结果优先排在前面。召回阶段不要追求完美,漏检一两个没关系,后续选择阶段会做二次判断。
选择阶段则是把召回结果交给大模型,让它根据当前对话上下文和参数条件,决定究竟调用哪一个。这一步我建议用结构化输出的方式,要求模型返回一个 JSON,包含技能名、参数、理由。结构化输出能极大降低模型“说了一堆话但没给出可执行指令”的概率。
3.2 多技能协作时的顺序编排
单个技能调用只是基础,真实场景里经常需要多个技能按顺序协作。比如“查一下 A 公司最近的新闻,并整理成摘要”这个任务,可能需要先调搜索技能,再调网页抓取技能,最后调摘要技能。
我的编排方案不是让模型一次性列出所有调用计划,而是采用“逐步决策 + 动态续跑”的模式。调度层维护一个执行上下文,每完成一个技能,就把结果追加到上下文里,再让模型决策下一步。这种模式的好处是容错率高,前一步结果不理想时,下一步决策还能及时修正,而不是抱着一个错误的计划走到黑。
当然代价是 token 开销和延迟会上升。如果任务模式非常固定,我也支持预定义 workflow,让调度层按固定 DAG 执行,模型只在分支点参与决策。两种模式并存,是我目前觉得最稳的做法。
3.3 并发与上下文窗口的取舍
技能调度的并行度是个容易被忽视的问题。当一个任务里同时有多个互相独立的技能调用,合理的并发能显著缩短总耗时。但并发并发来的问题也很现实:token 成本变大、上下文管理更复杂、模型决策时会看到更多半成品结果。
我现在的策略是:把技能分为“只读型”和“写操作型”,只读型允许 3 到 5 个并发,写操作型默认串行。写操作并发容易引发数据一致性问题,尤其在调用外部 API 时,对方接口往往有频控,串行反而更稳。这个策略在项目初期极大地减少了线上事故。
4. 实操:从零搭建一个 Skill 示例
4.1 背景与目标
为了把这套设计说得更具体,我带大家一起走一个实际的技能搭建过程。假设我们要给 agent 增加一个“股票数据查询”的能力,用户会提出类似“帮我查一下最近五天的市场指数走势”这样的请求。目标是用前面说的结构,把完整流程跑通,包括技能实现、注册、调度、调用。
出于合规和脱敏考虑,这里的示例数据我会用本地模拟数据源来代替真实行情接口,但整体流程和你接真实数据源时是一致的。
4.2 第一步:实现技能函数
技能函数我按 Python 来实现,因为生态和类型提示都方便。下面是简化版代码:
import json import random from datetime import datetime, timedelta def fetch_market_index(code: str, days: int = 5): """ 模拟获取市场指数的收盘价序列。 实际项目中这里会替换为真实的数据接口调用。 """ if not code or len(code) < 3: return {"error": "invalid code"} if days > 30: days = 30 end_date = datetime.now() data = [] for i in range(days): d = end_date - timedelta(days=i) data.append({ "date": d.strftime("%Y-%m-%d"), "value": round(random.uniform(3000, 3500), 2), }) return {"data": data}这个函数本身很简单,但要注意几个工程化细节:参数校验放在函数入口,超过阈值时归一化而非直接报错;返回值用嵌套结构,方便前端或者模型做进一步抽取;所有外部依赖尽量封装在函数内部,不要散落到调度层。
4.3 第二步:编写 Schema 与说明书
接着给这个技能配 schema。描述字段我这样写:
{ "skill_name": "query_market_index", "description": "查询市场指数在指定天数内的收盘价数据,适合回答关于指数走势、最近行情等问题。当用户没有明确指定天数时,默认返回 5 天数据。", "parameters": { "code": { "type": "string", "description": "指数代码,例如 IDX001", "required": true }, "days": { "type": "integer", "description": "最近多少个交易日的收盘价", "default": 5, "minimum": 1, "maximum": 30 } } }这里我想强调一下 description 的写作技巧,模型是真的会逐字读这个东西的。我一开始写的描述是“查询市场指数”,模型就经常把这个技能用于各种奇怪的问题,比如问“昨天天气怎么样”也往这里面匹配。后来我改成“查询市场指数在指定天数内的收盘价数据,适合回答关于指数走势、最近行情等问题”,召回准确率提升非常明显。
4.4 第三步:注册进技能库
我写了一个简单的注册客户端。启动时扫描技能目录,读取 schema 和函数对象,构建技能注册表并打印注册日志。这个日志看起来很基础,但调试时非常有用,每次新增技能后第一件事就是确认它有没有进注册表。
[register] skill=query_market_index version=1.0.0 status=ok注册成功之后,我在测试环境跑了一条模拟用户请求:“最近五天指数怎么样”。调度层召回阶段检索到了 query_market_index,模型选择阶段输出了调用参数,执行函数返回了五条模拟数据,整条链路是通的。
4.5 第四步:串一个完整任务流
光是单技能调用还不足以说明问题,我加了一个串联场景:用户说“查一下这两个指数最近三天的情况,对比一下波动”。调度层把任务拆成两个 query_market_index 调用,等两个结果都回来后,再调一个 summarize_skill 做对比总结。两个只读型技能并发执行,耗时大约压到原来的三分之一。
这个过程中最重要的经验是:一定要在调度层保留每次调用之间的依赖关系日志。我在调试阶段就遇到过一个问题,两个查询并发返回后,模型把两个结果搞串了,直到我加了调用序列 ID 之后才发现是上下文拼接顺序错了。现在每个技能执行完都会把结果打上来源标记,拼接上下文时按标记排列,串数据的情况基本绝迹。
5. 常见故障排查与避坑实录
5.1 模型老是选错技能怎么办
选错技能是 agent-skills 体系里最经典的问题。我遇到的案例是:用户说“帮我整理一下这个网页”,系统却调用了搜索技能,而不是抓取技能。排查下来,原因是两个技能的描述里都包含了“网页”这个词,向量检索时把搜索技能排到了最前面,而模型又倾向于选排前面的。
改进办法有三个。第一是拉大技能描述之间的语义差异,抓取技能强调“输入网址,返回正文内容”,搜索技能强调“输入关键词,返回链接列表”,减少交集。第二是在召回阶段加硬过滤规则,比如用户消息里出现了完整的 http 链接,就把抓取类技能加权。第三是提供典型示例,在技能 schema 里加一个 usage_example 字段,模型在模糊场景下的决策准确率会明显上升。
5.2 上下文被技能返回结果灌爆
技能返回结果经常比预期长得多。一个网页抓取技能可能返回几万字,塞进上下文后直接把后续决策的空间挤没了。我的做法是引入“结果摘要层”:每个技能返回的数据先经过一个压缩环节,保留关键字段,截断或摘要长文本,再进入上下文。比如抓取网页时,可以把正文摘要成 500 字,并附上原文链接。
这里有一个坑要注意:摘要环节不能做成无脑截断,否则模型会基于不完整的信息做错误决策。我的方案是分类型处理,结构化数据保留字段和关键值,长文本用独立摘要模型压缩。这样上下文占用能减少 70% 以上,而决策质量几乎没有下降。
5.3 技能内部的超时与重试策略
外部接口超时是技能执行层的常客。我现在的标准配置是:连接超时 5 秒,读取超时 10 秒,对幂等请求最多重试 2 次,间隔采用线性退避。重试时要特别注意“这次调用是不是幂等”,如果是查询类接口,大胆重试;如果是创建订单、发消息这类接口,绝不能盲目重试,宁可抛失败让模型走确认流程。
超时错误信息一定要返回给模型,并且写得具体。比如“请求超时,已重试 2 次,服务端无响应”就比“http error”好得多,模型看到后会倾向于换一个技能或者让用户稍后再试,而不是反复重试同一个失败点。
5.4 技能版本更新后行为漂移
技能改过一版之后,老任务突然跑不通了,这是版本漂移的典型表现。我吃过一次亏:某个摘要技能升级了模型参数,输出风格完全变了,但调度层没有强制版本号,线上跑的任务还在用新版本直接替换,结果摘要格式全乱。
后来我做了两个整改。第一,所有技能调用记录必须带版本号,线上排查时一眼看出用的是哪版。第二,重大变更不允许原地更新,必须新开版本号,并在注册表里做兼容性标记。调度层优先挑高版本,但如果任务级配置里指定了旧版本,就按旧版本执行。这个机制让升级变成了一个可回滚的过程。
5.5 技能之间的隐性依赖
有些技能看起来独立,实际使用时却存在隐性依赖。比如“生成报告”技能和“查询数据”技能,前者内部会自动调用后者,但调度层不知道这个关系,结果就是数据查询被调度层单独执行了一遍,报告技能又查了一遍,白花了双倍时间。
排查这个问题时我加了一个依赖声明机制:每个技能在 schema 里声明自身依赖的 skill 列表。调度层在编排时先生成依赖图,避免重复执行;如果依赖的技能没有被用户请求覆盖,调度层会先自动补齐。这个机制虽然增加了注册时的填写成本,但长期来看节约的资源远超成本。
6. 规模化运营时的维护经验
6.1 技能测试不应该只测函数
很多团队给技能做测试,只测函数的输入输出,忽略了“模型会不会正确调用它”这个问题。我后来规定,每个技能必须配一组评测 case,包含用户原始请求、期望的调用结果、期望的参数。回归测试时,不仅要跑函数级测试,还要跑模型级调用测试。
这个改动一开始大家觉得麻烦,但作用非常大。有一次我改了某个技能的描述,函数逻辑完全没动,单元测试全过,但模型级测试立刻暴露了召回率下降的问题。没有这层测试,这种回归会悄悄上线,直到用户投诉才发现。
6.2 技能数量的管理红线
技能库膨胀到上百个之后,召回和选择的难度会指数级上升。我给自己定了一条管理红线:技能库超过 50 个时,必须启动技能合并和归档流程。语义相近的技能合并成一个,使用频率极低的技能挪到归档区,不在默认索引里。
合并技能时要小心,不能只拼描述。合并的本质是让一个实现覆盖多个场景,如果实现逻辑差异太大,合并出来的技能反而两头都不讨好。我的经验是:先从相似度聚类开始,人工确认哪些技能属于同一能力域,再决定是合并实现还是保留独立实现、只合并索引入口。
6.3 观测与审计:技能调用全链路日志
最后想提醒大家,agent-skills 体系上线之后,观测是生存底线。我自己的日志规范是每次调用记录以下内容:任务 id、技能名、版本、入参、出参摘要、耗时、错误信息、决策模型和决策原始输出。这些日志聚合之后,可以产出一张很直观的技能质量报表,哪些技能高频且稳定,哪些技能总被错误调用,哪些技能经常超时,一目了然。
有了这张报表,后续做技能优化就不再靠猜。比如我优化过的一个抓取技能,报表显示 40% 的失败集中在超时,顺着日志发现是对方接口在高峰期响应慢,于是加了本地缓存和错峰策略,失败率从 40% 降到了 5% 左右。这个收益全靠观测数据才能拿到。
我个人在实际操作中的体会是:agent-skills 的价值不在于某个单独的技能有多强,而在于整个体系的“组装能力”。你有一个好技能,调度层不会用,那它就是一个死代码;你有一百个普通技能,但调度层能精准编排它们,那它就是一套能干活的系统。先把接口定稳,再把调度做细,最后用数据驱动迭代,这条路走下来,你的 agent 能力一定会肉眼可见地变强。