
每个做 AI 应用的人都绕不过一个问题模型很聪明但它不会干活。让它调用外部工具Prompt 写了一大堆效果还是时好时坏。后来我接触到 skill技能化的设计思路简单说就是把常用的能力封装成可复用的模块模型只需要负责“理解需求”和“调用技能”真正的逻辑交给可靠的代码去完成。这篇文章从我实际搭建一个技能库的过程出发聊聊我怎么设计目录、写元信息、做调度以及踩过的那些坑。这个方案适合正在做 Agent、工作流自动化的开发者也适合想让大模型真正落地的产品经理。我会尽量讲清楚每一步为什么这么选遇到问题怎么排查代码也都给你能直接抄的版本。1. 从“会聊天”到“会干活”技能化机制的设计思路1.1 Skills 的本质给模型装上可调用的能力模块先说概念。所谓 skills指的不是人的技能而是指一套面向大模型的“能力插件”。你可以把它理解成给模型准备一套工具箱箱子里每个工具都有一份说明书写清楚这个工具是干什么的、需要什么输入、会返回什么结果。模型读到用户的需求后先理解意图再决定调用哪个工具最后把工具结果整合成自然语言回复给用户。这套思路和传统软件开发的函数库非常像。写传统程序时我们不会把一段计算逻辑散落在各个业务代码里而是提炼成公共函数。Skills 就是大模型时代的公共函数。区别在于传统程序的调用者是程序员调用关系是写死的Skills 的调用者是模型调用关系是模型基于语义动态决定的。这一个区别决定了整个设计逻辑都要跟着变。模型不擅长精确计算也不擅长记住复杂的业务规则但它擅长理解意图、生成文本、做归纳总结。技能的初衷就是把“模型不擅长但业务需要”的事情交给普通代码把“模型擅长的事情”留给模型。比如让模型输出一段固定格式的 JSON 时直接让它生成经常出错但如果封装一个技能内部用代码来处理格式模型只负责提供原始内容可靠性就会高很多。1.2 场景拆解什么时候应该做成 Skill不是所有功能都适合封装成 skill。做得太碎模型光选择调用就晕了做得太粗技能缺少通用性复用率低。我自己的判断标准有三条第一这个功能有明确的输入输出边界。比如“把一段 Markdown 转成 HTML”输入是字符串输出是字符串边界清晰。再比如“帮我处理一下这份 Excel”输入可能是文件路径输出可能是统计结果边界同样清晰。反过来像“帮我写一篇吸引人的文案”这种事你很难定义输入输出这不是技能这是 Prompt 工程的事。第二这个功能有重复使用价值。一次性的脚本不值得封装只有团队里不同项目、不同场景会反复用到的能力才值得沉淀。我见过不少团队把一次性数据清洗脚本也包装成 skill结果技能库越来越臃肿模型每次选择时都被无关技能干扰。第三这个功能对准确性有硬性要求或者逻辑复杂度超过模型直接生成的能力。比如日期计算、金额换算、文件格式校验这些场景模型容易算错但代码可以保证百分之百准确。这些是最适合做技能的。反过来如果是写文案、做头脑风暴这类纯生成任务直接对话即可封装成技能反而画蛇添足。核心结论技能化的出发点是“把确定性交给代码把创造性留给模型”这一点贯穿了整个设计过程。你接下来看到的每一项设计决策都是在为这个核心结论服务。2. 技能库的骨架设计目录结构与元信息规范2.1 目录怎么规划才不乱我一开始搭技能库的时候习惯性地把所有脚本堆在一个文件夹里结果不到两周就乱了。技能数量一多模型根据描述选错工具的概率也会上升因为相近的功能描述看起来都差不多。后来我采用的是“分类目录 独立技能文件夹”的结构每个技能独占一个文件夹文件夹内部固定三种文件skills/ ├── text/ │ ├── format_markdown/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── requirements.txt │ └── extract_keywords/ │ ├── SKILL.md │ ├── main.py │ └── requirements.txt ├── data/ │ ├── excel_summary/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── requirements.txt │ └── csv_convert/ │ ├── SKILL.md │ ├── main.py │ └── requirements.txt └── api/ ├── weather_query/ │ ├── SKILL.md │ ├── main.py │ └── requirements.txt └── news_fetch/ ├── SKILL.md ├── main.py └── requirements.txt每个技能文件夹里SKILL.md 是给模型看的说明书main.py 是真正执行的代码文件requirements.txt 记录依赖。分类目录的作用是给“技能扫描器”一个遍历路径同时也在语义上做了一个初步聚类。这个结构最大的好处是低耦合。每个技能独立成文件夹增删技能不会影响其他技能调试时也能直接进入对应目录跑代码问题定位非常快。如果你之后想让技能支持热更新这个结构也能很自然地配合——单独监听某个文件夹的变化即可。2.2 SKILL.md 元信息怎么写模型才看得懂SKILL.md 是整个技能库的灵魂。它不是给人看的文档而是给模型看的说明书。模型根据这个文件决定“什么时候调用”“传什么参数”“期望什么返回”所以写作方式和我们平时写技术文档很不一样。我的 SKILL.md 固定分成五段name、description、when_to_use、input、output。--- name: format_markdown description: 将 Markdown 文本转换为结构清晰的 HTML 代码。适用于需要在网页端展示内容的场景。 when_to_use: 当用户提供 Markdown 文本并要求转为网页格式或者需要从 Markdown 生成邮件模板时使用。如果用户只是让简单预览效果优先使用本技能。 input: markdown_text: 字符串原始 Markdown 内容 add_toc: 布尔值是否自动生成目录默认 false output: html_code: 字符串转换后的 HTML 代码片段 title: 字符串从 Markdown 中提取的标题 ---为什么在 description 里要强调适用场景因为模型选择技能时本质是在做语义匹配。你的描述越贴近实际用户表达匹配越准。我见过很多团队把 description 写成“该功能用于 Markdown 转 HTML”结果用户说“把这个排版发到网页上”时模型完全没联想到这个技能。后来我把描述改成了包含用户话语习惯的版本匹配率明显提升。在when_to_use里我还会主动写“什么情况下不要用”。这个反常识的设计帮我排掉了大量误调用。模型是概率模型给它显式的排除条件等于给它画了一条红线比只给正向条件要稳得多。输入参数的定义也不能含糊。模型最怕的不是没有说明而是说明里存在歧义。比如“markdown_text”到底指文件路径还是内容本身我在 JSDoc 风格的注释里加了明确的“字符串原始内容”模型就不会自作聪明去填一个文件路径。2.3 参数声明与错误约定把规则前置参数声明的价值是把调用规则前置绑定到元信息里。模型看到 SKILL.md 时就知道该传什么不会运行时才出错。这里我强烈建议在技能的 main.py 里做一个“参数强制校验”类型不对直接报错而不是靠 Python 的鸭子类型隐式容忍。def format_markdown(markdown_text: str, add_toc: bool False) - dict: if not isinstance(markdown_text, str): raise TypeError(markdown_text 必须是字符串而不是文件路径或字节流) # 执行转换逻辑 ...这个校验非常重要。模型在生成参数时偶尔会把文件内容误当成文件路径传进来如果代码不做校验就会出现一连串奇怪的异常排查半天才发现是入参类型不对。把类型校验写在入口处等于防线前移。错误约定也值得提前定好。我统一规定任何技能的正常返回统一用字典包裹包含status、data、message三个字段遇到异常时status设为errormessage描述错误原因。为什么这么设计因为模型拿到返回结果后要学会判断是否调用成功。如果你只是抛一个裸异常模型通常只能看到“出错了”三个字并不知道接下来该怎么补救。但如果给它一个结构化的错误信息它就可以根据message里的内容调整输入重新调用或者向用户解释具体情况。在 SKILL.md 中我也把这些约定写进 docstring 的output段等于提前告诉模型“你会看到这样的返回结构”。这一步看着简单实际效果非常好模型处理异常的能力明显提升。3. 核心实现注册、调度与执行3.1 注册机制目录扫描还是显式注册技能写好了怎么让 Agent 知道它们的存在两种常见做法显式注册和目录扫描。显式注册就是在代码里手动 import 每个技能并添加到一个列表里。优点是完全可控不会误加载缺点是每新增一个技能就要改代码维护成本高。目录扫描是指定一个根目录程序自动遍历所有文件夹读取 SKILL.md 生成索引。优点是一键接入新技能缺点是可能加载到不完整的目录需要做好异常处理。我自己倾向用目录扫描为主显式注册作为补充。对绝大多数业务场景目录扫描省心得多。引入新技能时只要把文件夹丢进根目录下次启动 Agent 就自动识别了。from pathlib import Path def scan_skills(base_dir: str ./skills): skills [] for skill_file in Path(base_dir).rglob(SKILL.md): skill_dir skill_file.parent meta parse_skill_meta(skill_file) # 解析 YAML 头 if meta: meta[path] str(skill_dir) skills.append(meta) return skills注意这个函数里的rglob(SKILL.md)递归遍历。它可以自动按文件夹层级把技能归类。配置新的技能文件夹时整个流程是在分类目录下创建技能名/文件夹编写SKILL.md填入元信息编写main.py实现核心逻辑重启 Agent 或触发目录监听技能自动加载。整个流程中比较容易被忽略的是 SKILL.md 的 YAML 头解析。我建议解析失败时跳过该技能而不是整体报错否则一个格式错误会拖垮整个技能库。3.2 调度逻辑意图匹配与路由选择技能库就绪后下一步是让模型学会“在合适的时机调用合适的技能”。这里我用的方式是“元信息注入 模型决策”把技能列表名称、描述、参数概要作为上下文注入系统 Prompt让模型从中选择。def build_skill_prompt(skills): lines [可用技能列表] for s in skills: lines.append(f- {s[name]}: {s[description]}) return \n.join(lines)这个 Prompt 会被拼接到 System 消息末尾。模型看到这个列表后如果认为某个技能适合当前请求就会按照约定格式输出一个工具调用指令Agent 框架再据此执行对应函数。这里的核心难点是避免“技能干扰”。当技能数量超过 20 个模型经常选错因为描述相似的技能太多。我的解决办法是“两级路由”第一级让模型先确定大类text、data、api第二级再在类内选择具体技能。大类信息相当于给模型画了一棵决策树干扰项大幅减少。实测下来两级路由的准确率比单级高不少。原因很好理解模型先做粗粒度分类几乎不会错再做细粒度选择时候选集变小选错的概率自然降低。还有一种特殊情况需要兜底模型认为没有合适的技能时必须允许“不调用”直接回答用户。这一点我在设计里特意加了约定。否则模型会在不合适的场景下强行调用不相关的技能造成灾难性输出。比如用户只是在闲聊时如果系统强制“必须调用技能”模型会把闲聊话题也路由到一个技能上这就很糟糕。3.3 执行上下文与工具代码编写技能的执行上下文需要干净且确定。每个技能运行时应当只有自己所需的依赖和入参不依赖全局变量不读取其他技能文件。这样设计有两个目的一是隔离故障某个技能崩溃不会影响其他技能二是便于测试技能可以脱离模型单独跑。实际开发时我给每个技能定义了统一的执行入口def run(input_data: dict) - dict: markdown_text input_data[markdown_text] add_toc input_data.get(add_toc, False) # 业务逻辑 ... return {status: success, data: {html_code: html, title: title}, message: }Agent 框架通过反射机制调用run并传入模型生成的参数。这里有一个容易踩的坑模型生成参数时可能多传或少传键。因此我在 run 入口做了“宽容读取”必须参数用input_data[key]可选参数用input_data.get(key, default)。这样即便模型多传了没定义的参数也不会直接报错。至于技能内部的实现代码我从实践得出的建议是写得越“死”越好。不要在一个技能里尝试做太多事情比如 format_markdown 就只做格式转换不要顺带做内容摘要。模型的调度逻辑是基于技能描述而不是基于代码内部的隐藏行为。一个技能做一件事描述才好写模型才好判断测试也好写。执行完成后结果需要做一次序列化。我在返回字典前统一调用json.dumps确保没有非法对象。因为模型看到的返回内容最终要作为 Prompt 的一部分Python 对象不能直接参与拼接必须先转字符串。如果数据量特别大我会在返回前做截断只保留前几千字符防止撑爆上下文窗口。4. 真实效果评估从测试用例看技能化的收益4.1 一个完整的工作流示例为了验证这套架构我用它搭了一个“周报生成助手”。这个助手接收团队成员的原始工作记录自动完成分类、去重、格式化和摘要生成。项目涉及三个技能format_markdown、extract_keywords、excel_summary。整个工作流是这样的用户上传一份 Excel内容是成员填写的原始工作记录Agent 调用excel_summary技能读取 Excel 中的每行记录做基础清洗Agent 调用extract_keywords技能提取每条记录里的关键词Agent 调用format_markdown技能把清洗后的记录转成格式化的周报 Markdown 文本Agent 根据关键词和原文生成最终总结段落。这个流程里每个技能只负责一个明确的动作模型在整个流程中负责“串联”和“生成”而清洗、格式转换等确定性工作完全交给代码。即使某个成员的记录填写得乱七八糟excel_summary的代码也能保证输出结构稳定不会因为文本太乱而崩溃。4.2 数据对比有技能库和纯 Prompt 的差别我拿同样一批数据分别用两种方式做了对比测试一种是把所有处理逻辑堆在一个巨大的 Prompt 里让模型直接做另一种是用上面的技能库框架来跑。虽然这个对比不算严格意义上的实验但结果具有一定的参考价值。纯 Prompt 方式下格式违规率高得惊人由于模型需要自己记住输出格式一旦上下文稍微长一点格式就开始飘。比如分号错用成冒号、括号不匹配、日期格式从2025-01-01变成2025/1/1。而在技能化方案里格式转换由代码完成格式违规基本为零。时间开销方面纯 Prompt 方式单条任务平均需要 8 秒技能化方案需要 12 秒左右。多出的时间主要是模型选择技能和生成参数的开销但这部分换来的是稳定性和可控性。对生产环境来说多 4 秒换取格式一致性是完全划算的买卖。当然如果对延迟极其敏感可以考虑给模型加“缓存最近常用技能”的机制把选择时间压缩到接近零。成本方面技能化方案比纯 Prompt 模式的 Token 消耗略高因为技能描述和参数说明也要占上下文空间。但换来的是失败重试次数的下降整体成本反而更低。我统计过一周的调用记录技能化方案的重试率只有纯 Prompt 的六分之一这一项省下的 Token 远超技能描述的开销。4.3 边界情况处理当技能不够用时怎么办无论技能库设计得多丰富总会有覆盖不到的场景。我的处理策略是给 Agent 加一层“未找到技能”的兜底逻辑如果模型判断当前请求没有匹配技能就启动默认对话模式。这句话写起来很容易做好却不简单。模型常犯的错误是“强行匹配”明明用户只是想闲聊它却非要调个天气查询技能出来。为了抑制这个问题我在两级路由之外又加了一条硬性规则当大类匹配置信度低于阈值时直接跳入兜底对话。这个阈值一般设为 0.6 左右设置太高会忽略合理呼叫太低则兜底机制形同虚设。另一个边界情况是技能执行成功但结果异常。比如excel_summary运行结束得到空表这不算代码异常但结果确实无意义。此时技能返回的message字段就要发挥作用我写上“未检测到有效数据请检查文件格式”。模型读取后就会主动向用户表达这个信息而不是硬编一段看似正常的总结。5. 踩坑记录与排查速查表5.1 技能调用了但没生效问题往往出在哪技能写好后调试过程依然会遇到各种问题。我把自己踩过的坑整理成一张速查表每一条都对应一个真实的排障经历症状常见原因排查方法模型不调用技能SKILL.md 中的 description 与用户表达不匹配替换为更贴近用户话语的描述补充 when_to_use 场景技能报了 TypeError模型把字符串传成了文件路径在 main.py 入口加类型强校验并在 SKILL.md 参数说明处显式标注返回结果超大没有做数据截断撑爆上下文对返回数据限长超过阈值只保留摘要同一技能被反复选错description 和其他技能太相似在 when_to_use 中增加排除项或改用两级路由新增技能不生效目录扫描没覆盖新路径确认新文件夹放在扫描根目录内SKILL.md 命名不能错技能执行正确但 Agent 不展示结果模型没有收到结构化的输出提示返回值必须包含status、data、message三段方便模型理解这个表格的第一行也是我最常碰到的问题。很多人以为自己写的技能描述够清楚了但模型“看”这个词的方式和人不一样。它匹配的是语义空间里的相近概念如果你的描述用了太多技术黑话而用户用的是大白话两者在语义空间里距离很远匹配自然失败。所以我的建议是描述技能时把用户可能使用的多种表达方式都写进去而不是只写一个规范名称。第三行的输出过大问题也很典型。技能库跑了一段时间后数据规模会变大比如传入一个 10 万行的 Excelexcel_summary如果返回全部处理结果上下文直接爆掉。解决方案是在技能内部设定输出上限只返回前 N 条结果加统计概览并提示用户“数据量过大仅展示前 100 条如需全量导出请使用导出技能”。5.2 几条独家建议给新上手的人最后分享几条我在反复折腾中总结出来的经验不一定都写在教科书里但确实对我的项目帮助很大。第一技能描述一定要让“不知道这个技能存在的人”来读一遍。我后来发现让团队里没参与开发的同学只看 SKILL.md然后问他“什么情况下会用这个技能”如果他说不准说明描述写得不对。这个反馈循环比任何调试工具都有效。第二技能粒度宁小勿大。一个技能只做一件事不要试图在一个技能里同时完成清洗、转换、汇总、生成四个步骤。模型调度时靠的是技能描述不是你的代码注释。一个技能做的事越多描述就越泛模型就越容易在错误的场景下调用它。第三保留完整的调用日志。我在 Agent 框架里加了一行日志输出记录下每次模型选中的技能名称和入参。这个日志在后排查时极其重要你不需要去猜模型为什么选错直接翻日志就能看到它的“思考路径”。最初几周我几乎每天都要靠日志来反向修正 SKILL.md 的描述文字。第四给每个技能写一个脱离模型的小测试用例。我每次写完 main.py都会单独跑一遍用固定的输入验证输出是否符合约定。这一步看起来多余实际上节省了大量联调时间。没有测试用例的技能一旦被模型调用出了问题你根本分不清是模型选错了还是技能逻辑写错了。5.3 从“能用”到“好用”的进阶思路技能库搭起来以后接下来可以往三个方向做深。第一个方向是技能依赖图。当技能之间出现组合调用时比如先清洗再摘要可以显式声明技能间的依赖关系让模型按照图路径按顺序执行而不是靠它自己临时判断顺序。第二个方向是技能版本管理。我在本地用 Git 给每个技能文件夹单独建仓版本变化可追踪。这样一旦新版本效果不好可以直接回滚到上一个稳定版本不用整体回滚。第三个方向是技能的反馈闭环。我在技能返回结构里增加了一个suggestion字段当技能执行失败时代码会给模型提供一个“建议修正方向”。比如excel_summary发现格式不对就建议模型提示用户“文件应包含表头且第二列为日期格式”。这个机制让技能具备了自我纠偏能力生产环境里的用户满意度提升非常明显。根据我的实际体验技能化的投入产出比很高尤其是团队里已经有成型的 Python 工具脚本时把这些脚本包装成技能几乎是“零成本”的重新组织——本质上是给你原来写过的逻辑加一层说明书和调用壳。你不需要发明新东西只需要设计好接口让模型能够可靠地驱动这些接口。能完成这一步你的 Agent 距离真正“会干活”就不远了。