Agent能力拆解:用Skills构建稳定可控的智能体行为体系 1. 我为什么要把 Agent 的能力拆成 skills在跑一个真正面向业务的 Agent 项目之前我一直以为 Prompt 写得好、策略想得多Agent 就能稳定输出。后来发现提示词技巧能解决模型的表达问题解决不了行为组织问题。同一个 Agent跑一个月后你会看到它时而聪明得像资深工程师时而蠢得像刚接手的新人——不是模型变了是它的行为没有边界什么任务都往一个巨大的上下文里塞。agent-skills这个名字就是我后来整理这个问题的入手点。它本质上是一个思路也是一个配套的工程骨架把 Agent 能够执行的复杂行为拆成一个一个带有明确触发条件、输入输出契约和执行步骤的技能而不是把所有指令都堆在系统提示词里。一个典型的反例是这样的。你给 Agent 配了读数据库、发邮件、做数据分析、生成图表四个能力然后把每个能力的说明写进系统提示词。初期效果很好但随着业务需求增多提示词越来越长模型开始分不清什么时候该先查数据再分析什么时候该直接发邮件。有一次它甚至把发给客户的周报内容当成了数据库查询语句的一部分。这类问题出现得多了你会意识到Agent 缺的不是能力而是对能力的封装和调度。skills 就是解决这个问题的。这篇文章我把自己整理 agent-skills 的过程完整过一遍包括技能卡怎么设计、运行时怎么注册和调度、上线后踩过的几类故障、以及后续扩展方向。适合正在做 Agent 应用、尤其是已经开始被提示词过载困扰的团队参考。1.1 同一个输入两种完全不同的表现如果你做过带工具的 Agent肯定见过这种场景用户说帮我看看上个月销售数据怎么样挑重点写一封邮件给销售负责人Agent 有时候能正确拆解成读数据 → 生成摘要 → 写邮件三步有时候却只回了一句我建议查看销售看板。我第一次遇到这种不稳定的时候第一反应是换更强的模型。换完之后能撑两周然后又出问题。后来我发现问题不在推理能力而在行为规划没有落到一个可检验、可干预的结构上。Agent 把用户的请求映射到内部行动时如果只有一个巨大的抽象系统提示词它每次都在做开放性的即兴规划结果自然不稳定。而如果把拆解后的行动固化成一个个 skills每个技能负责一个明确的子目标模型的规划负担就小了很多输出也变得可预期了。这里要明确这些技能不是简单的工具函数。工具函数通常解决一个原子操作比如执行一段 SQL、调用一次 API而一个技能是带状态的完成一个目标它可能包含多个工具调用、中间判断和输出整理。打个比方工具是螺丝刀技能是把这张桌子组装好的完整流程既包含螺丝刀的使用时机也包含先装哪块板子的顺序。1.2 技能、工具、提示词之间到底差在哪里很多团队在开始做 Agent 时会纠结一个词我到底该把能力做成工具类还是做成技能类这个问题问多了我发现可以拿一张表格说清楚区别。维度提示词片段Tool工具Skill技能粒度行为建议原子操作组合流程是否含决策含不含含可控性弱强较强复用性低中高典型例子记住用户问数据时先读数据库execute_query(query)月度销售分析查数→分析→出报告出错影响只有引导作用单步失败整体结果异常需追踪多步我并不是说提示词不重要。提示词仍然是定义 Agent 人设和价值观的地方但它不适合承载流程性知识。一份好的技能定义应该能让你读它的时候就知道什么条件下调用、输入是什么、输出是什么、内部步骤大约有几条。这更像一份内部 Wiki而不是一句叮嘱。还有一个容易混淆的词是plugins。插件通常是技术产品形态决定你给 Agent 接入哪一组外部能力。而 agent-skills 更偏行为编排层决定 Agent 在一个具体业务场景中应该以什么顺序调用这些能力。同一个插件可能被多个技能复用一个技能也可能跨越多个插件。2. 给技能定边界设计一份能落地执行的技能卡agent-skills 的核心单元是技能。但它不能只是一个自然语言描述因为自然语言描述既没法被程序稳定解析也没法方便地做测试和版本管理。我最后采用的方案是每个技能对应一个结构化文件业内一般叫技能卡Skill Card内容包含元信息、触发条件、输入槽位、执行步骤、输出契约和误区提醒。把技能信息做成结构化文件是整套系统的地基。2.1 技能卡的基本字段和写法我第一次做技能卡的时候写得像一篇小作文把能想到的业务背景都写进去了。结果模型在判断是否调用该技能时反而被无关信息干扰。后来我精简成一套固定的字段原则只有一个让模型以最少的阅读量理解什么时候用、传什么、怎么跑、出什么。下面是实际使用的一份技能卡示例name: monthly_sales_report version: 1.3.0 description: 生成某产品线的月度销售报告包括销量趋势、地区占比和异动提醒。 trigger: - 用户要求“月度销售报告” - 用户提到“上月销售情况”且需要正式报告 inputs: product_line: type: string required: false default: all description: 产品线名称空默认全产品线 month: type: string required: true description: 报告月份格式 YYYY-MM recipients: type: list[string] required: false description: 收件人可为空 steps: - call: metrics_service.query_sales args: product_line: ${inputs.product_line} month: ${inputs.month} - call: metrics_service.calculate_change_ratio args: prev_month: ${input.prev_month} - call: report_writer.compose_summary args: charts: ${steps.result.charts} - call: mailer.send_report args: to: ${inputs.recipients} output: format: markdown schema: summary: string trend_table: list[row] abnormal_metrics: list[item] mistakes: - 不要把邮件正文和报告正文混淆 - 计算同比时先用上一个月的完整数据不能用当月至今的数据替代这套结构里面有三个很关键的地方我分别展开说。description 要短。它主要是给模型的快速检索卡。太长的描述会让技能选择的准确性下降。descriptions 一般控制在 40 个汉字左右宁可模糊一点也不要把触发细节写进去。trigger 是给预路由用的不是给模型自由发挥用的。可以设计成一组正则或语义规则当用户请求命中 trigger 时直接把该技能列为候选。mistakes 是很多人会忽略的字段。它记录历史上模型在执行这个技能时犯过的错相当于给这个技能加了局部经验缓存。模型在执行技能时读这个字段能显著降低重复出错率。2.2 以一个技能只解决一类问题为原则技能拆分粒度可以理解为如果某个技能的执行步骤超过 8 步或者内部包含多个独立的决策分支它就应该被拆成两个技能。为什么要这么做因为技能调度的最大敌人是高输入噪声——当技能内部要做的判断太多了模型在每一步都可能产生偏移正确的概率会被多次相乘。举个例子生产周报这个需求表面上可以拆成一个周报生成技能内部同时做数据拉取、异常检测、文字总结三件事。但这种设计有个问题如果用户只想看异常检测结果不想要完整周报这个技能就被迫走完整流程浪费了时间和上下文。更合理的方式是拆成数据拉取、“异常检测、报告渲染三个独立技能再加一个编排层来处理它们之间的关系。每个技能都尽量是一个输入到输出的清晰映射内部没有太多复杂分支。我在实践里会定一个简单的拆分检查表第一这个技能是否总被一起调用如果是可以不拆第二这个技能内部的某一段逻辑是否会单独被用户要求使用如果是拆第三这个技能的输入参数中是否存在大量如果……则……的情况如果是拆成不同技能分别应对。2.3 输入槽位设计宁可给默认值也不要让模型自由发挥Agent 技能经常卡在一个点上用户说帮我看看销量——到底是看哪条产品线哪个时间段很多时候模型会自行脑补一个参数然后吐出一份用户根本没要的东西。解决这个问题的办法不是指望模型猜对而是在输入槽位上设置合理的默认值并让默认值出现在技能描述的显眼位置。我在每个输入字段上会加 required 标记并为非必填字段设计一个明确的默认行为。比如 product_line 默认是 allmonth 则必须由用户指定如果用户没有给模型必须启动询问流程而不是直接假设成当前月份。这样设计的原因很简单时间类参数的错误会直接污染结果而全产品线这类范围类参数就算猜错了也能让用户快速纠正不会产生严重的语义错误。输入槽位还有一个技巧是参数预填。如果系统能从上文对话里提取出用户所在部门、常用产品线等信息就预先填进槽位而不是让模型从零开始解析。这样既减少了模型的负担也保证了参数的一致性。比如用户登录信息里有当前部门那月报技能里的部门槽位就可以直接从用户态里拿。3. agent-skills 的运行时核心注册、路由、串行调用技能卡写好了只是一堆存在磁盘上的 YAML 文件真正要让 Agent 用得起来还需要一个运行机制把这些静态定义变成可调用的能力。agent-skills 的运行时做三件事技能注册、技能路由、技能执行编排。下面我按启动 Agent 的完整流程来拆。3.1 技能注册表把零散技能变成可查询的目录注册表的设计目标是让所有技能在内存里形成一个可以按名称、标签、触发条件快速检索的索引。每次服务启动时加载器会扫描指定目录逐个读取技能卡文件做基本格式校验然后构建为 RuntimeSkill 对象。下面是一个简化版的注册逻辑class SkillRegistry: def __init__(self): self._index {} def load_from_dir(self, path): for file in Path(path).glob(*.yaml): skill parse_skill_card(file) self._index[skill.name] skill for tag in skill.meta.get(tags, []): self._tag_index[tag].append(skill) def get_skill(self, name: str): return self._index.get(name) def query(self, text: str, limit3): # 先用 trigger 规则做粗筛 candidates [s for s in self._index.values() if s.match_trigger(text)] # 再按语义相关度排序 return sorted(candidates, keylambda s: s.similarity(text), reverseTrue)[:limit]这里我建议不要把所有技能卡都丢给模型去读。注册表会维护一个精华索引每个技能只暴露 name、description、trigger 三字段给路由层。模型只需要通过这三个字段判断用哪个技能详细步骤在执行该技能时才展开注入。这样做的好处是大幅减少上下文消耗也让技能选择的准确率明显提升。构建注册表的过程中有一个很容易被忽略的细节是技能卡里 name 的命名规范。不能只用中文名也不能只用拼音缩写最好采用 snake_case 加业务前缀的形式比如 financial_weekly_report 或者 ops_alert_distribution。如果技能名可以是一个合法函数名后续做自动化测试和脚本调用都会方便很多。3.2 技能路由关键词匹配与语义路由的混合方案技能路由是整个运行时里最有意思的部分。纯靠规则匹配无法覆盖用户的各种表达方式完全靠语义路由又会出现不稳定和难调试的问题。我实际跑的方案是级联式先用规则粗筛再用语义精排。具体说用户输入会先过一层关键词和正则规则比如用户说到周报、报告、邮件这些词就能把候选技能缩小到三五个。这一步速度很快基本是毫秒级。接下来如果候选只有一个直接选它如果有多个再对候选技能的 description 向量化和用户输入做相似度排序取 top 1。如果语义相似度都低于某个阈值就让模型来选。这个混合方案最大的优势是可诊断。如果路由错了你可以先看看是不是关键词规则配少了还是语义排序有问题。纯语义方案则很难追溯因为向量空间的结果往往说不清。这个方案还可以做路由回退当模型在选择技能时犹豫不决可以引导它向用户确认而不是默认选一个。3.3 一个请求从进入到完成的执行链路把注册和路由说清楚我再用一个具体请求串一遍完整执行链。假设用户说给我出一份上个月的华东区销售日报。执行链路大体分六步意图解析请求进入路由层上月与日均等词命中 daily_sales_report 技能的 trigger。槽位抽取用小型解析器或轻量模型把华东区上个月填入 region、month 两个槽位。这一步的参数提取是可以单独调试的。技能加载运行时从注册表取出 daily_sales_report 技能卡把详细步骤注入当前上下文。子任务执行技能内部按照 steps 列表依次调用数据服务、计算服务、报告组件。每次调用都会有独立的 Trace ID。中途校验在执行完数据查询之后技能会做一次结果合理性检查比如查询结果是否为空、日期范围是否符合预期。校验失败则进入修复分支而不是继续往下走。输出提交步骤完成后按输出契约生成 Markdown 报告返回给对话层。这条链路能这样顺畅跑起来离不开一个前提每个技能在执行过程中产生的中间结果都要能被持久化和回放。这样一来如果第 4 步出了问题你只需要重放该技能链路而不必让用户重新说一遍需求。这个设计思路与后端服务里的请求链路追踪非常像。4. 上线后我踩过的四类坑每个都值得记录任何一个系统设计和真实上线之后的差距才是真正值钱的教训。agent-skills 也一样第一次上线跑通并不难难的是之后在真实流量下的稳定性。我把自己踩过的坑归成了四类每类都附上排查过程希望能让你少走一段弯路。4.1 技能描述太长直接把上下文塞爆上线初期为了让模型更懂技能我在技能卡里塞了很多业务背景比如采购流程的详细说明、历史数据的口径定义。结果发现每次会话只要同时装载两个技能上下文里的系统提示词部分就占了三四千 token留给用户对话和推理的空间大幅缩水。更尴尬的是技能选择准确率没有因此上升因为模型被过多信息干扰了。排查过程我先用 token 统计脚本查看每个请求的上下文分配发现问题集中在技能加载阶段。然后我做了一轮技能描述瘦身把业务背景全部从触发字段里抽离只放在技能执行步骤里按需读取。cutoff 之后每个技能的固定上下文开销平均降了 60%技能选择的准确率反而回升了 8 个点左右。这是个很反直觉的结果给模型的信息越少它反而越能选对。4.2 技能之间来回引用形成死循环有一次Agent 在做生成门店巡检报告的时候突然调用了客服反馈汇总技能然后又从客服反馈汇总技能里触发了门店巡检报告技能两个技能互相等待对方的输出导致整个请求超时。这种问题在单体提示词里不常见但在技能化之后就很容易出现因为每个技能对上游依赖的感知不完整。排查过程我让技能注册表记录了每次技能调用的父子关系生成调用树之后发现两个技能之间形成了环。修复分两层第一层在技能卡里显式声明 dependencies在运行时构建 DAG执行前先做环路检测第二层在路由阶段如果发现用户请求可能同时命中多个技能且这些技能互相引用就强制折成一个主技能来编排而不是让它们各自执行。这个规则后来救了我很多次凡是技能间需要嵌套调用都先做依赖关系检查别等出问题了再去追。4.3 技能更新后悄悄回归旧用例不通过技能卡是文本文本最大的问题就是看起来没改行为全变了。有一次我只是在技能卡里调整了一下 trigger 的措辞把查询上月销售改成了获取上月销售数据结果导致某条业务用例的调用路径彻底变了输出格式也不匹配。排查过程一开始完全没有定位到问题因为代码逻辑没有改动。后来我养成了一个强制习惯每次技能卡变更都必须跑一遍该技能的回归测试集。这个测试集不依赖模型做断言而是直接验证技能卡的调用路径是否满足预设路径比如是否先调用数据服务再调用计算服务。路径校验通过之后再跑一条端到端的真实用例确认输出格式。把技能卡纳入版本管理和 CI 检查是这套系统走向稳定的重要一步。4.4 技能使用的工具权限过大出了安全事故这个问题比较隐蔽但一旦发生就是大事。我给某个技能配置了数据库读取权限本意是让它读 view结果由于数据库账号权限控制不够细技能在实际执行中访问了不该访问的表。查日志时发现查询请求里出现了用户表的名字。排查过程问题的根源是我把技能权限和用户权限混为一谈。用户本身只有普通权限但技能运行时使用了一个功能更强的服务账号相当于越权了。修正方案是给每个技能配置独立的最小权限凭证并且在下发 SQL 前加一层字段级白名单。这里如果只做事后拦截风险还是大最好从一开始就给技能建立一个数据边界清单明确它能碰哪些域。我把这四类坑汇总到一个表格里方便对照检查问题表现根因解决办法上下文爆掉会话变慢后续推理质量下降技能描述太长瘦身描述按需注入循环调用请求超时技能间隐式循环依赖DAG 存根 环路检测回归异常旧用例输出变化技能卡文本被改动技能卡进 CI跑路径回归权限越权访问了无关数据表技能共享超权限凭证技能级最小权限 数据边界清单5. 怎么让技能体系更稳健康检查、预算机制与缓存技能体系跑顺了之后我开始关注一个更大的问题如何让它长期稳定而不是靠一次上线时的运气。稳定性的核心可拆成三件事这个技能现在能不能用用起来要花多少预算结果能不能少算一次就不算一次。5.1 给技能做健康检查而不是监听接口传统的健康检查是检查服务是否在监听端口但技能的健康比这复杂。比如某个技能依赖的数据表可能已经存在但结构变了或者技能依赖的外部 API 返回正常但数据语义已经不对。这些都不是端口能反映出来的。我给技能体系做了一套主动健康检查每个技能定义里带一个 probe_steps 字段内容是执行一次极简流程并验证输出。比如月报技能的健康检查是查一条最近的订单确认返回结构里包含 amounts 字段邮件技能的健康检查是调一次 API 的 ping 接口并确认返回 token 有效。这套机制会让每次发布前都跑一遍所有技能的健康检查检查结果会进入发布报告。这样技能的服务等级就被量化了。5.2 给技能加时间预算和步骤上限在实际运行中最容易失控的不是模型本身而是技能的调用链长度。有时候一个技能内部会自作主张循环拉数据把 3 步流程跑成 12 次调用。针对这一点我给每个技能设置了两个上限时间预算和调用次数上限。时间预算默认为 30 秒如果超时就终止当前技能执行并返回部分结果。调用次数上限通常设为步骤数的 1.5 倍超出就要触发告警。这两个上限还可以按技能类型细分。比如查询类技能时间预算可以短一点而报告生成类技能因为要等外部 API预算就要放宽。设置上限的价值不只是保护资源更重要的是让异常执行显性化。以前超时就一次原因是模糊的现在超时会记录一个中断指纹后续可以直接定位是某一步调用卡住了。5.3 中间结果缓存省下重复计算技能的很多步骤都存在重复计算的情况。比如用户先问了一次月度销售数据隔两分钟又问了一次那占比呢这时候如果技能重新拉一次全套数据显然浪费。agent-skills 的缓存层设计并不复杂但收益非常明显。我会给每个技能步骤的入参计算 hash把出参存进带 TTL 的缓存。第二次遇到相同入参时直接返回缓存结果。这里有一个关键的坑缓存 key 必须包含数据版本号否则数据源变更后缓存还会吐出旧数据。我的办法是在技能卡里加了一个 data_version 依赖字段比如依赖的数据库表更新到第几版。任何数据迁移之后数据版本号会自增那么对应缓存也自然失效了。这套机制跑下来部分高频技能的重复调用率降低了三成左右而正确率没有下降。6. 后续扩展空间技能微调、评价体系和团队共享agent-skills 做到中期会自然出现三个扩展方向一是让技能在执行中能学习二是让技能的好坏可以被度量三是让技能可以跨团队分享复用。这三个方向如果展开做每个都可以是一个独立项目我这里只讲自己已经试过的入口。6.1 技能微调把高频技能的执行路径固化成模型参数如果你有一个技能的调用频率特别高而且它的步骤非常稳定那么你可以考虑用一批真实的用户请求-技能调用路径-输出结果三元组去微调一个轻量模型让模型在用户提出请求时直接输出该技能的参数而不是依赖外部的槽位解析模块。这样做的好处是延迟更低鲁棒性也更强。代价是需要维护高质量的训练集和持续的评测集。不是所有技能都值得微调我建议只对 top 5 高频技能考虑这个方向。6.2 技能评价体系不只看成功率还要看纠偏成本很多人评价技能好坏只看成功率这太粗了。真正需要衡量的指标应该包括触发准确率、执行成功率和平均纠偏成本。纠偏成本的含义是如果技能输出让用户不满意用户需要说多少句话才能纠正它。一个成功率不错但纠偏成本很高的技能往往是槽位设计不合理导致的。把纠偏成本纳入评价体系后你才会去认真优化那些每次都要用户再补充一次时间范围的槽位。6.3 团队内共享技能标准化是前提而标准化本身就是收益技能一旦沉淀下来就会形成一个组织内部的能力资产。后续团队会发现做新的 Agent 功能时不需要再从零写一个技能只需要在现有技能的基础上做组合和微调。这个过程会让团队逐步形成一套自己的技能标准比如技能卡字段规范、测试用例规范、权限设计规范。这些标准本身就是很大的收益因为它会让新成员更快上手也会让 Agent 的每个功能都可追溯、可回滚。以上只是我从 agent-skills 这个思路出发整理出来的一套实践经验。实际跑到现在最深的感受是技能化真正的价值并不是让 Agent 瞬间变聪明而是让 Agent 的每次行为都可以被定位、被测试、被改进。当你把聪明从模型参数里转移一部分到工程结构里稳定性才会真正掌握在自己手里。