资讯详情

agent-skills:让智能体不再“有手不会用”的技能定义指南

📅 2026/10/13 6:01:05 | 华诺云谱 👁 阅读
agent-skills:让智能体不再“有手不会用”的技能定义指南
有一回我给内部某个数据分析工具接大模型能力让智能体去抓取网页内容、清洗完再返回结构化表格。模型选型、提示词调优都折腾得差不多了最后死在一个特别尴尬的环节上——工具本身完全正常单独调用也稳定可智能体就是不按预期去用它。要么不调用要么传错参数要么中途放弃。后来我把agent和skills拆开重新审视才发现问题根本不在模型强弱而在技能这一整层的定义方式。这也是我想写agent-skills这个项目的原因它不是一个特别玄乎的新框架就是一套关于怎么给智能体定义技能的组织规范和接口约定加上一组可复用的技能实现。如果你正在搭智能体应用或者被工具调用不稳定折磨过这篇应该能给你一些能直接落地的思路。agent-skills的核心思想很简单也很反直觉不要把技能当成普通函数来写要把技能当成一份给模型的说明书来写。函数签名是给人看的技能说明书是给模型看的。两者的关注点完全不一样。后来我把技能库整体重写了一遍调用成功率提升非常明显而且模型基本不再出现手里有工具却不用的情况。下面按我实际整理的顺序把这个项目的设计思路、落地步骤和踩坑过程完整拆开讲。1. 智能体有手不会用真正的瓶颈不在模型在技能定义方式先讲个我之前做过的对照测试。同一个智能体接入同一批功能接口一组用的是传统函数调用范式——就是那种函数名参数列表一句话描述的配置另一组用的是agent-skills式的完整技能说明结果差异大到让我怀疑模型是不是换了。传统范式下模型经常挑错工具或者参数填得莫名其妙换了完整技能说明之后同样的问题几乎销声匿迹。这说明一个经常被忽略的事实工具调用失败往往不是模型笨而是我们给模型的信息根本不够。很多人写工具调用描述喜欢写成parse_url(url: string)这种风格觉得参数名和类型说清楚就行。但模型不是一个编译器它不会去解析类型签名它是在做语义匹配——根据当前任务描述去猜哪个工具最可能帮自己完成目标。如果工具的语义信息不完整模型就只能靠猜猜就会出错。所以agent-skills第一条原则就是每个技能必须是一份独立、自包含的说明文档而不是几行配置。我举个例子你就明白了。假设你想让智能体去下载一个文件并保存到指定目录。普通写法是{ name: download_file, parameters: { url: {type: string}, save_path: {type: string} } }模型看到这个配置只会知道有个工具叫download_file需要url和save_path。它不知道这个工具适合什么场景不知道下载超时怎么办不知道文件太大怎么处理更不知道如果目标目录不存在是自动创建还是返回错误。于是就会出现荒谬的行为下载失败后模型换一个奇怪的URL重试或者把save_path填成临时目录默认其他工具会去拿文件。在agent-skills里同一个能力我会写成这样skill: download_file description: | 从远程地址下载文件到本机指定路径。适合从公开网页、对象存储或API直链获取文件。 下载完成后返回文件的绝对路径、大小和SHA256校验值。若保存目录不存在会自动创建。 不支持需要登录态认证的下载不支持带重定向跳转的JS渲染页面。 parameters: url: type: string desc: 文件直链地址仅支持HTTP/HTTPS协议 required: true save_path: type: string desc: 目标保存路径支持绝对路径和相对路径 required: false default: ./downloads constraints: - 目录不存在时自动创建 - 超过30秒无响应直接判定失败 - 文件大小超过500MB返回错误提示 post_check: - 文件存在且大小大于0字节 - 可选计算SHA256并记录这两份配置最大的区别在哪里第二份明确告诉了模型这个工具能做什么、不能做什么、边界条件是什么、成功标准是什么。模型拿到这个技能说明相当于看到了一小段操作手册它就知道在什么情况下应该调用这个工具、什么时候不该调。这才是agent-skills的核心认知技能定义本质上是在替模型降低决策成本而不是在做API封装。所以如果你现在也在做智能体先别急着上各种复杂框架。把现有的工具拿过来看看每个工具的描述信息量够不够。通常改成这种完整声明式描述之后光是工具选择的准确率就能上一个台阶根本不需要换更强的模型。2. 技能库的整体结构分层、命名和索引规则agent-skills不是一个单体程序它更像一个技能仓库。整个仓库里按运行边界分成几类技能而不是按工具类型来分。我一开始试过按文件处理网络请求数据处理这种传统分类后来发现这种分法对模型不友好。模型不关心工具本身属于哪一类它关心的是当前任务处于哪个阶段。后来我把技能按运行流程分成了三层给整个agent-skills的骨架打底感知层技能负责获取信息比如抓取网页、读取文件、查询数据库、调用开放API。这类技能回答的问题是现在能拿到什么数据。处理层技能负责转换数据比如文本解析、字段映射、格式标准化、数据清洗。这类技能回答的问题是拿到的数据怎么变成想要的形态。执行层技能负责产生结果比如写文件、发送消息、更新记录、触发下游流程。这类技能回答的问题是最终要做什么动作。这种分法有一个直接好处智能体在规划复杂任务时天然会经历先获取、再处理、后执行的路径。技能库按这个路径分层模型在每一步都能快速锁定候选技能减少全库扫描带来的选错风险。每一层内部我又规定了一套统一的技能清单格式每个技能由一个独立目录承载skills/ ├── index.yaml # 技能总索引声明所有可用技能 ├── perception/ # 感知层技能目录 │ ├── fetch_webpage/ │ │ ├── skill.yaml # 技能声明 │ │ └── impl.py # 实际执行逻辑 │ └── read_local_file/ │ ├── skill.yaml │ └── impl.py ├── processing/ # 处理层技能目录 │ ├── extract_table/ │ │ ├── skill.yaml │ │ └── impl.py │ └── clean_text/ │ ├── skill.yaml │ └── impl.py └── execution/ # 执行层技能目录 ├── save_to_file/ │ ├── skill.yaml │ └── impl.py └── send_message/ ├── skill.yaml └── impl.py总索引index.yaml是所有技能的总入口模型在大多数情况下只需要读总索引按需进入具体技能目录查看详细说明。这样可以控制上下文长度避免一次性把所有技能细节都塞给模型。index.yaml保持精简只放技能名、一句话用途、以及所属层skills: - name: fetch_webpage layer: perception summary: 抓取网页正文内容并转成纯文本支持CSS选择器提取。 - name: read_local_file layer: perception summary: 读取本地文本类文件内容支持指定编码和行数范围。 - name: extract_table layer: processing summary: 从非结构化文本中识别的表格数据输出为JSON数组。 - name: clean_text layer: processing summary: 清洗文本中的空白字符、乱码和不可见字符。 - name: save_to_file layer: execution summary: 将数据写入本地文件支持追加和覆盖两种模式。 - name: send_message layer: execution summary: 发送一条文本消息到指定的消息通道。之所以用独立目录而不是一个超大YAML文件有几个实际考量。第一技能的详细描述通常很长全部塞进一个文件会导致单次加载token过多第二独立目录方便做单元验证改一个技能不影响其他技能第三也是最重要的一点独立目录让我可以为每个技能单独写一个验证脚本这在后面做技能巡检时帮了大忙。这部分我踩过的坑是一开始把技能声明和实现放在同一个代码文件里由主程序直接import。表面上很简洁但后来技能数量到了十几个以后每次调整都要重新过一遍主流程而且技能的描述信息被埋在代码里根本没法单独做静态检查。改成目录结构之后主程序只依赖index.yaml的声明每个技能的描述和实现分离出问题能快速定位结构调整也不再让人头疼。3. 单技能落地示例从感知层到执行层的完整写法讲了半天框架我挑三个有代表性的技能把完整的编写过程拉一遍。这三个技能分别来自感知层、处理层和执行层能覆盖大多数场景的编写思路。3.1 感知层技能抓取网页正文这个技能的目的很单纯给模型一个网页URL返回网页的标题、主要文本内容和该页面的最终URL处理跳转后的结果。我写description的时候特别强调了适合什么和不适合什么skill: fetch_webpage layer: perception description: | 根据URL抓取网页内容提取标题和正文纯文本。 适用于文章、新闻页、帮助文档等以文本为主的页面。 不适用于需要登录、依赖JavaScript动态渲染或主要是视频内容的页面。 如果请求被拒绝或返回非200状态码请如实报告错误不要反复重试。 如果页面内容超过50000字符只截取前50000字符返回。 parameters: url: type: string desc: 待抓取的网页完整地址必须包含协议头例如http://或https:// required: true selector: type: string desc: 可选的CSS选择器用于提取页面指定区域 required: false default: null output: title: 页面标题 content: 正文纯文本 final_url: 经过跳转后的最终URL这里每一个字段的设计都有它的价值。不适用场景那两行看似可有可无实际上它能把模型从反复尝试不可能任务中解放出来。我见过太多智能体在登录墙页面上来回重试的场景——倒不是模型想跟那个页面较劲而是它真的不知道登录墙这个概念。你在描述里写清楚它就不会再去。另外output字段我也建议写清楚。现在很多技能输出靠自然语言但模型判断成功与否往往需要精确的状态标志。比如fetch_webpage就返回三个固定字段模型一眼能看出抓到没有、有没有跳转决策就快了。3.2 处理层技能从文本中提取表格这个技能我认为是整个技能库里最出彩的一个。它的任务是从一段非结构化文本里识别出表格结构输出成JSON数组。难点在于模型本身也能做这个事那为什么还需要一个独立技能关键在于——大模型做提取属于随机推测技能的规则提取属于确定性解析。当数据量大、字段固定、取值有边界时确定性解析不仅能省token和耗时准确率还永远稳定在100%。我把这个技能的目标函数设置成能不提大模型的提取能力就不提能靠规则搞定就靠规则。skill: extract_table layer: processing description: | 从结构化文本或半结构化文本中提取表格数据返回JSON数组。 表格行以换行符分隔列以竖线或连续的空白字符分隔。 适合处理markdown格式表格、命令行输出、日志汇总等文本。 如果文本中不包含明显的表格结构返回空数组并提示模型改用其他方式。 parameters: text: type: string desc: 待解析的原始文本长度不超过20000字符 required: true headers: type: array desc: 可选的表头字段名若提供则作为输出JSON的键名 required: false default: null有意思的事情来了。很多模型的默认行为是我擅长文本处理让我直接生成Json但实际上让模型直接用规则技能解析几百行日志的时候是又快又稳。但技能描述里必须强调如果文本中不包含明显的表格结构返回空数组。这能防止模型拿一段散文硬套表格格式强行输出一堆错乱JSON。3.3 执行层技能写入文件执行层技能有个特点一旦执行就是有副作用的操作不能随便试。所以它的安全性强调必须比其他层更靠前。skill: save_to_file layer: execution description: | 将给定的文本或JSON数据写入到本地文件。保存成功后返回文件路径和写入字节数。 仅允许写入工作目录及其子目录禁止通过路径跳转符访问工作目录之外的区域。 如果文件已存在且未指定overwritetrue默认追加到末尾。 不包含创建备份的功能如果有覆盖风险必须在调用前提示用户确认。 parameters: filename: type: string desc: 文件路径相对于工作目录禁止包含..这样的跳转部分 required: true content: type: string desc: 写入的文本内容最大不能超过1MB required: true overwrite: type: boolean desc: 是否覆盖已有文件 required: false default: false注意我把两类信息放在了description里一类是允许做什么另一类是禁止做什么。禁止信息尤其重要。智能体在上下文中如果不知道文件系统的边界真的可能因为误操作把别的重要文件覆盖掉。这种问题防的不只是技术漏洞更是一种建模层的风险——你希望模型做决策的时候天然避开危险路径而不是等它犯错了再靠程序拦截。这三个技能合起来就是我搭一套网页数据采集——结构化整理——落盘保存的最小闭环。这个闭环我建议每个做智能体的人都先跑通它能让后续所有技能扩展变得非常顺畅。4. 实际运行里的高频故障模型空转、结果截断与参数序列化框架和示例说完了聊聊真正让项目从能跑变成好用的一步把技能库丢进真实场景之后我遇到的一系列故障。这些故障任何文档里都不会写只能靠实际踩。4.1 模型陷入空转反复重试同一个失败技能第一种高频故障是模型面对某个技能持续失败后不换方案反而换个参数继续重试同一个技能。比如抓取网页请求被服务器拒绝模型会把URL再拿不同方式拼一遍连试四五次消耗大量时间。排查之后我发现根因在技能描述缺少失败语义。fetch_webpage的description里虽然写了如实报告错误但没写重试次数上限。模型收到非200状态码后它不知道该不该再做一次于是倾向于再来一次。这个倾向用在网络抖动场景本身是合理的但服务器明确拒绝的时候重试就是浪费时间。我当时的修复方法是给所有感知层技能补了重试语义改成类似这样failure_handling: max_retries: 1 retry_interval_seconds: 5 on_failure: 返回错误码和原因建议模型尝试其他技能或询问用户从模型行为上看补上这段之后空转基本消失。原因也不难理解模型在不确定能不能再试一次的时候会倾向于采取成本最低的动作——重试。你给了明确边界它就能果断停下来换路。4.2 技能返回超长内容被截断第二种故障是技能返回的数据量太大把上下文撑爆或触发截断导致模型看到的结果残缺。典型场景是一次读取一个几万行的文件模型只拿到前几千行误以为整个文件就这些后续分析自然全错。有人说那简单限制返回长度就行。但限制长度的副作用更隐蔽如果技能返回截断后的内容而不告知模型已经被截断了模型会把截断结果误认为完整内容。这比不返回更糟糕。我最终的解决思路是两层配合。第一层技能层设置合理返回上限比如单次读取最多返回500行第二层必须在输出末尾固定追加一个截断标记注意文件总行数为12000行当前仅显示前500行。如需继续读取请使用read_local_file并传入start_line参数。这样模型就知道数据不全也知道怎么去补齐剩余部分。这套返回上限加截断提示的模式我后来复制到了所有可能返回大数据的技能上包括查询数据库和读取日志文件。4.3 参数序列化格式不一致第三种故障更像工程问题。模型的工具调用模块和技能实现之间需要一个序列化协议比如把dict转成JSON字符串传给子进程。这个环节一旦出问题技能接收到的参数就会是None、null或者整个丢失。我第一次遇到这个故障的时候技能实现收到的save_path是None但工具调用日志里明明看到模型传了/data/tmp/result.txt。排查了很久才发现是参数传输层的序列化逻辑只处理了基本类型碰到嵌套dict就漏掉部分键。解决方案没有把全部参数堆在一个地方而是给技能参数定义了明确规定所有参数必须是JSON可序列化的基本类型禁止传对象实例禁止用非标准编码。同时在技能实现入口处统一做一次校验一旦发现必填参数为None立即返回参数缺失错误并附带期望的参数名和类型。这样即使将来序列化层又出bug载荷能显式地推回给模型让模型自行纠正而不是默默用一个空值继续执行。这套显式失败的思想我很喜欢。很多系统设计倾向于尽力掩盖错误但在智能体技能这一层失败信息越明确模型越容易自我纠偏。隐式错误会让模型做出更离谱的后续判断。4.4 技能间调用链失控最后一种故障发生在技能数量超过一定规模之后。单个技能都正常但模型有时候会把多个技能串成一个不健康的链条。比如想读取网页先调了fetch_webpage拿到了HTML又调extract_table去解析最后再调save_to_file落盘。问题不大。但有时候模型会把read_local_file和fetch_webpage串在一起理由是先看一下本地是否已有该URL的缓存——这个逻辑倒是通顺可惜本地根本不存在那个文件白白增加一次调用。后来我在技能描述里增加了关联技能和典型使用流程两个字段用来引导模型走正确路径而不是探索出奇怪链路related_skills: - fetch_webpage - extract_table - save_to_file typical_flow: | 采集网页数据先使用fetch_webpage抓取页面再使用extract_table提取表格 最后用save_to_file保存为文件。加上这个之后模型基本会按预设流程走很少再去探索奇葩顺序。这个现象背后是注意力机制的特点如果你不给出典型路径模型就自己生成一条概率路径而概率路径里往往夹杂干扰项。给出一条显式路径等于省掉它自己规划的心智负担。5. 技能库的自检与演进从能用到好用的最后一步技能库搭完并不代表一劳永逸。模型版本升级、应用场景变化、甚至提示词的小改动都可能导致某些技能失效或行为漂移。所以我从很早开始就给agent-skills配套了一个自检机制。这个自检机制不是传统意义上的单元测试而是更接近技能行为回归。它的核心思路是为每个技能准备一组标准的输入输出样例定期把所有技能连同样例整体跑一遍结果和预期不一致的技能标记为异常。比如fetch_webpage我准备了三个固定URL一个正常返回的、一个返回404的、一个需要跳转的。对每个URL预期输出分别是正常内容错误码报告跳转后的最终URL。只要这三个用例都通过这个技能的基本行为就算正常。我觉得这一套自检机制是agent-skills项目里最值钱的部分。它带给你的不只是一个静态技能库而是一个能持续演进的技能基础设施。模型在变、场景在变、数据在变但技能库的质量线始终被拉在那里。一旦有技能越线系统能第一时间告诉你而不是等真实用户踩中了才发现。另外我还做了一项技能覆盖率分析。我把历史对话日志拿来统计哪些技能实际被调用得多、哪些技能从未被调用。结果发现有一些精心设计的技能其实模型根本没用过。原因通常有两个要么索引里的summary写得不够吸引人要么和现有技能的边界重叠导致模型总是优先选另一个。针对这些僵尸技能我的处理方式分两种。如果技能描述没把场景讲透就重写summary把更典型的应用场景放进去如果和另一个技能功能重叠就干脆合并减少模型的选择负担。毕竟技能库不是越庞大越好模型需要做的决策越少反而越不容易出错。现在这个技能库在我这边已经稳定用了几轮迭代调用成功率维持在了一个让我很舒服的水平。但它的结构仍然保持轻量没有引入复杂的智能体框架没有做过度抽象。每个技能仍然是一份清晰的说明书加一段可靠的实现。这也是我在整个项目里体会最深的一点智能体的能力边界不取决于你接入了多少个工具而取决于工具能否被模型准确理解和使用。agent-skills这个名字说到底就是对这一理解的工程化沉淀。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑