1. Agent技能到底是什么:先拆掉两层误解
过去大半年我一直在折腾Agent相关的东西,从简单的文档问答到多步工具调用,再到带状态管理的复杂工作流,几乎每个阶段都会遇到同一个瓶颈:Agent看起来什么都懂,但做事情总是不稳定。你给它一个简单的"帮我查一下今天的天气再提醒我带伞",它能流畅完成;可一旦任务变成"每天早上自动汇总前一晚的监控告警、按业务线去重、生成日报并推送给对应的值班群",它就频繁卡壳——要么步骤顺序错乱,要么某个环节的输出格式对不上导致下游解析失败。
后来我逐渐意识到一个问题:问题不在模型本身,而在"技能"的缺失。市面上大量的Agent项目都在做同一件事——把模型接上工具、塞进提示词、然后期待它表现得像个人类助手。但"会调用工具"和"拥有技能"之间,隔着一条很深的鸿沟。
先说我的第一个误解:以为提示词就是技能。
早期我做意图识别Agent,为了让模型按照固定流程做事,我在System Prompt里写了长达两百行的步骤说明,比如"第一步获取用户意图,第二步确认参数完整性,第三步调用查询接口,第四步格式化结果"等等。结果是,简单场景确实能跑通,但只要用户表述稍微绕一点,模型就会跳过某个步骤或自创步骤,整个流程就崩了。后来我理解了:提示词是"说给他听的规则",模型确实读进去了,但它在每一步推理时都要重新理解这些规则,没有任何确定性保障。技能如果是靠文字约束来定义的,那Agent的表现上限就是提示词理解的波动上限。
第二个误解更隐蔽:以为接入工具就等于拥有了技能。
我见过很多项目把"技能库"做成一个工具清单——查天气一个函数、发邮件一个函数、算汇率一个函数,每个函数配一段OpenAPI描述,Agent需要时就从清单里挑一个来调。这当然比什么都没有强,但它离"技能"还差得远。原因在于,工具只是技能的执行末端,技能是一整套行为闭环:什么时候该用、用什么参数、中间有哪些校验和兜底、输出如何规范化、失败了怎么降级。一套成熟的技能,应该像人的一个职业习惯一样——遇到某种场景,自动触发一套已被验证过的行为模式,而不是每次都在"要不要用工具、用哪个工具、参数怎么凑"这些环节上反复犹豫。
我真正开始认真对待"Agent技能"这个词,是因为一个偶然看到的开源项目骨架(下文就叫它 agent-skills 项目吧)。它没有在提示词里教模型"你应该怎么做",而是把技能定义成一种可注册、可路由、可自省、可编排的模块化组件。我当时的第一反应是:这思路有意思,它默认Agent不是一个通用的推理机器,而是一个需要"装配"各种能力的运行时框架。沿着这个方向我重构了自己手里好几个Demo项目,后续的稳定性提升非常明显。
这篇文章就把我对Agent技能的完整理解拆开来聊,包括技能的分类、声明方式、注册路由、编排机制、以及我在实测里踩过的坑。适合两类读者:一类是把Agent接进了业务系统、正在被"不稳定"逼疯的开发者,另一类是刚接触Agent开发、想知道"除了堆提示词和工具,还能在哪用力"的新手。
2. agent-skills 的技能分类:为什么必须拆成三层
当我第一次打开 agent-skills 的技能目录时,发现它把所有技能分成了三个层级,而不是平铺一长串工具函数。这个设计一开始让我有点困惑,但跑了一段时间之后,我意识到这可能是整套项目最正确的决策。
2.1 原子技能:最小可执行单元
原子技能是 agent-skills 的底座。它对应一个不可再拆分的具体操作,比如"查询某个指标的历史数据""发送一条IM消息""从一段HTML里提取正文"。
我自己的定义标准是:如果一个操作还需要被其他逻辑选择"先做A再做B还是先做B再做A",那它就不是原子的。原子技能内部的逻辑是确定的、封闭的,外部只需要传参数并拿结果。以"查询监控指标历史数据"为例,它可以进一步拆成"连接时序数据库→拼装查询语句→执行查询→做降采样→返回序列",但在技能层我当然不会把连接数据库当成技能,那是函数内部实现的细节。技能对外暴露的是一个有语义边界的动作,内部实现随便换,不影响上层编排。
agent-skills 对原子技能有两个硬性要求,我觉得非常值得学习:
- 输入必须是强类型结构体。这一条直接把很多"模型抽参不准"的问题提前拦下了。模型只需要从用户话里抽取结构化参数,而不是口头描述要干什么。
- 必须有明确的成功/失败判定。比如查询接口超时、返回空数据、鉴权失败,各自对应不同的退出状态。Agent编排层会根据退出状态决定下一步动作。
我在自己的项目里复刻这个设计时,把每个原子技能写成一个仅依赖输入参数和全局配置的纯函数,不访问任何外部可变状态。好处非常直观:测试好写,复用率高,而且可以被多个上层技能安全地并发调用。
2.2 领域技能:有语义的流程编排
领域技能是"原子技能 + 决策逻辑"的组合,完成一个有语义的任务闭环。举个例子,"生成每日值班报告"可以是一个领域技能,内部依次调用三个原子技能:拉取告警数据、按业务字段聚类去重、渲染Markdown报告。整个过程对外只暴露两个参数——日期范围和业务线列表。
这里最关键的设计点是:领域技能需要携带自己的决策逻辑。也就是说,它不只是把一个工具清单串起来,而是包含"在什么情况下走哪条子路径"的判断。agent-skills 用一个轻量的条件指令来描述这些分支,比如"如果一级告警数大于0,则追加高优摘要模块;否则跳过该模块"。这一层判断原本需要召集大模型做,现在技能自己就能处理,既快又不会漂移。
我在实操中体会最深的是:领域技能是模型幻觉的重要屏障。因为模型不需要在每次执行时重新发明流程,它只需要选中正确的领域技能并传入正确的上下文,剩下的细粒度步骤全部由技能内部接管。换言之,模型负责"开锁",技能负责"进门之后的路怎么走"。
2.3 全局技能:跨领域的通用约束
全局技能是三层里最容易被人忽略、也最容易被滥用的。它不干实事,但它影响所有技能的执行边界。
agent-skills 把全局技能理解为一段跨领域的行为协议,比如"涉及用户隐私数据时必须脱敏""所有写操作在执行前需要二次确认""对上游服务的调用必须带链路追踪ID"。这些规则不属于任何一个业务领域,但又必须被所有领域技能和原子技能遵守。
我自己的实现方式是把全局技能做成一个可插拔的中间件链,挂载在技能执行的入口和出口。入口处统一注入上下文(当前用户、链路ID、权限级别),出口处统一做结果合规校验。这样,任何新增的技能只要挂到框架里,自动获得全套安全与可观测性保障,不会出现因为某个新人写了新技能而漏掉脱敏逻辑的情况。
2.4 分层后的实际收益
我把一个原本平铺了47个工具的Agent项目重构为 agent-skills 的三大层级后,收益非常显著:
| 观察维度 | 重构前 | 重构后 |
|---|---|---|
| 工具/技能总数 | 47个平铺 | 原子技能32个、领域技能9个、全局技能4套 |
| 一次多步任务的模型调用次数 | 6-9次 | 2-4次 |
| 任务成功率(同场景压测50次) | 68% | 93% |
| 新增一个业务动作的时间 | 改提示词+调试 | 写原子技能+注册+挂测试 |
| 排查一次流程故障的定位范围 | 整个提示词逻辑 | 收敛到单条领域技能链路 |
这个表未必有统计上的严谨性,但趋势在我多个项目里都是一致的。分层不是在增加架构复杂度,而是在给不确定的大模型行为提供确定性锚点。每个技能层级都对模型暴露更少的决策面,模型做得事情越少,出错概率就越低。
3. 技能注册与调用的运行时机制:从声明到路由的完整链路
在 agent-skills 的框架里,技能不是直接以函数形式暴露给模型的,而是经过了一套完整的注册、声明、路由、执行链路。这一套机制的目的是:让模型在正确的时机以最小的认知负担选到正确的技能。
3.1 技能描述文件的Schema设计
每个技能(无论原子还是领域)在 agent-skills 里都对应一个 YAML 描述文件。这个文件不是给人看的文档,而是给模型做路由用的核心元数据。我把一个我自己的技能描述文件简化后贴在这里,可以作为参考:
name: generate_duty_report description: 根据告警数据生成每日值班报告,支持按业务线聚合和降噪 type: domain_skill version: 2.3.0 tags: - monitoring - report - alert inputs: - name: start_time type: string format: datetime required: true description: 统计开始时间,ISO格式 - name: end_time type: string format: datetime required: true - name: biz_lines type: array item_type: string required: false default: [] description: 业务线列表,为空时表示全量 output: type: markdown description: 值班报告正文 timeout_ms: 15000 error_codes: - code: DATA_FETCH_FAILED description: 上游数据源不可用 - code: EMPTY_RESULT description: 时间范围内无告警 entrypoint: handler.generate_duty_report这段声明里值得深究的不是字段本身,而是"description"怎么写agent-skills 对 description 的要求是"面向模型的,而非面向人的"。很多开发者写技能描述习惯于写"处理告警数据",这对人来说简洁,但对模型来说信息量完全不够。我在实际调优中发现,更有效的描述是"当用户需要查看值班期间告警汇总、日报、周报或异常事件梳理时使用,注意如果用户只询问单条告警详情,则应调用 query_alert_detail,而不是本技能"。把相似技能的区分点也写进去,可以显著减少路由错误。
3.2 注册中心与模型路由的联动
技能写完以后不是自动生效的,而是要注册到 agent-skills 的技能注册中心。注册中心干的事很简单:把所有技能的元数据汇总,并在模型路由阶段做一次预筛选。
这里的核心问题是:当Agent需要执行"给张三发一份今日告警摘要"时,注册中心怎么从几十个技能里快速选中 generate_duty_report?
我实测过的 agent-skills 做法是两段式路由。第一段是关键词预筛:用模型轻量计算用户请求向量,与所有技能的 tags+description 向量做相似度匹配,取 TopK。第二段是精确命中:把 TopK 候选技能的完整描述(比向量索引用的摘要版本更长)拼接进模型上下文,由模型在候选范围内做最终决策。
这个两段式设计很狡猾。它既避免了把所有技能完整描述全塞进上下文的Token浪费,也避免了只用关键词匹配导致的语义偏差。我在自己项目里把TopK设为5,命中率最稳;设成3会偶发漏选,设成10则会让模型在太多候选里迷路。
3.3 自省机制:Agent如何知道"自己会什么"
这个机制是 agent-skills 另一处打动我的设计。它允许Agent在任何时刻"知道自己有哪些技能可用",而不是依赖系统提示词里写死的那句"你是智能助手,你可以使用以下工具"。
自省机制的实现方式比较巧妙:Agent内部维护一个技能清单视图,但这个视图不只是单调罗列,而是带状态的——哪些技能当前可用、哪些技能因为参数不全而处于"等待补充状态"、哪些技能由于上游依赖故障而暂时停用。
举例说明:A技能依赖消息推送通道,推送通道在维护中;此时Agent通过自省接口获取技能视图,发现推送相关的领域技能都被标记为 degraded 状态,于是它就不会贸然承诺用户"我帮你把日报告诉张三",而是会明确说"日报已经生成,但消息通道暂不可用,您可以稍后重试或手动查收"。这种边界感知能力极大地减少了"承诺了但做不成"的尴尬。
我自己的经验是:自省机制的引入必须在多技能项目里尽早做,不要等技能数量膨胀到二三十个再补。因为技能越多,模型越容易在犹豫中犯错;有了自省,模型才可能像一个"有经验的老员工",清楚地知道自己的能力边界和当前环境状态,而不是一个盲目许诺的实习生。
4. 编排层设计:从人写死流程到Agent自主组合的中间态
技能有了、路由也有了,下一步就是让多个技能协作起来完成复杂任务。这一章的标题我想写的是"编排层",因为这是 Agent 项目从"单个动作玩具"变成"业务流程执行器"的分水岭。我见过太多项目在这一步上走极端:要么让人把流程死写成硬编码,要么让模型自由发挥完全不设边界。agent-skills 选的是一条中间路线。
4.1 三种编排模式与我的选型标准
在 agent-skills 的运行时里,编排不是一种固定策略,而是三种模式的混合,根据当前任务的确定度自动选择:
- 顺序编排:适合步骤顺序明确、依赖关系硬性的任务。比如生成日报后推送,必须是先生成再推送,没有第二条路。这种模式我通常用在领域技能内部的原子步骤上,由技能自己决定顺序,不开放给模型商量。
- 条件编排:适合带有分支判断的任务。比如"如果有高优先级告警,就立即推送并触发页面告警;否则只写入日报"。条件分支既可以写在领域技能内部,也可以作为领域技能之间的跳转逻辑,由Agent根据上下文数据决定走哪条分支。
- 并行编排:适合相互无依赖的多个任务。比如同时查询不同数据源的指标、同时给多个群发通知。并行能显著缩短总时长,但必须小心共享资源竞争,后面我会讲案例。
我的选型标准说来也简单:如果业务方已经明确了流程,我不让模型自由编排,直接做成顺序+条件;只有当业务方自己也说不清所有边界情况,才让模型在有限的候选技能之间做编排。也就是说,不确定性越高、出错代价越低的任务,可以交给模型决策;确定性高、出错代价高的任务,一律提前固化。
4.2 技能间的数据契约:上下文如何平滑传递
编排一旦让多个技能协作,数据传递就成了隐形的地雷。最常见的坑是:上一个技能输出的字段名和下一个技能期望的输入字段名不一致。比如告警查询技能返回的是 code(数值型),而日报渲染技能期望的是 alert_code(字符串型),两者对不上,渲染出来的表格就变成空值或异常。
agent-skills 的做法是为每个技能定义严格的 inputs 和 outputs Schema,并在编排引擎里加了一层隐式映射器。映射器的职责是:按照数据契约把上游输出转换成语义等价但字段名/类型兼容的下游输入。对标准常见格式(时间字符串、枚举值、金额单位)直接转换,对复杂嵌套结构则要求技能开发者显式声明。
我在一次压测里就遇到过这种情况:告警数据里时间字段是时间戳(例如 1738800000000),而日报模板期望的是 YYYY-MM-DD HH:mm 格式,映射器自动完成了转换,但日志里打了一行告警"time_ts 已隐式转换,请尽快在技能声明中显式定义 format"。这种"能自动转,但必须让你知道"的设计很聪明,避免了隐性魔法带来的调试困难。
4.3 冲突解决与回滚:编排的最后一层防线
多技能协作的下一个问题:如果中间某个步骤失败了,整套流程怎么办?agent-skills 没有把这个问题留给上游模型去"临场发挥",而是提供了两套出可靠的策略:
- 先做检查再动手:任何写操作型技能(发送消息、修改配置、创建工单)都要求先走一次只读的"预检"技能,预检通过后才能真正提交。预检技能通常是轻量的,只验证参数合法性、权限是否存在、目标是否可达。
- 分级回滚:对于跨技能的任务,agent-skills 会记录每个已执行技能的状态,一旦后续技能失败,触发回滚策略——有的技能需要做补偿操作(比如已发送的群消息改为一条撤回消息),有的只需要标记"部分完成"状态并记录上下文快照。
这套机制让Agent从"一次只能做一个动作"进化为"能负责任地执行一个流程"。负责任的意思是:要么做成,要么在做成失败时留下可追踪的状态,让用户或上层系统知道卡在哪、为什么卡住、还能不能补救。
5. 实测复盘:压测这套技能框架时踩过的四个坑
前面讲了架构和原理,现在说点实在的——这套框架我在真实场景里压过几十轮,踩过的坑一点不比收益少。每个坑我都花了至少一个晚上定位,希望写下来能让大家少走弯路。
5.1 技能描述里的"幻觉吸引力"导致的错误路由
这是我在 route 层遇到的第一个诡异现象。我有一个技能 query_alert_detail,用来查单条告警的详情;另一个技能 generate_duty_report,用来生成值班报告。我在前者的描述里写了一句话:"如果用户需要生成报告,请使用 generate_duty_report"。听起来像贴心的互引,结果模型在用户没说报告需求时也频繁调用 generate_duty_report——因为描述里提到了这个词,模型被"吸引"过去了。
后来我把技能描述里所有涉及其他技能的字段全部删掉,只保留本技能的触发条件和边界,路由错误率立刻下降了约七成。结论很明确:技能描述不是文档,是路由条件;写进去的每个词都可能影响模型的路由决策,多余的信息就是噪声。这个教训我记到现在。
5.2 递归循环:技能自调用导致的资源耗尽
第二个坑比较有戏剧性。我设计了一个自定义技能诊断器,逻辑是"当某个技能连续失败时,尝试用诊断技能检查环境依赖是否正常"。结果在一次测试中,诊断技能本身因为依赖的某个内部服务临时不可用而失败,失败后又触发了诊断技能自身……于是形成了A技能失败→调用诊断技能→诊断技能失败→再调用诊断技能的死循环。直到外部超时机制介入,整个消息队列已经被塞满了诊断任务。
解决方案并不复杂,但必须强制实施:
- 为每个技能设定最大递归深度,默认1级。即技能内部不允许调用自身或调用已在本轮调用栈里的技能。
- 为每次编排设定全局执行预算(最大技能调用次数、最大耗时、最大Token消耗),超出即强制中止。
- 循环检测不只是看技能名,还要看"参数+技能名+调用链"的组合。同样的技能用不同参数查同一类数据,同样可能构成发散循环。
这类问题在纯函数工具时代几乎不存在,但在带决策能力的技能时代会悄悄出现,因为你赋予了技能"分析再行动"的能力,而分析本身可能依赖其他技能。
5.3 输出格式漂移:同一个技能的返回结构在不同版本模型的上下文里变化
第三个坑更隐蔽。某个文本摘要技能,我最初用较旧的模型压测时,返回的是结构化的 JSON(summary + keywords + affected_systems)。后来项目换了更强的新模型,这个技能的输出悄悄变成了意意生成的自然语言段落,不再返回 keywords 字段。接下来下游的日报技能解析时直接报错,整个编排链路全部变红。
排查过程很痛苦,因为我下意识以为是技能内部代码改了。后来查日志才发现输出变化发生在同一个技能、同一段代码、同一个上游数据下,唯一区别是模型版本。这件事让我明白:在技能链路里,凡是模型生成的中间产物,都必须有一层强制校验器。校验器的作用是:把模型输出解析到结构化契约里,不符合契约的部分宁可直接丢弃并标记降级,也不能放任不规范数据流向下游。
在 agent-skills 里,这层校验是全局中间件的一部分。我在自己的项目里做得更狠一点——所有领域技能的最终输出都必须经过一个 JSON Schema 校验,校验失败则自动进入"重试+修正提示"的循环,最多两次;两次都失败就宣告技能执行失败,而不是把脏数据传出去。
5.4 延迟感知:高延迟技能会拖垮整个编排
最后一个坑是性能维度的。我的系统里有几个需要调用第三方服务的原子技能,响应经常在2-3秒,偶尔飙到10秒。起初没太在意,直到我跑了一个包含5个串行技能的编排任务,总耗时直接冲到20秒以上,用户侧体验明显恶化。
agent-skills 给出的思路是:在技能元数据里显式声明 latency_profile,分为 fast(<500ms)、medium(<2s)、slow(>2s)。编排引擎根据这个配置做两件事:一是优先让快技能先执行并缓存结果;二是将可并行的高延迟技能调度到同一批并发线程里。我把几个慢技能的调用模式从串行改为并发后,同一任务总耗时从22秒降到11秒,接近一半。
但要注意,开启并行不是免费的。我当时为了追求快,把两个都会写同一份缓存文件的技能放进并行组,结果出现竞态条件,缓存数据互相覆盖。加了按 key 分片的锁之后才稳定。所以说,并行编排的收益明显,但它要求技能对共享资源有更强的隔离意识。
6. 让Agent长出"肌肉记忆":技能的记忆化与持续演进
最后聊一个很多项目都不会提、但实际体验差异巨大的话题:技能如何进化。
一套技能框架跑起来以后,你会发现一个有意思的现象:有些技能频繁被调用,有些技能几乎选不中。agent-skills 在这一点上提供的不是自动学习,而是一套观测与迭代的工具链。我用下来最有价值的三个功能如下。
6.1 命中率统计与动态权重
系统会为每个技能记录:在多少轮会话里被纳入候选、被最终选中多少次、执行成功率和平均耗时。有了这些数据,我每周可以很直观地发现某个技能描述改得太宽泛导致命中率虚高但成功率很低,或者某个技能已经长期没有被子路由选中,说明它可能已经被另一个技能的有效描述覆盖了。
这里有个非常实用的调参技巧:如果命中率低但人工评估觉得该技能确实有用,可以先调 tags——给它补充更细化的触发词。调完再看命中率变化。相比之下,大规模重写 description 的风险更高,因为可能影响其他技能的互斥关系。调参遵循"小步快跑、单点复测"的原则是我踩过不少坑后总结出来的。
6.2 新技能版本的A/B切换
技能升级最怕的事情是:新版本在某些场景下变聪明了,在另一些场景下变笨了,但整个系统已经在用新版,无法快速回退。
agent-skills 支持技能版本并行注册。我可以在路由层把同一个技能的 v1、v2 同时注册,然后设定灰度比例——比如10%的请求走 v2,90%走 v1。灰度跑一段时间后,对比两版本的成功率和平均耗时,再决定是否全量切换。这个过程让我对技能改动有了充分安全感,不再害怕上线新技能。
一开始我觉得这个功能是花架子,直到有一次我把日期解析逻辑全局替换后,灰度让成功率的下跌在影响10%用户时就被发现了,没有酿成全量事故。从那以后,任何技能大改我都会至少灰度半个周期再全量。
6.3 从使用日志中挖掘隐藏的技能组合模式
这个功能更像是一个离线分析工具,用来回答一个我很关心的问题:哪些技能经常被接连调用?比如日志显示 query_alert 之后,紧接着调用 create_ticket 的概率很高,那说明这两个技能之间存在一条高频组合路径。
发现它以后,我通常会做两件事:第一,把这条组合路径固化成一个新的领域技能,减少模型在中间环节的决策次数;第二,把组合技能的元数据在路由层做"打包推荐"——当候选技能里出现 query_alert 时,把 create_ticket 的权重略微上调。这个细微改动在压测里把"告警→工单"的场景成功率又拉高了几个百分点。
我觉得这就是Agent技能的终极形态:技能不再是静态的工具,而是一组会持续观察自身使用规律、不断把显式编排沉淀为隐式习惯的组件。项目跑得越久,系统就越懂得如何以最少决策成本完成最常见的任务。
回到我最初的问题——为什么我的Agent看起来什么都懂,但做事情总是不稳定?现在我有了更清晰的答案:因为它在"现场重造轮子"。它每一次都在重新理解任务、重新规划步骤、重新选择工具,而技能体系的本质,就是把这些反复发生的高频行为提前"预制"好,让模型只在真正需要创造力的地方花费推理资源。在这个方向上,agent-skills 这套框架给了我一个非常扎实的起点,也希望这篇拆解能帮到正在跟Agent的"不稳定"搏斗的你。