
1. 项目缘起从“会聊天的机器人”到“能干活的手”先说说我为什么要折腾这套 agent-skills 技能库。过去一年我一直在跟各种大模型 Agent 打交道从最早用 LangChain 串流程到后来基于 OpenRouter 自建调度再到给模型开放了十几个 function calling 工具。越往后越发现一个尴尬的事实模型越强越不满足于“只聊天”但真正让它干活的时候你给的工具如果组织得很烂它要么不会用要么用得极其别扭。举个例子。你给 Agent 一个“网页抓取”工具参数里有url、selector、headers、timeout、render_js、wait_seconds模型不是不知道怎么填而是常常把timeout填成字符串把render_js理解成“渲染一段 JavaScript 代码”甚至会在只该传 URL 的场景里强行补上不存在的参数。这不是模型不行是工具接口设计得太“程序员视角”了。后来我意识到与其让每个工具都做成一个孤立的函数不如把它们包装成一套带“使用说明书”的技能包让模型像人一样先看说明书、再动手操作。这也是这套 agent-skills 项目最核心的出发点把零散的 function calling 工具重构成一组高内聚、低耦合、自带使用场景说明的技能单元。每个技能都包含清晰的描述、参数约束、执行逻辑和常见用例模型拿到技能列表时不靠“猜”也能选对工具开发者拿到技能库时不靠“翻代码”也能快速了解能做什么。这套结构适配任何有工具调用能力的大模型不管是 Claude、GPT 还是国产开源模型只要按规范写技能文件就能直接挂进去用。如果你正在做 AI Agent 相关的应用或者想让模型真正完成“检索资料→处理内容→输出报告”这种闭环任务这篇文章应该能给你一份可以直接抄作业的参考方案。我会从设计思路、目录结构、核心技能实现、调用机制、调试经验几个维度把我在实际构建中踩过的坑和沉淀下来的套路完整展开。2. 整体设计思路与技能体系拆解2.1 先想清楚技能和工具到底有什么区别很多人一开始会混淆这两个概念。工具Tool是函数级别的能力单元比如“执行 Python 代码”“搜索网页”粒度小、职责单一。但技能Skill是带场景、带约束、带示例的能力封装它可能内部会调用多个工具也可能只是对某个工具的调用方式做了更严格的提示约束。我做的这套 agent-skills 有显式的层级设计agent-skills/ ├── skills/ │ ├── code_runner/ # 代码执行类技能 │ ├── document_parser/ # 文档解析类技能 │ ├── web_researcher/ # 网络检索类技能 │ ├── data_analyzer/ # 数据分析类技能 │ └── formatter/ # 输出格式化类技能 ├── registry.json # 技能注册表 ├── shared/ │ ├── schemas/ # 公共参数定义 │ └── validators/ # 输入校验逻辑 └── runtime/ ├── loader.py # 加载技能定义 └── dispatcher.py # 调用分发器每个技能文件夹内部统一放三个文件SKILL.md自然语言说明书给模型看、schema.json参数约束给校验器看、impl.py执行逻辑给运行时看。这套结构最直观的好处是人和模型读的是同一份说明程序校验走的是同一套 schema谁都不会误解谁。2.2 技能声明里的“使用说明书”怎么写才不失效SKILL.md是整个技能库的灵魂它决定了模型在工具选择阶段能不能精准命中正确的技能。我总结了一套固定的写法模板--- name: code_runner description: 在隔离沙箱中执行Python代码支持运行时间限制、内存限制和第三方库安装 version: 1.3.0 tags: [code, python, sandbox] --- # code_runner ## 适用场景 - 用户需要计算、数据分析、脚本运行时 - 需要生成图表、处理JSON/CSV等结构化数据时 - 任何需要“写代码来解决问题”的场景 ## 不适用场景 - 用户要求执行bash命令请使用shell_executor - 用户要求编辑本地文件请使用file_editor ## 参数说明 - code: 必填。要执行的Python源码必须是完整的可运行代码 - packages: 选填。需要预先pip安装的包名列表如 [requests, pandas] - timeout: 选填。最长运行秒数默认10秒最大60秒 ## 注意事项 1. 代码中不要包含input()交互式输入沙箱无法处理 2. 若需读取文件请先将文件内容写在code里或使用file_reader 3. 中文输出时请显式print(..., encodingutf-8)写过十几个技能文件之后我发现最容易踩的坑是描述写得太含糊。比如“处理文件”这种描述模型根本分不清你支持的是 PDF、Word 还是 CSV。我现在定了一条硬性规则每个技能至少写清楚“适用场景”“不适用场景”“参数的三要素类型、必填性、取值范围”这三块内容。不适用场景尤其重要它能帮模型在多个相似技能之间快速排除干扰项。2.3 注册表设计与动态发现机制registry.json是一个集中索引运行时通过它加载技能。格式如下{ version: 1.0.0, skills: [ { name: code_runner, path: skills/code_runner, enabled: true, entry: impl.py, timeout: 30 } ], precedence: [schema_validate, keyword_match] }加载器启动时会扫描skills/目录下所有包含SKILL.md的文件夹读取 front-matter 里的元信息再与注册表做对比。这样新增一个技能只需要丢进目录并在注册表登记一行不用改任何调度代码。dispatcher 收到模型发来的调用请求后先做 schema 校验再根据技能名定位到实现文件用动态 import 的方式执行。我还加了一层“关键词预匹配”把SKILL.md里的高频触发词同步到注册表请求进来时先过一个轻量关键词表能提前拦截明显选错技能的情况。实测下来这个机制能把技能错误命中率从最初的十几个百分点降到 3% 以内。3. 核心技能实现与关键技术细节3.1 code_runner在沙箱里安全执行代码代码执行是最常用的技能也是最容易出安全问题的技能。我最初的版本直接在宿主机上开 subprocess后来跑了一次包含os.system(rm -rf /tmp/test)的测试代码虽然没造成实际损失但吓出一身冷汗。现在实现里强制三层隔离import subprocess import resource import tempfile def run(code, packagesNone, timeout10): with tempfile.TemporaryDirectory() as workdir: setup_cmd pip install .join(packages) if packages else true exec_cmd fcd {workdir} python -c {quote(code)!r} container_cmd [ docker, run, --rm, --network, none, --memory, 256m, --cpus, 0.5, -v, f{workdir}:/workspace, python:3.11-slim, bash, -c, f{setup_cmd} {exec_cmd} ] proc subprocess.run(container_cmd, capture_outputTrue, textTrue, timeouttimeout) return {stdout: proc.stdout, stderr: proc.stderr, code: proc.returncode}这里有几个值得注意的点。--network none直接禁网避免代码偷偷往外传数据--memory 256m和--cpus 0.5限制资源防止恶意或失控的代码打爆宿主timeout参数从注册表传入SUBprocess 层和 Docker 层做了双保险防止进程变成僵尸态。整个沙箱容器的镜像固定为python:3.11-slim因为镜像越小启动越快实测冷启动大概在 800ms 左右体感上完全拖不垮对话流程。3.2 document_parser处理 PDF、Word 和 Markdown 的混合解析文档解析这类技能最烦人的不是“解析不出来”而是“解析出来了但内容乱掉”。PDF 里常见的多栏排版、页眉页脚、表格嵌套任何一个处理不好都会让下游效果崩盘。我前期踩过不少坑后来总结出一套分层的处理链路def parse(file_path: str, file_type: str) - dict: if file_type pdf: raw_text extract_with_pdfplumber(file_path) blocks segment_by_position(raw_text) # 按坐标分块处理多栏 content merge_blocks_by_order(blocks) # 根据阅读顺序合并 elif file_type docx: doc DocxFile(file_path) content \n.join(p.text for p in doc.paragraphs) content extract_tables_as_markdown(doc) elif file_type md: content Path(file_path).read_text(encodingutf-8) return {content: content, page_count: get_page_count(file_path)}segment_by_position这个方法值得多说两句。pdfplumber 能拿到每个字符的坐标我先按 y 轴聚合成行再按 x 轴判断该行属于哪一栏最后按“从左到右、从上到下”的规则重新排序。这套逻辑对付双栏论文效果显著但遇到三栏复杂排版时偶尔会乱序所以在SKILL.md里我特别写了一句“若输出内容顺序异常请尝试使用原始文本模式”给模型留一个后手手段。3.3 web_researcher带摘要的检索与去重网络检索技能的设计难点不在“搜索”本身而在“怎么把结果喂给模型”。直接返回一堆 URL 字符串模型还得一个个猜内容直接返回全文又容易超过上下文窗口。我采用的方案是用BeautifulSoup抽取主文本截取前 2000 字符作为粗摘要再用一个轻量级摘要模型压缩到 300 字左右最终以结构化格式返回{ results: [ { url: https://example.com/post/123, title: Agent Memory 机制详解, snippet: 本文介绍了三种常见记忆方案……, relevance_score: 0.92 } ] }relevance_score我用的不是向量相似度而是一个简单的词频加权算法把查询里的名词性关键词提取出来统计它们在页面标题、摘要、正文中的出现密度。虽然笨但在中英文混合场景下比向量库稳定得多关键是延迟只有几十毫秒。返回格式强制用 JSON是为了让模型在引用时能精确给出“来源链接”有效减少幻觉。3.4 data_analyzer统计计算与图表输出二合一这个技能是把 pandas 计算和 matplotlib 绘图封装在一起目的是让模型在分析完数据后直接生成图表。实现上有一个易踩的坑matplotlib 默认不支持中文字体画出来的图全是方块。我在初始化脚本里强制设置了SimHei或Noto Sans CJK字体族并注册自定义颜色循环。技能返回的图表以 base64 PNG 字符串传给前端展示同时附带一份输出的统计摘要文本。如果数据量特别大我会先在 impl 层做采样默认最多取 10 万行避免直接 OOM。这个技能内部依赖 code_runner 的沙箱能力但对外暴露的参数更偏业务化比如chart_type、x_axis、y_axis模型只需要描述“画一张按月份分组的柱状图”参数由技能内部模板自动填充。4. 技能调用机制与提示词工程让模型精准选对工具4.1 描述长度与选型准确率的关系我做了几组对照实验测试不同长度的技能描述对模型选型准确率的影响。结论是描述太短少于 50 字时模型经常混淆相似技能描述太长超过 500 字时模型会把注意力分散到无关细节上选型准确率反而下降。最佳区间是 150~300 字并且要把最关键的触发词放在开头前两句。这也是我为什么在SKILL.md里专门设计了## 适用场景这个段落还要求写在正文最前面。模型在读取工具列表时通常是从前往后扫描的把最重要的匹配条件放在前面等于帮它走了一条捷径。4.2 参数约束的 schema 设计schema 做得好不好直接决定模型能否正确传参。我用 JSON Schema 的$defs和enum做了严格约束{ $schema: https://json-schema.org/draft/2020-12/schema, type: object, properties: { code: { type: string, minLength: 1, description: 要执行的完整Python代码 }, packages: { type: array, items: {type: string}, description: 需要临时安装的pip包名 }, timeout: { type: integer, minimum: 1, maximum: 60, default: 10 } }, required: [code] }这里的关键在于description字段不能写“参数含义”要写“模型该怎么填这个参数”。比如timeout的 description 我写的是“最长运行秒数普通计算用默认值10处理大文件最多调到60”模型看到后通常会做出合理的选择而不是填个 9999 或者字符串。运行时校验器会在模型返回 tool call 后立刻跑一遍 schema不合法就直接返回一个错误信息给模型重试而不是让底层代码抛异常。4.3 错误处理与自动重试的策略模型调用技能不是每次都能一次成功的。我设计了三个层级的容错机制第一层是参数校验失败直接返回 schema 错误详情提示模型重新组织参数第二层是执行过程中出现错误比如代码编译失败把完整的 stderr 返回给模型让模型自己修复第三层是超时或资源耗尽返回“建议拆分为更小的任务”阻止模型盲目重试。实测中三层机制叠加能让模型在 3 次尝试内解决大约 80% 的失败任务。需要注意的是重试次数必须封顶不然模型可能在一个简单 bug 上转十几圈。我现在的配置是单个技能最多重试 2 次超过次数就把错误打包交给一个汇总模块生成“失败原因 失败尝试记录 用户可选的替代方案”。5. 实操从零到一搭建一个可复用的技能库5.1 初始化目录与注册表如果你也想照搬这套结构可以按下面的步骤操作。首先创建项目根目录并初始化registry.jsonmkdir agent-skills cd agent-skills mkdir -p skills code_runner impl.py SKILL.md schema.json python -m pip install jsonschema pyyaml注册表我建议直接手写而不是程序自动生成因为你需要给每个技能手动定义优先级、超时和是否启用的开关。初期技能少手写维护成本极低后期技能多了可以在 CI 里加一个校验任务确保新增技能的元信息合法。5.2 编写一个真实技能url_shortener假设我们需要一个把长链接转短链接的技能。最稳妥的做法是先找有没有适合的开放接口我常用的是一个自部署的自建短链服务内部调用POST /api/shorten。技能的文件结构如下skills/url_shortener/ ├── SKILL.md ├── schema.json └── impl.pyimpl.py是这个技能的最小可运行版本import json import urllib.request def run(target_url: str, alias: str None): payload {url: target_url} if alias: payload[alias] alias req urllib.request.Request( https://your-shorten-service/api/shorten, datajson.dumps(payload).encode(), headers{Content-Type: application/json}, methodPOST ) with urllib.request.urlopen(req, timeout5) as resp: data json.loads(resp.read().decode()) return {short_url: data[short_url], original_url: target_url}技能写完后在注册表补上条目加载器下次启动就会自动识别。这里有个小技巧SKILL.md里的name字段必须和文件夹名、注册表里的name完全一致否则 dispatcher 会找不到实现文件。我第一版因为大小写不一致排查了整整半个小时最后靠打印调试日志才发现是命名不一致的锅。5.3 联调测试与回归方案技能库不能写完就扔我建议至少准备两套测试一套是离线单测直接调用impl.run()不经过模型另一套是端到端测试让模型用提示词去选技能并执行。每次改动后跑一遍全量单测再抽 10~20 条真实用户 query 做回归。离线单测的场景要覆盖“必填参数缺失”“参数类型错误”“接口返回异常”这三类情况。端到端回归时我通常固定几个典型 query比如“用 code_runner 计算 1 到 100 的和”“把这个 PDF 转成 Markdown”然后对比模型选的技能名、最终输出是否满足预期。这一套流程下来技能库的稳定性会明显提升改代码也不怕踩坏已有功能。6. 踩坑记录与问题排查速查表6.1 常见问题与对策我把这半年实际遇到的坑整理成一张速查表方便你遇到类似问题快速定位现象可能原因解决思路模型调用技能时参数里出现字符串串类型schema 中缺少 type 描述或 description 不够明确检查 schema 是否每个字段都标明了 type 和取值范围description 要写“怎么填”而非“字段含义”技能描述里写了“不适用场景”模型还是选错模型没有仔细读描述或相似技能之间差异点描述不足在两个相似技能的 description 开头都显式写“与xxx的区别”并加否定性关键词代码执行技能超时但实际代码很快结束沙箱镜像未拉取首次执行消耗额外时间在部署时预拉取镜像或把 timeout 默认值调大到 15 秒PDF 解析结果乱序多栏排版识别错误使用基于坐标的分块策略并在 SKILL.md 中提示模型检查顺序中文图表变方块matplotlib 缺少中文字体初始化代码里强制指定rcParams[font.sans-serif][Noto Sans CJK]技能返回大量冗余文本超出模型上下文输出没有压缩或截断在技能层对返回结果做摘要、截断按固定 token 预算包装6.2 我亲自试过的两个性能优化除了问题排查还有两个优化手段投入产出比极高。第一个是把常用的数据文件预加载到内存缓存而不是每次技能调用都去硬盘读。比如 document_parser 里会反复用到 PDF 页数和字符坐标缓存我用了一个带过期时间的全局字典同一份文件二次解析的速度能提升 5 倍以上。第二个是给 dispatcher 加一层“技能预热”。Agent 启动时除了加载注册表还会把所有启用的技能模块用importlib预先导入一遍。这样第一次调用时不需要等待 import 耗时虽然单个技能可能只省几十毫秒但用户在对话流里体感到的是“秒回”而不是“卡一下”。6.3 调试技巧日志是救命稻草技能调试时最容易出现的情况是模型调用了错误的技能但日志里只看到参数结果看不到模型内部的决策链路。我现在的做法是在 dispatcher 里埋点记录三件事模型返回的 tool_call 原始 JSON、技能匹配的关键词命中列表、校验失败的具体原因。把这些信息统一输出到结构化日志配合一个简单的日志查询界面每次出问题都能在几秒内定位到是“模型选错”“参数不合规”还是“执行异常”。还有一个容易被忽略的点模型在对话过程中可能连续调用多个技能比如先调用 web_researcher 搜索再调用 document_parser 解析搜索结果。这类多步调用链路要在日志里打上 trace_id不然排查一个“最终报告里的数据来源不正确”问题会让你找得怀疑人生。我花了整整一个下午修复过一次类似的链路问题后来在所有技能入口和出口都加上了 trace_id排查效率瞬间就上来了。7. 个人经验一些关于技能颗粒度的取舍思考技能库做到第三版时我最大的体会是技能的颗粒度比数量更重要。一开始我总想把所有操作拆成最细的工具比如把“网页抓取”拆成“fetch_html”“parse_html”“extract_links”三个独立技能结果模型经常绕来绕去反而降低了整体准确率。后来改成把“抓取 提取正文 提取链接”合并成一个web_fetcher技能内部用参数控制输出模式模型选起来轻松执行链路也稳定得多。但颗粒度也不是越粗越好。如果你把“数据分析”“画图”“生成报告”全部塞进一个技能参数列表会膨胀到几十个模型依旧懵。我现在的判断标准是一个技能应该能在一句话里说清楚能干什么参数不超过 5 个内部最多串联 3 个操作。超过这个阈值就拆低于这个阈值就合。最后再分享一个小建议在技能库的README.md里单独维护一个“决策表”列出常见用户意图应该选哪个技能。这个表既可以给开发者看也可以作为提示词的一部分喂给模型。我实测发现喂给模型的决策表让技能选型准确率又提升了 5~8 个百分点效果非常明显。