agent-skills:让大模型Agent拥有可复用、可插拔的动手能力
那段日子我一直在调一个智能体项目最让我头疼的不是提示词写得不细而是模型到了真实业务环境里只会说不会做让它查个数据它回一段像模像样的话让它改个配置它把责任推给权限不足。后来我把项目做成了一套独立的能力层起了个直白的名字——agent-skills。它解决的正是一个问题如何让大模型 Agent 拥有可复用、可插拔、可维护的动手能力而不是把每件事都塞进对话上下文里碰运气。这篇文章适合两类人看。一类是正在做 AI Agent 产品原型、发现聊天链路根本扛不住真实业务流程的开发者另一类是想把 Agent 能力沉淀成团队资产、而不只是依赖某个提示词写得很好的技术管理者。我会从最开始的痛点出发把我实际搭建、调试、评估这套技能体系的过程完整拆开包括踩过的坑和现在仍在用的回调闭环。1. agent-skills 要填的坑Agent 不是聊天框而是一套执行系统1.1 只靠对话式 Prompt 驱动业务逻辑会越写越死我最早做的那个 Agent 原型其实和大多数人一样把业务规则、工具说明、回答格式全部写进 system prompt指望模型每次对话都按剧本走。刚开始跑 demo 确实有模有样但一旦把真实数据接进来问题立刻暴露。第一个问题是状态不连续。用户上一轮问帮我把上周的数据拉出来下一轮说按部门做个对比模型经常不知道上周的数据到底存在了哪里。第二个问题是上下文膨胀。我为了让它记住工具用法在 prompt 里塞了几千字的操作说明token 成本直线上升模型反而在小事上频繁出错。第三个问题是改动成本高。业务方提了一个很小需求日期格式加一种我改的不只是一段说明而是要重新调整个 prompt还要担心改动影响其他无关场景。我那时候才意识到如果 Agent 的能力全住在提示词里它本质还是个聊天机器人而不是一个可以交付的系统。1.2 技能层存在的意义把会做什么从对话中剥离出来agent-skills 的核心思路是不要教模型怎么做而是直接告诉它有什么可用的能力然后让代码去执行。每个能力被封装成一个独立单元我称之为 skill。一个 skill 其实长得并不复杂它就是一个带元信息的可执行函数。函数内部可以是任何东西调搜索接口、查数据库、读写文件、跑一段统计脚本、调内部服务 API。真正重要的是它对外暴露的那张说明书模型读说明书来决定什么时候调用它、传什么参数。这样一来提示词只负责一件事——让模型理解用户的意图并选择合适的技能。至于技能内部怎么实现、数据从哪里来、出错怎么处理对模型来说是黑盒。我选择这种设计还有一个更现实的原因可复用性。给 A 业务写好的按照关键词检索资料并提炼要点这个技能换到 B 业务几乎不需要重写。技能一旦从业务流程中被独立出来就变成了团队可以持续积累的资产而不是散落在各条 prompt 里的碎片逻辑。2. Skill 的统一建模一份能被机器读懂的能力说明书2.1 每个 Skill 必须自描述名字、描述、参数 Schema我把一个 skill 的元信息固定为四个部分name、description、parameters、execute。name 是技能的唯一标识必须简短稳定模型靠它来引用description 用自然语言说明技能的职责边界和适用场景parameters 采用 JSON Schema 描述执行所需的参数结构execute 是实际的执行入口接收参数后返回结构化结果。这里我最想强调的就是 description。很多人写技能时写代码十分钟写描述只花三十秒这是非常大的误区。模型决定调用哪个技能时完全依赖描述文本去理解。一个搜索技能如果描述成搜索资料模型的命中率就偏低如果我改成给出一段关键词返回近一年内与关键词相关的公开网页标题、摘要与来源链接适合用于资料收集和背景调研模型就清楚什么时候该调用它什么时候不该调用。下面是我维护的一个典型 skill 简化示例功能是按关键词检索内部知识库{ name: search_kb, description: 在内部知识库中按关键词检索条目返回每条记录的快照时间、标题和摘要检索范围仅限接入的内容库, parameters: { type: object, properties: { query: { type: string, description: 搜索关键词可以包含多个词用空格分隔 }, top_k: { type: integer, minimum: 1, maximum: 10, default: 5, description: 返回结果数量上限 } }, required: [query] } }2.2 目录即注册表用文件组织代替运行时注册有了单个 skill 的结构接下来就是如何组织一批技能。agent-skills 项目采用了一个很朴素的方案把每个技能放在一个目录里目录名就是技能名目录下包含 manifest 文件和代码文件。系统启动时递归扫描技能根目录读取每个 manifest动态生成技能清单。这么设计的直接好处是新增技能不需要改任何注册代码往目录里丢一个新文件夹重启服务就生效。版本管理也顺手每个技能目录是一个独立单元git 历史里能看见某个技能的完整演进过程。我在给团队其他小伙伴交付这套框架时他们几乎不需要学习手册看两个例子就会写新技能了。目录扫描的约定也很简单skills/ search_kb/ manifest.json run.py web_lookup/ manifest.json run.py suggest_schedule/ manifest.json run.py每个 manifest.json 保存上一节说的四项元信息run.py 对外暴露 handle 函数。加载器不关心技能内部是什么语言实现只管把 manifest 读进来、把 handle 函数引用注册到调度器里。2.3 参数 Schema 为什么选 JSON Schema 而不是手工解析关于参数解析我试过两种路线最后确定为 JSON Schema。早期版本的技能都是手工解析模型返回的参数先从模型输出里提取 JSON 字符串再用 try except 包裹缺了字段就在代码里补默认值。这种做法在小范围 demo 时没问题技能一多就失控了。每个技能自己处理一套参数校验逻辑异常信息五花八门模型收到错误后根本不知道怎么修正。换成 JSON Schema 之后参数校验变成统一的流程。模型填的参数先被校验器检查一遍类型不对、必填缺失、枚举值越界全都会被拦截在技能代码执行之前。校验器还会做一种非常实用的处理对可缺省字段自动填充默认值后再把完整的规范化参数传给技能函数。这在实践里极大提升了执行稳定性。我统计过未加 Schema 校验时技能执行阶段的报错率大概在 15% 左右有相当一部分是参数格式问题加上校验后这个数字下降到 4% 上下剩余的报错基本是外部依赖异常。统一的错误结构也帮助了下一步的自动纠错闭环。3. 调度链路的关键决策注入、选择、执行、兜底3.1 技能注入不是越多越好先筛选再给模型选择早期我以为技能清单越全模型的能力越强所以把几十个技能全部塞进上下文中。结果是 token 消耗巨大而且模型常常被不相关技能干扰——用户问个会议时间它居然去调用天气技能。后来我改成了两级注入策略。第一级是常驻技能数量控制在五个以内这些是最底层的通用能力比如获取当前时间、读取用户身份信息、记录待办用户每一轮对话都可能需要。第二级是按需技能根据当前用户消息做一次检索把候选技能从所有技能集合中筛出五个左右再拼接进模型上下文。检索我用的是向量相似度匹配每条技能描述离线生成 embedding线上对用户问题做同样处理之后取 TopK。这个改动带来的变化非常明显。模型的选择准确率上来了因为候选集里没有干扰项推理速度也快了因为上下文里少了很多无关的技能描述。我建议任何 agent 项目都尽量采纳这个思路技能注入不是越多越好而是越精准越好。3.2 模型填参与代码执行之间的安全边界模型做出技能选择之后参数经过了 Schema 校验接下来就进入真正的执行阶段。这个阶段我划定了严格的安全边界。每个技能运行时有一个独立的工作目录文件读写只能限定在该目录内技能代码拿不到系统级路径网络请求技能只允许访问白名单域名下的 HTTP 接口超时时间统一设置为五秒。为什么要这么较真因为技能一旦可以自由读写文件或访问任意网络资源几乎就等于把服务器的钥匙交了出去。模型本身不具备安全意识用户构造的恶意参数完全可能诱导它进入危险路径。我在一次内部演练里让测试用户输入请在系统日志目录创建文件如果技能没有边界约束这个操作就被执行了。加上白名单之后操作直接被拦截返回参数越界错误。对于执行超时的处理我采用了两层保护。单次技能执行超过十秒立即中断整个 agent 回合则限制在三十秒内。中断后调度器会记录技能名、参数、超时原因供后续排查。3.3 调用失败不直接抛错构造可理解的异样反馈闭环技能执行失败是个大概率事件但失败之后怎么办决定了一个 Agent 系统是玩具还是工具。最糟糕的返回方式是直接抛一段堆栈给模型模型面对一整屏英文 traceback 基本是无能为力的只会道歉然后继续硬试。我的做法是在执行层捕获异常之后把错误信息结构化成几类。参数类错误返回缺少哪些字段、哪个字段类型不对权限类错误说明该技能被限制了哪些路径超时类错误标注出执行时长和超时阈值外部依赖类错误明确说的是接口超时还是返回格式异常。模型收到这种结构化反馈后有能力调整参数或切换技能。这里举一个我实际遇到的例子。用户让 Agent把下周一的回顾会议安排在下午三点通知参会人技能要求日期格式是 ISO 标准字符串模型填了下周一被 Schema 校验拦下。错误反馈里明确写了date 字段需要 YYYY-MM-DD 格式模型在下一轮直接补上了具体日期整体任务没有中断。如果当初直接抛异常这轮交互就废了。4. 我实际跑通的三个 Skill 拆解从需求到实现4.1 信息搜索与要点提炼第一个落地的技能是信息搜索。团队成员经常需要在短时间内整理某个主题的公开资料人工搜索实在耗时所以我把这个流程做成了技能。skill 内部封装了两件事调用搜索接口拿回网页列表再对前几篇内容做摘要提取。实现上我踩过一个深刻的坑模型会把搜索结果的摘要段落当成确定事实直接引用。技能返回的内容是某篇文章说……的片段模型在总结时却把它变成了某事情是事实丢失了信息来源。后续我在技能返回结构里强制加入 source 和 publish_date 字段并约定一个规则技能返回的每条信息都必须附上来源模型总结时不得删除来源标注。这个技能上线后团队找资料的效率确实提升了不少。现在用户只需要说一句帮我整理一下智能座舱行业近半年的动态Agent 就会自动调用搜索、归纳、结构化三个子技能返回一个带来源的报告框架。4.2 本地数据分析与报告生成第二个技能是本地数据分析。用户上传一个 CSV 文件Agent 需要完成数据概况统计、关键指标计算、生成图表。这个技能拆成两个子步骤第一步读取列名和类型推断第二步按用户描述执行分析。这里最折磨人的不是算法而是一堆琐碎的环境问题。文件编码是首要问题CSV 文件经常是 GBK 编码默认按 UTF-8 读会乱码。我尝试过用字节采样检测编码但准确率一般最终的做法是技能先尝试 UTF-8 读取失败后自动切换 GBK并把这个信息作为分析报告的一条备注。图表生成时中文字体缺失也会造成导出图片变成方块字技能里专门绑定了某个字体文件路径确保生成报告在多个机器上一致。数据分析技能还有一个性能问题用户上传的 CSV 有几万行时某些统计计算在纯 Python 里会跑得很慢。我在技能实现里做了行数阈值判断超过阈值自动切换成更高效的第三方处理库执行时间从分钟级降到秒级。这个细节让用户在体验上几乎没有感受到数据量带来的延迟。4.3 待办与日程管理第三个技能是与日历交互。最开始我们团队没抱太大期望因为时间处理是最容易出错的部分。但实际做下来这个技能反而是用户使用频率最高的。技能需要理解相对时间。下周一上午明天下午三点这类说法必须被解析成具体时间戳。我的做法是在技能内部维护一个当前时间的注入参数执行时由调度器传入当前真实时间技能内部用这个基准时间做日期推算。一开始我们尝试过让模型自己把相对时间转成绝对时间结果发现它经常算错尤其是跨月、跨年的场景。后来调整为技能暴露两个参数一个叫 time_desc接收下周一上午这样的原文一个叫当前时间技能内部调用日期解析库完成转换。这给模型省了很多计算负担日程冲突检测也可以在技能内部完成比如检测到同一时段已有安排就返回该时段已占用是否查看附近空闲时间的建议。这个技能的设计思路很有代表性不要把需要精确计算的工作交给模型让技能层去算模型只负责做选择和表达。5. 评估与经验沉淀怎么知道一个 Skill 是真的好用5.1 三维度评估命中率、执行成功率、任务完整率技能越多越需要一套客观的评估方法否则你根本说不清某个技能到底是能用还是好用。我在项目里定了三个指标。命中率看的是模型是否在正确的场景调用了正确的技能。比如用户问天气调用天气技能算命中调用搜索技能算误判。执行成功率看的是技能被调用之后能不能跑通并返回有效结果这部分受参数校验、外部接口稳定性影响较大。任务完整率看的是多技能协作场景下一个完整需求是否被闭环处理比如搜索资料并生成报告这个任务要求搜索、归纳、报告三个技能按顺序成功任何一环失败都不算完成。下面这个表格是我记录评估口径时常用的参考指标计算公式统计来源使用场景命中率正确调用某技能的次数 / 测试用例总数链路日志中的技能调用记录验证技能描述质量与注入策略执行成功率技能执行成功返回数 / 技能被调用总数技能执行结果标记排查参数异常、外部依赖异常任务完整率全链路无中断的任务数 / 任务总数对话会话结束标记评估多技能编排的稳定性5.2 维护一套 Skill 库的真正成本持续迭代、回归与废弃技能不是写完就结束了它更像是被持续运营的小产品。每个技能都经历过至少一次 description 的改写。第一次写的描述往往不精确模型不知道该不该用通过分析误用日志我会发现有些技能被滥用就细化描述的边界条件。我建议为技能库建立回归集。我维护了一个大概两百条测试请求的集合每次改动某个技能描述或参数 Schema 之后跑一遍回归看看哪些测试用例的命中率下降了。这个习惯帮我避免了很多改了 A 技能导致 B 技能行为异常的问题因为多个技能共用同一套注入策略时改动确实会产生牵连。还有一个比较隐蔽的成本是技能废弃。旧技能没人用、被新技能替代之后应该及时标记 deprecated 并从候选列表中移除。否则它在检索阶段会一直占着候选位还可能跟新技能抢调用。我采用的是维护一个启用开关的方式废弃技能保留在仓库里方便回溯但不会被注入模型上下文。5.3 几条我认为最有价值的实践经验真正把这个项目从原型做成可用系统之后我总结出了几条反复验证有效的经验单独写在这里。技能描述的第一个词尽量用动词。搜索、生成、计算、更新这样描述在向量检索和模型理解两个维度上表现都更好。我对比过同一技能两种描述的命中率动词开头比名词开头的命中率高大概六个百分点。这个差异主要来自模型在做意图匹配时的语义关联习惯。宁可技能数目少而精也不要贪多。在我现在的技能库中启用的技能不超过十五个。技能越多每轮注入的候选选择压力越大误调度率也会上升。真正需要新增能力时优先看看现有技能能不能通过参数扩展覆盖新场景。执行结果的返回结构必须统一。不管技能内部做了什么对外返回都要保持一致的格式状态标记、结果数据、错误信息、来源说明。这看起来是小事却是后续评估、日志分析、自动纠错闭环能跑通的地基。我在实际使用中还有一个体会别急着让 Agent 学会所有事情先让它把三五件高频小事做到接近零失误。agent-skills 从第一批三个技能走到现在最稳定的恰恰是那些反复打磨过的常用技能而不是后来堆砌的复杂能力。如果你正准备给自己的 Agent 项目加一层技能系统我会建议你先挑两个每天都用的动作把它们做成标准技能跑上两周再谈扩展的问题——这个节奏看起来慢实际上比一开始铺开几十个技能要快得多。