AI Agent技能管理:用结构化文件实现轻量技能库与动态加载机制 如果你做过一段时间的 AI Agent大概率遇到过这个尴尬模型本身很聪明但翻来覆去就是记不住那些放不进一句话指令里的操作细节。我最初也把各种能力写成提示词片段塞进系统提示词里结果 prompt 从几百行膨胀到几千行改一个动作就像在雷区里布线牵一发动全身。后来被逼着做了个项目就叫agent-skills。它不是什么新框架而是一套把“技能”从 Agent 主体里拆出来、按结构化文件管理、由模型按需加载的轻量方案。这个项目能解决的问题很具体技能不再散落在代码分支和 prompt 字符串里而是收敛到独立目录模型不再靠“碰运气”去调用能力而是通过统一格式感知到“有哪些技能、什么时候该用、参数怎么传”。适合想自己搭建 Agent 轮子、又不想被重型框架绑架的开发者参考也适合已经在用 Agent 但经常被“漏调用”“乱调用”困扰的团队。下面我会从设计思路、技能文件结构、加载器实现、常见问题几个方面完整拆一遍这次实操的沉淀。1. 项目整体设计与思路拆解1.1 核心问题技能散落导致 Agent 不可控先说为什么要单独做一个 agent-skills。我早期做 Agent 时技能基本以三种形态存在第一直接写在系统提示词里比如“当你需要查天气时调用 weather 函数”第二以大量 if-else 分支硬编码在调度代码里第三散落成若干个外部脚本靠模型偶尔想起来拼出完整命令。这三种形态各有各的问题。提示词里的技能描述多了以后模型注意力会被稀释经常出现该调用的时候不调用、不该调用的时候乱调用硬编码分支虽然稳定但每加一个新能力都要改主流程代码被各种业务条件塞满外部脚本则完全没有索引模型不知道它们的存在自然也不会主动用。更麻烦的是技能一多测试和排查只能靠翻日志猜改了 A 技能还可能影响 B 技能的触发条件。agent-skills 的核心设计就是把技能从这些地方“抠”出来变成一个目录 约定文件的结构每个技能有自己的目录目录里包含说明文件、实现脚本、参数定义、示例。Agent 启动时扫描目录生成一份能力清单后续模型就是拿着这份清单去决策。这样做的好处是技能之间天然隔离新增技能只需要新建目录不需要动主循环。1.2 方案选型为什么用结构化文件而不是纯函数设计之初我犹豫过既然最终都要执行代码为什么不直接写成 Python 函数再用装饰器注册这样不是更简单吗试过之后发现纯函数方案有一个致命短板模型感知不到函数内部实现它只能通过“函数名 一句话签名”判断要不要调用。当函数数量超过几十个时光靠装饰器里的 docstring模型很难稳定区分两个功能相近的函数触发冲突率会显著上升。后来我参考了“工具描述 参数 schema”的思路把技能拆成三个部分人读的描述文档SKILL.md、机器读的参数定义schema.json、实际执行逻辑main.py。模型决策时主要看描述文档和 schema执行时才加载 main.py。这和“菜单式点菜”很像菜单要把每道菜的原料、口味、适合场景写清楚后厨才根据订单做菜。如果菜单信息模糊客人就会点错菜。我把纯函数、prompt 内嵌、结构化文件三种方案做过一次对比结论很直观方案模型感知成本新增技能成本运行稳定性调试友好度纯函数注册低但易混淆中高中提示词内嵌极低随篇幅下降低低差结构化文件高低高好最终选定结构化文件核心原因是它把“模型的注意力”当成稀缺资源来对待。我们做的每一次技能描述都是在帮模型降低决策时的认知负担。描述写得好不好直接决定模型选得准不准这一点光靠代码注册很难做到。1.3 技能与工具、工作流的边界划分做 agent-skills 之前我还经常搞混“技能”“工具”“工作流”三个词。如果不把边界划清楚目录结构就会变成一锅粥。我现在的定义比较朴素工具是最小的原子操作比如“发送邮件”“读取文件”“调用某个接口”它只做一件事不关心上下文技能是为了完成某个子目标而组合出来的能力比如“整理日报”它可能要读数据、渲染模板、然后发送邮件工作流则是多个技能加上业务规则、状态流转的编排比如“每天早上自动生成日报并发送给指定人”这已经不是单个技能能覆盖的范畴了。用生活里的例子类比工具箱里的扳手、螺丝刀是工具会修水龙头算是一项“技能”而“根据业主电话诊断问题、带工具上门、修完做测试”就是工作流。agent-skills 主要管中间那一层也就是技能。它向上承接工作流的调度向下封装工具的调用。边界划清以后技能目录的粒度反而好定了如果一个技能需要用户反复确认“数据的起始时间和截止时间”那它可能更接近工作流应该再拆细一点如果一个技能小到只是读取一个文件那它应该归属于工具层不应该单独立项。我在实际迭代中一直守着这个粒度少走了很多弯路。2. 技能库的核心细节与实操要点2.1 技能文件的目录与字段设计agent-skills 的技能目录长这样skills/ 日报生成/ SKILL.md main.py schema.json examples/ case1.json case2.md 数据汇总/ SKILL.md main.py schema.json 文件归档/ SKILL.md main.py schema.json其中 SKILL.md 是给模型看的也是整个系统里最关键的“人机接口”。它的编写规范我经过了好几轮调优才稳定下来。初始版本我写得很随意模型老是选错后来慢慢沉淀出一套固定字段。SKILL.md 的字段表格如下字段是否必填作用说明name是技能名称尽量是动宾结构如“生成日报”description是模型决策依据说明什么时候用、什么时候不用arguments是参数定义以 JSON Schema 格式写在 schema.json 里returns否返回说明描述返回给模型的内容格式env否运行依赖所需环境变量或外部服务tags否分类标签便于按场景索引version否版本号技能变更追踪用main.py 是实际执行入口我统一约定一个run(params)函数params 是一个字典执行完返回一个字符串或可序列化的结构。schema.json 则直接定义参数格式。这两层分离的一个额外好处是模型只接触 SKILL.md 和 schema.json不会看到实现代码某些包含密钥或内部逻辑的技能也能更安全地暴露给 Agent。2.2 描述文本怎么写才能被模型稳定命中这部分属于“看着简单做起来全是坑”的核心环节。大语言模型不是靠代码编译去理解技能的它靠的是对自然语言描述的语义匹配。也就是说描述文本里出现的动词、场景词、边界词直接决定了模型能不能在正确时机命中技能。我总结了一套描述公式在[什么场景/任务]下当[关键条件]时使用本技能[做什么]。如果[什么情况]请不要使用本技能。比如一个“生成日报”的技能我会写成name: 生成日报 description: | 在用户需要查看项目进展、汇总当日工作、生成日终报告时使用。 当用户提供日期或提到“日报”“日终总结”时优先使用本技能。 如果用户只是询问数据走势没有要求生成报告请不要使用本技能。注意最后一句边界描述它非常重要。我早期没有写负向约束模型经常在用户说“给我看下今天的数据”时直接触发日报生成最后生成了一堆客户根本没想看的总结。后来我统一补齐了“不要使用”的描述误触发率下降了一大截。此外我还会在 SKILL.md 里加一到两个典型示例。示例不需要长两句就够比如示例 - 用户说“帮我写一下今天的日报”输出结构化 Markdown 报告。 - 用户说“把最近三天的数据整理成日报发我邮箱”先生成报告再发送邮件。这个细节能让模型更快建立“文本到技能”的映射尤其当两个技能描述相似时示例是区分它们的最有效手段。2.3 参数定义与输入校验参数定义我直接采用的是 JSON Schema 的一段子集没有引入额外依赖。schema.json 的结构如下{ type: object, properties: { date: { type: string, description: 日报对应的日期格式 YYYY-MM-DD默认值为当天, enum: [2024-01-01, 2024-01-02] }, include_metrics: { type: boolean, description: 是否包含核心指标模块, default: true } }, required: [date] }这里踩过一个大坑如果只给 type不给 description模型就很容易传错值。比如 date 字段模型可能传成“今天”“昨天”这种相对表达或者传成时间戳。所以我在每个 property 的定义里都写清“格式是什么、默认值是什么、从用户哪句话里提取”这对提升参数命中率帮助很大。参数校验我放在执行器里不放在 main.py 里。原因是 main.py 更关注业务逻辑校验逻辑集中起来才能统一处理错误。校验失败时返回给模型的错误信息必须明确不能只是一个“bad params”的笼统报错我会拼出具体是哪个字段不合法、期望什么格式、实际收到了什么值。这样模型在下一轮对话里还能自纠。3. 实操过程从零搭建一个技能加载器3.1 准备目录与装载逻辑有了技能目录和约定格式接下来要写一个轻量加载器。我用 Python 实现核心逻辑就是递归扫描skills/目录读取每个技能的子文件然后构建技能索引。import json from pathlib import Path from typing import Dict def load_skills(base_dir: str skills) - Dict[str, dict]: skills {} base Path(base_dir) for skill_dir in base.iterdir(): if not skill_dir.is_dir(): continue skill_md skill_dir / SKILL.md schema_file skill_dir / schema.json main_file skill_dir / main.py if not (skill_md.exists() and schema_file.exists() and main_file.exists()): continue skills[skill_dir.name] { name: skill_dir.name, description: skill_md.read_text(encodingutf-8), schema: json.loads(schema_file.read_text(encodingutf-8)), main: skill_dir / main.py, dir: skill_dir, } return skills这段代码不长但有一个细节很重要skills的 key 用的是目录名而不是 SKILL.md 里的 name 字段。目录名是唯一标识name 字段更像给人或模型展示的名字。两者不一致时以目录名为准避免重名冲突。目录装载完成后我会把技能列表压缩成一段“总清单”交给模型。清单里不包含 main.py 实现只包含每个技能的名称和 SKILL.md 的描述前几十个字符。模型决策只看清单真正执行时才 import 对应脚本。3.2 技能注册与动态加载加载器写完后要解决的是“何时重新扫描”的问题。Agent 进程如果常驻技能目录很可能在运行中新增或修改。最省事的办法是每次调用前全量扫描但技能多了以后会浪费不必要的磁盘 I/O。我折中了一下给每个技能目录记录文件哈希值调用时先看哈希有没有变化有变化才重新加载。import hashlib def _dir_hash(path: Path) - str: h hashlib.md5() for f in sorted(path.iterdir()): if f.is_file(): h.update(f.read_bytes()) return h.hexdigest()注册逻辑我用一个简单字典维护技能对象包括meta元信息、schema、runner三个部分。runner 是动态 import main.py 后拿到的run函数。注意 import 之后要用importlib.reload处理修改场景否则同一个模块只会被加载一次后续改代码不生效。import importlib.util def _load_runner(main_path: Path): spec importlib.util.spec_from_file_location( main_path.stem, main_path ) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module.run这一层看起来简单但它是“动态加载”的根基。很多 Agent 框架把技能写死在代码里本质是放弃了热更新能力。而我这边改一个技能实现只需要替换对应目录下的 main.py下次调用自动生效这在实际迭代中非常省事。3.3 与 Agent 主循环的对接方式有了技能索引接下来要把它接入 Agent 主循环。我没有用复杂的消息总线而是走一个非常轻的协议Agent 拿到用户消息Agent 读取技能清单Agent 决策可能直接回复也可能请求调用某个技能Agent 请求调用时会带上“技能名 参数”执行器校验技能名和参数调用run得到结果执行器把结果回传给 AgentAgent 根据结果继续生成回复。伪代码大致是这样def handle_turn(user_message, skill_registry, llm): skill_list [{ name: s[name], description: s[description], arguments: s[schema], } for s in skill_registry.values()] prompt build_prompt(user_message, skill_list) response llm.complete(prompt) if response.tool_call: skill_name response.tool_call.name params response.tool_call.arguments if skill_name not in skill_registry: return 错误技能不存在 result skill_registry[skill_name].run(params) final_prompt build_final_prompt(user_message, result) return llm.complete(final_prompt) return response.text这里的核心思想是“模型只输出意图不执行代码”。技能列表只是决策上下文真正的执行由可信的执行器完成。这样做一方面防止模型生成的不安全代码被直接执行另一方面也方便在调用前后加日志、限流、审计。3.4 一次完整的调用链路示例为了更直观地展示效果我模拟一个请求链路。用户输入是“帮我生成昨天的日报只看核心指标。”执行链路如下第一步Agent 读取技能清单发现“生成日报”技能描述里明确说明“用户提供日期或提到日报时优先使用”。模型判断符合条件生成技能调用请求。第二步执行器收到请求技能名是“生成日报”参数是{date: 2024-06-10, include_metrics: true}。第三步执行器校验 schemadate 格式合法include_metrics 是布尔值进入执行。第四步main.py 里的run函数读取数据源、计算指标、渲染模板返回一段 Markdown 报告字符串。第五步执行器把返回值交给模型模型基于报告生成一段自然语言摘要补上“昨天核心指标整体向上其中新增用户较前日提升 12%”这类解读。最终呈现给用户的内容是“报告 解读”而不是冷冰冰的数据。这个过程里技能只负责“生成”模型负责“理解与表达”分工非常清晰。结构化的调用链让每一步都可观测、可回放、可测试。4. 常见问题与排查技巧实录4.1 模型总是选错技能怎么办这是我在使用 agent-skills 过程中最常被问的问题。选错技能通常表现为用户明明问数据汇总模型却调了日报生成同一个功能有两个相似技能模型每次都用错。排查顺序我固定为三步。第一步看两个技能的 description 语义是否重叠第二步看名称是否容易混淆第三步看示例是否写清了区分边界。一旦找到重叠点就给其中一个加上更严苛的边界描述比如“仅当用户明确提到‘终稿’时使用导出草稿请使用另一个技能”。还有一种情况是模型在犹豫时倾向于调用“默认技能”。解决方法是把更通用的技能排在描述清单前面或者把容易混淆的技能名改成表意更明确的长名称。比如“文件归档”和“数据快照”容易混淆我就把后者改成“按时间点保存数据快照”模型识别度立刻提高。4.2 技能之间的命名冲突随着技能数量增长目录名重复是迟早的事。比如团队里两个人分别做了“导出报表”但导出格式完全不一样。我在加载器里做了一个“命名空间隔离”机制技能目录名允许重名但会把父级分类目录作为前缀。skills/ 财务/ 导出报表/ 运营/ 导出报表/加载时生成技能 key 为财务/导出报表、运营/导出报表。模型看到的是带分类前缀的技能名自然能区分。如果确实需要全局唯一的短名字我支持在 SKILL.md 里加一个alias字段给模型提供一个更友好的别名。但运维排错时永远用完整路径。4.3 参数类型不匹配与隐式转换技能执行时报错常见原因就是模型传参类型不对。比如 schema 里要求date是字符串模型却给了一个数字时间戳。做严格校验当然可以但会导致假失败增多。我采用的策略是“显式转换 失败兜底”。执行器会维护一张转换表字符串、整数、浮点数、布尔值之间的常见写法都做一次隐式转换尝试。比如布尔值模型传true或True都能接受日期字段模型传时间戳时会尝试转成YYYY-MM-DD。如果转换失败执行器不会直接崩溃而是把错误信息拼好回传给模型让模型自行修正后重新调用。这里有个容易忽略的小细节隐式转换不能“闷头改”。我要求每条转换逻辑都必须输出日志例如“date 参数由 1718000000 转换为 2024-06-10”这样排错时能分清是谁的锅是模型传错还是转换逻辑设错了。4.4 技能调试的“黑盒”问题最常遇到的调试场景是“模型没调用技能但用户觉得它应该调用”。为了还原现场我在执行器里加了三层日志调用前打印“即将调用技能 X参数为 Y”执行后打印“返回值为 Z耗时 T”另外对所有旁路分支也打印决策原因。比如模型因描述不足没有调用技能日志里会清晰看到模型只生成了文本回复执行器根本没有收到调用请求。你要做的事情就聚焦到“为什么描述没被命中”上而不是在代码里海捞。另外一个实战技巧是调试时把模型温度固定为 0并固定随机种子。否则同一个 prompt 跑五次模型行为都不一样根本没法对比改动效果。我通常在技能库的调试配置里直接写上固定参数上线前再放开这个小小习惯帮我省了大量反复验证的时间。5. 写在最后几个让我少踩坑的习惯抛开技术细节agent-skills 这个项目给我最大的收获不是代码写得多漂亮而是建立了一套“技能也是一种产品”的思维。每一个技能都要有明确的使用边界、参数说明、示例场景这和给用户写功能文档没有本质区别。技能数量少的时候可能感觉多余但一旦超过二十个你就能体会到结构化的力量。我再分享两个一直在坚持的习惯。第一个是每次新增技能强制自己先写 SKILL.md 再写 main.py描述都写不清晰的能力不配被模型调用第二个是每个技能都配一个最小可运行的 example 文件它既是模型少样本示例又是回归测试的基线。后面我做技能重构时就是靠这些 example 文件保证行为不一致时能第一时间被发现。如果你也在被 Agent 技能管理折磨不妨从一个小目录开始把最常用的三五个能力先拆出来跑通加载和调用链路再慢慢扩展。不必一开始就追求完美让模型先稳定命中一个技能再复制这套模式去覆盖更多场景这条路是走得通的。