
做过一阵子 AI Agent 应用之后我最大的感受是很多人把“让模型学会调用工具”和“让 Agent 真正掌握技能”混为一谈。前者的技术门槛不高写个 function calling 的 schema 就行后者却是一整套工程问题涉及技能怎么定义、怎么存、怎么路由、怎么测、怎么兜底。我最近在一个内部项目里把整套流程重新捋了一遍代号就叫 agent-skills这里把完整思路和实操笔记整理出来希望能给正在从“玩具 Agent”走向“能上生产的 Agent”的团队一点参考。先交代一下背景。我当时要解决的问题很具体团队里已经有十几个 Agent每个 Agent 都各自维护提示词、工具列表和固定的执行步骤。看起来功能不少但真正加到业务里就露馅了——换一个场景同样的工具调用逻辑要重写一遍模型版本一升级之前调通的流程开始抽风更麻烦的是任何一次外部 API 的异常都会让整个任务链崩掉根本没有“退一步再试一次”的能力。agent-skills 的出发点就是把这些“藏在 Agent 提示词里的隐性逻辑”抽出来变成显式的、可复用、可测试、可版本管理的技能单元。后面文章里我会从一个最简架构开始讲逐步说到技能仓库用什么数据结构、路由怎么做、压测怎么设计以及在真实运行里遇到的坑和兜底策略。这套东西不一定适合所有团队但如果你也在为“ Agent 能力不可控、不可复用、不可度量”发愁大概率能从中找到对症的方案。1. 为什么“会调用工具”和“拥有技能”是两码事1.1 只看工具调用的 Agent其实是个“没有经验的实习生”我见过不少 Agent 演示视频看起来非常惊艳用户说一句“帮我分析这份销售数据”Agent 就自动调起数据库 API、画出图表、写一段总结。但你把演示里的模型换成真实业务场景马上会发现它本质上是“一个没有经验的实习生”——它知道有哪些工具可以用知道工具的参数格式但它不知道什么时候该用、用完怎么判断结果靠不靠谱、中途出错了怎么补救。举个例子。一个调天气 API 的 Agent工具描述写得再清楚它也只能拿到“温度 26 度湿度 60%”这样的原始数据。但真实需求往往是“今天适不适合户外跑步”Agent 至少还得知道超过多少度算高温空气湿度大了体感温度怎么变有没有降雨概率甚至昨天和今天的温差有多大。这些判断逻辑不属于任何单个工具而是跨工具、带规则、能应对边角情况的一套能力。这就是技能和工具的本质区别——工具是动作技能是“在什么条件下做动作、怎么做、做完怎么验证、失败了怎么补救”的完整闭环。1.2 技能的可复用性来自“边界清晰”而不是“功能强大”很多人一提“把工具升级成技能”第一反应是给工具写更长的描述让模型更懂怎么用。这个方向不能说错但远远不够。我在 agent-skills 里定义的技能必须满足三个条件有明确的触发条件和退出条件。技能知道自己在什么场景下该登场也知道任务完成的标准是什么不会无休止地尝试。有独立的上下文。技能执行过程中产生的中间状态只留存在技能内部不污染 Agent 的主对话流。有标准的输入输出协议。无论内部逻辑多复杂对外部暴露的只是结构化的 input 和 output。这三个条件里最重要的是“边界清晰”。只有边界清晰技能才能像乐高积木一样被组合、替换、单独测试。我见过一些团队把技能写成了“万字长文提示词”里面什么逻辑都有看起来强大无比但真正复用的时候就会因为耦合了太多场景细节而根本拆不出来。技能不是越长越好而是越“可预期”越好。1.3 技能的本质把“模型即时推理”变成“预置执行策略”从运行机制上看技能和普通提示词最大的区别在于执行策略是否预置。普通提示词是“把问题抛给模型让模型临场发挥”技能则是“提前把成熟的执行策略固化成代码和校验规则模型只负责在关键节点做决策”。这个转变非常关键。我举个内部跑通的例子。财务对账场景里Agent 要下载银行流水、解析 Excel、匹配内部账单、标记差异。如果用普通提示词模型每一步都要重新推理“接下来该干嘛”一旦某一步解析结果不符合预期后面就全乱了。在 agent-skills 里我把它定义成一个对账领域的技能执行逻辑是固定的先拉数据、再标准化、然后匹配、最后生成差异报告。模型在技能内部的角色只是处理“标准化规则覆盖不到的异常情况”比如某个银行导出的字段名变了模型根据上下文判断映射关系。这样整个流程是可预期的也是可测试的——因为大部分路径是确定性的只有小部分决策点交给了模型。2. 技能系统的最小架构注册表、装载器与运行时2.1 三个核心组件各干各的活agent-skills 的架构没有搞复杂核心就是三个组件注册表、装载器、运行时技能执行器。注册表负责“登记”知道自己有哪些技能、技能的版本是什么、技能依赖哪些外部资源。装载器负责“加载”把技能的配置、代码、校验规则从存储介质里读出来做合法性检查然后放进运行时环境。运行时执行器负责“跑”把技能真正执行起来管理执行过程中的状态处理异常和结果返回。这三个组件一定要分开不能揉在一起。我最早把注册和加载逻辑写成同一个模块结果每次要加一个新技能都要重启整个服务因为技能清单是启动时一次性加载进内存的。后来把注册表独立成服务支持动态注册和版本切换才彻底解决了“加技能要重启”的问题。2.2 注册表的数据结构别小看这个看似简单的表注册表的存储我用的是关系型数据库核心表结构大概长这样CREATE TABLE skills ( id VARCHAR(64) PRIMARY KEY, name VARCHAR(128) NOT NULL UNIQUE, version VARCHAR(32) NOT NULL, description TEXT NOT NULL, trigger_hint TEXT, definition JSONB NOT NULL, dependencies JSONB, enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() );definition字段是整个技能的核心里面存放完整的执行配置文件。trigger_hint是给路由层看的用来快速判断“这个技能可能适不适合当前任务”。dependencies记录技能运行时需要的外部依赖比如需要调用的 API 名称、需要读取的模型配置等。有一点要特别提醒注册表设计一定要从第一天就把“版本”做成显式字段不要用 updated_at 模糊代替。Agent 技能是会和模型行为、外部 API 耦合的线上跑着的技能版本和正在开发的版本必须能明确区分。我后来还加了一张版本发布记录表每次灰度发布都留痕出现线上事故能秒级回滚。2.3 装载器的两个职责校验与沙箱化装载器听起来就是个“读文件加载到内存”的工具实际上有两件事不做不行。第一是配置校验。技能配置如果写错了轻则技能无法执行重则整个 Agent 主流程崩溃。我用 JSON Schema 做强制校验技能定义里必须包含 name、version、description、input_schema、execution 这五个字段缺一个直接拒绝加载。第二是沙箱化执行环境。不要让技能代码直接跑在主进程里。A 技能的代码写得再稳你也没法保证它和其他技能在全局变量、环境变量上不冲突。agent-skills 里的技能执行是放在受限容器里的每个技能有自己的临时文件目录和环境变量空间技能内部的 import 和网络请求都受网络策略限制。这不仅是安全问题更是稳定性问题——执行环境隔离后一个技能的崩溃不会拖垮整个 Agent。分区隔离的代价是性能有一定损耗尤其是启动新环境的那几十毫秒。但对比收益完全值得。有一次线上事故就是某个技能读取了一个不该读的环境变量导致把测试环境的数据库地址当成了生产地址辛好沙箱化策略提前拦截了这种危险操作没有造成实际影响。3. 技能仓库设计用统一规范替代一堆零散提示词3.1 一个技能定义文件的完整样例既然要把技能“仓库化”第一步就是定一套统一的技能描述规范。我在 agent-skills 里用的核心配置文件是 YAML 格式一个技能就是一个目录里面放skill.yaml和若干执行脚本。这是个简化的示例name: data_diff_reporter version: 1.2.0 description: 对比两个数据源的记录差异生成格式化报告适用于对账、数据一致性校验等场景。 trigger_hint: 对账、比对、差异、两个表、数据不一致、核对 input_schema: source_a: type: object required: true description: 数据源A包含连接信息和查询语句 source_b: type: object required: true description: 数据源B包含连接信息和查询语句 diff_fields: type: array required: false description: 指定需要比对差异的字段默认比对全部字段 execution: steps: - name: connect_a type: database_query config: conn_ref: {{input.source_a.conn_ref}} query: {{input.source_a.query}} - name: connect_b type: database_query config: conn_ref: {{input.source_b.conn_ref}} query: {{input.source_b.query}} - name: normalize type: custom_script config: script: scripts/normalize.py - name: match_records type: custom_script config: script: scripts/match.py params: fields: {{input.diff_fields}} - name: build_report type: render_template config: template: templates/diff_report.jinja2 fallback: - type: retry max_attempts: 2 on_error: [database_query, network_timeout] - type: degrade action: sample_report message: 数据量过大已生成抽样差异报告 output_schema: report_path: { type: string } diff_count: { type: integer } sampled: { type: boolean }这个文件看起来内容不少但核心其实就几块技能是什么、需要什么输入、执行哪些步骤、步骤之间怎么流转、失败怎么办。所有字段都是机器可读的装载器可以据此做依赖检查路由器可以据此做任务匹配测试脚本可以据此自动生成用例。3.2 为什么不建议把执行步骤全写成“自然语言提示词”在设计这个格式的过程中我一度纠结 execution.steps 要不要允许纯自然语言描述让模型自己决定怎么执行。实验结果非常明确可以有但不能全是。纯自然语言步骤的优点是灵活缺点是“每次执行的结果都不一样”。我给你看一组真实对比数据。同一个数据清洗任务用确定性代码步骤执行成功率稳定在 97% 以上耗时固定在 4 秒左右用纯自然语言步骤让模型自由执行成功率在 82% 到 95% 之间剧烈波动耗时从 3 秒到 20 秒都出现过。对内部工具类 Agent 来说这种不稳定性是致命的——业务方宁可要一个“稳定但笨一点”的结果也不要一个“聪明但时好时坏”的结果。所以我在 agent-skills 里的原则是能用代码固化的逻辑绝不给模型自由发挥模型只负责处理“规则之外的语义理解”。比如上面例子里的 normalize 和 match都是确定性脚本但如果是“识别这张发票里的供应商名称”那就要留给模型做语义抽取。3.3 技能的版本管理与发布流程技能一旦多了版本管理就成了刚需。我把每个技能目录都放到独立的 Git 仓库里通过 Git 的 tag 做版本标识再配合 CI 流水线做自动测试。具体的发布流程分成四步开发者在 feature 分支修改技能定义和脚本本地跑技能测试用例。合并到 main 分支后CI 自动执行全量技能测试包括单元测试和模拟场景测试。测试通过后打 tag例如v1.2.0推送触发注册表更新。注册表更新后技能进入“灰度阶段”只在 5% 的新会话里生效观察半天无异常后全量放开。这套流程跑起来之后“加技能”“改技能”变成了一个标准化动作不再是开发者在生产服务器上手动改配置的冒险操作。有一点经验分享一定要把“灰度阶段”做成强制项不要因为改的是一个小规则就跳过。我因为跳过灰度吃过亏——改了一个日期格式化脚本里的时区处理自测完全没问题上线后直接导致某海外用户数据全部偏了一天那个小时大家都在等着修复。4. 路由与调用代理怎么知道该用哪个技能4.1 两种主流路由方式以及我为什么选了混合式技能多了以后第一个要解决的问题就是当一个任务进来Agent 到底该调用哪个技能最直接的方式有两种一种是把所有技能的描述塞给模型让模型选另一种是写规则靠关键词匹配决定。两种我都在项目里试过各有明显缺陷。纯 LLM 路由的问题是“选择不稳定”。技能数量一多描述文本就长模型选错技能的概率直线上升尤其当两个技能在功能上有重叠时模型经常会“选择困难”。纯规则路由的问题则是“覆盖不全”真实用户请求的措辞千变万化关键词匹配很难覆盖全。agent-skills 的最终方案是混合式路由分两轮。第一轮用规则层做粗筛基于技能定义里的 trigger_hint 和用户的原始输入做关键词匹配、同义词扩展把候选技能从几十个缩小到三五个。第二轮把这三五个技能的 name 和 description 交给模型做精排让模型输出最匹配的技能标识。4.2 路由层必须输出“可解释的结果”而不是一句“我选了它”这一点非常容易被忽略。路由不仅是技术组件还是与可观测性直接相关的关键节点。我给路由配置加了一组输出日志字段候选技能列表、每个候选的匹配分数、最终选择的理由模型生成的简要说明、是否落入兜底分支。每次任务结束后这组数据会汇入追踪平台。这部分数据的价值在排障时比什么都管用。有一次线上出现“用户要求生成报表Agent 却调用了数据删除技能”的严重事故还好路由日志清晰记录了当时模型精排时看到了哪几个候选技能、最终靠什么理由选中了删除技能——原来是技能描述里“清理、清除、删除”这几个词和用户需求里的“删除不需要的报表”高度重合。我用一天时间优化了技能描述将数据删除场景的触发条件写得更加严格并给同类危险技能加上了调用前确认机制。如果没有路由日志这种问题排查起来就是大海捞针。4.3 技能描述怎么写路由成功率差异巨大既然模型要靠技能描述来做精排描述怎么写就很关键。我后来总结出一套“技能描述三段论”第一句说清楚技能“在什么任务下用”越场景化越好不要写“通用数据处理技能”这种废话。第二句说清楚技能“不用在什么任务下”把容易混淆的边界场景点名。第三句写一个典型调用案例帮助模型理解输入输出形态。这套写法让路由命中率从 78% 提到了 91%。注意这个提升不从任何算法优化来纯粹是文本层面的信息量优化成本几乎为零建议任何做技能系统的团队都先把这条做掉。5. 技能质量测试先用合成场景压测再上真实业务5.1 没有评测集的技能系统就是一辆没有刹车性能的车很多团队开发 Agent 功能时测试方式还停留在“我手动跑几次看看效果”。手动测试不是完全没用但它无法回答三个关键问题这次改动是否比上次更好哪些场景会稳定失败全量技能回归大概需要多久回答不了这些问题技能就是不可度量的。agent-skills 里我建了一套轻量级的评测集每个技能对应至少 20 个测试用例分成三档happy path正常场景、edge case边界场景、failure path异常场景。每个用例包含输入、期望输出、允许误差。评测集跑完之后会输出一份结构化报告核心指标有三个任务成功率、平均执行耗时、平均失败恢复时间。这套评测集不需要一次建全我的建议是“边开发边积累”每遇到一个线上失败的 case就把它沉淀成一条测试用例。一个月下来评测集自然会长到几百条技能质量的基线也就有了。5.2 合成场景怎么设计既要像真实业务又要有毒性合成场景不是把真实数据换个名字再跑一遍那么简单。真正的合成场景要能暴露问题得包含一些“有毒”的输入。我常用的设计方法包括极端长度文本长度为正常值的 10 倍或 1/10看技能是否会因为输入格式假设而崩溃。字段缺失故意漏掉必填字段看技能是报错退出还是优雅降级。格式变异比如日期用 “2024/3/1” 和 “2024-03-01” 两种格式同时出现看标准化步骤能不能处理。网络抖动模拟外部 API 超时和返回 500 错误看技能的重试和 fallback 是否能正确触发。这些“毒”输入看起来有点刻意但正是它们帮我发现了大量线上才有机会踩到的坑。比如有一次评测集跑出一个结果当输入文本长度超过 8000 字时技能会把截断后的内容当作完整输入继续处理导致最终报告结论完全错误。这个问题不靠评测集根本发现不了因为开发时谁也不会用上万字的文本去测。5.3 评测指标里最容易骗人的任务成功率任务成功率这个指标看起来最直观但也最容易被“作弊”——技能只要在所有用例里输出一个不报错的结果成功率就是 100%哪怕结果完全是错的。所以我额外增加了一个指标叫“结果无关退出率”统计“技能正常结束但没有产出有效结果”的比例。具体实现方式是给每个技能的 output_schema 加一个valid_result校验函数评测时不仅看技能有没有跑完还要看输出是否符合预期语义。拿数据清洗举例技能输出一个空表也可能会把整个 pipeline 跑完但 valid_result 校验收到了空表就会标红从而把这个 case 计入“结果无关退出”。加了这层校验之后我才真正看清哪些技能是“假装干活”哪些技能是真的靠谱。6. 实测验证中的异常情况与兜底策略6.1 技能执行时的三层异常处理技能一旦上线到真实业务环境外部依赖的不可靠性是躲不掉的。我做了一套三层异常处理策略从底向上分别是重试、降级、人工介入。底层是重试针对瞬时故障网络抖动、超时、并发冲突。我限定最多重试 2 次每次重试之间指数退避。这里的关键是“必须做幂等保证”如果一个技能步骤重试了两次产生的副作用不能叠加。拿扣费接口举例重试时必须带上 request_id避免重复扣款——这个问题在财务场景里是 P0 级别的事故隐患。中间层是降级针对“重试也救不回来”的情况。比如主数据源挂了就降级到本地缓存数据完整报告生成不了就降级生成抽样报告。降级的核心原则是“先给用户一个凑合能用的结果而不是让用户干等或看见报错”。顶层是人工介入针对重试和降级都不适用的场景。这条通路被我做成一个“工单队列”技能内部判断“当前情况已经超出我的能力边界”时就把完整的执行上下文、已经完成的部分结果、失败原因打包成一个工单推给人工处理。这一步很重要它是技能系统的安全阀也是我敢在业务里大规模放开自动化任务的前提。6.2 一次真实故障技能配置里的一个小数点怎么让整个对账崩了讲一个具体的事故案例。对账技能上线后某天凌晨自动任务跑完早上业务方发现系统把所有“金额差异小于 0.01 元”的账目都标记成了异常。排查链路是这样的先看注册表的技能版本确认它已经从 1.1.0 升到了 1.2.0再看路由日志确认任务确实走的是对账技能最后看技能执行日志发现 normalize 脚本里一个数值处理逻辑的容差参数被改为了 0.001而不是原来的 0.01。根因是开发者在优化时手滑多写了一个 0。这类看似不起眼的错误之所以能漏过测试是因为评测集里的测试用例没有覆盖“恰好处于新旧容差阈值边缘”的数据样本。修复方案很快——把参数改回来、灰度上线但真正让我记住的是另一件事这次事故暴露出的不是代码问题而是“技能配置的变更评审流程有漏洞”。后来我给技能配置变更加了一道强制的人工管线任何参数变更必须附上解释说明和影响面评估否则 CI 直接拒绝合并。6.3 兜底策略不是“备而不用的保险”需要定期演练很多团队的兜底策略写得漂亮但从来没真正触发过一次等事故来了才发现兜底代码本身就有 bug。我在 agent-skills 里安排了一个“故障演练日”每两周挑一次低业务峰值的时段人为制造外部 API 故障、技能超时、配置错误强制系统走一遍重试、降级、人工介入的完整链路。第一次故障演练简直惨不忍睹。人工介入队列的通知服务配置错了环境变量工单发出去了但没人收到降级逻辑在重试次数用尽之后抛出了一个未捕获异常直接让整个 Agent 退出。这些问题平时根本不会暴露但都是生产事故的真前置条件。演练了三轮之后系统应对异常的能力才算真正形成后来在多人情况下遇到真实故障整个团队不再手忙脚乱。7. 技能演进从单技能到组合技能的迭代路径7.1 什么时候该把“一个大技能”拆成“多个子技能的组合”技能初版通常是一个粗粒度的“大块头”比如“经营数据分析”一个技能就覆盖了取数、清洗、计算指标、生成图表、写报告五个步骤。前期这样没问题因为功能少、改动频繁拆太细反而增加维护成本。但有一个明显信号出现时就是在提醒你该拆了当技能内部开始频繁出现分支条件时说明它在应对多种差异很大的任务变体。拿“经营数据分析”举例当它开始出现“如果用户问的是财务指标走 A 路径如果是销售指标走 B 路径如果是用户增长走 C 路径”这种逻辑时正确的做法是拆成三个场景技能再定义一个编排层决定调哪条路径。拆完之后每个子技能都有独立的版本、独立的评测集、独立的发布节奏改其中一个不会再影响另外两个整体回归测试的范围也大幅缩小。7.2 组合技能的编排让子技能互相协作而不是互相干扰组合不是简单的“把多个子技能串起来”而是要处理子技能之间的数据转换和上下文隔离。我把组合技能分成三种模式串联模式前一个子技能的输出是后一个的输入、并行模式多个子技能互不依赖同时执行后合并结果、选择模式根据任务类型路由到特定子技能。实际业务里往往是三种模式的混合但定义清晰后编排层就能用统一的方式做数据传递和错误处理。组合技能最需要小心的是“上下文污染”。子技能 A 执行过程中产生的中间临时变量如果没有隔离好很可能被子技能 B 错误引用。我在 agent-skills 里给每次子技能调用都分配独立的命名空间子技能之间只能通过显式的数据总线传递输出结果不允许直接读其他子技能的中间状态。这样设计会牺牲一点代码优雅度但换来了极低的调试成本。7.3 演进节奏别指望一步到位每两个版本做一次“可用性复盘”技能系统的演进不需要宏大的总体规划我的经验是每两个版本做一次可用性复盘重点看三个问题技能的使用频率如何是高频常用还是长期吃灰失败模式有没有出现“集中化”趋势比如是不是某个子技能贡献了大部分失败替换技能的成本有没有降低改一个字段是否仍然需要联动改很多地方。复盘的结果通常是一些微小的结构调整而不是重写。比如某个高频技能的平均输入长度一直很大导致 token 消耗严重复盘后就给它的输入加了一个“预压缩”子步骤某个低频技能长期没人用复盘后直接下线避免了它在路由层干扰其他技能的命中。这些小调整单独看都不值一提但累积起来技能系统的健康度会有一个质的提升。最后分享一点我个人的体会做 agent-skills 这套东西最难的其实不是技术实现而是心态转变。一开始你很容易把关注点放在“让模型更聪明”上想尽办法优化提示词、选更好的模型做得越多越会意识到真正决定 Agent 上限的是工程基础设施——技能有没有清晰的边界、故障有没有兜底、质量有没有度量。把这些基础打好换更强的模型只是水到渠成的事基础不牢换再强的模型也顶多是把原来的问题跑得更快而已。如果你也在规划自己的 Agent 技能体系建议从最小的闭环开始选一个高频场景定义成技能建起注册表和评测集跑通一次完整的发布流程。先跑起来再逐步完善。