如果把AI Agent比作一个刚入职的新人,那模型是他的大脑,提示词是入职时的口头叮嘱,而技能库,就是他那本能随取随用的岗位操作手册。agent-skills这个主题,讲的就是怎么给Agent搭出一套可靠、可扩展、可维护的“操作手册”——也就是技能库。我在几个落地项目里反复验证过这套设计,今天把思路、配置样例和踩过的坑一起整理出来,希望能帮到正在做Agent应用的同学。
1. 先想明白:agent-skills 到底是给谁用的
1.1 从“会聊天”到“会干活”,Agent差在哪一步
现在很多团队做AI Agent,Demo阶段跑起来都挺兴奋,模型能聊天、能推理、能给出各种建议。但往生产环境一推,问题就出来了:你让一个纯对话Agent“帮我把昨天的销售数据整理成图表发到群里”,它大概率会回你一句“我无法访问您的数据库,建议您手动导出后上传”。这不是模型笨,而是它压根不知道你的数据在哪、表结构长什么样、用什么工具画图、画完发到哪里。
这一整条链路,就是技能的用武之地。技能(skill)本质上是提前定义好的、可供模型按需调用的能力单元,每个技能解决一个具体的“怎么做”问题。以“生成销售图表”为例,它背后可能拆成四个技能:查询销售数据、处理空值和异常值、绘制趋势图、把图表发送到指定渠道。模型不需要知道SQL怎么写、matplotlib怎么调,它只需要在收到用户请求后判断“该调用哪个技能”,然后按技能说明书生成参数。
很多人会想:“这些步骤直接写进System Prompt不就行了?”短期可以,长期一定不行。提示词长度有限,几十个任务塞进去既放不下,也没法维护。而且业务技能会频繁变化,每次都改提示词,改一处漏三处。更重要的是,不同业务线的Agent需要的技能差异很大,提示词根本没法复用。技能库把“任务怎么做”从提示词里剥离出来,模型只负责理解意图和编排调用,具体专业步骤由技能模块负责,两边职责清晰,迭代互不干扰。
1.2 技能库在Agent系统里扮演什么角色
如果你画过Agent系统的架构图,会发现技能库处在很特殊的位置:它把模型和真实世界连接起来,像一个“中介层”或“路由器”。模型每次拿到用户请求,先理解意图,然后去技能库里检索候选技能,选定技能后生成调用参数,参数通过校验,执行体完成真实操作,最后把执行结果返给模型,由模型组织成用户能看懂的回答。
这个中介层的价值主要体现在三个地方。第一是解耦。模型层不直接依赖具体工具代码,某个底层数据库换了,只要技能执行体内部改,模型和技能描述都不用动。第二是复用。一个技能可以被多个Agent共用,不需要每个Agent都单独写一套。第三是可控。权限校验、调用审计、频控限流都能钉在技能库这一层做,出了问题有日志可查,出了事故有权限可追。
我有一个比较土的类比:没有技能库的Agent系统,像一群人各干各的,每个人口袋里塞着一堆便签,上面写着自己摸出来的土办法;而有了技能库,系统像一个正规组织的服务目录,所有人都查同一份手册,用同一套规范对外提供服务。生产环境的稳定性,很大程度上取决于这种“规范感”。
1.3 技能粒度怎么拆,才不会拆成一堆“废操作”
拆技能有一个很常见的误区:为了追求“模块化”,把技能拆得特别细,比如“打开邮箱”“填写内容”“点击发送”各做一个技能。看着很规整,实际用起来是灾难——模型要连续选三次技能,每次都可能选错、每次都可能传错参数,任何一步断了整个链路就失败。
我自己的判断标准就一条:这个技能单独拿出来,能否独立完成一次有意义的、可验收的任务。“打开邮箱”单独拎出来没有任何交付价值,不算一个合格技能。“发送邮件”可以独立完成“把一封邮件发出去”这件事,算合格。再往上,“群发每日汇报”这种任务是复合技能,它在内部编排“统计数据→生成图表→发送邮件”三个步骤,但对模型只暴露一个入口。这样做的好处是模型不需要操心内部步骤,调用链更短,出错的概率也更低。
粒度上还有一个数量参考:单个业务域的原子技能最好控制在20到40个左右。太少说明你偷懒了,把太多逻辑压在模型身上靠提示词硬凑;太多会让模型在技能选择环节犯难,这个后面第4章会专门讲。总之,原子技能管“单点能力”,复合技能管“业务流程”,两层配合,既灵活又稳定。
2. 技能库设计三大件:描述、参数、执行体
2.1 技能描述:写给模型看的“使用说明书”
技能描述是我见过被低估最严重的东西。很多人写描述像写代码注释,就一行字:“根据数据生成报告。”结果模型在用户问“今天销售为什么下降了”这种需要直接回答的问题时,也跑去调用生成报告技能,生成一份几十页的PDF发给用户,场面非常离谱。
我后来总结出一个配方,写技能描述至少要覆盖四块内容:一句话功能定义、适用场景列表、明确排除场景、调用前需要确认的信息。举个例子,一个销售日报技能,之前描述写的是“生成销售日报”,改成这样之后,调用准确率明显上了一个台阶:
name: generate_sales_report description: | 当用户需要将销售明细汇总为正式日报/周报文件时使用。 适用场景:运营复盘、管理层汇报、报表自动发送。 不适合:用户只想快速了解销售额大小、趋势变化的问答型需求,直接回答即可。 调用前需要确认:统计口径、日期范围、是否需要包含图表。为什么“不适合”这一段这么重要?因为LLM的技能选择本质上是一个文本匹配任务,模型读描述,然后判断“用户这句话跟哪个技能描述语义最接近”。你只写正面信息,它看到一点沾边就调用;你把边界也写清楚,它的判断就有了约束。我们内部在一组40个技能规模的数据集上压测过:只写正面描述时,技能选择准确率在82%左右;加上负面约束后,提升到94%左右,提升非常可观。
另外注意,技能描述不是越长越好。太长会占用上下文窗口,而且可能把目标任务本身的指令挤没掉。我自己一般控制在100到200字之间,重点是让模型看懂“什么时候用、什么时候别用、用之前要问什么”。
2.2 参数规范:用JSON Schema把边界焊死
技能参数是模型要生成的结构化数据,它不是一个自然语言片段,而是要能被执行体直接解析的JSON。我们统一用JSON Schema来声明,每个技能的参数都定义成一个对象,包含字段类型、描述、示例、是否必填。看一下这个例子:
{ "name": "query_sales_data", "parameters": { "type": "object", "properties": { "start_date": { "type": "string", "description": "开始日期,格式 YYYY-MM-DD,示例 2025-06-01" }, "end_date": { "type": "string", "description": "结束日期,格式 YYYY-MM-DD,示例 2025-06-07" }, "metrics": { "type": "array", "items": { "type": "string" }, "description": "需要查询的指标列表,可选值包括 gmv、order_count、customer_count" } }, "required": ["start_date", "end_date", "metrics"], "additionalProperties": false } }几个细节值得说道说道。第一,additionalProperties: false一定要开,否则模型会自己发明字段,比如加一个channel: "all",但你的执行体根本不认识这个字段。第二,所有日期、金额、百分比等字段,都要把格式示例写在description里。模型看到“2025-06-01”这样的示例,才知道不能填“2025年6月1号”或者“上周”。第三,有默认值的字段要显式说明。比如一个“时间范围”参数,如果不传默认是最近7天,你要直接写进描述,不要指望模型自己知道。
还有一个很实用的技巧:模型经常会把“最近7天”“上个月”这种自然语言直接填进日期参数,而不是真正算成日期区间。我们后来在参数校验和执行之间加了一个“时间表达预处理层”,专门把“最近7天”解析成具体的起止日期。别指望大模型自己会做日期换算,它会给你一个看起来合理但实际算错的结果,这个坑我已经踩过不止一次。
2.3 执行体:技能背后到底跑的是什么代码
技能定义只是“说明书”,真正干活的是执行体。执行体可以是一段Python函数、一个HTTP接口、一条SQL模板、一个外部系统的SDK调用,无所谓形态,关键是边界要清晰。我们内部习惯把执行体写成标准的“请求-处理-响应”结构:
def handle(query_skill_request): # 第一步:按schema校验参数 payload = validate_with_schema(query_skill_request.parameters) if not payload: return error_response("参数校验失败") # 第二步:权限与频控检查 check_permission(query_skill_request.user_id, "query_sales_data") # 第三步:执行业务逻辑 data = sales_db.query( start_date=parse_date(payload["start_date"]), end_date=parse_date(payload["end_date"]), metrics=payload["metrics"] ) # 第四步:结构化返回 return success_response({ "status": "success", "summary": "上周GMV 128万,环比增长15.2%", "rows": data.rows })这里我最想强调的是返回结构。执行体返回给模型的,不应该是一大坨原始数据,而应该是一个包含“状态码+摘要+完整数据”的结构。模型最需要的是summary那几句话,直接拿来组织回答就够了;detail部分是用来应对追问的,放完整数据。如果执行体报错,也要把错误信息结构化成文本返回给模型,让它知道这次调用为什么失败、下次该怎么改,这样模型才有机会自己修正重试。
顺带一提,技能执行体最好做成无状态的,也就是每次调用互不依赖。这个跟普通后端服务的无状态设计一个道理,方便横向扩容,也方便排查问题。
3. 实操:给Agent加一个“取数出图”复合技能
3.1 第一步:把任务拆成原子技能清单
理论讲再多,不如跑一遍完整流程。我拿“生成上周销售日报”这个任务来演示。先做任务拆解,这一步不用想技术,先把要做的事列清楚:
- 查上周销售汇总:订单量、GMV、目标完成率
- 生成趋势折线图
- 生成明细数据表格
- 组装标题、摘要、图表、表格,输出完整日报
按这个顺序,我拆出四个原子技能:
| 技能名 | 职责 | 独立可验收? |
|---|---|---|
| get_sales_overview | 查询上周销售汇总数据 | 是,可单独回答“上周卖了多少” |
| build_trend_chart | 根据明细生成折线图并返回访问链接 | 是,可单独完成“画一张趋势图” |
| build_data_table | 把明细转成Markdown表格 | 是,可单独回答“给我一份明细表” |
| compose_daily_report | 把汇总、图表、表格组装成日报 | 是,可单独完成“生成一份日报” |
为什么没有把“连接数据库”“清洗数据”拆成独立技能?因为“连接数据库”单独拎出来没有任何交付价值,它是get_sales_overview内部的实现步骤,不是能给用户交付结果的技能。判断标准又回到之前说的:能不能独立完成一次有意义的可验收任务。
3.2 第二步:写注册配置并挂在技能库上
原子技能清单列好,接下来要写具体的技能定义文件,并挂到技能库上。我们内部的文件组织是这样的:
skills/ ├── manifest.json ├── database/ │ └── get_sales_overview.yaml ├── chart/ │ └── build_trend_chart.yaml ├── format/ │ └── build_data_table.yaml └── report/ └── compose_daily_report.yaml技能定义文件岗位操作手册,每份手册对应一个能力单元。manifest.json是注册表,声明所有技能的名称、路径、版本和启停状态:
{ "schema_version": "1.0", "skills": [ { "name": "get_sales_overview", "path": "./database/get_sales_overview.yaml", "version": "1.3.0", "enabled": true, "owner": "data-team" }, { "name": "build_trend_chart", "path": "./chart/build_trend_chart.yaml", "version": "2.0.0", "enabled": true, "owner": "chart-team" }, { "name": "compose_daily_report", "path": "./report/compose_daily_report.yaml", "version": "0.9.1", "enabled": true, "owner": "agent-team" } ] }为什么要用YAML/JSON而不是直接写Python类?因为技能库的维护者不一定是开发人员,产品、运营、数据分析师也都需要能看懂、能提修改意见。用结构化配置文件,版本对比、权限管理、灰度发布都好做,谁改了什么一目了然。
接着,我再定义一个复合技能generate_daily_sales_report,它把四个原子技能串起来:
name: generate_daily_sales_report type: composite description: | 当用户需要生成上周/上月的销售数据日报时使用。 输出包含汇总摘要、趋势图表、明细表格。 执行顺序:get_sales_overview -> build_trend_chart -> build_data_table -> compose_daily_report。 请勿在用户只询问单指标数值或需要交互式问答时调用此技能。 parameters: type: object properties: period: type: string description: 统计周期,可选值为 last_week、last_month target_channel: type: string description: 需要覆盖的销售渠道,为空时默认全渠道 required: [period] components: - get_sales_overview - build_trend_chart - build_data_table - compose_daily_report这里有个细节:执行顺序一定要写进描述里。模型虽然是推理模型,但在复杂多步骤任务上,它真的会搞错先后关系。你明确写成“先查数→再画图→再做表→最后组报告”,它就会按这个顺序走。不写,它可能先画图,而图的数据还没查到,直接就报错。
3.3 第三步:调试Agent调用链与返回效果
配置都挂上去之后,开始调试。我第一次跑通这个流程大概花了半天,其中大部分时间花在跟模型“对齐认知”上。几个典型问题分享一下。
第一个问题,模型选中了复合技能,但生成参数时漏了必填字段。比如用户说“生成日报”,模型没填period。我们处理方式是:参数校验失败后,把错误信息回填给模型,让它自己修正。也就是把“缺少必填参数period,可选值为last_week、last_month”这段文本拼回上下文,让模型重新生成。不要简单粗暴地报错重试,那样模型会陷入死循环,试了几次不行就直接放弃了。
第二个问题,模型生成的日期格式不对。用户说“上周”,模型直接填了2025-06-01到2025-06-07,但实际“上周”应该是6月2日到6月8日。这种日期换算问题,靠模型自己算不靠谱。我们后来在参数校验层加了一个时间表达预处理,专门把“上周”“最近30天”“上个月”这类自然语言解析成确切的日期区间,模型只需要识别意图,做算术的活交给代码。
第三个问题,build_trend_chart返回的图片URL是临时地址,第二天打开就404。这个就不是模型的锅了,是执行体写得糙。我们调整了执行逻辑,把生成的图片保存到固定的对象存储路径,返回带有效期的访问链接。模型层不需要改任何东西,问题就解决了。
调试期间我有一个强烈建议:一定给技能调用加日志。每次模型选了哪个技能、候选技能有哪些、生成参数是什么、校验结果如何、执行结果如何,全部落日志。没有这些日志,你排查问题全靠猜,有了日志,问题基本一眼就能定位。我们内部把这个叫“技能调用回放”,是Agent调试的第一生产力。
4. 上线之后踩过的坑与排查思路
4.1 技能调用错乱:模型为什么老选不对
系统上线一段时间后,你接到的用户反馈会变得五花八门。技能调用错乱是最常见的一种,现象通常分三类:过度调用、选错口径、发明参数。
过度调用的典型场景:用户只是问“这个月销售额多少”,模型却调用报告生成技能,给用户产出一份完整PDF。原因基本是技能描述缺少负面约束,或者技能库里存在一个跟“销售额”高度相关的大而全技能。解法就是按第2.1节的配方改描述,把“问答型需求不要调用”这类边界写清楚。
选错口径更隐蔽。用户说“本季度销售情况”,模型用“最近30天”的口径去查。这种情况往往是参数描述里没有定义清楚周期类型,模型只能自己猜。我在这类问题上的处理办法是在参数schema里增加一个period_type字段,枚举值写死:current_quarter、last_quarter、current_month、custom_range,不给模型自由发挥的空间。
发明参数是另一个高频问题。用户没指定渠道,模型自动加了channel: "all";用户没指定图表类型,模型传了个chart_type: "pie_3d",而你的执行体根本没见过这个值。解法就是additionalProperties: false加上枚举值约束,双管齐下。
我把常见的几个问题整理成了一张速查表,方便团队快速定位:
| 现象 | 可能原因 | 处理方案 |
|---|---|---|
| 用户问数值,Agent却生成完整报表 | 技能描述缺少负面约束 | 增加“问答型需求不要调用此技能” |
| 日期参数格式混乱 | description中没有格式示例 | 给日期字段补充YYYY-MM-DD示例 |
| 模型自己发明不存在的参数 | schema没限制多余字段 | 开启additionalProperties: false |
| 选错统计口径 | 周期类型定义不完整 | 增加枚举字段,限定可选值 |
| 返回内容与目标技能无关 | 执行体返回结构不规范 | 统一为status+summary+data结构 |
4.2 技能数量膨胀:从选错到“选择困难症”
技能库用久了,技能数量一定会膨胀。我们内部从最初十几个技能,半年时间涨到上百个。这时候遇到一个新问题:模型在几十上百个技能里做选择,准确率肉眼可见地下降。你可以把这件事理解成一个分类问题,如果让模型从100个类别里选1个,它的分类错误率必然比从10个里选1个要高。
我们做了三件事来缓解。第一是技能分组。把所有技能按业务域分成查询、图表、文档、消息几个组,模型先选组,再在组内选技能。这个“组级路由”相当于先做粗分类再做细分类,准确率提升非常明显。第二是会话级动态技能集。根据当前会话关联的业务域,动态决定给模型暴露哪些技能,其他不相关的技能根本不参与候选。第三是冷技能下架。超过一段时间没有调用、也没有新需求增长的技能,标记为deprecated,不再参与选择。这不仅提升了准确率,还省了Token。
这里也分享一个教训:我一开始舍不得下架冷技能,总觉得“万一以后用得上”。事实证明,技能库和冰箱里的东西一样,塞得越多越找不到要用的。定期清理不是可选项,是必选项。
4.3 技能迭代:改一个描述引发的连锁反应
最后一个坑是迭代过程中的“副作用”。技能描述这个东西,改起来看似很简单,一行字的事。但它影响的是整个Agent系统的行为边界,牵一发动全身。
举一个我们真实踩过的例子。有个叫做build_trend_chart的技能,原本描述比较宽泛:“根据数据生成可视化图表”。后来为了更精准,我们改成“只生成折线图,适用于展示时间序列趋势;不要生成柱状图”。结果呢?另一个需要柱状图的场景,模型在选技能时认为这个技能描述明确排除了柱状图,于是干脆一个图表技能都不调用,导致该场景的自动出图量下降接近三成。
这个事故之后,我们定了三条硬性流程。第一,技能描述改动必须过回归集。每个技能配一个20条左右的历史真实问题集,改动前用旧描述跑一遍,改动后用新描述跑一遍,对比技能选择结果,差异超过阈值就停止上线。第二,灰度发布。技能库配置支持版本切换,改完先让5%流量验证,观察几天指标没问题再全量。第三,可回滚。每次技能改动的上一个版本必须能一键恢复。这三条看着繁琐,但真的能救命,尤其是技能库规模大了以后,一个描述的微小变化,下游可能炸一片。
我在实际项目中养成的另一个习惯,是给每个技能指定一个“技能负责人”。不是谁都能随手改描述。技能描述看似是一段短短的文本,实际定义的是Agent系统的行为边界,改动之前必须想清楚会影响哪些场景、哪些用户、哪些下游依赖。这个机制听起来偏管理,但到了生产环境,它就是稳定性的最后一道防线。
回头再看,agent-skills表面上是给Agent装技能,本质上是给复杂的业务逻辑建立一套可控的服务目录。一次一次的踩坑,一次一次的调描述、改参数、加日志,最后沉淀下来的,就是这套越用越顺的技能库体系。