那段日子我一直在调一个智能体项目,最让我头疼的不是提示词写得不细,而是模型到了真实业务环境里只会"说"不会"做":让它查个数据,它回一段像模像样的话;让它改个配置,它把责任推给"权限不足"。后来我把项目做成了一套独立的能力层,起了个直白的名字——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 项目加一层技能系统,我会建议你先挑两个每天都用的动作,把它们做成标准技能跑上两周,再谈扩展的问题——这个节奏看起来慢,实际上比一开始铺开几十个技能要快得多。