“agent-skills”这个标题乍一看很简短但真正动手维护过 Agent 项目的人会明白它背后其实是一整套关于“如何让大模型稳定地调用外部能力”的工程问题。我在过去几个月的实践中把一个小仓库从最初只有几个 prompt 脚本逐步迭代成了一套带注册、校验、调用、反馈闭环的技能库系统。这篇文章不打算讲理论就把我在这个过程中的设计取舍、踩坑记录和最终落地方案整理出来。1. 从“一堆 prompt”到“一套完整技能库”agent-skills 到底在解决什么问题先说一个很现实的场景你写了一个 Agent最初它只会聊天。后来你给它接上了搜索、计算器、数据库查询、邮件发送这些工具每次都是把工具描述塞进 system prompt让模型自己决定什么时候调用。这个方案在小规模时挺好用但一旦工具数量超过十个、二十个问题就来了——模型经常选错工具、漏掉参数、甚至因为描述太含糊而反复尝试同一个动作。我最初接触 agent-skills 这个方向就是被这个问题逼的。当时我们一个模拟项目里需要让 Agent 同时处理客服问答、订单查询、售后工单创建等十几个任务。每个人写工具函数的方式都不一样有的返回 JSON有的返回纯文本有的直接打印日志。结果 Agent 的行为非常不稳定同样的用户问题今天能正确调用订单接口明天就跑去查库存了。后来我们逐渐意识到缺的不是单个工具而是一套统一的“技能封装层”。所谓技能skill就是让 Agent 能够理解、调用、并从中获得反馈的最小任务单元。它比普通函数多一点东西要有给模型看的描述、有入参出参的 schema、有校验逻辑、有错误恢复机制。agent-skills 这个名字听起来像是一个普通仓库名但它的核心思想是把 Agent 的能力组织方式从“临时拼装”升级成“标准化资产管理”。技能和插件、工具函数的关系也值得说清楚。插件通常指一套完整的扩展包可能包含 UI、存储、网络等能力工具函数是最底层的代码实现技能则是介于两者之间的抽象层。技能可以封装一个或多个工具函数并且附带了“模型应该如何理解和使用这个能力”的说明。打个比方工具函数是零件技能是装配好的模块Agent 是工人它只需要看模块上的说明书就能决定用哪个、怎么用。这套抽象带来的直接收益有三个。第一技能可以被复用一个项目里调试好的技能换个场景稍微改一改描述就能迁移第二技能可以被测试每次改动都能用固定的用例集去回归第三技能可以被观测调用记录、成功失败率、耗时都能统一采集。这些都是散装工具做不到的。1.1 一个最小可运行的技能样例先给一个最简化的技能定义方便建立直观印象。技能通常包含两部分一份 YAML 或 JSON 的描述文件加上一个可执行脚本。name: order_query description: 根据用户提供的订单号查询订单状态、物流信息和商品清单。当用户询问“我的订单到哪里了”“订单发货了吗”时使用。 version: 1.2.0 author: agent-platform inputs: order_id: type: string description: 订单号通常是长度 16-20 位的数字或字母组合 required: true output: type: object schema: status: string logistics: array items: array allowed_calls: - database:orders - http:logistics_api timeout: 15s对应的执行脚本只需要遵循一个约定从 stdin 读取 JSON 入参处理完向 stdout 输出 JSON 结果错误时写 stderr 并返回非零退出码。#!/usr/bin/env python3 import json, sys def main(): payload json.load(sys.stdin) order_id payload[order_id] # 这里内部查询数据库和物流 API result query_order(order_id) print(json.dumps(result, ensure_asciiFalse)) if __name__ __main__: main()可能有人会问为什么不直接让模型调用 Python 函数原因在于隔离。Agent 进程和技能进程分离之后技能崩溃不会拖垮主进程技能升级可以热切换而且还能对技能做权限限制——比如某个技能只能访问只读数据库不能执行写操作。这些在单体函数调用里做起来很麻烦在技能框架里是默认能力。2. 目录结构与元信息约定我在第一版里踩过的坑技能库的设计第一关就是目录结构。我先讲一下自己第一版有多天真当时的做法是把所有技能脚本平铺在一个skills/目录下每个技能一个文件夹里面放run.py和README.md。刚开始只有五六个技能时确实很清爽但等技能数量到了二十多个问题集中爆发。第一个问题是命名冲突。两个技能一个叫report_generator一个叫report_builder模型在选择时根本分不清谁是谁。第二个问题是归属混乱有些技能明显属于数据分析领域有些属于客户服务领域但目录上完全看不出来。第三个问题是依赖管理每个脚本用自己的requirements.txt但整个仓库没有一个统一的环境声明导致换一台机器跑一半技能直接 import 失败。后来我参考了不少开源仓库的布局方式结合自己的需求最终固定成下面这套结构agent-skills/ ├── manifest.yaml ├── skills/ │ ├── order/ │ │ ├── order_query/ │ │ │ ├── skill.yaml │ │ │ ├── run.py │ │ │ ├── tests/ │ │ │ │ ├── cases.json │ │ │ │ └── expected_outputs.json │ │ │ └── dependencies.txt │ │ └── order_cancel/ │ │ ├── skill.yaml │ │ ├── run.py │ │ └── ... │ ├── customer/ │ │ ├── complaint_create/ │ │ └── satisfaction_survey/ │ └── data_analysis/ │ ├── trend_report/ │ └── anomaly_detect/ ├── runners/ │ ├── docker/ │ └── local/ ├── registry/ │ ├── index.json │ └── validator.py └── scripts/ ├── build_index.py └── run_tests.py这套结构里有几个关键决定。第一技能按业务领域分组而不是按技术类型分组。因为模型在理解任务时会先判断“用户要做什么”再决定“用什么工具”业务领域分组和模型的任务分类天然对得上。第二每个技能拥有独立的测试用例目录这一点极其重要。技能是给模型用的模型的调用方式千奇百怪没有固定用例做回归任何一次描述修改都可能引入隐性问题。第三最外层有manifest.yaml统一声明整个技能库的版本、默认运行时、全局依赖避免散落管理导致的环境分裂。2.1 技能元信息字段的设计逻辑技能描述文件是模型理解技能的唯一入口所以字段设计直接决定了模型会不会用、用得好不好。我从多次试错中总结出这几个必填字段。name是技能的唯一标识建议采用“领域_动作”格式比如order_query、ticket_create、data_trend防止出现两个语义模糊的名字。description是最核心的字段它决定了模型何时选择这个技能。描述里要写清楚三件事这个技能做什么、什么情况下用、什么情况下不要用。很多人只写前两件忽略负面触发条件结果模型在用户说“我要退货”时调用了订单查询技能。inputs字段要严格定义参数的 type、required、description最好还要有示例值。有经验的调用方会在描述里写明常见格式比如“订单号通常是 16 位数字如果用户只提供了 8 位需要先让用户补充完整才能调用”。这一条看似是给参数说明实际是在引导模型做好前置澄清能大幅减少因为参数缺失导致的调用失败。allowed_calls是我后来加的字段用来声明这个技能内部会访问哪些资源。它有两个作用一是安全审计时可以快速看到每个技能的权限范围二是在部署时可以作为生成防火墙规则和数据库账号权限的依据。虽然现在很多项目还没做到这个粒度但我强烈建议从一开始就预留这个字段否则后续要做权限收敛时需要逐个翻代码成本极高。timeout不能省略。模型对耗时不敏感但用户对耗时非常敏感。给每个技能设定合理的超时时间并且在超时后给出统一的错误提示可以让 Agent 在技能卡死时迅速回退到其他路径而不是一直等下去。2.2 版本与依赖锁定的教训版本管理这一块我踩过两次比较疼的坑。第一次是因为技能升级没有做版本区分模型同时看到了新旧两个版本的描述导致行为飘忽不定。第二次是因为依赖没锁死技能运行所在的容器重建后某个底层库从 1.x 升到了 2.x一个技能在本地测试全绿部署到线上直接报错。现在我的做法是每个技能的skill.yaml里除了版本号还要写明兼容的最低技能框架版本同时dependencies.txt里的每个依赖都固定到精确版本不允许使用或*。在 CI 里加一道校验凡是依赖不锁精确版本的技能一律拒绝合并。这个规则执行起来有阻力因为开发总想省事但你会发现技能库维护到后期最大的成本是回归测试和排查环境差异而不是写那几行。3. 注册-调用-反馈闭环让 Agent 真正学会“使用”技能有了目录和描述文件下一步就是把技能变成 Agent 可以动态发现和调用的服务。如果只是把描述拼进 system prompt那还不能叫技能库只能叫 prompt 模板。技能库的第三个核心是“动态注册-调用-反馈”的闭环。3.1 注册与发现机制在系统启动时技能库扫描器会读取所有skill.yaml解析出每个技能的 name、description、input schema、output schema然后写入一个统一的索引文件registry/index.json。这个索引文件就是 Agent 的“技能列表视图”。这里有个重要的点索引文件不应该在每次请求时实时生成那样太慢而应该在启动时生成一次并随技能版本变更而重新构建。我们用一个很简单的脚本scripts/build_index.py来维护它还会做几件额外的事检查技能描述文件是否合法、检查是否有重复的 name、检查必填字段是否齐全。相当于在注册阶段就把脏数据拦下来而不是让模型在运行时面对一个残缺的技能列表。import yaml, json, glob skills [] for path in glob.glob(skills/**/skill.yaml, recursiveTrue): with open(path, r, encodingutf-8) as f: skill yaml.safe_load(f) # 合法性校验字段缺失直接抛错 assert skill.get(name), f{path} missing name assert skill.get(description), f{path} missing description skills.append(skill) with open(registry/index.json, w, encodingutf-8) as f: json.dump(skills, f, ensure_asciiFalse, indent2)这个索引文件在每次 Agent 启动时会被加载作为上下文的一部分。如果技能数量较多还可以把 description 用某种 embedding 预先向量化在运行时只筛选和用户问题相关的 Top K 个技能描述。不过我这个项目目前还在用“按领域先粗筛 完整描述全给”的方案技能数在 30 个左右时效果尚可。3.2 调用协议输入、输出与退出码技能的调用协议一定要简单且严格。我们的统一规则是外部调用方把参数 JSON 通过 stdin 传给技能进程技能处理完后通过 stdout 返回 JSON错误信息走 stderr退出码 0 表示成功非 0 表示失败。这条规则的好处是语言无关。不管技能是用 Python 写的还是 Node.js甚至是 shell 脚本只要遵守 stdin/stdout 约定就能被同一个调度器管理。调度器负责设置超时、捕获输出、记录日志、统计耗时技能本身不关心这些横切逻辑。这里有一个容易被忽视的细节技能返回的 JSON 结构必须是固定的外层框架。比如统一用{success: true, data: {...}}或{success: false, error: {code: ..., message: ...}}。不要允许技能自由发挥外层结构否则 Agent 需要去猜测返回结果里到底哪个字段才是真正的内容非常容易出错。我一开始没强制这个规范导致有的技能返回数组有的返回对象无谓地消耗了很多上下文和调试时间。3.3 反馈与错误恢复不要把技能当黑盒技能调用会失败这是常态关键是失败之后的反馈路径要设计好。当技能返回失败时调度器不应该直接把原始错误信息扔给 Agent而应该先把错误归类。常见错误类型我分成三类参数错误、业务规则错误、系统异常。参数错误通常是模型给的参数值不符合 schema比如订单号短了、日期格式错了这类错误可以通过解析校验信息直接反馈给模型让它补充或修正参数后重试。业务规则错误是参数没问题但业务上不允许比如订单已取消、商品已下架这类错误不应该让模型盲目重试而应该让它基于错误内容直接回应用户。系统异常是代码 bug、网络超时、依赖崩溃这类错误要把脱敏后的堆栈信息记录下来同时对模型输出一个标准提示“系统暂时无法完成此操作”。为什么要把错误分类因为原始的报错信息往往有敏感内容数据库连接串、文件路径、内部 IP直接传给模型再生成回复给用户存在泄露风险另外原始报错信息过于技术化模型理解后会生成一些让用户困惑的回答。分类之后再反馈既安全又清晰。3.4 技能选择与记忆让 Agent 学会取舍最后是技能选择这一环。很多模型的 system prompt 里塞了几十个技能描述后注意力会被稀释主要表现为该用 A 技能时选了 B 技能或者干脆不调用任何技能直接用已有知识胡编答案。我尝试过几种缓解办法其中最有效的是“技能选择前置过滤”。在把技能描述送入大模型之前先用一个轻量级的意图分类器对用户问题做粗筛只把与该意图相关的技能描述展示给模型。这个意图分类器不需要很重甚至可以用规则关键词完成因为它的目标是缩小候选集而不是做最终决策。粗筛之后候选技能通常从 30 个降到 5 个以内模型的调用准确率会明显提升。另外技能库里应该支持让 Agent 记录“什么场景下用了什么技能、结果如何”。这个记录不一定要长期保存但至少在单轮多轮会话里要有。比如用户先问“我的订单到哪里了”Agent 调用了order_query用户接着问“那这个订单还能改地址吗”模型看到上文已经查到了订单号就应该直接使用这个订单号调用order_modify的预检查接口而不是反问用户要订单号。这种上下文衔接能力依赖技能调用历史的回传必须有意识地在交互协议里设计否则模型就会变成“每次都要用户重复一遍的笨蛋助手”。4. 可靠性工程测试、评估、灰度上线技能库要能长期演进不能只靠“在本地跑了一次好像没问题”来保证质量。这一章分享我在测试、评估和上线环节的具体做法。4.1 离线测试用固定用例守住行为边界每个技能在tests/目录下都有一套离线用例格式是输入 JSON 和期望输出 JSON 的配对。测试脚本会遍历所有用例执行技能进程比对输出结构、关键字段和退出码。[ { input: {order_id: 20240601001}, expected: { success: true, data: { status: shipped, logistics: [已发货, 运输中], items: [手机, 保护壳] } } }, { input: {order_id: 123}, expected: { success: false, error: {code: INVALID_ORDER_ID, message: 订单号长度不足} } } ]用例的设计要多考虑模型的调用习惯。我发现模型调用技能时经常不按常理出牌比如日期参数给定成“2024年6月1日”而不是标准化的2024-06-01再比如明明 schema 里标了order_id是 string模型还是会传出 int。所以在离线测试里要专门加入“输入格式容错”的用例让技能在收到非标准输入时也能给出可处理的反馈而不是直接抛异常。4.2 在线评估模拟用户任务而不是模拟用户对话离线测试之外我还会跑一组“在线评估”任务。这组任务不在单元测试里做因为需要真实的大模型参与。做法是准备 30 个模拟用户任务比如“用户想查询订单 20240601001 的物流信息”“用户想取消订单 20240601002”“用户想知道最近 7 天的销量趋势”每个任务都有期望的技能调用序列和最终回复要点。跑评估时Agent 使用当前技能库的索引完整地执行任务然后自动检查三件事第一是否在合适的时机调用了正确的技能第二技能调用的入参是否完整合理第三最终生成的回复是否包含期望的信息点。这个评估体系的价值在于它能捕捉到“技能描述写得不好”导致的隐性失败。比如某个技能在离线测试里逻辑全对但描述写得含糊模型在真实任务里根本不调用它离线测试完全发现不了在线评估一跑就露馅。4.3 灰度与回滚技能变更也能走发布流程技能库也是一个软件系统所以也要有灰度发布和回滚机制。我们的做法比较朴素索引文件打版本 tag旧版本索引保留备份。发布新技能或修改描述后先在线上环境让 10% 的流量使用新索引其余 90% 用旧索引。对比两组流量的技能调用成功率、任务完成率和人工反馈确认没问题再全量切换。回滚其实更容易被忽略。有一次某个技能的新版本把所有订单查询都引到了一个新的只读副本结果那个副本的数据延迟了 10 分钟用户看到的订单状态明显不准。好在我们保留了旧索引一条命令就切换回去了。所以这里给的建议很直接对技能库做任何改动都要能在一分钟内恢复原状这是底线。4.4 技能规模增长带来的性能与维护问题技能数量超过 30 个之后每次构建索引、加载描述、跑全量回归的时间都会明显上升。我在实践里做了三件事来缓解。一是给技能描述做“两级展示”。索引文件里只保留一句话摘要详细信息在模型决定使用某个技能时再按 name 拉取完整描述。这样 system prompt 不会因为技能过多而膨胀。二是给技能分域构建索引按领域分别生成customer.json、order.json、data_analysis.json运行时只加载和用户问题最相关的那个域。三是全量回归测试改成“改动技能 关联技能”的范围回归而不是每次都跑全部用例节省时间的同时还能更快获得反馈。维护方面技能库一定要定期做“废弃清理”。有些技能当时为某个临时需求创建需求结束后还留在索引里模型时不时错误地调用它产生奇怪的回答。给每个技能加一个deprecated标记并在描述里明确写“该技能已废弃不要在任何场景使用”比直接删除要安全因为有时候旧会话还依赖这个技能名。5. 从稳定到异常五个值得警惕的失败模式技能库跑起来之后真正的挑战是各种边界情况。我这里列出几个在实践里反复出现的问题以及对应的规避方案你可以直接把这几条当成自检清单来用。5.1 描述越具体模型反而越不敢调用这可能有点反直觉。我一开始以为描述写得越详细模型越容易理解。后来发现不少技能因为描述里写了“仅当……且……才使用”模型在模糊场景下就会犹豫干脆不调用了。问题的根源在于模型对否定条件往往过于保守。好的描述策略是先写正面的使用场景把最典型的用户问法放在最前面负面触发条件放在后面并且用“优先使用”这样的词来明确鼓励模型。比如description: 当用户询问订单状态、物流信息、收货时间时优先使用。可接收 16-20 位订单号。如果用户没有提供订单号应该先向用户索要如果用户只是在咨询退货政策则不要使用本技能。把“索要订单号”写进描述而不是靠模型临场发挥效果会好很多。5.2 技能循环调用另一个经典问题模型调用 A 技能失败根据错误码自动修改参数重试重试再失败再重试形成循环。如果技能里有副作用比如创建工单、发送邮件循环会造成严重后果。我建议做两层防护。第一层是调度器硬性限制同一技能在同一轮任务中最多执行 3 次超过次数直接返回“多次尝试后仍然失败建议人工介入”。第二层是在技能描述里写明某些操作的“单次性”——比如“本技能一旦调用就会创建真实的工单请务必确认所有参数准确后再调用”。模型读到这种提示后会在入参不完整时主动向用户确认而不是蒙着头调用。5.3 上下文被技能返回结果塞爆技能返回大量数据时Agent 的上下文窗口会被快速消耗。比如查询一个商品的详情数据库里可能包含几十个字段、几百条日志把全部内容返回给模型既浪费又容易混淆模型判断。解决办法是在技能内部做字段裁剪。技能输出给模型的内容和技能实际存储的内容可以有各自的 schema。比如查询订单时给模型展示的字段只有 status、logistics_summary、item_names、estimate_delivery_time而完整日志保存在本地存储用户如果追问“为什么物流卡住了”再触发一个order_logistics_detail技能去拉详情。给每个技能的 output schema 设置max_tokens上限也是一个有效手段。如果输出超过上限截断后添加“已省略部分内容”的提示模型通常能理解还有更多数据可查。5.4 多技能并行调用的冲突有些场景需要同时调用多个技能比如用户问“对比这周和上周的销量并且告诉我哪些商品需要补货”模型可能会同时触发trend_report和inventory_check。这两个技能如果内部都写同一个临时文件、或者并发更新同一个缓存就可能互相踩数据。做并行执行前一定要确认技能有“只读优先”的设计原则。默认情况下一个技能除非明确声明会写外部状态否则应当保持纯计算和只读访问。对于有写操作的技能要给调度器声明写锁让这类技能在串行队列里执行避免和其他技能并发。5.5 外部 API 的响应结构变化技能对接的外部 API 一旦响应结构变化技能会静默失败或返回一些奇怪的字段。比如外部物流接口原来返回tracking_list某天改了字段名traces技能代码没同步更新模型拿到的结果就是一个空数组。应对方案是给技能内部的 API 响应加一层“结构校验”校验不通过时返回一个明确的UPSTREAM_CHANGED错误码而不是把解析失败后的空数据当成正常输出。这个错误码一旦出现监控系统就要报警提醒维护者及时排查上游接口变更。很多技能问题都是这样静默发生的等用户发现时已经产生了一堆错误回复。6. 技能库的部署形态与运行环境最后聊一下技能库实际跑起来的环境选项。这里不追求大而全只说我用过的、并且觉得靠谱的基础配置。6.1 运行沙箱本地进程还是容器最简单的方案是让技能在 Agent 同一进程内作为子进程运行。好处是延迟低调度逻辑简单坏处是技能代码里的隐患可能影响 Agent 主进程所在机器的稳定性。稍微安全一点的方案是容器化每个技能或每个技能组运行在一个独立的容器里通过标准 I/O 传递数据。我的实践是把技能分为两个安全等级。只读且无外部副作用的技能知识库检索、数据分析、日志查询可以在本地子进程运行延迟低。有写操作或需要访问敏感系统的技能订单取消、邮件发送、支付操作必须容器化运行并且赋予最小化的网络权限。6.2 目录挂载与依赖管理技能容器或者本地子进程最好工作在一个固定的工作目录里。对于有依赖的技能启动时通过依赖清单构建镜像或虚拟环境。我的做法是写了一个极简的runners/local/run_skill.py它负责从 manifest 读取依赖信息自动把当前目录切到技能目录再调用对应的 run 脚本。这里有一个实操建议:在技能开发阶段不要直接修改共享环境的依赖而是为每个技能建一个虚拟环境。否则 A 技能升级了某个库B 技能可能被牵连挂掉排查非常痛苦。虚拟环境方案虽然占用磁盘多一点但隔离性好值得付出这一点代价。6.3 日志与可观测性技能库的可观测性比普通后端服务更难做因为需要把模型决策和技能调用关联起来。我的日志方案包含三级第一级是调度日志记录每次技能调用的时间、入参摘要、出参摘要、耗时、错误码第二级是模型决策日志记录模型选择了哪个技能、为什么选择如果模型能输出思考过程的话第三级是业务结果日志记录用户对最终回复的反馈比如“有帮助/没有帮助”。串联三者的关键是 request id。一场会话的所有技能调用和模型决策都带上同一个 request id之后任何一次异常回查都能从日志里还原完整的链路。这对分析“为什么这个用户问题被错误地路由到了订单取消技能”非常有帮助。6.4 技能库的后续演进方向目前这个 agent-skills 项目对我来说已经从“给 Agent 加能力”进化成了一个“Agent 外设能力的运维平台”。后面我计划再做两件事一是把技能描述和元信息也纳入版本差异对比这样每次改描述后能清楚地看到变了什么二是尝试让技能库有简单的自学习能力比如记录哪些技能被高频调用但在线上评估中表现较差自动生成待排查清单。如果你也在维护类似的 agent-skills 仓库我的建议是不要一上来就追求高深的设计先把目录、注册、调用、测试这四件事做扎实。等到一切稳定了你会发现技能库本身比 Agent 的 prompt 更值得花时间打磨——毕竟它承载的就是 Agent 真正可以去行动的那部分能力。