ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

agent-skills:为智能体构建可复用技能仓库的完整实践

2026/10/7 22:03:07 拓冰建站 浏览量
agent-skills:为智能体构建可复用技能仓库的完整实践 做 Agent 落地的这几年我踩过最深的一个坑不是模型选型也不是编排框架而是技能散落满地。几乎每个智能体项目走到中期都会出现同一个症状工具函数越来越多提示词越来越长模型却越来越不知道在什么场景下该调哪一个。后来我下定决心把所有可复用的能力收拢成一个独立的仓库名字就叫agent-skills。它不是模型权重也不是重框架而是一套给智能体用的可复用技能集——把搜索、解析、校验、格式化、对接业务系统这些高频动作封装成带清晰说明书的最小执行单元。这篇文章就把我搭这个仓库的完整思路、布局约定、实现模式和维护一年后踩过的坑一次性讲透希望能帮你少走一段弯路。1. 为什么我需要一个 agent-skills 仓库1.1 当工具数量突破临界点混乱是必然的早期做一个问答型智能体的时候我只有五个工具每个工具对应一个函数提示词里明明白白写清楚五个入口模型调用得很稳。后来业务扩张从五个加到十二个再到二十多个乱象开始出现。最直观的问题是提示词膨胀。每个工具都有一段描述二十个工具的描述拼在一起差不多填掉三千多个 token留给对话上下文的空间被严重挤压。第二个问题是描述质量参差不齐有人写了三行参数说明有人只留一句使用这个工具模型根本无法判断工具之间的适用边界。第三个问题更隐蔽旧工具实际上已经没人维护接口还在逻辑已经过时但模型并不知道依然会按提示词里的描述去调用。这类问题不是单靠写更好的工具函数能解决的。工具的粒度太细、彼此之间没有组织关系模型在面对大量候选时选择准确率会显著下降。我后来在内部复盘里写了一句单个工具的质量决定执行的下限而工具的组织方式决定调用的上限。1.2 技能和工具的分界线值得较真我一开始也把 agent-skills 当成一个普通的工具集仓库放了一堆函数进去。用了两周发现不对劲它应该承载的东西比工具高一层。在我现在的定义里工具是单一、原子、无状态的能力入口比如发送 HTTP 请求解析 PDF计算两个日期间隔天数。而技能是内聚的、有上下文的操作配方它可能串联多个工具调用内置参数校验、重试策略、结果规整甚至包含什么情况下不该用的说明。用厨房来类比最清楚工具是一把好刀、一口好锅而技能是一道完整的菜谱——番茄炒蛋怎么做。菜谱当然需要刀和锅但它还规定了先后顺序、火候、调味时机。给智能体一堆工具等于递给它一堆锅碗瓢盆却没说做什么菜给智能体一套技能等于给了它一本经过验证的菜谱它只需要判断今天适合做哪道菜。这个边界直接影响仓库的目录设计。工具类的能力我会下沉成技能内部的基础操作而对外暴露的注册单位永远是技能。Agent 调一个技能就能完成一个有明确交付物的任务而不是每次都要自己琢磨我该先调哪个工具、再调哪个工具。2. 仓库的可猜测布局与技能注册表2.1 让新技能在一个目录下被盲猜到仓库的目录结构我改过三版最终保留下来的是一套强约定的布局。核心原则是任何人——无论是人类协作者还是接入自动化流程的智能体——只需要看一眼目录就能猜到每个技能放哪、长什么样。agent-skills/ ├── registry.yaml ├── skills/ │ ├── web-collect/ │ │ ├── skill.yaml │ │ ├── run.py │ │ ├── README.md │ │ └── examples/ │ ├── document-normalize/ │ │ ├── skill.yaml │ │ ├── run.py │ │ ├── README.md │ │ └── examples/ │ └── ... ├── lib/ │ ├── http_client.py │ ├── validators.py │ └── logging.py └── tests/我没有用复杂的插件机制也没有引入类似微前端的动态加载框架。刻意选择这种朴素结构基于两个判断。第一Agent 技能仓库大概率会长期与多种运行环境共存它可能被 Python 进程调用也可能被 Node 服务通过子进程执行还可能被另一个 LLM 应用直接读文档后仿写动作。如果依赖特定的语言框架仓库的复用半径会大幅缩小。拆开成skill.yaml run.py的通用约定任何语言都能通过解析 YAML 拿到技能元数据再去执行脚本这是最低成本的互操作方式。第二新技能上手的路径要足够短。新来的同学要加一个网页正文提取技能他应该能在十分钟内完成复制一个已有技能目录改run.py做实际逻辑改skill.yaml描述能力边界跑一遍测试用例提交。如果这个过程需要理解框架路由、插件声明周期、依赖注册那仓库维护成本会迅速超过收益。2.2 registry 不是摆设一份诚实元数据的力量每个技能目录里的skill.yaml是技能的身份证明但 agent-skills 顶层还有一个registry.yaml它更像整个仓库的目录索引。我花了很久才理解到注册表不是为了给程序看一个全量列表而是为了给智能体/路由层一个可以快速扫描的目录页。一个典型注册条目长这样- id: web-collect name: web-collect description: 抓取指定网页内容并提取正文支持指定 URL 列表和正文最小长度过滤。当用户要求打开网页提取文章内容读取链接里的正文时使用。 entrypoint: skills/web-collect/run.py runtime: python3 version: 2.1.0 tags: [web, extraction, fetch] input_schema: skills/web-collect/schema.json output_contract: skills/web-collect/output.md deprecated: false描述字段是灵魂。后面我会专门讲描述怎么写这里先强调注册表里每条记录的长度预算整张索引被注入系统提示时单条描述不要超过八十个字。否则索引很快变成一段模型读不下去的说明书发现效率反而降低。另外deprecated字段是我强烈建议加的。Agent 领域变化太快一个技能可能上线三个月就发现被更优的方案取代。显式标记废弃同时保留历史入口可以防止模型继续调用旧技能也方便排查为什么技能库里有两个功能几乎一样的东西。3. 核心实现模式把会做拆成会说 会做3.1 智能体负责决策脚本负责执行agent-skills 里每个技能都严格遵循一个模式LLM 做选择和决策确定性脚本做执行和保障。这个边界是我在好几个项目里反复试出来的。早期我尝试让技能本身也带一点智能比如在run.py里调一个小的语言模型来理解参数。结果非常糟糕一是延迟不可控二是故障链路变长三是当技能内部出错时根本分不清是脚本逻辑问题还是内部模型问题。后来我彻底放弃把所有技能收敛成纯确定性程序。智能体只需要决定用哪个技能、传什么参数剩下的脏活、累活、重复活全部由脚本在规定时间内完成。每个技能由三件套构成run.py实际可执行逻辑。自动参数校验、超时控制、结构化输出。schema.json输入参数的 JSON Schema声明每个字段的类型、必填性、取值范围。output.md输出契约文档说明成功和失败时分别返回什么结构。run.py的可执行体骨架我一般这样写#!/usr/bin/env python3 web-collect skill entrypoint import argparse import sys import json from urllib.parse import urlparse def validate(params: dict) - dict: # 入参校验必须返回统一错误结构而不是抛裸异常 errors [] if not params.get(url): errors.append({field: url, reason: missing}) parsed urlparse(params.get(url, )) if parsed.scheme not in (http, https): errors.append({field: url, reason: invalid_scheme}) if errors: raise SystemExit(json.dumps({status: invalid_params, errors: errors})) return params def execute(params: dict) - dict: # 核心逻辑抓取、清洗、提取正文 ... return {status: ok, data: {title: ..., content: ...}} def main(): parser argparse.ArgumentParser() parser.add_argument(--params, requiredTrue) args parser.parse_args() params json.loads(args.params) params validate(params) result execute(params) print(json.dumps(result, ensure_asciiFalse)) if __name__ __main__: main()关键设计是入参从标准输入或命令行参数进入不是靠环境变量东一个西一个。这样任何调用方——不管是 Python subprocess、Node 的child_process还是 shell 脚本——都能用统一方式调用。3.2 状态上报和返回契约比函数更先确定下来我最常被问的一个问题是run.py的返回值到底该怎么设计。我的答案是先画契约后写逻辑。每个技能的返回必须包含这几个字段{ status: ok, data: {}, meta: { started_at: 2024-06-01T10:00:00Z, finished_at: 2024-06-01T10:00:01Z, attempts: 1 } }失败时返回长这样{ status: failed, error: { code: HTTP_TIMEOUT, message: request to example.com timed out after 10s, recoverable: true, suggest: 可以重试一次或检查目标网站是否可访问 } }recoverable和suggest两个字段是我后来加的它们的作用是给智能体一个明确的后续行动选项。当技能返回失败时模型能根据错误码和suggest决定是直接重试、换参数、还是放弃并告知用户。没有这两个字段的时候模型经常会卡在无限重试的死循环里。用 JSON 作为技能之间的通用语言还有一个额外好处后续如果要把多个技能组合成工作流每个技能的输入输出都可以被下一个技能直接消费省掉了大量的字段映射代码。3.3 每个技能必须配至少两个示例这个要求最初是我审 PR 时定下的硬性规定后来被证明是性价比最高的一条规则。在每个技能目录下必须有一个examples/文件夹里面至少放一个正常输入 期望输出示例以及一个边界输入 期望失败输出示例。示例的价值在于它是给智能体做少样本提示的最佳素材。同样一段技能描述模型可能需要来回试错才能真正理解该传什么参数但如果你把成功示例和失败示例直接放进内容模型几乎可以无脑复刻。我把这当成一种技能的自举演示——任何技能自己就能说清楚自己怎么用。4. 让智能体在正确的时间发现正确的技能4.1 不要把整个注册表一股脑塞进系统提示我见过最常犯的错误把registry.yaml的完整内容直接拼接到系统提示词里。技能少的时候没问题技能一多token 消耗爆炸而且模型在超长列表里做选择准确率会随 item 数量增加而下降。我实测过三种技能发现策略的效果整理成一张表策略做法技能调用准确率平均 Prompt 增量适用场景全量注入把 registry 直接拼进系统提示中等技能 15 后明显下降高约 1500-3000 token技能 10 的轻量场景分组白名单按任务域分组路由层只注入当前任务域对应的技能索引较高中任务域边界清晰、可提前判断检索召回先对用户输入做向量检索只注入 top-K 个匹配技能的描述最高低约 300-800 token技能库大、场景开放的通用场景我最终在大多数项目里用的是混合方案。先按任务域分白名单任务域内部再做一个轻量检索召回只注入得分最高的三到五个技能的完整描述。有趣的是完整技能描述只需要给被命中的候选。没有被召回的技能只需要在注册表里保留一个非常短的名字和一句话标签让模型知道它存在但看不到过多的调用细节。这样既保证广覆盖又控制上下文长度。4.2 技能描述里的何时用句式是召回质量的分水岭写技能描述时千万不要只写它是什么要写清楚什么情况下用、什么情况下千万别用。我总结出一套固定的句式模板每个技能的 description 都按它来当需要 {做什么} 且 {关键前提} 时使用。输入为 {主要输入}输出为 {主要输出}。 优于单独调用 {相关工具}因为 {自带能力}。 除非 {不适用条件}否则不要使用本技能。举个例子之前我写过一个 fetch 类技能的 description当需要抓取一个或多个网页并提取干净的文章正文时使用。 输入为 URL 列表和最小正文长度输出为标题清洗后的正文块。 优于自行拼 requests 获取页面因为本技能已内置 UA 伪装、重试和正文去噪。 除非目标是登录后的私有页面或页面以 JS 动态渲染且无静态 HTML否则不要使用本技能。看起来比一句干巴巴的抓取网页长不少但在检索召回和模型决策两个环节里这种描述带来的收益是远大于 token 成本的。模型很容易根据登录后的私有页面JS 动态渲染这些否定条件做排除不会误选。4.3 让调用层暴露技能选择过程给上层编排agent-skills 不是最终应用它最终要嵌入到某个 Agent 框架里。为了让上层编排知道模型选了哪个技能、为什么选我建议在调用出口处打印结构化的选择日志。这类日志我通常统一放在skill_selector模块中输出格式类似{event: skill_selected, skill: web-collect, score: 0.87, reason: 用户希望提取链接正文符合技能适用条件}这些日志在开发调试时价值不大但在线上观测时作用极大——后面讲数据指标时还要用它。5. 维护一年后积累的避坑手册5.1 文档里全是目标没写失败条件的技能会被反复误用第一版 agent-skills 里有个技能叫translate-doc描述是翻译文档中的指定段落并保持格式。看起来很清晰对吧但实际运行中模型的误用率相当高。问题出在没写失败条件。有一次用户给出一段 Excel 内容模型直接调这个技能去翻译表格结果脚本抛异常因为表格数据不是文档格式另一次用户给的是 PDF模型也调这个技能结果脚本对 PDF 的排版处理一塌糊涂。后来我把描述的否定条件改成除非输入是纯文本或 Markdown 文档否则不要使用本技能Excel、PDF、扫描件、图片中的文字请使用 document-normalize 技能预处理后再调用。同一份脚本没改一行代码误用率直接降了一大半。这说明早期的问题根本不发生在执行端而发生在学习说明端——模型没被告知边界它就默认什么都不管。5.2 技能里 try/except 吞掉异常智能体会失去重试判断力这个问题花了我两周才定位清楚。某个技能在内部把所有网络错误都 catch 住了统一返回一个{status: ok, data: 网络读取失败}。于是模型拿到结果后以为任务成功直接把一段网络读取失败当作了正文回复给用户。这是我见过的最隐蔽的坑。技能作为一个执行单元当然可以吞异常做个兜底但必须把失败的信号结构化地暴露出来而且要区分可重试和不可重试。统一的错误契约有四个要素状态码ok / failed / invalid_params / timeout错误码机器可读如HTTP_TIMEOUT、PARSE_ERROR错误消息给人/模型读的上下文恢复建议retry、adjust_param或give_up有了这个结构上层 Agent 才能做出正确判断。比如模型看到timeout retry建议会尝试等待后重试看到parse_error且不可恢复会直接告诉用户内容格式无法解析而不是硬着头皮把错误信息包装成答案。5.3 没有观测技能库就是一团看起来能跑的灰色逻辑技能库上线第一周我问自己的第一个问题是模型到底调了哪些技能结果发现根本没有日志。没有调用记录就无法评估任何改动等于盲飞。我在lib/logging.py里统一埋了四个事件点skill_selected路由层选中了某技能带 scoreskill_attempted触发了技能执行skill_succeeded返回 okskill_failed返回非 ok带错误码这几个事件全部通过结构化日志输出最终汇总到统一的可观测平台。后续做告警、做回归对比都从这个事件流里取数据。这件事我建议在上线第一天就做不要等出问题再补。6. 用数据评判仓库是否真的在变好6.1 三个在团队内可复用的核心指标技能库不能只靠感觉来优化。我最终保留三个核心指标这三个指标每个团队都能直接复用到自己的技能库上。指标定义计算方式评估意义技能命中率被选中的技能中实际成功完成的占比skill_succeeded / skill_selected反映技能与场景的匹配度、描述清晰度技能覆盖率所有对话中至少调用一次技能的比例有技能调用的会话数 / 总会话数反映模型是否愿意使用技能而非瞎答回归故障率某技能版本更新后失败数环比变化(新版本失败率 - 旧版本失败率) / 旧版本失败率反映技能改动是否引入回归从我的实际数据看一个健康的技能库大约需要两周到一个月的持续迭代命中率才能稳定在 0.8 以上。如果某一个技能命中率长期低于 0.5优先考虑的不是优化脚本而是重写描述或拆分技能粒度。6.2 我学到的最后一课技能库永远是活文档不是静态资产agent-skills 建了一年多我最大的感受是它不是一个写完就固定不变的资产而是一个需要持续照看的活体。模型的版本在变调用模式在变用户的需求在变技能库必须随之演进。我现在每个迭代周期都会做一次技能清理把命中率低、用途重叠、长期无人调用的技能标记 deprecate对新出现的高频场景优先从旧技能里抽公共部分组合新技能而不是推倒重写。每次技能库变更都像一次小型的结构重构节奏很快但因为它有注册表、有约定、有契约所以改动始终是可控的。如果你也正在给智能体攒技能我个人的建议是从最小的三个技能开始先把技能描述的句式、返回契约、调用日志这三件事立好规范再逐步扩容。越到后期你越会发现当初纠结的那点目录结构、那几行描述模板帮你节省的时间和想象力远比想象中多。