agent-skills:面向大模型应用的标准化技能库设计与实现

发布时间:2026/10/13 15:58:20
agent-skills:面向大模型应用的标准化技能库设计与实现
做Agent应用开发的这段时间我最大的感触是模型的理解力早就不是瓶颈真正卡住项目进度的是那些围绕模型外围的动手能力。你可以让模型把一句话拆成三步计划但它调用工具时照样会把参数填错、把接口调炸、把不该执行的写操作直接执行掉。这些问题的根子不在模型而在我们给模型准备的技能太潦草。于是就有了这个名为 agent-skills 的项目。它不是又一个Agent编排框架而是一套面向大模型应用的能力扩展层核心思路是把一个Agent需要执行的每一个具体动作——查列表、改配置、算数据、发指令、做检索——都封装成标准化、可插拔、可测试的技能模块。这篇博文会把整套设计从需求拆解、对象模型、核心实现到避坑经验完整记录下来适合正在做Agent的人机交互产品、想把LLM接入业务系统、或者被工具函数越写越多最后根本不敢让模型随便调折磨过的开发者参考。1. agent-skills的起源工具越堆越多模型越用越不敢信开始这个项目之前我手里的第一版Demo其实能跑但状态只能用脆来形容。所有工具函数堆在一个大文件里一个函数负责查询订单列表一个函数负责修改配置项还有一个函数的副作用是直接向外部系统发送指令。模型只要一开口我就得把整份函数清单连同所有参数说明拼进提示词里。工程上这就像把整栋楼的电线全都塞进一根管道不短路才怪。1.1 最初的问题形态硬编码工具函数的三宗罪第一宗罪是调用逻辑与提示词强耦合。每次新增一个工具我不仅要写函数还要同步修改系统提示词里的工具说明而且说明一长模型就开始选择性失明经常在明明有更合适工具的情况下调用错了那个。第二宗罪是参数校验形同虚设。模型填参数的时候是按照它自己想象的格式填的和接口真正需要的格式经常出现微妙错位——日期少一位、枚举值大小写不匹配、数组传成了逗号分隔字符串。第三宗罪就是写操作没有保护。我踩过一个很典型的坑测试环境里模型把一条本该追加的备注直接覆盖了整条配置记录就因为工具函数里少了一个确认影响范围的环节。1.2 为什么最终选择了技能这个抽象层级把函数改名叫技能听起来像换汤不换药但关键在于抽象层级不一样。一个普通的工具函数只定义了输入变成输出的映射而一个技能模块需要同时回答五个问题这个能力什么时候该被模型选中参数长什么样执行逻辑是什么执行结果如何反馈给模型失败时怎么办这五问其实就是我在 agent-skills 里定义技能的核心骨架。每个技能是一个自包含的目录里面有描述文件、参数Schema、执行器、前置检查逻辑和回退策略。这样一来技能和技能之间天然隔离团队可以并行开发模型看到的是一份统一的、经过打磨的技能清单而不是一堆裸露的函数签名。1.3 设计目标一天之内让一个新技能上线在开工之前我给项目划定了边界避免做成一锅粥。目标非常明确一个新业务能力从开始写代码到能被模型稳定调用全过程控制在一天以内并且不需要改动Agent主流程的任何一行代码。同时我也明确不做什么。第一不碰模型推理逻辑模型的选路、决策、对话生成都交给上层框架。第二不做通用工作流引擎复杂的多步骤编排要拆成多个技能由模型自己组合我不打算替模型做计划。第三不追求覆盖所有业务场景只把这套技能接入层做到足够舒服让新增技能变成复制目录、改配置、写函数三步走。这条边界帮我挡掉了大量需求膨胀项目才能真正聚焦。2. 技能对象模型一个技能就是一个自包含的模块把技能当作一种一等公民对象来设计是 agent-skills 区别于普通函数库的核心。这部分的重点是对象模型的设计决策因为一旦确立后面新增任何技能都只是在往这个框架里填内容。2.1 技能注册表的核心数据结构我定义了一个技能注册表每个注册项包含六个核心字段直接映射到前面说的五问。字段作用说明name唯一标识全小写加中划线比如order-list-querydescription给模型看的描述说明用途、适用条件和典型调用场景parameters参数Schema采用JSON Schema格式描述每个字段的类型、枚举和默认值executor执行函数真正的业务逻辑入口接收经过校验的参数precheck前置检查执行前校验环境、权限、参数合法性fallback回退策略执行失败后给模型返回什么样的替代建议这里有一个当时纠结了很久的点要不要把 description 写得像一本说明书那么长。试了很多版本之后我倾向于两句话原则第一句话说明这个技能是干什么的第二句话说明什么情况下应该用它。就这么简单。描述写太长模型反而不容易抓住重点。真正详细的调用说明应该放在参数Schema的字段描述里而不是技能描述里。2.2 把给模型看的描述和给代码执行的逻辑分开很多工具库的通病是模型看到的东西和代码执行的东西混在一起。比如函数注释里写了一段给人类维护者看的话结果也被当作工具说明拼进提示词模型读了半天抓到一堆无用信息。所以在 agent-skills 中每个技能模块的文件结构是明确分离的。skills/ order-list-query/ SKILL.md # 给模型看的描述会被加载进提示词 schema.json # 参数Schema用于校验和生成调用说明 executor.py # 执行逻辑 precheck.py # 前置检查逻辑 fallback.py # 失败回退逻辑 test_cases.json # 用于自动化回归的测试用例SKILL.md 文件里只有纯文本描述任何代码相关的内容都放不进这个文件。这样维护提示词的人只需要编辑一个文件不会误伤执行逻辑。另一个好处是我可以针对这个文件做独立的文案测试对比不同写法下模型调用的准确率而不用动代码。2.3 技能模块的基础骨架示例下面这个例子是查询待办列表技能最简化的实现用来展示一个技能模块长什么样。实际业务中会比这个复杂但骨架不变。# file: skills/todo-list-query/executor.py async def execute(params: dict, context: dict): # params 已经经过 schema 校验和类型转换 page params.get(page, 1) page_size params.get(page_size, 20) # 调用底层接口这里只是示例 data await context[http_client].get( /api/todos, params{page: page, page_size: page_size}, ) # 裁剪返回结果避免把大段原始数据直接丢给模型 return { total: data[total], items: [ {id: item[id], title: item[title], status: item[status]} for item in data[items][: page_size] ], page: page, }注意 executor 返回值不是一段过滤后的自然语言而是结构化的数据。让模型自己根据这些数据生成回答比在这里硬拼提示词要灵活得多。另外我刻意在返回前裁剪字段这是为了避免把内部字段名、数据库主键、无关的时间戳全部暴露给模型既减少token消耗又降低信息干扰很实用。3. 五个代表性技能模块的实现复盘有了对象模型之后真正学习效果最好的方式是逐个实现具体的技能。这里挑五个比较有代表性的模块复盘它们在调用模式上分别覆盖了读操作、写操作、计算型操作、检索型操作和异步后台操作。认真看一遍这五类基本就能举一反三覆盖绝大多数业务场景。3.1 列表查询类技能分页与返回裁剪列表查询是Agent用得最多的技能之一但也最容易踩分页的坑。模型默认只调用一次就完事如果第一页数据不够它不会主动翻页而是直接跟用户说没有了。所以我在参数Schema里同时提供了 page 和 page_size 字段并且在描述里明确写了如果需要更多结果请根据 total 判断是否需要再次调用本技能获取下一页。这个把翻页责任交给模型的设计一开始我也有点担心但实测下来只要 total 返回得清晰模型是能够自己决定翻页的。另一个值得注意的点是返回裁剪。原始接口可能返回二十多个字段我一般只保留 id、标题、状态这几个核心字段。之前有过一个反面教训某个接口返回里包含一个内部备注字段模型读到之后居然把备注内容当成用户订单信息回答了出去。信息暴露的问题往往就是这么来的。3.2 写操作类技能天然需要两阶段提交如果让模型直接执行插入、更新、删除一类的操作风险相当大。比如用户说帮我把这个任务的优先级改成高如果模型直接调用了 update 接口万一它理解错了任务改的就是另一条记录了。所以写操作类技能我基本都做成两阶段先预检后执行。预检阶段其实是复用 precheck 机制。技能被调用后precheck 会先把参数中的关键对象信息查出来让模型做确认。比如模型要改任务优先级precheck 会返回类似即将修改的任务是《发布上线方案》当前优先级为普通你确认让我改成高吗这样的信息。这个信息再喂给模型由模型生成一句确认话术。只有用户明确说确认可以模型才会带着一个确认标记重新调用执行接口。实测下来误操作率几乎降到了零代价就是多一轮对话用户完全能接受。3.3 推理计算类技能本地执行器的资源防护有一类任务不适合让模型口算比如算一笔订单折扣、核对一串数字求和。让模型直接算结果常常是错的尤其当数字超过十位时。我的一般做法是把这类计算封装成独立的执行器在沙箱中调用 Python 计算。沙箱执行必须限制两项超时时间和输出长度。我在 executor 里给每个计算任务设置了 5 秒的超时超出就直接判失败。输出长度同样限制得很死防止某个计算函数被打日志打到吐。这里有个容易被忽略的点要禁止执行器访问网络和环境变量。计算类技能需要的是纯函数不应该让模型借计算之名触达系统敏感信息。我甚至会把所有计算逻辑放到一个独立的子进程中执行一次失控也不会拖垮主服务。3.4 信息检索类技能相似度不是万能的检索类技能是RAG应用的基础这里最关键的是给检索结果加一个可信度阈值。模型很喜欢脑补如果检索结果和问题相关性不够它宁可自己编一段也不愿意说我没找到。所以我在检索技能里加了一个 min_score 参数低于阈值就直接返回空结果并附上一句未找到足够相关的内容你可以换一个问法或提供更多关键词。这个开关比在提示词里反复强调不要编造有效得多因为它是逻辑层面的硬约束。另外一个很细节的点检索返回的内容也要做截断。比如知识库每篇文章可能有几千字直接全部塞给模型会稀释重点。我通常会按相似度排序后取前三条每条只截取命中的相关段落并且把来源标识也带上方便模型回答时注明出处。这样Agent的答案可信度和可追溯性都会好很多。3.5 异步任务类技能任务状态要可查询最后是异步任务类。比如触发一个数据同步、生成一份报表这类操作往往要几十秒甚至几分钟。如果让模型一直阻塞等待体验非常差。我把这类技能设计成提交即返回立刻给用户一个任务ID并告知任务正在执行稍后我会查询结果。然后系统后台跑任务队列执行完成后把结果写入一个缓存表。模型需要主动调用另一个查询任务状态技能来获取结果。这个设计带来一个额外好处如果任务执行时间太长或者失败了模型可以在回复中如实解释而不是假装自己什么都知道。异步任务类技能的关键参数是 task_id 和轮询策略。我在描述里告诉模型调用后请等待至少30秒再查询结果避免模型疯狂轮询接口把服务打爆。4. 技能调度与自动纠错把模型乱调用变成可控行为技能模块本身只是零件真正让整套系统稳定运转的是调度和纠错机制。我会把这一层看作一个软性网关让模型调用技能的自由度还在但危险动作都被挡在外面。4.1 技能描述SOP如何写一段模型不会误读的描述前面提到两句话原则但具体怎么写还是有很多门道。我在打磨了几个版本之后总结出一套写法模板。第一句主语是技能名用动词开头比如查询订单列表。第二句写适用条件把什么情况下用和什么情况下不要用都点出来。比如一个查询订单的技能描述我会写成查询订单列表。适用于用户想查看订单、物流、支付状态时。当用户只是询问订单金额汇总时不要使用本技能请使用订单汇总技能。这里最重要的其实是写清楚不要用的场景。模型误调用往往不是因为它不知道技能有什么能力而是因为多个技能的能力边界有重叠。重叠地带一旦出现模型就会随机选一个。与其模糊地祈祷不如在描述里明确划线。实测对比过加上否定句描述的技能误调用率降低了三成左右。4.2 调用前、调用中、调用后三层防线技能执行并不应该无脑放行。我在调度层做了三道拦截分别对应不同风险点。调用前拦截由 precheck 负责主要用于校验环境与权限。比如当前用户角色是否允许执行写操作、目标资源是否存在。以前出现过一次模型拿着测试环境的资源ID去调生产环境接口的情况就是因为环境隔离没有做进 precheck。从那以后每个技能的 precheck 第一件事就是检查上下文里的环境标记发现不匹配立即拒绝。调用中拦截是超时控制和重试策略。每个技能都会声明自己的最大执行时长调度器统一管理。需要重试的操作要有幂等性支持比如更新操作必须带上期望版本号否则拒绝执行有效防止重试造成的数据覆盖。调用后拦截是结果校验。我会定义每个技能返回结构的最小要求比如列表查询必须返回 total 字段检索必须返回 sources。如果返回结构缺失说明执行链路可能被绕过或者接口返回异常调度器会把这个结果标记为失败而不是继续喂给模型。4.3 失败时的自动回退策略没有任何技能是永远不失败的所以 fallback 是必备字段。回退策略我设计了三种模式。第一种是建议替代技能比如订单查询失败时建议调用订单汇总两者数据同源至少让用户还能得到一个大致的数量级概念。第二种是参数降级当模型填的参数范围太大导致接口超时时回退逻辑会自动改用较小的 page_size 重新执行。第三种是转人工引导直接告诉用户当前系统繁忙请稍后重试或联系管理员这种一般用于下游依赖完全不可用的场景。回退策略的设计要特别注意一点不要让回退动作隐藏了原始错误。如果原技能失败但你回退成功后返回了一个看似正常的结果模型可能完全意识不到这个结果不是用户想要的最精确形式。我一般会在回退结果里加一个data_source_note字段让它透出本次为降级结果以便模型在回答时向用户说明。4.4 技能调用日志每次调用都是可复现的实验Agent开发里最痛苦的事情之一就是问题不可复现。模型这次的调用链和上次不一样参数也可能略有差异想要定位问题往往无从下手。我在调度层做了一份结构化的技能调用日志记录每次调用的完整上下文触发了哪个技能、模型给的原始参数、经过Schema校验后的参数、precheck结果、执行耗时、返回的前若干字符、命中错误码。这些日志不进业务库单独存一份并且保留原始请求的 trace_id方便关联。日常开发中我大量依赖这批日志做回归分析。每次发现模型调错技能我不改代码先把这份日志拉出来看看是描述不清晰、参数误导还是模型本身选路错了。很多看起来是模型智障的问题最后都证明是技能设计的锅。这套日志体系省下的排查时间远比写日志本身多得多。5. 与Agent框架的对接接口保持轻薄是关键agent-skills 只负责技能管理和执行真正让模型开口调用的活在上层Agent编排框架。为了让两边解耦驱动侧要设计得足够薄。只要接口面窄将来替换上层框架就只动适配层不动任何技能模块。5.1 只暴露三个核心入口我对外只提供三个入口其它一律不许过墙。list_skills()返回技能清单和描述让上层框架在初始化时加载execute_skill(name, params)执行技能并返回标准化结果get_skill_status(task_id)查询异步任务状态的进度。就这么简单。这样设计有三个好处。第一上层框架不需要知道技能内部结构减少耦合。第二安全审计时只需要盯着这三个入口不需要翻遍全部技能代码。第三上层框架可以彻底静态化不随技能数量增长而增长。对一个几十个技能的项目来说保持三个入口意味着维护成本几乎不增加。5.2 同步调用和异步调用的适配不是所有Agent框架都天然支持异步。我一开始对接某个框架时发现它的工具调用模型只支持同步返回而我的异步任务类技能注定要秒回一个 task_id。解决方案是在适配层自己维护一个轮询线程当Agent框架同步调用异步技能时适配层立刻返回任务已提交同时线程后台等待任务完成。等到Agent下一轮询问时再通过查询技能把真实结果拿回来。同步技能这边则简单一些唯一要注意的是超时时间要可配置。不同任务的时延特征差别很大查询列表几十毫秒要搞定生成报表可能要几十秒。灵活调整超时能避免两大派生问题超时太紧导致正常任务被误杀超时太松导致坏请求长时间挂起。5.3 多技能并发执行的共享状态隔离当模型一次要调用多个技能时比如先查订单再算折扣再发通知这三个技能如果共享了同一个上下文对象非常容易出现状态串味。我最开始把所有技能的数据访问都挂在同一个全局 client 上结果某个技能改了配置下一个技能读到的就是改后的值查了半天才发现原来是共享状态污染。现在的做法是为每个技能调用栈分配独立的 context 对象只允许显式声明共享的数据进入。比如用户ID可以共享给整条调用链但当前查询结果集这种中间数据绝不能共享。另外需要并发访问的资源要加锁或走消息队列避免并发写入同一份数据。这个设计的复杂度在于你要搞清楚哪些该共享哪些不该共享但这是保证多技能稳定并发的必经之路。6. 实测效果与最容易翻车的三个场景任何架构设计都得用数据说话。在内部小规模联调两周后我整理了一组对比数据可以把这次重构的收益看得比较清楚。6.1 任务完成率与拦截指标的量化对比我准备了一组包含200条典型用户请求的评测集分别跑在硬编码工具函数的旧方案和 agent-skills 的新方案上结果如下。指标旧方案agent-skills任务完成率61%82%平均调用轮数5.2轮4.1轮参数错误导致的任务失败每百次约14次每百次约3次写操作误执行率每百次约7次每百次约1次数据说明了两件事。第一任务完成率提升主要来自参数校验和返回裁剪模型拿到干净参数和结构化结果生成答案的准确率自然上去了。第二平均调用轮数下降是因为技能描述里明确的不要用写好了模型选路的盲目性降低不需要反复试错。6.2 最容易翻车的三个场景与对应解法第一个高频翻车场景是多个技能描述相似。比如订单查询和订单汇总技能名字相近功能相近模型经常选错。我的解法是给高频技能加更详细的使用场景示例并明确获取全部明细用查询获取合计用汇总。第二个翻车场景是模型无视参数约束。例如 schema 里明明写了枚举值只有 open 和 closed模型还是有可能传一个 filtered。单纯靠描述提醒作用有限最后还是要靠执行层的严格校验。校验不通过时返回一个友好错误告诉模型可接受值列表模型通常会自动纠正参数重试。第三个翻车场景是长周期异步任务的冷启动。第一次提交的报表生成任务往往需要更长时间初始化如果模型在任务还在起步阶段就查询结果就会拿到还没开始执行的状态它就会跟用户说系统没反应。后来我在异步任务描述里加了一句首次执行可能需要60秒初始化请等待至少60秒后查询这个问题基本消失。6.3 技能回归测试集让重构不怕改东西任何技能模块的改动都可能影响模型对其它技能的判断。所以我开始维护一份技能回归测试集。这份测试集包含两大类一组是技能语义层面比如用固定的测试描述让目标模型选择技能检查是否能正确匹配另一组是执行层面比如对每个技能输入边界参数和典型参数检查返回结构是否稳定一致。现在每当我修改某个技能描述或者参数Schema时我不会说感觉没啥问题就发布反而先跑一遍回归测试集。实际有过一次仅仅修改了一个技能的 description 格式结果导致另一个技能被选中的概率显著下降。如果当时没有回归测试集这个副作用可能要过很久才发现。这也是我强烈建议任何Agent技能库都要配一套自动化测试的原因。6.4 维护方式把技能当产品而不是代码最后聊聊维护。一个技能库做出来不是一劳永逸的业务在变模型在变。模型版本升级之后原来写得很清楚的描述可能就不再被准确理解所以描述文案要像产品文案一样持续打磨。我现在每隔两周会拉一次调用日志专门看那些被模型跳过不用的技能分析是不是识别度下降了。另外一个维护技巧是技能QoS分的概念。我给每个技能记录调用成功率、平均耗时和回退触发次数对经常失败的技能标记为高风险。这些高风险技能不会直接禁用但是在调度时如果有替代技能系统会优先选择更稳的那一个。这种灰度式的健康管理方式比一刀切禁用要平滑得多。聊到这里agent-skills 这个项目最核心的经验基本都覆盖了。我从一个到处硬编码工具函数的雏形走到一套标准化、可插拔、带自纠错能力的技能库最有价值的并不是代码本身而是那套关于模型能力边界的认知。个人体会是模型的表现上限并不完全由模型决定反而更取决于我们喂给它的技能干净不干净。如果你打算做自己的Agent技能库我的建议是先把技能对象模型定死无论多急都别把描述、schema、执行器混在一起。然后哪怕再忙也要把上面的基本功做扎实日志、校验、回归测试这三样是保命的。这套框架最近还在持续迭代下一步我准备把技能和技能之间的组合策略也做成可配置项让模型可以更安全地完成多步骤任务等有进展再来分享。