agent-skills 实践指南:从技能定义到编排调优的完整方法论
把agent-skills拆开看就是「智能体的技能」。这阵子我一直在折腾这个方向起因很简单给 Agent 接了一堆工具、堆了一堆提示词之后发现它并没有变得更强反而是越来越难管。模型不知道该调哪个工具、参数传得七零八落、技能之间互相干扰一个看起来能干的 Agent 实际用起来像个只会背台词的新人演员。后来我意识到问题不在模型也不在工具而在「技能」本身。我们需要一套结构化的方式把 Agent 能做的事封装成可复用、可编排、可评估的技能单元让模型像人一样按需调用自己的经验而不是每次都在一堆散装指令里碰运气。这篇文章就是我基于agent-skills这个思路做的一轮完整实践总结包括技能的定义方式、注册加载流程、编排调度策略还有踩过的坑和调参心得。适合正在做 Agent 应用落地、想从 demo 走向稳定系统的开发者参考也适合刚接触 LLM 应用、想理解「工具箱」背后该怎么组织的朋友。我会把设计思路和能直接抄的配置都摊开来讲。1. 项目整体设计与思路拆解1.1 先想清楚技能到底解决什么问题先说结论Agent 现在最大的瓶颈不是「模型笨」而是「能力组织混乱」。你可以把大模型理解成一个精力充沛但缺乏经验的新员工它什么都能聊两句但真要它完成一个具体任务比如「把这份销售数据按地区汇总成图表」它需要的不只是知识而是一套完整可执行的动作序列。这些动作序列就是技能。我在实践里最直观的感受是没有技能分层的时候所有逻辑都堆在系统提示词里系统提示词动辄三四千字模型每次推理都要反复读这些文本既费 token又容易在长文本里丢失关键指令。一旦把每个能力封装成一个独立的技能模块模型只需要知道「我有哪些技能、各自管什么、输入输出长什么样」剩下的细节全部藏在技能内部推理负担一下子就降下来了。用生活里的例子类比一个厨师不会把每道菜的完整菜谱都背在脑子里才进厨房他掌握的是「切配」「火候」「调味」这些基本技能看到菜单上的菜再按需组合这些技能。Agent 也是一样技能就是它反复锤炼出来的局部能力组合起来才能应对千变万化的任务。1.2 为什么需要统一的技能框架而不是各写各的我见过不少团队做 Agent初期都是「一个需求写一个函数」今天加个查天气的明天加个算账的后天加个读取数据库的到最后几十个函数散落在代码各处连作者自己都说不清哪些在用、哪些已经废弃了。这就是缺少统一框架的后果。agent-skills的设计目标很简单让每一个技能都有标准化的外壳外壳包含技能的名称、描述、输入参数 schema、执行逻辑、依赖环境和输出格式。模型不直接面对 Python 函数或 API 端点而是面对一套可理解的技能目录。框架负责技能的注册、检索、调用和结果回传模型只负责「选哪个技能、传什么参数」。这个设计有一个关键好处技能本身和模型解耦。今天用模型 A明天换模型 B技能逻辑不用改最多调整一下描述措辞。我把这个思路称为「面向技能的 Agent 设计」它和传统的「面向函数的 Agent 设计」最大的区别在于技能是带语义的是模型可理解的而函数只是代码层面的接口。具体落地时我参考了常见的工具调用和插件协议的做法但做了几个调整一是把技能描述写得像工作说明书而不是函数注释二是给每个技能加了使用场景和限制条件减少误调用三是支持技能的动态加载和卸载运行期可以按任务需要临时挂载技能。这些调整后面会详细展开。2. 核心细节解析与实操要点2.1 技能定义的「配方」名称、描述、参数和约束技能定义是整个框架的地基。一个技能描述得不好模型就不知道该不该用它、怎么用后面一切编排都是空中楼阁。我实践的技能定义结构包含五个要素技能名称短、无歧义、动词开头让模型一看就懂。技能描述说明这个技能的功能、适用场景和典型输入。输入参数 schema定义参数的名称、类型、是否必填、范围。执行逻辑技能真正做什么可以是代码函数、API 调用、或者一段子 Agent 流程。调用约束包括注意事项、失败兜底、权限要求、成本提示。用一个具体的例子来说。假设我要给 Agent 加一个「查询订单状态」的技能大多数人的第一版描述会写成「查询订单」。这个描述太模糊了。模型拿到一个用户问题「我上周买的那个耳机到哪了」它不知道这算不算「查询订单」的场景。我实际用的描述长这样name: query_order_status description: 根据订单号或购买人手机号查询订单的当前状态、物流信息和预计送达时间。 适用场景用户询问商品物流、订单处理进度、退款到账情况。不适用用户询问已收货商品的售后问题。 input_schema: order_id: type: string description: 订单号如 SO202401001 required: false phone: type: string description: 购买人手机号作为 order_id 缺失时的备选查询条件 required: false constraints: - 如果 order_id 和 phone 都缺失直接返回参数错误提示不要猜测。 - 如果查询不到订单返回「未找到匹配订单请核对订单号」。 - 本技能只读不修改任何订单状态。这里最关键的部分不是参数 schema不是代码实现而是「适用场景」和「不适用场景」的描述。我踩过的坑是技能描述写得太泛比如「用这个技能获取订单信息」模型会把它用于所有和订单沾边的问题包括「我想退掉这个订单」。而明确写出不适用场景之后误调用率明显下降。这算是agent-skills实践里性价比最高的一处改动。2.2 技能注册与加载让框架认识每一个技能技能定义好之后需要有一个统一的注册机制。我采用了类似插件系统的做法每个技能是一个独立的目录目录里包含skill.yaml定义和executor.py执行逻辑。框架启动时扫描技能目录解析所有skill.yaml构建技能目录索引。注册表的核心数据结构就是一个列表每个元素是解析后的技能定义。为了支持运行时检索我额外给每条技能生成了一组 embedding 向量存入向量索引。这样既能按名称精确匹配也能按语义模糊匹配。加载策略上我分了两层静态加载框架启动时加载所有基础技能比如时间查询、计算器、文档读写。这类技能体积小、延迟低常驻内存。动态加载遇到复杂任务时根据任务语义动态挂载相关技能包。比如检测到任务涉及数据分析才加载数据统计类技能涉及发邮件才加载邮件类技能。这么做的原因很实际每个技能的描述和 schema 都要嵌入主提示词或作为工具定义传给模型技能太多会撑爆上下文窗口。动态加载可以保持上下文精简同时让模型每次只面对一小撮高度相关的技能选择准确率更高。2.3 技能描述怎么写模型才愿意用这部分我想单独拿出来说因为它是纯经验活。我试过几种描述风格效果差别很大。最早我用的是「技术文档风」例如「执行 order query operation and return JSON result」。模型能看懂但会把技能当成一个冷冰冰的接口在判断「什么时候该用」上表现很差。后来我改用「任务说明风」把技能描述写成「当用户想要了解订单处理状态或物流进度时使用此技能查询并返回最新信息」。这个改动带来的准确率提升是很明显的。原因是模型在预训练阶段接触过大量「任务指令」类型的文本对这种表述天然敏感。再往后我又加了一层「否定描述」明确写出不需要用这个技能的场景。我犹豫过要不要加因为这会增加 token 消耗但实测下来减少的误调用远比多花的 token 值得。还有一个值得注意的细节技能描述里尽量避免使用同一批近义词反复堆砌。比如又写「查询」又写「检索」又写「获取」模型反而会困惑这些词是否对应不同技能。统一用词保持描述一致性会让技能路由更稳定。3. 实操过程与核心环节实现3.1 环境搭建与基础框架选型先说我的实践环境。整个agent-skills项目我用 Python 实现核心依赖是模型服务 SDK、向量存储和一个轻量级的任务编排库。模型我同时测过几款主流的大模型接口技能框架本身和具体模型无关这是设计时就要保证的。项目目录结构大概是这样的agent-skills/ ├── skills/ │ ├── query_order_status/ │ │ ├── skill.yaml │ │ └── executor.py │ ├── generate_report/ │ │ ├── skill.yaml │ │ └── executor.py │ └── send_email/ │ ├── skill.yaml │ └── executor.py ├── core/ │ ├── registry.py # 技能注册中心 │ ├── router.py # 技能路由与选择 │ ├── loader.py # 技能加载与解析 │ └── executor.py # 技能执行器 └── main.pyregistry.py是核心负责在启动时扫描skills/下的所有技能目录解析skill.yaml做成技能目录。技能目录会同时提供给两个模块使用一个是给模型看的可读文本形式一个是给路由模块用的结构化形式。我实际写的一个简化版注册中心代码如下import yaml from pathlib import Path class SkillRegistry: def __init__(self, skills_dir: str): self.skills_dir Path(skills_dir) self.skills [] self._index {} def load_all(self): for skill_path in self.skills_dir.iterdir(): manifest skill_path / skill.yaml if not manifest.exists(): continue with open(manifest, r, encodingutf-8) as f: skill_meta yaml.safe_load(f) skill_meta[_dir] str(skill_path) self.skills.append(skill_meta) self._index[skill_meta[name]] skill_meta return self.skills def get_by_name(self, name: str): return self._index.get(name) def build_catalog_text(self): # 生成给模型看的技能目录说明 lines [] for skill in self.skills: lines.append(f- {skill[name]}: {skill[description]}) return \n.join(lines)代码不复杂但有一个点需要注意build_catalog_text输出的文本是直接拼进系统提示词的所以它必须简洁、结构化、只保留路由所需的信息。不要把整个skill.yaml都塞进去否则又要撑爆上下文。3.2 核心技能实现示例从定义到执行我拿一个实际用过的技能举例「多表格数据合并统计」。这个技能解决的问题很常见用户丢来几个 CSV 或 Excel 文件要求按某些维度汇总。skill.yaml定义如下name: merge_and_aggregate description: 合并多个表格文件CSV/Excel按指定维度做汇总统计。 适用场景用户上传多个数据文件要求求和、平均、计数、分组汇总。 不适用用户只要求转格式、只看文件内容、做数据清洗。 input_schema: file_paths: type: array items: string description: 至少两个文件的路径 required: true group_by: type: string description: 分组字段名如 region required: false agg_columns: type: array items: string description: 需要汇总的字段列表 required: true agg_type: type: string enum: [sum, avg, count] default: sum description: 聚合方式 constraints: - 文件必须存在且可读否则返回明确的错误提示。 - 分组字段不存在时自动忽略该字段并提示用户。executor.py的实现逻辑是先用pandas读取所有文件拼接成一个长表再按group_by分组和agg_type聚合。整个过程封装在一个execute()函数里框架统一调用。有一个执行细节我想强调技能执行函数一定要做防御式编程。Agent 传进来的参数经常是不完整的比如group_by字段名可能拼错、文件路径可能带引号。我在执行函数里统一做了参数清洗比如把「region」和「region」归一化把全角引号去掉。这种小处理看着不起眼但能大幅降低线上报错率。3.3 半自动技能路由模型选完规则兜底技能路由是agent-skills里最微妙的一环。全交给模型它偶尔会抽风全走规则又失去了自然语言理解的灵活性。我最终用的是「模型为主、规则兜底」的半自动路由。流程分三步意图收缩系统提示词里告诉模型当前可用的技能列表模型根据用户请求输出一个结构化意图包含技能名称和参数。这一步我用的是自然语言推理加输出 JSON 的方式而不是直接调 function calling因为这样对模型的兼容性更好。规则校验框架收到模型输出的技能名和参数后先做一轮规则校验。校验内容包括技能是否存在、必填参数是否齐全、参数类型是否匹配、是否触发了技能约束里的禁止条件。校验不通过就返回错误让模型重新选择而不是直接执行。执行与回传校验通过后执行器调用技能实现拿到结果后回传给模型让模型基于结果生成最终答复。我举个实际对话的例子。用户说「把这三个文件合并一下算算每个城市的平均销售额。」模型第一步可能选择merge_and_aggregate传参如下{ skill: merge_and_aggregate, arguments: { file_paths: [/tmp/a.csv, /tmp/b.csv, /tmp/c.csv], group_by: city, agg_columns: [sales], agg_type: avg } }规则校验发现三个路径都存在、参数完整就放行执行。执行完返回一个 Markdown 表格模型把它转成用户友好的话术。但如果说用户只有两个文件第三个路径是模型幻觉出来的规则校验就会拦截提示「文件不存在」。模型收到错误后会主动纠正参数而不是硬着头皮执行。这一步是基于诚实原则设计的宁可让模型多问一轮也不要执行错误操作。3.4 复杂任务的技能组合编排单技能调用只是基本功。真正体现agent-skills价值的是多技能组合。我设计了一个简单的编排器支持把任务分解成多个技能的串行或并行调用。编排器的实现思路是模型先生成一个执行计划计划里是一个有序的技能调用列表前一个技能的输出可以作为后一个技能的输入。比如「把销售数据按地区汇总然后生成一张图表再把这个图表通过邮件发出去」这个任务可以编排成三个技能的串行链路merge_and_aggregate→render_chart→send_email其中render_chart的输入参数chart_title可以从merge_and_aggregate的输出中提取send_email的附件路径是render_chart的输出。为了让模型能理解这种依赖关系我在技能定义的描述里增加了「output_hints」写明这个技能返回什么样的数据方便模型判断如何衔接。并行调用场景也很常见比如同时查多个系统的数据再汇总。编排器会对这类任务做并行调度缩短整体响应时间。并行这块我一开始没做后来发现串行太慢才补上。实测一个需要查三个来源的任务并行比串行快约三倍效果很直观。4. 常见问题与排查技巧实录4.1 模型总选错技能不是模型的锅是你描述不清agent-skills实践里我遇到的第一大问题就是技能路由不准模型老选错技能。一开始我以为是模型能力问题后来做了几个实验才发现问题出在技能描述上。典型场景我有query_order_status和query_inventory两个技能用户问「这个商品还有货吗」模型有时候去查订单状态了。查了日志才发现两个技能的描述里都出现了「查询商品」「状态」这些共同关键词模型分不清边界。我的解决办法分两步第一步给技能描述加上强烈的场景锚定词query_inventory的描述改为「当用户询问商品是否有货、库存数量、补货时间时使用此技能」让适用场景和问题意图强对齐。第二步在技能描述里增加「差异提示」例如query_order_status里补充一句「本技能只处理订单级别的状态不处理商品库存」。有了这个对比信息模型的区分能力明显提升。排查这类问题时我的建议是先看路由日志再看描述文本。大多数路由错误都能从「技能描述和用户问题之间的语义距离」上找到原因。4.2 参数传递的隐形地雷类型、缺省和幻觉路径第二个高频问题是参数传递。模型在生成结构化参数时经常出现三类错误类型不匹配用户说「查一下订单 12345」模型把order_id传成数字12345而 schema 里定义的是字符串。看似无害但在某些严格类型系统里会直接报错。必填参数缺失技能要求至少传一个查询条件模型只传了一个空对象。幻觉路径模型编造了一个不存在的文件路径或订单号。针对这些问题我在执行器内核做了一个统一的参数归一化层专门负责类型强转、缺省值填充和路径存在性校验。归一化层不通过时不是直接抛异常而是返回友好的结构化错误信息把它回传给模型作为「重新思考」的依据。我一直坚持一个原则错误信息也是给模型看的提示词。返回「参数错误」和返回「参数 order_id 类型应为字符串当前收到数字 12345请修正后重试」完全是两个效果。前者模型可能二次犯错后者模型基本能一次修正。4.3 技能多了之后上下文膨胀和检索噪声随着技能数量增加新的问题又出现了。技能目录越来越长全部塞进提示词后模型反而开始「乱花渐欲迷人眼」路由准确率下降响应延迟也升高。这个阶段让我意识到技能管理和传统的「函数数量越多越好」不一样技能库也需要做沉淀和修剪。我的应对策略是分级技能库L1 常用技能不超过十个常驻提示词。这些是基础能力覆盖面广、调用频率高。L2 扩展技能几十个级别通过语义检索按需加载。任务进来后先用用户的请求文本做 embedding 检索召回最相关的三到五个技能临时注入提示词。整套流程实测下来技能检索的召回准确率在八成以上偶尔漏召回的情况也能靠模型追问兜底。上下文长度从「全部加载」时的几千 token 降到「按需加载」时的一千 token 左右路由准确率反而提升了。这里我想提醒一句技能检索排序只看 cosine 相似度是不够的。要把技能的调用频次、最近使用时间、历史路由成功率都作为排序特征。我在排名字段里加了一个priority权重热门技能优先展示效果比纯相似度好不少。4.4 技能调用的安全边界权限和敏感操作最后一个常见问题是安全边界。Agent 的技能如果包含写操作比如发邮件、删除文件、修改数据库就必须设一道硬性的权限门槛。我的做法是在技能定义里加permission字段取值包括read_only、confirm_required、admin_only。框架在执行前会检查当前会话的权限级别未授权操作直接拦截。confirm_required类技能框架会先生成一个确认卡片让用户点击确认确认后才执行。这条经验是我在一次事故里总结出来的。当时我在测试一个「批量重命名文件」的技能模型按照用户描述执行后把一批文件名改得面目全非还好是在测试环境。从那之后所有带副作用的能力一律强制加确认步骤。理论和实践都告诉我模型对「用户是否真的想要做这件事」的判断能力远没有我们想象的那么可靠权限这层不能省。5. 个人经验与后续扩展方向5.1 让技能库活起来技能评估和迭代闭环技能不是写完就完事它需要持续评估和迭代。我会给每个技能记录三条数据调用次数、路由成功次数、执行成功次数。每周做一次复盘重点看那些「被模型选中但是执行报错」的技能大概率是参数设计有缺陷。我做一个周复盘时列过一张技能体检表这成了后续迭代的基础技能名调用次数路由准确率执行成功率问题描述优化动作query_order_status12892%98%手机号查询偶尔失败增加手机号格式校验merge_and_aggregate3774%88%分组字段误传描述中补充字段示例send_email2186%95%无保持现状这个表格看起来简单但每次复盘都能挖出几个可以优化的点。技能库的质量不是靠写出来的是靠一次次评估和修正养出来的。5.2 后续可以怎么玩技能市场化和跨 Agent 共享agent-skills这个框架做到后面我产生了一个想法技能能不能像应用商店一样定义好接口标准之后跨 Agent、跨项目共享我设想了一套「技能包」规范每个技能包除了核心定义之外附带一套测试用例和一个演示截图。其他开发者拿到技能包后只需要跑一遍测试用例就能验证技能在自己的模型和业务场景下是否可用。这样一来技能从一个项目里的内部工具变成了一种可流通的资产。虽然这个设想目前还只停留在原型阶段但我个人非常看好这个方向。Agent 的竞争到最后很可能会演变成技能生态的竞争。谁拥有更多高质量、可编排、经过持续打磨的技能库谁就能做出更可靠的 Agent 产品。5.3 最后分享一个调试小技巧每次改动技能定义尤其是描述措辞或参数结构之后不要急着接真实业务。我习惯先用一组固定的测试问题集跑一遍路由对比改动前后的路由准确率。测试问题集大概二十条覆盖每个技能的正向用例和反向用例。这个习惯帮我拦下了很多「改完后部分场景变差」的回退问题。另外一个不起眼但很实用的细节技能目录索引的构建成本不高但不要让它在每次请求时都重建。我都是启动时构建一次运行时只在技能有增删的时候更新。不然高并发场景下光重建索引就能拖垮整体性能。折腾agent-skills这段时间我最大的体会是Agent 能力的上限不取决于你接了多少个模型而取决于你把模型能做的事情整理得多清楚。技能不在多但每一个都应该定义得明明白白用起来稳稳当当。