"agent-skills" 这个标签,最近在智能体开发圈子里出现的频率越来越高。我这次做内部数据助手重构,用的正是这套"技能化"思路:把 Agent 的能力拆成一个个可注册、可复用、可单独测试的技能模块。这篇文章不做概念科普,而是把整个设计思路、落地实现、排错过程完整复盘一遍,方便正在做类似项目的朋友直接对照参考。
先交代一下背景。某公司内部想做一个数据汇总助手,让运营同事在群里问一句"昨天的销售额怎么样",助手就能自己去查数据、算环比、生成柱状图,最后把结论和图表一起推到工作群里。看起来不复杂,但第一版实现就让我栽了跟头——因为我把所有能力全塞进了一个系统提示词里。这篇文章适合谁看?正在做 Agent 应用、想把"演示级项目"推到"产品级"的人,以及第一次接触技能化设计、想弄明白这个概念到底在解决什么问题的读者。我会把技能的结构、实现路径、踩坑记录全部摊开讲,不整虚的。
1. 为什么我最终放弃了"一个提示词包打天下"
1.1 一千行系统提示词的真实下场
第一版方案很直接:把助手能做的事,全部写到系统提示词里——查数据的 SQL 规则、环比计算口径、周报模板、图表配色偏好、通知发送格式……写了整整一千多行。当时觉得,反正模型上下文窗口够大,写详细点总没错。
实测下来问题非常明显。第一是准确率下降:模型在长提示词里抓不住重点,经常把两个相近指令的措辞混在一起。比如我规定"查询销售额时必须按业务线分组",又在后面写了"环比计算用自然日对齐",结果模型有时候会在分组上加了环比,有时候又把对齐规则套到别的字段上。第二是响应变慢:每次请求都要把上千行提示词重新处理一遍,平均延迟比后期技能化方案高了将近一倍。第三也是最难受的——改一处需求,要全网重新测试。运营说"图表颜色换一下",我都得担心会不会影响查询逻辑。
这不是模型能力问题。把几十条指令混在一个上下文里,每条指令都在争抢模型的注意力,代码里这叫全局变量过多导致耦合,提示词里是一样的道理。
1.2 Skills 模式到底解决了什么
后来我换了一个思路:模型不再负责"知道怎么做",只负责"判断该用哪个技能,然后调用它"。核心逻辑变成了三个问题:这个需求是什么类型?该调用哪个技能?参数是什么?至于 SQL 怎么写、图表怎么画、消息怎么发,全部交给对应的技能执行体去处理。
这套模式解决了几件事。
一是关注点分离。模型只需要做规划和选择,不需要把大量实现细节塞进上下文,指令之间的互相干扰基本消失。二是可独立测试。每个技能都能单独输入参数验证结果,不用为了改一个图表配色把整条链路重新跑一遍。三是可复用。一个技能写好了,换一个项目可以直接挂载。四是职责清晰。负责数据的只碰数据,负责通知的只碰通知,排错时边界非常清楚。
1.3 一个关键认知:Agent 和 Skill 的关系
我想通了一件事:Agent 和 Skill 的关系,就是大脑和工具箱的关系。大脑不需要知道扳手的钢材标号,也不需要知道螺丝刀的手柄材质,它只需要知道"拧螺丝用哪个"。每个技能就是工具箱里的一个工具,有自己的名字、适用范围、使用说明。
对应到代码上,Agent 是一个"调度器"加"决策器",Skill 是一段封装好的执行逻辑。两者通过一份"描述文本"对话。这个认知在后面帮我避免了很多弯路的判断——比如技能代码写得多精湛不重要,模型能不能通过描述正确选中它,才是真正决定成败的部分。
2. 拆解一个 Agent Skill 的标准结构
2.1 从一个具体任务说起
拿上面那个需求举例:"帮我把这个 Excel 的销售额统计出来,画个柱状图,再给销售群发条通知。"拆开看,里面其实是三个独立能力:读取并统计 Excel 数据、根据数据生成柱状图、发送群消息。这三个能力之间有关系,但没有强耦合——先有统计结果才能画图,画完图才能发通知,但每一步本身都是独立的。
这就是技能拆分的起点:把一个任务切成多个"单点能力",每个单点能力做成一个 Skill。模型先识别出任务里有三个诉求,然后依次调用三个技能,前一个技能的输出作为后一个技能的输入。
2.2 Skill 的四要素:名称、描述、参数模式、执行体
一个标准的 Skill 结构,在我看来由四部分组成。这里我用一个示例表格说明:
| 要素 | 作用 | 示例 |
|---|---|---|
| 名称 | 全局唯一标识,模型用来精确指向目标 | stats_sales_from_excel |
| 描述 | 告诉模型"这个技能做什么、什么时候用、什么时候不该用" | 读取 Excel 文件并按指定维度统计销售额 |
| 参数模式 | 定义模型需要提供的入参结构和约束 | file_path,group_by,date_range |
| 执行体 | 真正干活的代码 | 用 Python 读取文件、聚合计算、返回结果 |
名称要短且语义清晰,最好动词开头,让模型一眼看懂意图。参数模式建议直接用 JSON Schema 描述,包括字段类型、是否必填、枚举值范围,这些约束越明确,模型传错参数的概率越低。执行体则是普通函数,输入参数、输出结果,只做自己的事,不掺和其他逻辑。
2.3 描述即契约:LLM 眼中的 Skill 长什么样
这里我要重点强调一个容易被忽略的事实:模型根本不会看你的执行体代码,它看到的只有"名称 + 描述 + 参数模式"。每次请求时,框架会把所有已注册技能的这些元信息全部塞给模型,让模型从中选择最合适的一个。
所以描述文本的质量,直接决定了技能会不会被正确调用。我第一版写的描述很"人话",比如:"这个技能可以帮你处理各种表格数据,快速得到你想要的统计结果。"看起来没问题,实际一测就出乱子——模型遇到"把 PDF 里的表格转成 Excel"这个任务时,居然也选了它,因为它描述里有"各种表格数据"这几个字。
后来我把描述彻底重写了,规则很简单:说清楚做什么,更要说清楚不做什么,必要时给一个边界示例。改成这样:
读取 Excel 文件(.xlsx/.xls)并按指定维度统计数值字段。适用于本地已存在的表格文件,不适用于 PDF、网页表格、图片中的表格,也不执行任何数据写入操作。示例:用户提供"2024年销售明细.xlsx"并要求按月份汇总,可使用本技能。
模型看到这个描述,就不会再把扫描 PDF 的任务往这个技能上带了。把这个原则放大到整个技能库:描述就是技能和模型之间的 API 契约,写得模糊,模型就靠猜,一旦猜错,后面的所有结果都是错的。
3. 从零搭建技能库:设计与落地路径
3.1 技能粒度怎么切
最常被问的问题就是:一个技能到底该切多大?我的经验是看"业务上能不能独立验收"。
比如"发送邮件"是一个技能,它要负责生成邮件内容、配置收件人、调用邮件接口、处理发送失败。但"生成邮件内容"和"发送邮件"我建议拆开,因为前者可能被"生成日报并发送"这种场景复用,后者也可能被"只发附件不生成正文"这种场景单独调用。反过来,如果切得太碎,模型要在几十个技能里做选择,选择成本会急剧上升,误选率也会上升。
我自己的参考标准:一个技能尽量只做"一件业务上可以说得清的事"。查询是一个、画图是一个、发通知是一个、计算环比是一个。尽量控制在 10 到 20 个技能以内,超过这个数量就要开始考虑归类设计,而不是继续堆。
3.2 注册、发现与调用的完整链路
落地时我用了一个很轻量的注册机制,核心思路就是装饰器加一个全局注册表。下面是简化后的示例:
# 技能注册表 SKILL_REGISTRY = {} def skill(name, description, parameters_schema=None): def decorator(func): SKILL_REGISTRY[name] = { "name": name, "description": description, "parameters_schema": parameters_schema or {}, "func": func, } return func return decorator @skill( name="stats_sales_from_excel", description="读取 Excel 文件并按指定维度统计数值字段。适用于本地已存在的 .xlsx/.xls 文件,不适用于 PDF、网页或图片中的表格。", parameters_schema={ "type": "object", "properties": { "file_path": {"type": "string"}, "group_by": {"type": "string", "enum": ["month", "week", "business_line"]}, "date_range": {"type": "string"} }, "required": ["file_path", "group_by"] } ) def stats_sales_from_excel(file_path, group_by, date_range=None): # 实际读取文件、聚合计算 return {"total": 12345, "items": [...]}调用链路是这样的:用户输入 → 模型看到全部技能的名称和描述 → 模型决策输出一个结构化的调用意图(选哪个技能、传什么参数)→ 框架解析并对参数做校验 → 执行技能 → 把结果返回给模型 → 模型基于结果组织最终回复。
整套链路里,参数校验是我后来才补上的,补完之后错误率下降了一大截。校验层做的事情包括:类型强制转换、枚举值核对、必填字段检查、默认值回填。比如技能要求group_by取month、week、business_line三者之一,模型传了个"by month",校验层直接规整成month,而不是带着错误参数硬执行。
3.3 让技能可以组合:从单技能到工作流
单技能能解决单点任务,但真实业务往往是多步骤的。上面那个需求就需要三个技能串起来:先统计、再画图、最后发通知。
组合方式有两种。第一种是让模型自己编排:模型在一轮回复里连续输出多个调用,前一个结果喂给后一个。这种方式灵活,适合步骤不固定、依赖不强的场景,但代价是模型可能在中间步骤出错,而且排错时要看完整调用链才能定位问题。
第二种是显式工作流:把"统计销售额→生成柱状图→发通知"定义成一个固定的技能序列,甚至干脆把整个流程封装成一个"周报生成"大技能,内部顺序写死。这种方式稳定,适合流程固定的高频场景,代价是不够灵活。
我的做法是两种混合:原子技能保留,同时提供一组流程技能,流程技能内部通过代码编排原子技能。模型层面只看到"日报推送"这个高层技能,不用关心中间分了几个步骤。这样既稳定,又保留了底层技能的复用性。
4. 实测中踩过的坑与完整排查链路
4.1 坑一:描述写得越"像人话",Agent 越容易选错
这个坑我在前面已经埋了伏笔,这里说完整排查过程。某天我看调用日志,发现"表格处理"这个技能的调用次数异常上升,但业务方并没有新增相关需求。直觉告诉我哪里不对,于是我把最近一周的误选样本全部拉出来,逐条对比用户原话和模型选中的技能,发现大量用户问的是"把这份 PDF 里的表格提取出来""这张图片里的数据帮我读一下",模型统统选到了 Excel 处理技能上。
根源就出在描述里的"各种表格数据"。我当时为了让描述好懂,用了一些概括性措辞,结果概括性措辞恰恰成了模型的错误锚点。修复动作很简单:把描述改成精确边界,明确写"不适用于 PDF、网页、图片中的表格",并补了一个正面示例。改完之后,表格处理技能的误选率从之前的 18% 降到了 4% 左右。这次排错让我养成了一个习惯:每写一条技能描述,都会反问一句——"如果用户问的是相邻场景,模型会不会选错?"
4.2 坑二:参数校验缺失引发连锁错误
第二个坑是在跑"周报推送"流程时发现的。流程技能要求传入weekday参数,取值范围是 1 到 5,代表周一到周五。某次测试时模型传了字符串"Monday",执行体直接抛了异常。问题在于,异常信息原样返回给了模型,模型看到报错后没有意识到是参数格式错误,反而开始"脑补"重试,连续三次都传了同一个错误格式,最终任务失败,还多耗了几秒钟。
完整的排查链路是这样的:先在日志里看到三次连续调用同一个技能,参数完全一致,返回值都是异常——这明显不是随机抖动,而是模型在徒劳重试。再往前翻,发现第一次调用时参数校验层根本没有,异常直接抛给了模型。修复方案分两步:第一步给所有技能挂上统一的参数校验层,规则是"先校验,后执行",并把错误信息改写成模型容易理解的形式,比如明确写"weekday 字段只接受 1-5 的整数,你传入的是 'Monday',请转换后重试";第二步对同类错误做熔断,同一个技能连续两次因参数问题失败后,不再让模型无意义重试,直接结束本轮并提示用户。
这里想强调一点:错误信息不是写给人看的,是写给模型看的。错误提示里越明确地说清"错在哪、该怎么改",模型越有可能自我纠正。把异常信息写得像代码注释那样晦涩,模型只会继续瞎试。
4.3 坑三:技能副作用污染后续对话
第三个坑比较隐蔽,是状态污染问题。当时有一个"更新订单状态"的技能,被集成到售后的自动处理流程里。某次流程中模型第一次调用后,返回结果是"订单已更新",但流程因为网络问题中断了,模型在下一轮恢复时没有拿到历史的更新结果,于是又调用了一次技能,同一个订单被更新了两遍。
再比如发通知技能,因为重试机制,同一条消息被推了两次,运营同事直接跑来投诉"群里的消息重复了"。这个问题的本质是技能除了返回值之外,还对外部系统产生了副作用,而模型在重新规划时看不到这些副作用。
修复思路有三条。第一,把"读技能"和"写技能"分开,读操作可以随意重试,写操作必须谨慎;第二,给写操作加幂等键,同一个业务请求带上唯一的请求 ID,外部系统根据 ID 去重,重复调用也不会产生重复效果;第三,技能返回结果里加上变更前后对比,让模型在后续规划中能感知到状态已经变化,避免基于旧数据做判断。这三条加完之后,"重复更新""重复通知"这两类问题就基本绝迹了。
5. 把技能库做成产品级:可观测、可迭代、可回滚
5.1 给每次技能调用装上"行车记录仪"
技能化之后,排错的最大便利就是可以精确定位到某一次调用。我建议从第一天起就给每次调用做结构化日志,记录的内容包括:模型选择技能时的原始推理文本、选中的技能名称、实际传入参数、参数校验结果、执行耗时、返回值摘要、本次调用归属的会话 ID。
这里有个细节值得说明:很多框架会屏蔽模型的推理文本,只保留最终的技能选择结果,我强烈不建议这么做。"为什么选这个技能"的推理文本,是排错时最宝贵的线索。比如后来定位"表格处理技能误选"问题时,全靠推理文本里那句"用户提到表格,这个技能和表格有关"才确认模型是被模糊描述带偏的。
5.2 从失败样本反推技能描述
技能库维护不是一次性的。我把线上失败样本按照四类归类:决策错误(模型选错技能)、参数错误(模型传错参数)、执行错误(技能代码或依赖有问题)、模型协议错误(返回格式不可解析)。每两周做一次归类分析,重点看决策错误和参数错误,因为这两类绝大多数都能通过改描述和加强校验解决。
真实数据参考一下:第一个月决策错误占全部失败样本的比例是 46%,我逐个重写了相关技能的描述,加入边界条件和反例,第二个月这个比例降到 22%。参数错误从 30% 降到 9%,靠的是一轮完整的参数校验层。整个过程不涉及任何模型调参,收益全部来自"工程侧修描述、修校验",这也验证了技能化设计的一个核心优势——优化对象是描述和代码,而不是把希望全押在模型升级上。
5.3 技能的版本管理与灰度发布
技能也是代码,也会有迭代,所以版本管理不能省。我的做法是:每个技能保留多个历史版本,但注册表里只暴露一个"当前生效版本"给模型。新版本上线前先小流量灰度,只让一部分会话使用新技能,观察几天的调用准确率和失败率,稳定后再全量切过去,出问题可以秒级回滚。
版本管理还要管住一处细节:模型的缓存可能会让旧描述滞留一段时间。所以在更新技能描述后,我会主动让使用到该技能的会话刷新一次上下文,否则模型手里拿的还是旧描述,新版本等于没上。这个坑我踩过一次,明明改了描述,线上行为没变化,查了半天发现是上下文缓存没清。
最后分享一点个人体会
技能库做到最后,我发现最花时间的不是写执行函数,而是打磨那几百字的描述文本。你在描述上花的每一分钟,都会在调用准确率上找回来。这句话说起来轻巧,但我是踩了三个坑才彻底信了。再补一个小技巧:让模型在调用技能前输出一句简短的选择理由,哪怕只有十几个字,排错时能省一半时间。它不占用太多 token,带来的可解释性收益却非常大。这套模式现在已经成为我做 Agent 类项目的基础框架,如果你正在被"长提示词不听话"困扰,不妨把能力拆开试试,让每个技能自己把自己的事说明白。