
直接做Agent应用的人这两年应该都有同感单个Agent的对话能力已经不是瓶颈了真正拉开差距的是Agent能不能稳定地调用外部能力、按流程完成任务。我最近在整理和重构一个内部项目名字就叫“agent-skills”本质上是一套给智能体挂载可复用技能的系统化方案。这篇就把我从设计到落地过程中踩过的坑、总结出的套路完整写出来希望对正在做Agent工程化、或者准备把Agent从Demo推向生产环境的团队有点帮助。1. 项目概述与整体设计拆解1.1 为什么需要“技能”而不是堆Prompt先说清楚这个项目解决的是什么问题。我之前做过几个Agent相关的模拟项目最直观的感受是Agent的核心推理能力进步很快但工程侧的东西一直没跟上。具体表现就是每个业务场景都在重复写工具调用代码、重复拼Prompt模板、重复处理上下文截断和工具返回结果的解析。代码到处复制粘贴Prompt一改就崩工具一多模型就开始胡乱调用。后面我意识到问题的根源在于把“让Agent干活”这件事拆解得不够细。多数团队的思路是写一个大Prompt把所有工具的描述、调用规则、输出格式全塞进去然后祈祷模型表现得足够好。但实际跑起来就会发现Prompt越长模型的行为越不稳定工具描述之间的干扰越严重排查问题也越困难。“agent-skills”这个项目的核心思路就是把这些散落的东西抽象成一层独立的技能层。每一个技能是一个自包含的模块包含触发条件、输入输出Schema、执行逻辑、依赖的上下文信息以及一组经过验证的调用示例。Agent本身不直接面对十几个甚至几十个工具描述而是先面对一组技能描述技能内部再去管理具体的工具和流程。这就像把一个项目的所有公共逻辑抽成服务层而不是让每个页面都直接操作数据库。1.2 技能、工具、流程三者的边界划分项目设计最开始纠结的一个问题就是技能、工具和流程这三个概念怎么切分。我见过不少方案把三者混在一起结果技能既管工具调用又管业务编排最后变成一个四不像的大杂烩。我最终采用了三层划分工具层最底层的能力单元一般对应一个具体的API调用、一个代码函数、一个数据库查询。工具不做业务决策只执行单一动作。技能层面向场景的能力封装负责决定“用哪些工具、按什么顺序、怎么处理中间结果”。技能是Agent可直接调用的最小业务单元。流程层跨技能的组合编排负责管理“多个技能之间的状态流转和数据传递”通常由Agent根据用户目标动态决策也可以由预设的流程模板驱动。这个划分带来的直接好处是每一层都可以独立测试。工具层只需要验证入参出参是否正确技能层只需要验证给定的意图能不能正确匹配并执行流程层只需要验证多个技能组合能不能完成端到端任务。定位问题的时候不再需要从头捋一遍Prompt和代码直接定位到是工具的问题还是技能编排的问题。1.3 技能库要解决的核心痛点把agent-skills做成一个库而不是一堆散脚本目标是为了解决几个高频痛点。第一个是上下文污染。传统做法把所有工具描述都塞进系统Prompt工具一多Prompt占用上下文就猛涨模型在长上下文中容易遗忘关键指令或者被不相关工具的描述干扰。技能层可以做动态装载只把当前场景相关的技能描述注入上下文其余技能保持休眠状态。第二个是行为不可控。直接让模型从几十个工具里自由选择经常出现调用错工具、传错参数、漏掉必要步骤的问题。技能层可以通过前置条件和输入Schema强约束调用方式把模型的自由度限制在可控范围内。第三个是复用成本高。同一个业务能力在不同场景里反复使用比如“查天气”“发通知”“查订单状态”每次重新写一遍工具描述和调用逻辑成本很高。技能注册一次、全局复用这才是一个技能库该有的样子。注意技能层不是为了取代Agent的推理能力而是把“推理”和“执行”解耦。Agent负责判断该调用哪个技能、怎么解析结果技能负责保证执行过程可靠。两件事分开之后整个系统的稳定性会明显上一个台阶。2. 核心细节解析与实操要点2.1 技能定义的五要素我做了几个版本迭代后最终把技能定义收敛为五个必需字段每个技能就是一个结构化的描述块。这个结构看起来简单但每一项都有讲究。第一个是技能ID和名称。技能ID用于程序内部引用建议用英文短横线命名法例如order_query、email_digest_send。名称是给模型看的自然语言描述一般控制在20个字以内要能直观表达技能做什么。第二个是描述信息。这一项最关键因为它是模型选择技能的主要依据。我踩过一个坑是描述写得太抽象比如“处理订单相关操作”这种描述完全无法帮助模型区分具体技能。正确的做法是写清楚什么时候该用、什么时候不该用、主要处理什么类型的输入甚至可以加一两句反例说明。第三个是输入输出Schema。输入Schema建议用JSON Schema格式要明确每个字段的类型、必填性、取值范围和示例值。输出Schema描述返回值结构方便模型正确解析结果。这里有一个容易忽略的点Schema的字段命名和提示词里给的示例命名必须一致否则模型很容易按照示例生成字段最后解析失败。第四个是执行处理器。实现方式取决于技术栈可以是一个Python函数、一个HTTP接口、一段SQL模板或者一个独立微服务。关键是这个处理器必须是无状态的不保存任何上下文所有需要的数据都通过输入参数传入。这样才能保证技能可以被并发调用和自由组合。第五个是示例和回归用例。每个技能至少配三个示例一个标准用法、一个边界输入、一个失败场景。这些示例不仅用于测试还用来做示例学习在注入技能描述时附上几条高质量示例可以显著提升模型调用的准确性。这比在Prompt里反复强调规则有效得多。2.2 技能注册与动态装载机制技能库的另一个设计要点是动态装载。如果不管什么场景都把全部技能描述塞给模型技能数量一多效果必然变差。我做过一个实验技能描述总量从十几个涨到四十几个之后调用准确率大概掉了十几个百分点。动态装载是解决这个问题的关键路径。我的方案是加一个路由层在Agent进入技能选择之前先根据用户的当前目标和上下文做一个粗粒度筛选把可能用到的技能候选集缩小到五六个以内再把候选技能的描述注入到Prompt里。这个过程不需要模型参与用简单的关键词匹配加规则引擎就能实现。比如用户说“帮我看看订单到哪了”路由层通过“订单”“物流”“查询”这些关键词把订单查询相关的技能选出来。这样做还有一个额外好处技能库的容量上限提高了。因为每次实际暴露给模型的技能只有一小部分整个库可以容纳几百个技能而不必担心上下文被撑爆。技能的组织上我还按业务域做了分类目录每个分类单独维护互不干扰。2.3 技能编排与组合方式技能不只是独立调用很多时候需要组合使用一个完整的任务往往需要多个技能协作完成。我在agent-skills里实现了三种编排模式分别应对不同的场景。第一种是顺序编排。前一个技能的输出作为后一个技能的输入比如“获取用户订单列表”然后“查询每个订单的物流状态”。这种模式适合流程固定、依赖关系清晰的任务。第二种是条件分支。根据前一个技能的执行结果决定后续走到哪个分支比如先判断用户是否为会员再决定调用会员专享技能还是普通技能。这种模式适合业务规则明确的场景。第三种是Agent自主编排。这是最灵活也最不稳定的方式Agent基于所有可用技能的描述自行决定调用顺序和依赖关系。这种模式对模型的推理能力要求很高适合开放式的任务但也需要做更多防护比如调用次数上限、超时控制、循环检测。实际项目中我倾向于把顺序编排和条件分支固化成技能内部的流程减少对模型自由发挥的依赖。Agent自主编排只保留给那些规则实在不好穷举的场景。2.4 技能测试与回归保障技能库和维护代码库有一个相似的痛点改动一个技能不知道会影响多少上游调用。我在项目里专门搭了一套技能回归测试框架每个技能注册时强制要求配套一组测试用例每次改动代码或者调整Prompt描述都跑一遍全量回归。测试用例分三个层级单技能测试验证技能在给定输入下能不能正确执行、返回结构是否符合Schema组合测试验证多个技能按编排流程执行时数据传递是否正确端到端测试模拟真实用户话术验证Agent能不能正确选择和调用技能。其中最花时间的是端到端测试因为模型有随机性同样的输入每次输出不一定一样。我的做法是用一套固定的测试话术数据集跑多轮取稳定命中率并且记录每次失败的上下文。这个做法的价值在于帮你区分哪些失败是模型随机导致的哪些是改动引入的回归。3. 实操过程与核心环节实现3.1 技术选型与项目结构agent-skills的技术选型我延续了团队的现有栈用Python实现声明部分用YAML格式执行处理器部分直接写Python函数。之所以不用JSON而选YAML是因为技能定义里描述文本较长YAML的注释和可读性对维护者友好很多。项目结构设计如下agent-skills/ ├── skills/ │ ├── order/ │ │ ├── order_query.yaml │ │ ├── order_cancel.yaml │ │ └── order_evaluate.py │ ├── logistics/ │ │ ├── logistics_track.yaml │ │ └── logistics_track.py │ └── notify/ │ ├── notify_email.yaml │ ├── notify_sms.yaml │ └── notify_base.py ├── registry/ │ ├── loader.py │ ├── router.py │ └── schema_parser.py ├── runtime/ │ ├── executor.py │ ├── compositor.py │ └── context_guard.py └── tests/ ├── test_skills_unit.py ├── test_skills_combo.py └── test_skills_e2e.py每个技能一个YAML文件定义元信息同名Python文件实现执行逻辑。公共逻辑提取到父类或者util模块里避免每个技能里都复制一份请求库、解析函数之类的代码。3.2 技能注册器的实现思路注册器是整个库的基础组件负责任务就是扫描技能目录、加载YAML定义、校验格式、注册执行函数。核心逻辑不复杂但有几个关键环节值得注意。YAML定义和Python执行函数的绑定我用的是命名约定order_query.yaml对应order_query(),函数名和YAML文件名保持一致。这样加载的时候只需要遍历目录就可以完成绑定不需要额外的映射配置。绑定后的技能对象统一存放在内存字典里键是技能ID。全部都做了懒加载只有技能被实际选中时才导入对应的Python模块目的是减少启动时间和内存占用。Schema校验这一层我用了JSON Schema的Python实现库对输入参数做严格的类型检查、必填检查、枚举检查。校验不通过的请求直接返回结构化错误而不是把脏数据透传给执行函数。路由器的实现是动态装载的关键。路由器基于一个轻量级关键词索引和业务分类规则把用户的当前意图映射到候选技能集合。这里的规则不需要写得太复杂覆盖80%以上的常见场景即可剩下的场景就算候选技能多了模型也还是能从描述中挑出正确的。3.3 从零实现一个技能注册器的核心代码技能加载是注册器的骨架。核心函数大约几十行就能说清楚。import os import yaml from jsonschema import validate SKILL_DIR skills class SkillRegistry: def __init__(self): self._skills {} self._metadata {} def load_all(self): for root, _, files in os.walk(SKILL_DIR): for name in files: if not name.endswith(.yaml): continue skill_id name[:-5] meta_path os.path.join(root, name) impl_path os.path.join(root, skill_id .py) with open(meta_path, encodingutf-8) as f: meta yaml.safe_load(f) self._metadata[skill_id] meta self._skills[skill_id] self._load_impl(skill_id, impl_path) return list(self._metadata.keys()) def _load_impl(self, skill_id, path): import importlib.util spec importlib.util.spec_from_file_location(skill_id, path) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) if not hasattr(module, skill_id): raise RuntimeError(f技能文件缺少入口函数: {skill_id}) return getattr(module, skill_id) def get_available_skills(self, intent: str): candidates [] for skill_id, meta in self._metadata.items(): keywords meta.get(trigger_keywords, []) if any(k in intent for k in keywords): candidates.append(skill_id) return candidates[:5]这里有一个容易被新手忽略的细节importlib导入模块后模块对象会被缓存如果后续修改了技能文件进程不重启就加载不到新代码。我在开发模式下加了强制刷新逻辑修改技能实现之后自动重新加载调起效率高很多。3.4 技能描述注入模板与上下文控制技能被动态装载之后下一步就是生成注入模型的技能描述文本。这个环节直接影响模型的选择准确率值得多花心思打磨模板。我采用的模板结构是先是技能总览一句话说明“你可用以下技能完成任务”然后是每个技能的详情包括名称、描述、输入输出Schema的关键字段、使用示例。这里有几条经验描述文本不允许出现无关信息只保留模型需要知道的内容。我见过一些团队把技能的代码实现细节、内部函数名、技术栈说明都写进描述里这些信息不但没用还容易让模型产生错误的理解。让模型知道“这个技能能不能解决我的问题”就够了怎么实现不是模型该操心的事。示例要给最典型的而不是最复杂的。有些技能覆盖了很多边界分支但注入的示例最多放三个放多了反而稀释关键信息。我一般精选一个正常输入、一个需要注意特殊参数的输入、一个比较容易用错的输入覆盖度基本就够。上下文控制还涉及一个很实际的问题技能的描述文本如果很长加上对话历史很容易顶到上下文限制。我在runtime层加了一个context_guard模块对注入的技能描述做总长度预算预算不足时先压缩对话历史再压缩技能示例。优先级是核心技能描述优先于示例示例优先于对话历史中的闲聊内容。3.5 一个完整的技能定义示例下面是一份订单查询技能的YAML定义我用了比较标准的写法可以直接作为模板参考。skill_id: order_query name: 查询订单状态 description: | 根据用户提供的订单号或下单手机号查询订单的当前状态、物流信息和预计送达时间。 当用户询问“我的订单到哪里了”“订单状态是什么”“快递什么时候到”时使用。 不要用这个技能处理订单退款或售后申请那些请使用 order_after_sale 技能。 trigger_keywords: [订单, 物流, 快递, 到哪, 发货] input_schema: type: object required: [query_type, query_value] properties: query_type: type: string enum: [order_id, phone] description: 查询依据类型订单号或手机号二选一 query_value: type: string description: 具体的查询值 output_schema: type: object properties: order_id: { type: string } status: { type: string, enum: [pending, shipped, delivered, closed] } logistics_trace: { type: array, items: { type: string } } examples: - input: { query_type: order_id, query_value: 20241001A001 } output: { order_id: 20241001A001, status: shipped, logistics_trace: [已揽件, 运输中] }trigger_keywords是给路由器做粗筛用的不进模型上下文。description里加了反例说明实测下来比只写正例的情况下模型把“售后类问题”错派到这个技能的概率要低得多。3.6 技能编辑的完整走查技能定义写好之后还需要走一遍完整编辑流程才能上线。我的流程分四步第一步是自测。对应的实现函数先独立跑一遍确认输入输出正常。这一步主要是排除代码层面的低级错误比如字段名拼错、接口不通、数据结构对不上。第二步是单技能评测。把技能的YAML定义注入一个基准模型构造五个典型输入话术看模型能不能正确触发该技能、填对参数。这里经常发现的问题是参数值提取不对比如用户说“订单号是A001”模型可能把整个“A001”当成订单号但其实前面的前缀也要带上这类问题只能靠调整描述和示例来纠正。第三步是做组合评测。验证新技能和已有的上下游技能组合运行时有没有数据格式冲突。最典型的冲突是上游技能的输出字段名和下游技能的输入期待不一致YAML严格定义了字段名因此冲突可以在执行前就被识别出来。第四步才是更新线上路由规则和技能索引。把新技能挂到正确的业务分类下并且跑一遍全量回归测试。4. 常见问题与排查技巧实录4.1 模型选错技能的排查方法这是使用agent-skills后最常遇到的问题。用户意图是“取消订单”模型却调用了“查询订单”技能。这类问题我总结了一套排查顺序。第一步看日志里路由器筛出的候选技能列表。如果候选技能里根本没有“取消订单”那是路由规则的问题关键词没覆盖到加上对应关键词即可。如果候选技能里有但模型还是选错了那就是描述信息有歧义需要优化两个技能的描述让它们区分度更明显。一般来说两个技能如果描述相近最有效的做法是各自增加反例说明。查询技能描述里写“不做退款取消”取消技能描述里写“仅处理取消不处理查询”。模型对这类互斥说明的敏感度比较高效果比绕来绕去的说法好得多。4.2 参数提取不完整的处理策略另一个高频问题是模型调用技能时参数不完整。比如技能要求query_type和query_value都必填模型只填了一个就发起了调用。早期的处理方式是在执行器里抛异常让模型重新调用。但这种方式很消耗对话轮次用户体感也差。后来我调整了策略当参数缺失且能够从上下文中推断时执行器会在补全参数后自动重试一次并把“补全了什么、为什么补全”作为一条提醒信息回传给模型。比如用户前面说“用我上次那个订单”虽然没直接给订单号但上下文里能查到最后一次订单的ID就可以自动补上。只有遇到真正缺失且无法推断时才报错。这里有一条经验不要过度依赖模型自己补参数。大模型经常自行“脑补”一个看起来合理的订单号结果调用接口后根本查不到数据。我在Schema里加了字段来源标记区分“用户明确提供”和“模型推断生成”推断生成的参数必须在执行前单独校验一次。4.3 技能并发调用时的状态隔离技能模块设计成无状态之后并发调用的安全性问题基本就解决了但在实际运行中还是遇到过一个隐蔽的坑技能内部用了全局变量缓存数据导致并发请求互相覆盖。排查这种问题看表象很难判断因为单次调用永远正常只有并发压测时报错数据错乱。后来的规范里加了硬性要求技能执行函数不允许引用任何模块级可变变量。所有状态要么通过参数传入要么在函数内部创建后即用即弃。需要缓存的数据统一走独立的缓存服务不要在技能进程内存里自己搞一套。4.4 上下文中技能描述污染问题技能描述自己写得好不好是一回事注入到Prompt之后会不会互相干扰是另一回事。我在一次评测时发现技能A的描述里提到了技能B的某些功能结果模型在处理技能A的任务时因为描述里见过B的名字偶尔会直接去调B。原因是我在技能A的描述中写了一句“此技能与B技能不同B处理售后”本意是区分反而造成了污染。这个教训后来转化为一个维护规范技能描述中不要出现其他技能的名称不要对比式说明。需要区分的场景统一使用反例描述例如“当用户需要申请退款时此技能不适用”。这样既给出了边界又不会给模型留下可乘之机。4.5 技能版本管理与线上变更控制技能也是代码也需要版本管理。我在skills目录维护了一份SKILL_VERSION文件每次技能定义或实现有变更就更新版本号并将变更描述记录到变更日志里。线上Agent调用时会把版本号传给执行端一旦发现执行结果和预期不符可以根据版本号快速定位是不是技能变更导致的回归。更重要的是灰度策略技能变更上线后先切一定比例的请求到新版本跑一段时间对比新旧版本的调用成功率、参数校验通过率、最终任务完成率。数据稳定后再全量切换。这个过程听起来繁琐但对于依赖Agent完成核心业务的场景是必须投入的成本。5. 影响范围分析与落地效果评估5.1 对开发效率和维护成本的直接改善agent-skills落地到我们内部的一个跨平台系统模拟项目后最直观的变化是开发速度层面的。以前要新增一个Agent能力流程是先写工具函数、写Prompt描述、然后调Agent反复试一个新场景动辄一两天。现在新增技能只需要写一个YAML、一个执行函数、三个示例走一遍评测上线半天时间基本够用。维护成本方面改善更大。之前修改一个公共工具的调参逻辑要全局搜索所有引用过这个工具的Prompt逐个改。现在改的是技能实现函数或者技能描述引用该技能的地方会自动生效因为有注册器统一加载。5.2 对模型行为可控性的提升可控性提升是这套方案最大的价值之一。纯Prompt打法下模型行为像黑盒出了问题只能加提示词反复试。技能层给了系统一个可测试、可观测、可回滚的边界。哪些行为是技能保证的哪些行为是模型自由发挥的分得很清楚。我做过一次对比评测同一个任务场景用传统全工具注入方案和agent-skills方案各跑了100轮技能方案的调用准确率高了大概11个百分点关键参数的填写完整率也提升了不少。模型本身的推理能力没有变变化的是给它提供的选择空间变得更清晰了。5.3 技能库的边界与局限也需要客观地说说这套方案的边界。技能模式适合任务边界清晰、流程相对稳定的场景比如客服工具、数据查询、内容生成、通知发送。如果任务是极端开放式的比如“帮我写一篇文章并发布到全平台并分析阅读数据”这种场景的每一步都充满不确定性技能层能起到的作用就比较有限。另外技能库的引入也增加了一定的工程成本包括技能定义维护、路由规则更新、回归测试执行。团队规模很小或者Agent数量很少的时候这套机制的收益不会很明显。只有当Agent要处理的任务种类足够多、技能复用频率足够高时这笔投入才是划算的。5.4 项目后续的三个扩展方向第一是往技能依赖方向扩展。目前技能编排还停留在数据传递层面后续想加入显式的依赖声明比如某个技能执行前自动检查所需前置技能是否已执行未执行则先触发前置技能这样编排能力会更健壮。第二是往技能自学习方向扩展。目前技能库的演进靠人工维护描述和示例后续计划增加一个反馈回流机制Agent执行失败时记录失败原因和最终人工处理结果定期从中提取新的示例、修正描述逻辑让技能库能沿着实际使用数据持续优化。第三是往多智能体共享方向扩展。当前设计是单Agent内技能复用接下来准备往多Agent协作方向延伸不同Agent共享一套技能注册中心按Agent角色和权限控制可见的技能范围从根本上解决技能重复建设的问题。这套agent-skills方案并不是什么灵光一现的发明更像是对Agent工程化中反复出现的痛点做了一次系统性归纳。我在实际项目中试过用纯提示词约束、也试过参考市面上的工具调用写法效果始终不稳定。最终收敛到技能层这种“结构化封装动态装载严格测试”的组合拳才把Agent从Demo阶段推上了相对可靠的生产路径。再分享一个个人感触比较深的小技巧技能描述里的示例值得花时间去打磨。很多人把示例当摆设随便写两个正常案例就交差。但实际上示例对模型行为的影响远大于描述文字。我做过实验一组精心挑选的示例包含一个边界输入和一个易错反例比在描述里多写两百字规则管用得多。如果你想用最短时间提升现有Agent工具的调用准确率从优化示例开始性价比是最高的。最后想说的是技能库不要一口气追求大而全。先用一套最小可用的框架跑通一条端到端链路再接两三个真实技能等整个流程稳定了再逐步往里填充。项目做大之后你会发现技能库的维护和代码库一样质量和纪律永远比数量重要。