
1. 从“agent-skills”这个标题说起它到底在指什么第一次看到“agent-skills”这个词我脑子里冒出来的第一个念头是这大概率不是一个具体的框架名也不是某个现成的库而是一个概念性的项目代号。它把两个词拼在一起——“agent”和“skills”。在当下的技术语境里“agent”通常指能自主感知环境、做出决策并执行动作的智能体而“skills”则是指这个智能体所具备的一项项具体能力比如读写文件、调用接口、执行命令、检索信息、操作浏览器等等。把这两个词组合起来它指向的核心问题就很清晰了如何让一个智能体拥有可插拔、可组合、可复用的技能体系。这不是一个“从零训练一个大模型”的问题而是一个“如何围绕模型构建一套工程化的能力调度层”的问题。换句话说模型是大脑skills 是手脚和工具箱而 agent-skills 要解决的就是大脑如何高效地指挥手脚。我之所以对这个方向特别有感触是因为过去一年多里我参与过好几个智能体相关的项目从最简单的“单轮问答机器人”到后来需要多步骤规划、多工具协同的复杂流程。踩过的坑几乎都集中在同一个地方技能的组织方式。一开始大家都是把工具函数写死在一个大文件里模型通过一段固定的提示词去选择调用哪个。项目小的时候没问题一旦技能数量超过十个提示词就开始膨胀模型选错的概率直线上升维护也变成噩梦。agent-skills 这个标题背后其实就是在回应这个痛点。这篇文章适合谁看如果你是刚接触智能体开发、想知道“技能系统”到底该怎么设计的开发者我会从最基础的概念讲起把每一步的取舍逻辑说清楚。如果你已经做过一些智能体项目但正被“技能越来越多、越来越乱”困扰那我在后面章节里分享的分层设计、路由策略和避坑经验应该能直接拿来用。整篇内容我会尽量用从业者之间聊天的口吻把原理、步骤、参数和实测心得揉在一起讲不堆砌术语也不搞那种“先定义再分类最后总结”的教科书套路。需要提前说明的是agent-skills 目前并没有一个官方统一的实现标准不同团队、不同平台对它的理解差异很大。所以我在文中给出的方案是基于我实际项目中反复验证过的一套思路属于“常见实践中的合理选择”而不是唯一正确答案。你在参考的时候重点看背后的逻辑具体参数和工具名可以按自己的技术栈替换。2. 技能系统的本质为什么不能把工具函数堆在一起2.1 从“一个大文件”到“技能注册表”的演进逻辑我最早做智能体的时候做法特别朴素新建一个tools.py里面写十几个函数每个函数上面加一段注释说明用途然后把这些函数的描述拼成一段超长的提示词塞给模型。刚开始只有三五个工具模型表现还不错选得挺准。但当工具增加到十五个以上问题就来了。第一个问题是提示词长度爆炸。每个工具的描述、参数说明、返回值格式加起来动辄两三百字十五个工具就是四五千字还没算上系统提示词和对话历史。上下文窗口被大量占用模型对每个工具的印象反而变模糊了。第二个问题是选择准确率下降。工具之间功能有重叠的时候比如“读取文件”和“搜索文件内容”模型经常搞混。第三个问题是维护困难。改一个函数的参数名要同步改注释、改提示词模板、改调用处的解析逻辑牵一发动全身。后来我意识到问题的根源在于我把“技能的定义”和“技能的调度”混在了一起。正确的做法应该是把技能抽象成一个独立的、自描述的单元每个技能自己声明“我叫什么、我能干什么、我需要什么参数、我会返回什么”然后由一个统一的注册表来管理这些声明。模型看到的不是一堆函数代码而是一份结构化的技能清单。这就是 agent-skills 最核心的设计思想。用一个生活化的类比早期的做法就像把家里所有工具都堆在客厅地上找东西全靠翻而技能注册表就像给每个工具贴了标签、放进带格子的工具箱还配了一本目录。找起来快加新工具也方便不会把已有的搞乱。2.2 一个技能单元应该包含哪些字段基于上面的思路一个合格的技能单元我通常会定义成包含以下几个核心字段的结构。这里我用一个通用的 JSON 结构来说明具体语言实现可以灵活调整{ name: read_file, description: 读取指定路径的文本文件内容返回字符串。适用于需要查看文件原始内容的场景。, parameters: { type: object, properties: { path: { type: string, description: 文件的绝对路径或相对于工作目录的路径 }, encoding: { type: string, description: 文件编码默认 utf-8, default: utf-8 } }, required: [path] }, handler: handlers.file_ops.read_file }这几个字段各有讲究。name是技能的唯一标识我习惯用下划线命名保持和代码函数名一致方便映射。description是最关键的部分它不是写给人类看的文档而是写给模型看的决策依据。所以描述里必须包含“这个技能做什么”和“什么时候该用它”最好再补一句“什么时候不该用”减少误选。parameters用标准的 JSON Schema 来描述这样模型能准确理解每个参数的类型和含义也方便做参数校验。handler指向真正的执行函数实现定义和执行的解耦。提示description 的写法直接决定模型的选型准确率。我实测下来把“适用场景”和“不适用场景”都写进去比只写功能描述选对率能提升不少。比如“读取文件”这个技能加上“不适用于读取二进制文件或超大文件”模型就不会拿它去读图片了。2.3 技能注册表解决了哪些具体问题把技能抽象成上面的结构之后我把它统一注册到一个注册表里。这个注册表本质上就是一个字典或者列表启动时把所有技能加载进来运行时供调度器查询。它带来的好处是实打实的提示词动态生成不再手写超长提示词而是从注册表里自动生成技能清单格式统一改一个技能只改一处。按需加载技能多了以后可以根据当前任务类型只把相关的技能子集塞进提示词减少干扰。参数校验前置模型返回的调用请求先经过注册表里的 Schema 校验参数不对直接拦截并返回错误信息让模型重试避免执行到一半崩掉。可测试性每个技能可以单独写单元测试不依赖模型也不依赖其他技能。这套东西听起来不复杂但真正落地的时候细节特别多。下一章我会把从零搭建这套系统的完整步骤拆开讲包括目录结构、加载流程和调度逻辑。3. 动手搭建一套可用的技能系统目录、加载与调度3.1 目录结构怎么划分才不乱我在多个项目里试过不同的目录组织方式最后稳定下来的结构是这样的。这个结构的好处是职责清晰技能定义、执行逻辑、注册调度各占一块互不干扰agent_skills/ ├── registry.py # 技能注册表核心逻辑 ├── dispatcher.py # 调度器负责选技能、校验参数、调用执行 ├── skills/ │ ├── __init__.py │ ├── file_ops.py # 文件操作类技能 │ ├── web_ops.py # 网络请求类技能 │ ├── data_ops.py # 数据处理类技能 │ └── ... ├── schemas/ │ └── skill_schema.json # 技能描述的 JSON Schema 模板 └── config/ └── settings.py # 全局配置如超时、重试次数skills目录下按功能域分文件每个文件里定义若干个技能。我强烈建议按功能域分文件而不是按技能一个一个文件。早期我试过每个技能一个文件结果文件数量爆炸找个东西要翻半天。按域分一个文件里放五到十个相关技能维护起来舒服很多。registry.py负责扫描skills目录把每个技能的定义收集起来。这里有个细节我不用自动扫描导入的方式而是显式注册。自动扫描虽然省事但容易出现“改了技能没重启导致注册表不一致”的问题而且调试时不好追踪。显式注册就是在每个技能文件末尾调用registry.register(...)启动时按顺序导入这些模块。多写一行代码换来的是可控性。3.2 技能加载的完整流程与关键参数加载流程我拆成四步每一步都有需要注意的地方导入技能模块按预设顺序导入skills下的所有模块。顺序很重要因为有些技能可能依赖其他技能的输出格式导入顺序决定了注册顺序。收集技能定义每个模块在导入时会把技能定义注册到全局注册表。注册表内部用一个有序字典存储保证顺序稳定。校验技能定义对每个技能检查name是否重复、parameters是否符合 JSON Schema 规范、handler是否可导入。这一步能在启动阶段就发现大部分配置错误。生成技能清单根据当前任务上下文从注册表里筛选出相关技能生成给模型看的清单文本。这里有个参数值得单独说技能筛选的粒度。如果技能总数少于二十个我一般全量塞进提示词简单直接。超过二十个就要做筛选了。筛选策略我常用两种一种是按任务类型打标签比如“文件处理任务”只加载file_ops下的技能另一种是用一个轻量的检索模型根据用户输入召回最相关的技能。前者实现简单后者更灵活但多一次模型调用。我实测下来技能在二十到五十个之间时按标签筛选的性价比最高。注意技能清单的文本格式要固定不要这次用 JSON、下次用 YAML。模型对格式一致性很敏感格式一变选型准确率会波动。我统一用带缩进的纯文本列表每个技能一行包含名称、描述和参数摘要。3.3 调度器如何把模型的选择变成真实调用调度器是整套系统的“执行中枢”。模型返回的通常是一个结构化的调用请求比如{skill: read_file, params: {path: /tmp/a.txt}}。调度器拿到之后要做四件事第一查表。根据skill字段去注册表里找对应的技能定义。找不到就直接返回错误让模型重新选。第二校验参数。用技能定义里的 JSON Schema 校验params。类型不对、必填项缺失、枚举值越界都在这一步拦截。校验失败时我会把具体的错误信息比如“path 字段缺失”返回给模型让它修正后重试。这个重试机制特别重要能救回大量因为模型手滑导致的失败。第三执行。调用handler指向的函数传入校验后的参数。执行时要加超时控制我一般设 30 秒超过就中断并返回超时错误。有些技能比如网络请求还要加独立的超时和重试配置。第四格式化返回。执行结果不能直接扔给模型要做一层包装。成功时返回{status: success, data: ...}失败时返回{status: error, message: ...}。统一格式能让模型更容易理解执行结果也方便后续做错误处理。def dispatch(skill_name, params): skill registry.get(skill_name) if not skill: return {status: error, message: f未知技能: {skill_name}} errors validate_params(skill.parameters, params) if errors: return {status: error, message: f参数错误: {errors}} try: result execute_with_timeout(skill.handler, params, timeout30) return {status: success, data: result} except TimeoutError: return {status: error, message: 技能执行超时} except Exception as e: return {status: error, message: f执行异常: {str(e)}}这段代码看着简单但每一行都是踩坑换来的。比如超时控制早期我没加结果一个网络请求卡住整个智能体流程就挂在那里用户等半天没反应。加上超时之后至少能快速失败并让模型换条路走。3.4 多技能协同时的顺序与依赖处理单个技能调用跑通之后真正的挑战是多技能协同。比如一个任务需要“先搜索文件、再读取内容、最后写入新文件”这三个技能有严格的先后依赖。模型有时候会一次性返回多个调用请求有时候会分多轮返回。我的处理原则是调度器只负责执行不负责规划顺序。顺序由模型自己决定调度器按模型给的顺序依次执行每次执行完把结果反馈给模型让模型决定下一步。这样做的好处是逻辑简单调度器不需要理解任务语义。坏处是如果模型规划能力弱可能会绕弯路。为了缓解这个问题我会在系统提示词里加一段“规划建议”告诉模型“对于多步骤任务先列出步骤再逐步执行”。实测下来加了这段提示后模型的多步执行成功率明显提升。还有一种情况是技能之间有隐式依赖比如“写入文件”之前必须先“读取文件”拿到内容。这种依赖关系我不会硬编码在调度器里而是通过技能描述来暗示。比如在“写入文件”的描述里写“需要先通过 read_file 获取内容”。模型看到这种提示一般会先调读取再调写入。4. 实测中暴露的问题技能选错、参数幻觉与上下文膨胀4.1 技能选错的三种典型场景与应对技能选错是我遇到频率最高的问题没有之一。细分下来有三种典型场景每种的原因和对策都不一样。第一种是功能相近导致混淆。比如“搜索文件内容”和“读取文件”如果描述写得不够区分模型经常拿搜索去读整个文件。我的对策是在描述里强化差异“搜索文件内容”强调“只返回匹配的行和行号不返回全文”“读取文件”强调“返回完整内容”。把差异点写进描述模型就能区分了。第二种是描述过于笼统。比如一个技能描述写“处理数据”模型根本不知道它能处理什么数据、怎么处理。这种描述等于没写。我的标准是描述里必须包含输入类型、输出类型和一个具体例子。比如“接收 JSON 数组按指定字段排序返回排序后的数组。例如输入[{age: 3}, {age: 1}]按 age 排序返回[{age: 1}, {age: 3}]”。第三种是技能数量过多导致注意力分散。前面提过超过二十个技能就要做筛选。我做过一个对比测试同样三十个技能全量塞进提示词时选对率大概七成按任务标签筛选到八个相关技能后选对率能到九成以上。这个差距非常明显所以技能筛选不是可选项是必选项。4.2 参数幻觉模型编造不存在的参数怎么办参数幻觉是另一个高频问题。模型有时候会返回技能定义里根本不存在的参数比如给read_file传一个format参数或者把path写成file_path。早期我没做校验直接把这些参数传给执行函数结果就是各种莫名其妙的报错。后来我在调度器里加了严格的 Schema 校验并且把校验错误信息原样返回给模型。这一步是关键模型看到“未知参数 format可用参数为 path、encoding”这样的反馈下一轮通常就能改对。我统计过加上错误反馈重试机制后参数错误导致的失败率下降了八成左右。还有一个技巧是在技能描述里明确列出所有参数包括可选参数和默认值。模型看到完整清单编造参数的概率会降低。另外对于枚举类型的参数我会把所有合法值都列出来避免模型自己发挥。提示参数校验不要只校验类型还要校验取值范围。比如一个表示“重试次数”的参数如果模型传了 1000虽然类型对但明显不合理。加上范围校验比如 1 到 5能拦住这类问题。4.3 上下文膨胀技能清单和对话历史怎么平衡上下文膨胀是个慢性问题一开始不明显项目跑久了就越来越严重。技能清单、对话历史、技能执行结果三部分都在抢上下文窗口。我的应对策略是分层管理技能清单只放当前任务相关的技能控制在十个以内。每个技能的描述精简到两行以内参数只列名称和类型详细说明放在需要时再展开。对话历史保留最近若干轮完整对话更早的做摘要压缩。摘要只保留“用户目标、已执行的关键操作、当前状态”三要素。执行结果大结果比如读取了一个大文件不直接塞进上下文而是存到外部只把摘要或前若干字符放进上下文需要时再通过技能读取。这套策略的核心思想是上下文里只放决策必需的信息细节按需拉取。我实测下来同样的任务做好上下文管理后能支撑的对话轮数翻了一倍多而且模型的选择准确率没有下降。4.4 一个完整的排查链路从现象到根因分享一个我印象最深的排查案例。现象是智能体在执行“整理文件”任务时反复调用“列出目录”技能就是不进入下一步。我按下面的链路一步步排查第一步看日志。发现模型连续五轮都返回了list_directory调用参数都一样。说明模型陷入了循环。第二步看执行结果。发现list_directory每次返回的结果格式略有不同有时候是列表有时候是带额外字段的字典。模型可能因为格式不稳定误以为每次都是新结果。第三步定位根因。检查技能实现发现这个技能在不同代码路径下返回格式不一致有的分支返回[a.txt, b.txt]有的返回{files: [...]}。模型看到格式变了以为环境变了就重新列一次。第四步修复。统一返回格式为{files: [...], count: N}并在描述里写明返回结构。修复后循环问题消失。这个案例给我的教训是技能返回格式的稳定性和技能描述一样重要。格式不稳定模型就会做出错误判断。从那以后我给每个技能都定了固定的返回结构并且写进了描述里。5. 让技能系统真正好用的几个进阶设计5.1 技能分组与按需加载的落地方式技能分组不是简单地把技能分到不同文件夹而是要在运行时能按组加载。我的做法是给每个技能加一个group字段比如file、web、data。注册表支持按 group 查询调度器根据当前任务类型决定加载哪些组。任务类型怎么判断我常用一个轻量分类器输入是用户当前这句话输出是任务类型标签。这个分类器可以是一个小模型也可以是一组关键词规则。规则版实现简单准确率也够用比如出现“文件”“目录”“读取”就归到file组。分类器不需要很准宁可多加载一组也不要漏掉相关技能。按需加载的收益在技能规模大的时候特别明显。我有个项目技能总数接近六十个全量加载时提示词超过八千字模型选型准确率只有六成左右。按组分载后每次只加载十到十五个技能提示词降到两千字以内准确率回升到九成。这个对比让我彻底放弃了“全量塞”的做法。5.2 技能执行结果的标准化封装前面提过返回格式要统一这里展开说具体怎么封装。我定义的统一结构包含四个字段字段类型说明statusstringsuccess 或 errordataany成功时的返回数据messagestring错误时的描述信息metaobject附加信息如耗时、数据量status让模型一眼判断成功失败。data放实际结果结构由各技能自己定但要在描述里写明。message只在失败时有值要写得具体方便模型修正。meta是可选的我一般放执行耗时和数据条数方便调试和做上下文裁剪决策。这个封装结构看起来多了一层但收益很大。模型不用去猜每个技能的返回格式统一按status判断按data取数。错误处理也统一了不用为每个技能写不同的错误解析逻辑。5.3 超时、重试与降级让系统在异常时也能跑技能执行难免遇到异常网络抖动、文件被占用、外部服务不可用都会导致失败。我的处理原则是能重试的重试不能重试的降级降级不了的明确报错。重试策略按技能类型区分。幂等的技能比如读取、查询可以重试两到三次每次间隔递增。非幂等的技能比如写入、删除不自动重试避免重复执行造成副作用。重试次数和间隔我放在技能定义里每个技能可以单独配置。降级策略是给关键技能准备备选方案。比如“在线搜索”失败时降级到“本地缓存查询”。降级逻辑不写在调度器里而是写在技能实现内部对外表现一致。这样模型不需要知道降级的存在调用体验不变。超时控制前面提过这里补充一个细节超时时间要按技能类型区分。本地文件操作设 5 秒足够网络请求设 30 秒涉及大模型调用的技能可能要设 60 秒。统一设一个值要么误杀快的要么放过慢的。5.4 技能版本管理与灰度发布技能多了以后改一个技能可能影响多个任务流程。我引入了简单的版本管理每个技能定义带一个version字段注册表支持同时注册多个版本调度时默认用最新版但可以通过配置指定用某个旧版本。灰度发布的做法是新版本技能先注册但不设为默认选一部分任务流量走新版本观察成功率和耗时。稳定后再切为默认。这套机制在技能频繁迭代的项目里特别有用能避免“改一个技能搞崩一片任务”的情况。版本管理还有个附带好处出问题时可以快速回滚。把默认版本切回旧版几秒钟的事不用重新部署。6. 我在实际项目里踩过的坑与总结出的经验6.1 描述写得好胜过调参调半天这句话是我做了多个智能体项目后最深的体会。很多人遇到模型选错技能第一反应是换更大的模型、调温度参数、改提示词模板。但我实测下来把技能描述写清楚收益比换模型大得多。同一个模型描述优化前后选型准确率能从六成提到九成。而换一个更大的模型提升可能只有几个百分点成本却高很多。描述怎么写算清楚我的标准是三条说清楚做什么、说清楚什么时候用、说清楚返回什么。再加一条进阶的说清楚什么时候不该用。这四条写全模型基本不会选错。6.2 不要试图让调度器变聪明我早期犯过一个错误想让调度器自动处理技能之间的依赖关系比如自动判断“写入前要先读取”。结果调度器逻辑越来越复杂各种边界情况处理不完最后变成一个难以维护的怪物。后来我想通了调度器就该做简单的事——查表、校验、执行、返回。复杂的规划交给模型模型擅长这个。调度器越简单越稳定越好调试。把智能放在该放的地方这是架构设计的一条重要原则。6.3 日志要记全但不要记敏感内容调试技能系统日志是命根子。我记录的日志包括模型返回的原始调用请求、参数校验结果、执行耗时、返回结果摘要、错误堆栈。这些信息在排查问题时缺一不可。但日志里不能记敏感内容。技能参数里可能包含用户隐私数据、密钥、内部路径这些在写日志前要脱敏。我的做法是定义一个脱敏规则对特定字段名如password、token、key自动替换成占位符。这个规则在日志模块统一处理技能实现不用关心。6.4 技能粒度太细和太粗都不好技能粒度是个需要反复权衡的问题。太细比如把“读取文件”拆成“打开文件”“读取内容”“关闭文件”模型要调三次才能完成一件事效率低还容易出错。太粗比如一个“处理文件”技能包揽读写删改模型没法精确控制参数也复杂得没法描述。我的经验是一个技能对应一个完整的、有明确语义的操作。“读取文件”是一个技能“写入文件”是一个技能“列出目录”是一个技能。每个技能做完一件事输入输出清晰。判断标准是如果这个技能的名字需要用“和”来连接两个动作那它就太粗了该拆。6.5 测试要覆盖模型行为不只是代码逻辑技能系统的测试分两层。第一层是代码逻辑测试验证每个技能的执行函数在给定输入下返回预期结果。这层用常规单元测试就行。第二层是模型行为测试验证模型在给定任务下会不会选对技能、传对参数。这层测试容易被忽略但恰恰是最重要的。模型行为测试怎么做我准备一组“任务-期望技能”的对照样本跑一遍看模型的选择是否符合预期。样本要覆盖典型场景和边界场景比如功能相近的技能、参数容易混淆的技能。每次改技能描述或调整提示词后都跑一遍这组测试看准确率有没有下降。这套测试帮我拦住了好几次“改了一处描述搞坏另一处选型”的回归问题。6.6 给后来者的几条实在建议如果你正准备动手做 agent-skills 相关的东西我最后分享几条实在的建议。第一先从五个技能以内做起把注册、调度、校验、返回这套流程跑通再逐步加技能。一上来就搞几十个技能你会被各种问题淹没分不清是架构问题还是配置问题。第二描述和返回格式的规范要一开始就定好后期再统一成本极高。第三日志和测试从第一天就要有不要等出问题了才补那时候你连问题出在哪都定位不到。第四不要追求一步到位技能系统是迭代出来的先能用再好用最后才是优雅。这套东西我摸索了挺久中间推翻重来过两次。现在回头看最值钱的不是某个具体实现而是那些“为什么这么设计”的判断逻辑。工具会变模型会变但这些判断逻辑是通用的。希望这篇内容能帮你少走一些我走过的弯路。