
这两年只要聊到 Agent绕不开一个词agent-skills。我看过太多项目模型选的是最强的那一档框架也套得特别齐全编排链路拉得比PPT还漂亮最后卡死在同一个地方——Agent 不会“干活”。所谓 agent-skills说土一点就是给大模型配一套可以被发现、被调用、被组合的原子能力层。没有这层东西模型再聪明也只会聊天有了这层东西它才真正从“会说话”变成“会办事”。这篇文章我想把 agent-skills 从设计思路到落地实现完整拆一遍包括技能粒度怎么定、技能库怎么组织、模型怎么发现并调用技能以及我在实际项目里踩过的那些坑。适合正在做 Agent 产品、搭个人 Assistant、或者研究自主智能体的朋友不论你是用 LangChain、自研框架还是自己撸一套底层核心方法论都是通用的。1. 先把“技能”这件事想清楚——Agent 技能体系的整体设计思路1.1 从工具调用到技能体系的演进早期做 Agent大家通常从 function calling 入手也就是给模型开一堆函数接口帮它完成上网、查库、发消息这些事情。这种思路简单直接小 Demo 跑起来飞快。但只要你把函数数量涨到几十上百个问题立刻暴露模型选函数像抽盲盒选错了就瞎执行上下文被塞得密密麻麻调用链一长就乱套。技能不是把函数改个名字而是把函数、约束、上下文、执行策略打包成一个完整的单元。你调用的不只是一个“方法”而是一个“能力”。这个能力自带说明文档、入参规范、执行逻辑和反馈格式模型拿到它之后知道自己能干什么、该怎么干、干完该返回什么。这相当于把原来裸奔的 API 变成了有完整服务协议的微服务系统复杂度和可用性完全不在一个量级。我在做 Agent Skills 设计时一直强调一件事技能是给模型用的“产品”。它的用户不是程序员而是读 description 来决策的大模型。所以设计的核心不是“代码写得好不好”而是“模型看懂没有”。这个视角转变很关键后面所有细节都围绕它展开。1.2 技能到底长什么样拆开看五个部分一个规范化的技能至少要包含五个组成部分缺一个都会在实际运行中出问题。第一是 name必须是机器可读、语义清晰的唯一标识比如send_email_v2而不是func_384。第二是 description这是给模型看的说明文档要写清楚这个技能是做什么的、在什么场景用、有什么边界。第三是 params也就是入参定义每一项都要说清类型、格式和含义。第四是 execute也就是真正干活的逻辑可以是一个函数、一段脚本、一个 API 调用。第五是 metadata记录权限、依赖、超时时间、成本上限等附加信息。用一个最小示例来说明{ name: fetch_webpage_content, description: 抓取指定 URL 的网页正文内容返回清理后的纯文本。适用于需要获取文章、公告、新闻页面信息的场景。仅限抓取公开页面不做登录后页面的处理。, params: { url: { type: string, description: 目标网页的完整 URL必须以 http:// 或 https:// 开头。, required: true }, max_chars: { type: integer, description: 返回内容的最大字符数默认 5000最大不超过 20000。, required: false } }, execute: { type: function, entry: fetch_page, timeout_ms: 15000 }, metadata: { permission_required: true, cost_limit_per_call: 0.01, version: 1.2.0 } }这里的 description 不是写给人看的注释而是写给模型看的操作手册。它决定模型在什么样的问题下会想到用这个技能又在什么样的情况下应该放弃。我在实际项目里见过太多人把 description 写成一两句话结果模型要么不敢调用要么乱调用这个问题后面专门展开讲。1.3 粒度技能太碎还是太胖决定Agent的上限技能粒度的把控是最考验设计功力的地方。粒度太碎模型为了完成一个任务要连续调用五六次上下文开销大、单点失败概率高、路由也容易乱粒度太胖技能内部塞了一大堆分支和特殊情况复用性断崖下跌模型也搞不清楚这个“大家伙”到底适用什么场景。我一般用一条简单的实践准则来卡粒度一次技能调用应该能独立完成一个“对用户有意义的闭环动作”。比如“给指定用户发送邮件通知”是一个闭环“发送邮件”里面“选择 SMTP 服务器”就不应该单独做成技能它是内部实现细节。反过来说“为用户执行完整的工作流”也不是一个技能那是一条编排逻辑应该由 Agent 在运行时组合多个技能完成。在实际项目里我把技能分成三层原子技能、领域技能、流程技能。原子技能是最小闭环比如“计算两个日期相差的天数”领域技能是在某个业务域内做组合比如“生成订单报表”流程技能则是对前两种技能的编排比如“新用户入职办理”。层与层之间的技能不互相调用代码而是通过 Agent 的推理层串联这样每一层都可以独立测试、独立迭代没有耦合负担。2. 动手搭一个技能库——从定义到组织2.1 技能发现机制模型是怎么找到对的技能的技能库建好之后下一个核心问题是模型怎么从几百个技能里挑出当前任务需要的那一个。这个过程叫技能发现Skill Discovery我试过三种方案各有取舍。第一种是命名前缀规则把技能按业务模块命名比如mail_send、mail_read、order_create然后用前缀做粗过滤。好处是零额外成本坏处是当技能数量上来之后前缀会膨胀而且同一个技能可能横跨多个模块命名容易自相矛盾。第二种是检索式语义匹配把技能的 description 向量化存进向量库用户请求来了先检索 Top-K 候选再塞进模型上下文。这种方式扩展性好但它依赖检索质量向量召回失败的场景下落差很大。第三种是分类路由在技能库之上加一层意图分类器先判断当前请求属于哪类任务再在类内做进一步选择。这个方案最稳但需要维护分类器适合技能数量超过两百个的成熟项目。实话说没有一种方案是银弹。我现在的做法是把三种结合小规模项目直接用前缀过滤中等规模用前缀加关键词粗筛真正的大厂级项目才会引入分类路由。表格对比如下方案成本扩展性适用规模主要风险命名前缀规则极低差少于 30 个技能语义覆盖不全检索式语义匹配中好30 到 200 个向量召回失败分类路由高最好超过 200 个分类器维护成本这里想提醒一句不管用哪种发现方式都应该把最终的决策判断交给模型本身。检索和路由只是帮你缩小候选范围真正决定“用什么、不用什么”的还是模型对 description 的理解。所以哪怕你的检索做得很强description 写得稀烂照样白搭。2.2 描述即接口把 description 写成模型的说明书技能描述这件事我在前面反复提现在展开说透。你看待 description 的方式决定你的 Agent 是聪明还是智障。好的 description 要回答清楚四件事这个技能做什么在什么情况下该用它在什么情况下不该用它调用时需要注意什么。以一个真实场景举例——我之前做过一个“网页正文提取”技能最初 description 只写了一句“提取网页正文”结果模型经常拿它去处理 PDF 文件也在需要登录的页面上报错。后来我把 description 改成这样抓取指定 URL 的网页正文内容返回清理后的纯文本。适用于获取文章、公告、新闻等公开页面的信息。不适用于PDF 文件、需要登录才能访问的页面、动态加载明显的 SPA 页面。如果页面内容以图片为主请优先使用图片处理技能。改完之后技能被误调用的比例直接下降了 60% 以上。模型读 description 的方式和人不完全一样它更依赖明确的正反例。你写清楚“不要用它做什么”比只写“它能做什么”更重要。我自己后续做技能设计时固定模板里一定会包含“适用场景”和“不适用场景”两个字段宁可多加一句否定描述也不能漏。还有一个小细节description 里不该堆营销词汇。“高级的”“强大的”“灵活的”这些词对模型理解毫无帮助反而会稀释关键信息。描述的本质是接口文档不是宣传文案每句话都要有信息量。2.3 文件夹即技能库目录组织的实践技能库的组织方式影响技能库的长期可维护性。我推荐一种很朴素的方案把每个技能作为一个独立文件夹技能描述、执行代码、依赖声明全收纳在里面整个技能库就是一个人与模型都能读的机械目录。我的标准目录结构长这样skills/ fetch_webpage/ SKILL.md fetch_page.py requirements.txt test_cases.json send_mail/ SKILL.md send_mail.py requirements.txt test_cases.json整个结构看着朴素但每一层都有明确含义。SKILL.md是模型看到的 description 与参数定义来源我们团队规定它必须能单独被人阅读并理解fetch_page.py是真正干活的代码requirements.txt 列出这个技能的外部依赖不跟全局依赖混在一起test_cases.json 存放这个技能的回归测试样例每次改动后都跑一遍。为什么坚持“文件夹即技能”而不搞花哨的注册中心因为技能库本质上是一个需要持续演进的知识资产最怕的就是散落各处。用文件夹做模块边界新人一看就知道往哪里加东西CI 脚本一扫描就能自动收集所有技能排查问题时也能顺着路径快速定位。相比把所有技能塞进一个大字典、靠注释区分这个方案的长期价值要高太多。3. 让Agent真正会用这些技能——实操落地全流程3.1 一个能直接抄的技能模板理论铺垫完了下面直接上一个可复用的技能模板。我拿“天气查询”技能举例这是一个最简单、最容易理解、也最容易本地模拟的场景。SKILL.md 这样写# Skill: query_weather Describe: 查询指定城市当前天气状况包括温度、湿度、风速和天气现象。适用于用户询问“今天天气怎么样”“明天会不会下雨”等场景。数据来源为公开气象接口仅支持国内主要城市。 Parameters: - city: string, required. 城市中文名例如“北京”“上海”。 - date: string, optional. 查询日期格式 YYYY-MM-DD。留空则查询当天。 - unit: string, optional. 温度单位celsius 或 fahrenheit默认 celsius。 Return: - JSON 对象包含 city、date、temperature、humidity、wind_speed、condition 字段。execute 部分的参考实现import json def query_weather(city: str, date: str None, unit: str celsius): # 实际项目中替换为调用真实气象 API data { city: city, date: date or today, temperature: 23 if unit celsius else 73, humidity: 45, wind_speed: 12km/h, condition: sunny } return json.dumps(data, ensure_asciiFalse)模板的核心不在于代码写得多漂亮而在于三个点参数定义要精确到取值合法性返回值要明确给出字段结构和示例description 要交代清楚边界条件。你把这三个点做好模型调用时的准确率自然就上来了。3.2 技能注册与加载从配置文件到运行时状态技能写好了就需要被 Agent 框架加载。加载过程我建议分两步先做静态登记再做运行时绑定。静态登记对应你刚才写的 SKILL.md框架启动时扫描技能目录把 name、description、params 组成索引运行时绑定则是把技能索引里的 name 和执行函数对应起来做成可调度的映射。如果你用 Python加载流程可以简化成下面这种思路import importlib import json from pathlib import Path def load_skill(skill_dir: Path): with open(skill_dir / SKILL.md, r, encodingutf-8) as f: meta parse_front_matter(f.read()) module importlib.import_module(fskills.{skill_dir.name}.{skill_dir.name}) executor getattr(module, meta[entry]) return SkillItem( namemeta[name], descriptionmeta[description], paramsmeta[parameters], executorexecutor )这段代码不是万能药它只是展示一种特别务实的加载方式目录扫描加约定式文件名。你在真实项目里可以把 parse_front_matter 换成自己的配置解析也可以用 JSON/YAML 代替 Markdown但要保证一个原则——技能描述和技能实现必须同源同目录不能拆到两处维护。还有一个别忽略的点技能加载时要校验依赖完整性。我碰到过技能代码里有第三方库但 requirements.txt 没写的情况运行时直接 ImportError。后来在加载流程里加了一步pip check的等价逻辑每次启动时用虚拟环境比对声明依赖和实际依赖不一致直接拒绝加载并报错。这个防错机制虽然简单但能省掉非常多半夜排查问题的时间。3.3 技能执行的三条策略单步、串联、并行技能执行策略直接决定 Agent 的任务完成质量。我把策略分成三类实际项目中基本够用。单技能直调是最稳的策略适用于“用户问天气、Agent 调天气技能”这种一对一的场景模型只需要把参数抽出来执行后把结果转述。顺序编排适用于有明确先后逻辑的任务比如“查城市经纬度”再“查天气”、先“创建草稿”再“发送邮件”这种场景里前一个技能的返回值是后一个技能的入参来源必须在编排层定义好数据传递规则。并行调用适用于彼此不相关的任务比如同时查五座城市的天气Agent 可以一次性发起多个技能调用然后合并结果。为了支持顺序编排和并行调用技能的返回值必须设计成结构化数据而不是一段字符串。我见过很多团队把技能返回写成“上海今天晴23 摄氏度东南风 2 级”看着没毛病但下一个技能想直接拿这个数值做计算就抓瞎了。最好的实践是技能返回 JSON 对象——温度是数字、风速是数字、天气现象是枚举值Agent 拿到之后不管是回复用户还是继续编排都能畅通无阻。这就是我在前面强调 Return 结构化的原因它的影响在组合调用时会充分体现。4. 技能上线之后的事——迭代、评估与反馈闭环4.1 技能运行日志记录哪些内容才能复盘技能上线只是起点真正的难点在于持续迭代。迭代的前提是你知道每个技能干得怎么样。所以技能运行日志必须记录下来数据维度越全后面定位问题越省事。我个人实践整理了一份必记字段清单调用时间、技能名称、调用来源的任务 ID、传入参数、返回内容摘要、执行耗时、token 消耗、是否成功、失败原因分类、用户是否反馈了不满。这些字段看起来简单但要真正做到每次调用都对齐全需要对框架做一点记录埋点。埋点时有一个容易被忽略的问题很多项目的日志只记“成功或失败”不记录参数详情。这样一来就算你知道某技能失败率高也看不出来是参数套路不对还是技能本身逻辑错了。我建议日志系统要把参数哈希和全文分开存全文进冷存储哈希放进热索引。排查问题时先用热索引做初筛再拉冷存储看原始内容。这套方案比起全量存热存储廉价很多排查效率也不掉链子。4.2 技能评估每次改技能都要过的“三道关”技能库会不断增删改每次改动都有可能影响模型对技能的理解与调用。为了防止越改越糟糕我给自己定了一个规矩任何技能改动必须过三关测试。第一关是成功执行回归用 test_cases.json 里预先定义的正常用例和边界用例验证技能逻辑本身的正确性。第二关是描述误导测试人工上传一批来自真实对话的请求样例跑一遍 Agent 全链路重点看模型在候选技能很多时是否选对了技能。第三关是稳定性压测主要在技能依赖了外部服务时执行看它在接口抖动、超时、返回异常数据的情况下会不会崩溃。这三关看起来麻烦但实际上算是技能体系里性价比最高的投资。你不做回归等到某一天线上模型行为突然异常再去翻几十个技能的历史 diff那个酸爽程度谁试谁知道。尤其在 Agent 场景里技能行为一变用户观感是“机器人突然变傻了”而你根本不知道是哪次改动造成的所以针对技能版本打标签、与输出结果做关联几乎是可追溯性的底线要求。4.3 干中学从失败调用里沉淀新技能技能库也会从运营反馈里“长出来”不是每次新增技能都由产品规划驱动。我常用的方法是定期拉取失败调用日志把失败原因按类型聚类比如“多技能纠缠不清”“现场缺关键信息”“技能间组合逻辑太绕”“模型不识别当前任务对应技能”。针对高频失败类别我会去设计新技能或者调整已有技能的 description而不是反复修一个技能的代码去兜底。举个例子很早之前我做过一个“根据用户备注批量排班”的技能单独跑没问题但一旦用户要求“按小时工类型筛选后再排班”模型就在备注清洗和排班之间反复横跳。后来我没有去改排班技能的逻辑而是拆出一个“小时工类型识别”技能在排班之前先调用它做一次预分类。新技能上线后失败率降了大半而且这个识别技能后面也被复用在别的场景里。这就叫“干中学”从失败中提炼出真正缺失的能力单元然后补齐它而不是打补丁式的堆逻辑。5. 避坑指南与常见问题速查表5.1 那些我踩过的坑希望你绕开第一坑是技能描述模板没统一。最初我们团队不同人写技能有的写两百字有的写二十字模型在技能选择上非常挣扎很多技能被埋没。后面强制统一模板包含“描述/适用场景/不适用场景/参数说明/示例返回”五段结构情况立刻好转。第二个坑是为了让模型知道技能存在把技能描述写得特别长特别细结果上下文被无关内容占满每次请求浪费大量 token。描述要精炼控制在 100 到 300 字之间只写高频有用的信息低频细节放到技能内部文档去。第三个坑也是我强烈建议大家提前预防的技能权限边界。技能一旦可以访问网络、发送消息、修改数据它就不再是“玩具能力”了。必须从 Day 1 开始做权限标记哪些技能默认禁止在未显式授权时调用哪些技能只能访问脱敏数据至少要在 metadata 里写清晰。别等到用户真的被发错一封邮件、删错一条数据时再补权限体系那时候挽救成本已经太高了。第四个坑是版本混乱。技能改动后旧版本还留在系统里模型在候选列表里同时看到新旧两版行为时好时坏。我后来设定规则每次更新只能保留一个 active 版本旧的必须标记 deprecated 并在下一轮清理掉。这个看似基本的管理动作在项目人多之后极其关键。5.2 常见问题速查表现象可能原因排查方法处理建议模型总是选错技能description 太模糊人工对比错误请求的检索/路由结果重写 description明确正反例技能经常超时外部依赖无调用时长控制排查外部接口响应耗时设置超时时间考虑加入重试机制必要时降级方案技能调用偶尔成功偶尔失败返回结果结构不一致查看同技能多次调用的返回日志统一返回 JSON schema加字段约束Agent 重复执行同一技能执行结果未反馈到状态中观察编排逻辑中返回值状态更新情况设置任务状态标记避免幂等缺失导致重复动作每次改动技能后模型行为不稳定存在多个技能版本查询技能库中版本数量管理版本策略保证单 active 版本5.3 技能库做大了之后别忘了“遗忘机制”技能库超过一定规模之后真正考验你的不是怎么塞进更多技能而是怎么让 Agent 在有限的上下文里依然找到最正确的那个技能。除了前端的检索和路由技能库本身也需要做“遗忘机制”。所谓遗忘就是一个技能如果长期没有被调用成功过或者是描述与当前用户群需求匹配度极低就要主动把它从活跃技能集中摘出去放到归档区。与其保留着一个偶尔还要被翻出来误调用的劣质技能不如先把它摘出去等有需要再激活。我在项目里会定期写一个统计脚本按周计算每个技能的调用成功率和被选中率连续两周没被选中且成功率低于阈值的技能自动进入“候选冻结”名单人工确认后移出活跃区。这一步看起来有点违背“技能越多越好”的直觉但实测下来对路由准确率和整个 Agent 的响应稳定性都是正向收益。少而精的技能库远比多而杂的技能库更容易让模型做出正确决策。就我个人的体会来说agent-skills 这个事最迷人的一点在于它把“让 AI 学会干活”这个问题变成了一个可拆解、可验证、可迭代的工程问题。不再是你对着模型祈祷它理解力超群而是你可以通过一套规范把一个原本黑盒的智能行为变成有章可循的系统能力。最后再分享一个小技巧技能上线初期最好先限制一次对话里可供模型调用的技能数量控制在十五个以内等调用行为稳定了再逐步放开这个节奏对调试体验特别友好。