
如果你和我一样把智能体的代码从原型一路堆到可以上线大概率会在某个版本迭代的深夜开始怀疑人生功能越加越多每个任务都要写一遍工具调用逻辑参数格式各写各的甚至同一个技能在三个文件里有三套近似但不完全一样的实现。我在这个阶段重新审视了一遍代码结构最后把所有能力封装成了统一协议项目名就叫agent-skills。它不是一个花哨的大框架而是一套让能力变成可插拔积木的规范配合调度引擎和沙箱边界解决智能体技能复用、组合和管控的问题。这篇文章写给正在做 Agent、被技能管理缠住手脚的朋友尤其适合那种“技能已经超过十个、每次加需求都要动核心代码”的阶段。1. 为什么会冒出agent-skills这个项目——从混乱的智能体代码说起1.1 尝试挂接十几个工具之后的“技术债爆发”最开始我做智能体只是为了实现一个问答机器人当时只有三个技能查天气、查订单、算运费。代码结构很简单一个工具函数对应一个接口然后在系统提示词里把所有工具描述手写进去。Agent 要调用哪个函数完全依赖它在对话里自己选择我只需要把每个函数的 JSON Schema 传给模型。到第八个技能的时候问题开始出现了有人写了get_user_info有人写了fetchUserInfo返回字段一会儿叫nickname一会儿叫name。最难受的是下游业务需要同时查库存、优惠券和物流状态我不得不在智能体外层写一段两百行的胶水代码把这三个工具的返回值拼成一段适合模型阅读的文本。每加一个新技能这段胶水代码就要跟着改一遍测试用例也越来越复杂。真正让我决定重构的是一次促销活动运营希望 Agent 能在用户询问“快不发货”时自动判断订单状态、从客服知识库找模板、再生成一条安抚消息。这需要在一次对话里串联三个技能还要处理“订单已发货”和“订单未发货”两种不同的回复分支。我在现有架构里硬写越写越觉得不对——这种组合逻辑放在业务代码里本质上等于把 Agent 的“思考”做成了 if-else根本没有发挥模型自己的判断能力。那段时间我意识到所谓“Agent 能力”一定得有一个统一的载体让每个技能都能回答几个固定问题你叫什么你接受什么参数你返回什么你需要在什么环境里运行你能不能访问网络而不是各写各的靠程序员记住每个函数的差异。1.2 项目边界一套协议而不是一个框架agent-skills从一开始就定了一个原则不重写智能体框架不接管模型对话只做能力封装和调度。它的定位很像电脑上的 USB 接口设备技能只要按照协议实现就能插到主机Agent上被识别和使用而主机不需要关心设备内部的具体电路。我见过很多团队一上来就自己写一套 Agent 编排平台最后演变成了一个绑定业务的重型框架升级模型要改框架换场景要改框架。agent-skills刻意把范围收窄它只负责三件事定义技能协议每个技能用一份独立配置描述自己“是什么、怎么调用、需要什么权限”。提供调度引擎根据用户问题和上下文从技能库里挑选合适的技能并负责参数校验、执行、结果整理。构建运行沙箱让技能在受控环境里执行避免一个失控技能拖垮整个智能体进程。业务代码还是你自己的数据库连接、第三方 API 调用都留在技能实现里协议层不关心这些。这样带来的好处是你可以先跑通一个小场景再加技能再换不同的大模型评估效果不会出现“牵一发动全身”的架构风险。我用这张表提醒自己和团队成员遇到“要不要把 X 加进框架”的问题时先判断边界agent-skills 负责业务团队负责技能描述、参数 Schema、输出解析技能内部的实际业务逻辑技能召回与编排业务规则、知识库内容沙箱、权限、审计权限审批、数据合规技能版本、依赖声明具体模型适配、Prompt 调优2. skill协议把能力封装成一张干净的身份证2.1 一份技能至少要回答的五个问题在设计协议时我参考了函数签名、HTTP API 和 JSON Schema 的经验最终抽象出五个问题每一份技能配置都必须能回答这个技能是干什么的——一段简洁描述给模型选技能时阅读。输入长什么样——完整的 JSON Schema包含必填字段、类型、约束。输出长什么样——同样用 JSON Schema 描述方便下游自动解析。怎么执行你——入口函数、运行文件、执行环境。你能做什么/不能做什么——权限声明例如只读、可写、需要外网。这五点是底线。看起来简单但大部分智能体项目的问题恰恰出在“没说清楚”上模型不确定该不该调用某个技能调用前参数要靠猜调用后返回的结果又要人肉解析。协议把这些问题前置到开发阶段反而省了大量调试时间。在团队协作里这五问还有一个好处非技术同事也能参与维护描述信息。业务运营可以修改“技能描述”让它更符合用户表达习惯而不用碰代码。我后来把技能索引文档直接从这些配置生成新人上手速度明显变快。2.2 用YAML声明能力用Python实现逻辑一个典型的技能由两部分组成声明文件和实现脚本。下面是我项目里一个查询订单技能的真实简化版。技能声明query_order.yamlname: query_order description: 根据订单号查询订单状态、物流公司和最新物流节点适合回答“我的订单到哪了”。 input_schema: type: object properties: order_id: type: string description: 订单完整编号例如 SO20240001 required: - order_id output_schema: type: object properties: order_status: type: string enum: [pending, shipped, completed, cancelled] logistics_company: type: string latest_trace: type: string required: - order_status execute: file: skills/query_order.py function: main runtime: process permissions: network: true filesystem: - tmp read_only: true对应的实现脚本skills/query_order.pyfrom typing import Any, Dict def main(args: Dict[str, Any]) - Dict[str, Any]: order_id args[order_id] # 实际项目中这里会调用订单服务我只保留核心逻辑 order fetch_order(order_id) if order is None: return {order_status: not_found, error: 订单号不存在} return { order_status: order[status], logistics_company: order[logistics_company], latest_trace: order[latest_trace], }你可能看出来了这个协议的核心思想是“把函数签名提升为显式配置”。我原先也想过直接用 Python 装饰器做比如skill(query_order)但后来发现 YAML 更合适一方面可以脱离代码独立看能力清单另一方面方便做前后端分离——只要协议不变技能实现换语言也能接进来。2.3 JSON Schema 在这里的价值很多人会问为什么非要写两套 Schema直接把 Python 参数注释写好不就行了吗我的回答是JSON Schema 在agent-skills里不只是校验工具它还是模型与技能之间的“翻译层”。大模型在收到用户问题后需要决定调用哪个技能、填哪些参数。模型本身不能直接读 Python 函数签名它更适合读结构化的 JSON Schema。我们在调度引擎里把技能描述列表传给模型时模型会一眼看到input_schema然后生成一段符合 Schema 的参数 JSON。这个过程避免了“模型生成字符串参数技能侧没法解析”的经典事故。我在项目里用jsonschema库做两处校验模型传入的参数在执行技能前先校验一次类型不对就直接返回错误不让脏数据进业务函数。技能返回的数据在交给模型前也校验一次防止实现脚本返回了缺失字段导致模型在后续决策时犯迷糊。有一次线上案例让我印象深刻某个技能返回物流时间时字段名写错了应该是estimated_delivery写成了delivery_time。如果没有输出校验模型可能“硬着头皮”把这个字段当作预计送达时间展示给用户甚至产生幻觉。有了 Schema 校验这个错误能在技能运行时被立刻捕捉到并记进错误日志。3. skill调度引擎让Agent从“会点技能”到“会安排技能”3.1 先规划再执行Planner的选单逻辑当技能数量到十几个时把所有技能描述都塞进模型上下文是不现实的既浪费 token又会让模型在判断时出现“选择困难”。我一开始确实试过把所有描述一次性传给模型结果召回率不高模型经常忽略一些不那么常用但恰好适配的技能。agent-skills的调度引擎采用“两步式”结构先召回再让模型选择。召回阶段我会把每个技能的description字段转成向量存到一个轻量级向量库里。当新用户问题进来时先把问题也转成向量用余弦相似度找到 TopK 个技能。这个“粗筛”阶段完全不需要模型参与成本极低耗时基本可忽略。召回之后才把 TopK 个技能的description和input_schema拼装成一份候选工具清单交给模型做最终选择。这样做既能减少上下文噪音又能保证模型的每一次工具选择都有足够的依据。一个简单的召回伪代码from agent_skills import SkillRegistry from agent_skills.embedding import embed_text def plan_skills(user_query: str, registry: SkillRegistry, top_k: int 5): query_vec embed_text(user_query) candidates registry.search(query_vec, top_ktop_k) return [skill.short_description() for skill in candidates]3.2 技能组合的三种常见模式串联、并联、跳转调度引擎要解决的不只是“选哪个技能”还包括“多个技能怎么配合”。我在项目里整理了三种最常用的组合模式它们覆盖了绝大部分业务场景。串联模式前一个技能的输出作为后一个技能的输入。典型场景是“查余额——算优惠——生成支付链接”每一步都依赖上一步的结果。在agent-skills中串联通过把上一个技能输出的关键字段填充到下一个技能的input_schema实现模型会基于之前的执行结果决定后续动作。并联模式多个只读技能同时执行互不依赖。比如用户咨询“我想买投影仪顺便看看有没有配套的幕布”Agent 可能同时去查商品库和库存表然后把结果合并回复。并联能显著降低多轮对话延迟但要注意各技能返回结果都必须在 Schema 内否则合并阶段会乱。条件跳转模式根据当前执行结果决定走哪条分支。比如查询物流时如果order_status是shipped就走“展示物流轨迹”分支如果是completed就走“展示确认收货提示”分支。这个分支逻辑不适合写死在代码里我选择让模型根据结构化输出自行判断通过给模型一段“分支说明”来实现。下面是我在系统提示词里常用的一段说明帮助模型理解组合方式你可以使用以下技能完成用户请求query_order、check_coupon、calc_discount。 如果用户只问订单状态调用 query_order 即可。 如果用户希望知道订单优惠明细先调用 query_order 获得订单金额再调用 check_coupon 和 calc_discount。 每一步调用后请检查返回结果是否包含必要字段再决定下一步动作。3.3 上下文窗口不足时的技能裁剪策略调度引擎还要处理一个很现实的问题即使只召回 TopK 技能每个技能带有完整input_schema时描述文本依然可能把上下文塞爆。尤其当某些技能字段特别多一个 Schema 就占一百多行。我的处理方式是把技能描述分成“完整版”和“简版”两套。召回阶段喂给模型的默认是简版——只保留name、一句话description和必要的required字段只有当模型真正决定调用某个技能时调度引擎才临时把完整 Schema 注入进去。这样选技能阶段的 token 开销被压缩了至少一半。如果上下文还是紧张我还会在召回数量上做文章。我实测过把 TopK 从 8 缩到 5在多数场景下不会降低任务成功率反而降低了模型误选率。真正需要的不是给模型看更多技能而是确保被选中的描述足够精准。4. 沙箱隔离与权限校验技能增多后绕不开的安全底线4.1 把技能当不可信代码处理很多开发者的第一反应是“技能都是我们自己写的为什么要沙箱”这个想法在早期还能成立但技能一多参与的人变多甚至开始有第三方技能接入时就必须改变心态。更关键的是技能的执行参数来自大模型生成而大模型生成的内容又可能被用户提示词污染。举个简单例子一个“删除文件”的技能如果没有做权限校验用户可以在对话里尝试让 Agent 把file_path参数设成一个系统路径结果技能脚本真的去调用了删除接口。这不一定是有意的攻击哪怕只是用户随口说“帮我把日志清了”也可能造成误操作。所以在agent-skills里我把“权限声明”当作协议的一部分。每个技能执行之前调度引擎会先检查它的permissions字段判断当前环境是否允许它访问网络、文件系统、环境变量等敏感资源。没有声明权限的技能默认只能拿到最小执行环境。4.2 进程级隔离、文件白名单、网络策略沙箱设计我并没有直接上一个完整的容器方案而是做分层处理。第一层是进程隔离通过multiprocessing或subprocess把技能放到独立进程里执行。这样即使技能内部抛出异常、占用内存也不会影响主进程稳定。第二层是文件系统限制默认技能只能读写系统分配的临时目录任何试图访问项目目录或系统目录的操作都会被拦截。第三层是网络策略。技能不是每个都需要联网比如纯计算类技能就完全没必要暴露外网访问。我在技能声明里增加network字段只有显式设为true的技能才能发起 HTTP 请求。对需要网络访问的技能我会进一步用代理或防火墙规则限制它只允许访问特定域名。一个沙箱策略示例sandbox: cgroup_memory_limit: 256M cpu_limit: 1.0 filesystem: readwrite_dirs: - /tmp/agent_skills readonly_dirs: - /usr/share/zoneinfo network: allowed_domains: - api.example.com dns: false env: - AGENT_SKILLS_RUNNINGtrue这里我想强调一个细节依赖安装也要锁版本。技能运行在隔离进程里不代表它装的第三方库可以随意升级。项目里我会对每个技能维护一份requirements.lock保证同一个技能在不同时间执行的结果一致。否则今天跑得好好的明天某个依赖发了一个小版本返回值格式变了Agent 可能直接“看不懂”结果。4.3 链路追踪与审计日志沙箱解决了“能不能做”的问题审计日志解决的是“出了问题怎么查”的问题。我在每次调用开始时生成一个trace_id贯穿技能执行全过程。日志里除了记录 Python 的 print 输出还会记录哪个 Agent 进程发起了调用当前用户会话 ID模型生成入参的原始文本校验后的参数 JSON执行结果摘要敏感字段自动脱敏耗时和错误码。我设计了一个简单的审计日志表线上排查问题时会直接查它字段示例说明trace_idacc_8f3a2c全局链路 IDskill_namequery_order技能名input_hash7d2e1a入参哈希便于去重定位statussuccess成功/失败/超时duration_ms120执行耗时permission_usedread_only实际权限范围有一次技能返回异常数据我通过trace_id完整还原了那次调用的参数、返回内容和模型后续反应五分钟就定位到是上游接口改了字段名。如果没有这套日志排查这类问题可能要花半小时以上。5. 在真实业务里跑通的接入效果与参数调优5.1 一次数据报表任务的实测过程接入agent-skills之后我拿团队内部一个高频场景做了验证运营每天问“今天的销售概况怎么样”原来需要人工写 SQL、跑脚本、再粘贴到群里。我们把这个流程拆成了四个技能查询数据库、计算汇总指标、生成图表、发送到即时通讯。调度引擎的完整过程是这样的用户问“今天销售概况按品类看一下发群里”。召回阶段匹配到query_sales_data、compute_summary、render_chart、send_im_message四个技能。模型先生成第一步调用query_sales_data参数是“今日”和“品类维度”。执行后结果返回到模型模型再调用compute_summary计算同比、环比。接着调用render_chart生成一张柱状图最后send_im_message把文字摘要和图片发送到指定群里。整个链路跑完大约 6.4 秒。裸调函数方式其实只需要 2 秒左右但多出来的时间主要是模型多轮选择与生成换来的是运营不再需要人工凑数据。我更关心的是任务成功率从原来的 65% 提升到了 91%关键是失败场景变得可解释——要么是数据库字段不存在要么是技能描述写得不够清楚。5.2 模型选型对技能调用的影响agent-skills的调度引擎本身不绑定模型但我必须提醒你模型的能力会直接影响整套系统的上限。我对比过几个主流模型在同样技能集上的表现模型技能选择准确率参数格式合规率适用场景模型 A82%95%简单工具调用延迟低模型 B93%98%复杂多步编排成本略高模型 C88%90%中文描述理解较好一个实际结论是如果你的技能数量少、参数简单选一个速度快、成本低的模型就够了如果你要处理串联跳转这种复杂编排就要选工具调用更强的模型。agent-skills允许按技能类别配置模型比如简单查询用快模型综合汇报用强模型这样能在成本和效果之间取得平衡。5.3 延迟、并发、缓存这些需要提前量化技能调度不是免费的。我在集成后专门做了一次性能剖析发现额外开销主要来自三部分向量召回约 15ms、参数校验约 5ms、沙箱进程启动约 60ms。总共约 80ms 左右这个数字在绝大多数业务里都可以接受但如果你的技能被高频调用到每秒几十次进程启动的开销就不能忽略了。我的优化方式有三个使用进程常驻池让技能执行进程预启动把“冷启动”变为“热调用”。对高频只读技能加结果缓存比如用户查同一个订单状态时1 分钟内直接返回缓存。把向量召回和技能描述索引加载放到独立服务避免每次请求都重复加载模型权重。另外召回数量不要贪多。我在项目里的经验是 TopK 设置在 5 到 8 个比较合适如果超过 8 个模型反而会被干扰。技能库规模到 50 以上时召回质量比召回数量更关键与其堆更多描述不如花时间优化每个技能的描述文案。6. 工程化经验技能开发者的调试工具链与版本治理6.1 离线单测在没有模型的情况下验证技能技能作为独立协议模块最大的好处是可以不启动完整智能体就单独测试。我提供了一个命令行工具开发者可以直接指定技能名和输入参数然后看到完整的执行结果、日志和 Schema 校验报告。agent-skills test skill query_order --input {order_id: SO20240001}这条命令会输出{ skill: query_order, status: success, output: { order_status: shipped, logistics_company: 顺丰, latest_trace: 快件已到达【杭州转运中心】 }, validation_passed: true }我强烈建议所有技能作者在写实现时先跑通这条离线测试再接入调度引擎。很多“Agent 调用失败”的问题追根究底是技能本身对异常参数处理不够好。你不需要把每个边界都测到但至少要把 Schema 中声明的必填字段和枚举值测一遍。6.2 技能版本的语义化管理与权限升级技能上线后一定会持续迭代比如某个查询接口从 v1 切到 v2字段名变了。如果不管理版本Agent 可能仍在调用旧版本的技能描述导致频繁失败。agent-skills给每个技能加了一个version字段并采用语义化版本规则主版本号变更破坏性变更旧调用必须跟着改。次版本号变更新增功能但保持兼容。补丁版本号变更Bug 修复不影响外部行为。调度引擎在选择技能时会默认选取满足版本范围的最新版本。在涉及权限变更时我要求必须人工审核比如某个技能从read_only变成可写需要管理员在配置中心里显式审批不允许通过一次普通 PR 直接变更。技能之间的依赖也做了声明。比如语音转文字技能依赖一个音频处理库音频处理库需要单独安装如果你在部署时漏了这一步技能运行时才会报错体验很差。我在技能目录里维护了一份dependencies.yaml标明每个技能需要的系统包、Python 包和服务调用地址。6.3 给技能库维护一份索引与运行状况大盘技能数量超过二三十个之后靠 README 维护技能清单已经不现实了。我会从所有技能配置文件里自动抽取信息生成一份技能索引包含技能名、描述、负责人、版本、最近调用频率、平均延迟、错误率。它不只是一张静态表格还会汇总到监控大盘上。这份索引至少有四个用途新同学了解系统能力产品经理跟用户解释“Agent 能做哪些事”老板看整体调用趋势开发者发现某个技能长期没有被调用就可以考虑下线或者优化描述。我实测下来索引对技能召回质量也有间接帮助。当你看到某个技能平均延迟很高、错误率居高不下去优化它的说明文案和执行性能比盲目加新技能更有效。技能库不是越大越好而是“能被模型正确选中并稳定执行”的技能越多越好。最后再分享一个小技巧我每次新增技能后都会用“反向测试”验证描述质量——让模型读一遍技能描述然后人工写五个典型用户问题看模型能不能正确选中它。如果模型在三个问题以上都选错说明描述不够清楚需要改写而不是强行加更多示例进 Prompt。这套方法在agent-skills的迭代里帮我避开了很多“技能存在但模型看不见”的尴尬情况。