1. 从手搓到技能封装Agent 开发正在经历什么变化前两年做 Agent 相关的东西最头疼的不是模型能力不够而是每次换一个场景就得从头写一遍提示词、工具调用逻辑、输出解析、异常兜底。一个能跑通查天气发邮件的 Agent代码量轻松上千行而且换个业务场景几乎没法复用。我见过不少团队光是维护三五个 Agent 的提示词版本就焦头烂额更别提让非技术同事参与调优了。最近一段时间一个叫SKILL.md的东西开始在圈子里被反复提起。它的核心思路很朴素把 Agent 的每一项能力拆成独立的技能包每个技能包用一个 Markdown 文件描述清楚——这个技能干什么、需要什么输入、输出什么格式、依赖哪些工具、边界条件是什么。Agent 运行时不再靠一堆硬编码的 if-else 去调度而是读取这些技能描述动态决定该调用哪个技能、怎么组合。说白了SKILL.md想解决的是 Agent 开发从手工作坊走向标准化装配的问题。以前你是在手搓一个机器人每个关节都要自己打磨现在更像是给机器人准备一套标准接口的零件库需要什么能力就插什么模块。这个变化对独立开发者、小团队尤其友好因为你不必再为每个新需求重写整个调度层。这篇文章适合三类人看一是正在做 Agent 应用、被提示词和工具调度折磨的开发者二是想理解 Agent 工程化趋势的技术负责人三是对 AI 应用落地感兴趣、想找一个可复现路径的爱好者。我会从设计思路、文件结构、实操步骤、踩坑经验几个角度把SKILL.md这套东西拆开讲清楚尽量让你看完就能动手试。2. SKILL.md 到底解决了什么问题2.1 传统 Agent 开发的三个死结在聊SKILL.md之前得先搞清楚它针对的痛点。我总结下来传统 Agent 开发有三个绕不过去的死结。第一个是提示词与代码强耦合。你写一个客服 Agent提示词里塞满了如果用户问退款就调用退款接口如果用户问物流就查询订单状态。这些逻辑散落在系统提示、函数描述、后处理代码里改一处经常漏三处。我试过在一个中等复杂度的 Agent 里改一个退款规则结果因为提示词和代码里的条件不一致线上跑了两天才发现。第二个是能力无法复用。你为 A 项目写的网页摘要能力想搬到 B 项目发现提示词风格不一样、输出格式不一样、依赖的工具封装也不一样最后只能重写。这就像每次做饭都要重新发明一遍切菜刀效率极低。第三个是调试和协作困难。Agent 的行为是黑盒的出了问题很难定位是提示词的问题、工具的问题还是调度逻辑的问题。产品经理说这个回答不对开发者得从头到尾捋一遍才能找到原因。非技术角色基本没法参与优化因为改动都藏在代码里。2.2 SKILL.md 的核心思路把能力变成可读的契约SKILL.md的做法是把每个能力抽象成一个技能契约。这个契约用 Markdown 写人看得懂机器也解析得了。一个典型的技能文件包含几个部分技能名称和描述、触发条件、输入参数说明、输出格式约定、依赖的工具列表、边界与异常处理。这种设计的好处在于技能描述和调度逻辑分离了。Agent 的运行时只需要一个通用的调度器它读取所有SKILL.md文件根据当前任务匹配最合适的技能然后按契约执行。新增一个能力就是新增一个 Markdown 文件不用动调度器代码。我打个比方以前的 Agent 像是一个全能但混乱的瑞士军刀每个功能都焊死在刀身上SKILL.md更像是把每个工具做成标准接口的模块你需要什么就装什么而且每个模块都附了一张说明书。2.3 为什么是 Markdown 而不是 JSON 或 YAML有人可能会问为什么不用 JSON 或 YAML 来描述技能毕竟它们更结构化。我的理解是Markdown 在人机可读之间找到了一个更好的平衡点。JSON 和 YAML 对机器友好但对人不够友好。你让一个产品经理去读 YAML 里的嵌套结构他大概率会懵。而 Markdown 天然适合写说明文档技能描述里可以自然地嵌入自然语言解释、示例、注意事项这些对模型理解技能意图很有帮助。另外Markdown 的解析成本低大模型对 Markdown 格式的文本理解也更好。你可以在技能描述里直接写当用户询问订单状态时使用此技能模型能直接理解不需要额外的映射层。这一点在实际调试中非常关键因为你可以直接用自然语言微调技能行为而不用改代码。3. 一个 SKILL.md 文件的完整结构拆解3.1 技能元信息名称、版本与描述一个规范的SKILL.md开头通常是技能的基本信息。这部分看起来简单但写不好会直接影响调度准确率。# 技能名称订单状态查询 ## 版本 v1.2 ## 描述 根据用户提供的订单号或手机号查询订单的当前状态、物流信息和预计送达时间。 适用于用户询问我的订单到哪了什么时候能到等场景。这里有几个细节值得注意。技能名称要具体不要叫查询技能要叫订单状态查询这样调度器匹配时歧义更小。描述里要包含触发场景的自然语言示例这相当于给模型提供了 few-shot 的线索。我实测下来描述里带两三个用户可能说的原话匹配准确率能提升不少。版本号也别忽略。技能是会迭代的有了版本号出问题时可以快速回滚也方便追踪哪个版本的技能导致了行为变化。3.2 输入参数类型、必填与校验规则输入参数部分要写清楚每个参数的类型、是否必填、格式要求。这部分直接决定了技能能不能被正确调用。## 输入参数 | 参数名 | 类型 | 必填 | 说明 | 示例 | |--------|------|------|------|------| | order_id | string | 否 | 订单号18位数字 | 202405171234567890 | | phone | string | 否 | 手机号11位数字 | 13800138000 | | query_type | string | 是 | 查询类型status/logistics/eta | status | 注意order_id 和 phone 至少提供一个否则无法查询。用表格来写参数是我比较推荐的方式清晰直观。这里的关键是把校验规则写进描述里比如18位数字至少提供一个。调度器在调用前可以先做一轮校验避免把明显不合法的参数传给后端接口减少无效调用。query_type这种枚举参数要列出所有可选值并说明每个值的含义。我见过不少技能因为枚举值没写全导致模型猜了一个不存在的值调用直接失败。3.3 输出格式结构化约定与示例输出格式是技能契约里最容易被忽视、但实际影响最大的一部分。如果输出格式不固定下游处理逻辑就得写一堆兼容代码。## 输出格式 返回 JSON 对象包含以下字段 - success: boolean查询是否成功 - status: string订单状态待付款/待发货/已发货/已签收/已取消 - logistics: object物流信息包含 company 和 tracking_no - eta: string预计送达时间格式 YYYY-MM-DD ### 示例输出 json { success: true, status: 已发货, logistics: { company: 某快递, tracking_no: SF1234567890 }, eta: 2024-05-20 }把示例输出写进去模型在生成或解析时就有了明确的参照。这一点在让模型直接生成技能调用结果时特别有用能显著降低格式错误的概率。 ### 3.4 依赖与边界工具列表和异常处理 技能不是孤立的它往往依赖外部工具或接口。这部分要写清楚依赖什么、失败了怎么办。 markdown ## 依赖工具 - order_service.query_order(order_id/phone) - logistics_service.get_tracking(tracking_no) ## 异常处理 - 订单不存在返回 successfalseerror_codeORDER_NOT_FOUND - 接口超时重试2次仍失败则返回 successfalseerror_codeTIMEOUT - 参数缺失返回 successfalseerror_codeINVALID_PARAMS异常处理这块我的经验是错误码要统一规范。不同技能用同一套错误码体系调度器就能用统一逻辑处理失败情况不用为每个技能写特殊的兜底。比如INVALID_PARAMS、NOT_FOUND、TIMEOUT、INTERNAL_ERROR这几个基础码基本能覆盖大部分场景。4. 从零搭建一个 SKILL.md 驱动的 Agent4.1 环境准备与目录结构设计动手之前先把目录结构规划好。我推荐的结构是这样的agent-project/ ├── skills/ │ ├── order_status/ │ │ └── SKILL.md │ ├── refund_request/ │ │ └── SKILL.md │ └── logistics_track/ │ └── SKILL.md ├── tools/ │ ├── order_service.py │ └── logistics_service.py ├── dispatcher.py └── config.yaml每个技能一个独立目录里面放SKILL.md。这样增删技能就是增删目录非常清晰。tools/放具体的工具实现dispatcher.py是通用调度器config.yaml放模型配置、超时时间等全局参数。这种结构的好处是技能和实现分离。SKILL.md描述做什么tools/里的代码实现怎么做。换一个后端实现只要接口契约不变SKILL.md不用改。4.2 编写第一个技能文件我们以订单状态查询为例写一个完整的技能文件。前面拆解过的结构这里整合起来# 技能名称订单状态查询 ## 版本 v1.0 ## 描述 根据订单号或手机号查询订单状态。适用于用户询问订单进度、物流信息、预计送达时间等场景。 触发示例我的订单到哪了帮我查一下订单什么时候能发货 ## 输入参数 | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | order_id | string | 否 | 18位订单号 | | phone | string | 否 | 11位手机号 | | query_type | string | 是 | status/logistics/eta | order_id 和 phone 至少提供一个。 ## 输出格式 返回 JSON字段包括 success、status、logistics、eta。 示例略同前文 ## 依赖工具 - order_service.query_order ## 异常处理 - ORDER_NOT_FOUND订单不存在 - TIMEOUT接口超时 - INVALID_PARAMS参数不合法写完这个文件技能本身就算定义好了。接下来是调度器怎么用它。4.3 调度器如何读取和匹配技能调度器的核心逻辑分三步加载所有技能、匹配当前任务、执行并处理结果。加载部分遍历skills/目录解析每个SKILL.md提取名称、描述、参数、依赖等信息存成一个技能列表。解析 Markdown 可以用现成的库也可以自己写简单的正则提取因为格式是我们自己定的可控性高。匹配部分是关键。最简单的做法是把所有技能的名称和描述拼成一个候选列表让模型根据用户输入选择最合适的技能。提示词大概是这样你是一个技能调度器。根据用户输入从以下技能中选择最合适的一个。 技能列表 1. 订单状态查询根据订单号或手机号查询订单状态... 2. 退款申请处理用户的退款请求... 3. 物流追踪根据运单号查询物流轨迹... 用户输入{user_input} 请只返回技能名称不要解释。模型返回技能名称后调度器再读取该技能的参数定义从用户输入里抽取参数或者再调一次模型做参数抽取。参数齐全且合法就调用对应的工具执行。执行结果按技能定义的输出格式返回如果失败按异常处理约定返回错误码。整个流程里调度器代码是通用的新增技能完全不用改它。4.4 参数抽取与校验的实操细节参数抽取这一步实际做起来比想象中麻烦。用户说帮我查一下13800138000的订单你得把手机号抽出来。用户说订单202405171234567890到哪了你得抽订单号。我的做法是先用正则做一轮快速抽取手机号、订单号这种格式固定的正则命中率很高而且不消耗模型调用。正则没抽到的再交给模型做兜底抽取。这样既省成本又提准确率。校验环节按SKILL.md里写的规则来。手机号必须是11位数字订单号必须是18位query_type必须在枚举范围内。校验不通过直接返回INVALID_PARAMS不要浪费一次后端调用。这里有个细节校验失败时给用户的提示要友好。不要直接甩一个错误码而是说您提供的手机号格式好像不太对能再确认一下吗。这个提示可以写在技能的异常处理描述里让模型生成。5. 多技能协作与冲突处理5.1 技能组合什么时候需要串联多个技能单个技能能解决的问题有限真实场景往往是多技能串联。比如用户说帮我查一下订单如果还没发货就取消掉这需要先查订单状态再根据状态决定是否调用取消技能。SKILL.md体系里技能串联有两种做法。一种是调度器显式编排在配置里定义查订单→判断状态→取消订单的流程。另一种是让模型自主决策把可用技能列表给模型让它一步步选择。我倾向于混合使用主流程用显式编排保证稳定性分支和异常用模型自主决策保证灵活性。纯靠模型自主编排在复杂场景下容易跑偏而且调试困难。5.2 技能冲突同名或功能重叠怎么办技能多了之后冲突不可避免。两个技能都叫查询或者一个叫订单查询一个叫订单状态查询功能高度重叠。处理冲突的第一个原则是命名规范。技能名称用领域动作的格式比如订单-查询订单-取消物流-追踪避免泛化名称。第二个原则是描述里写清楚边界比如本技能只处理已支付订单的查询未支付订单请使用XX技能。如果还是冲突调度器可以引入优先级字段。在SKILL.md里加一个priority元信息匹配到多个技能时按优先级选。优先级高的通常是更具体、更专用的技能。5.3 技能版本管理与灰度发布技能是会迭代的。今天写的订单查询技能明天可能因为接口变更要改。版本管理就很重要。我的做法是技能目录带版本号比如order_status_v1/、order_status_v2/。调度器默认加载最新版本但可以通过配置指定使用某个版本。这样新版本上线时可以先灰度一部分流量观察没问题再全量。SKILL.md里的版本号和目录版本号要对应避免混乱。回滚时直接改配置指向旧版本目录不用改代码非常快。6. 常见问题与排查技巧实录6.1 技能匹配不准的排查思路技能匹配不准是最常见的问题。用户明明想查订单调度器却选了退款技能。排查时我一般按这个顺序看先看技能描述是否足够具体。描述太泛比如处理订单相关操作模型很难区分。改成根据订单号查询订单状态和物流信息区分度就上来了。再看触发示例是否覆盖了用户的表达方式。用户说我的包裹呢你的示例里只有查订单模型可能匹配不上。多收集真实用户问法补充到描述里。最后看候选技能数量。如果一次给模型几十个技能匹配准确率会下降。可以先用关键词做一轮粗筛把候选缩到5个以内再让模型精选。6.2 参数抽取失败的典型场景参数抽取失败常见的有几种。用户用了代词比如查一下它的状态它指什么模型不知道。这时候需要结合对话历史把上文提到的订单号带进来。用户给了多个参数比如查13800138000和13900139000的订单一次抽了两个手机号。这时候要么让技能支持批量要么让模型追问您想查哪一个。用户给的格式不规范比如手机号中间有空格138 0013 8000。正则要能容忍这些变体抽取前先做一轮清洗。6.3 输出格式不稳定的处理模型生成输出时偶尔会不按格式来。明明要求返回 JSON它返回了一段自然语言。这种情况我的经验是在技能描述里把格式要求写得更强硬比如必须返回JSON不要有任何额外文字。同时在调度器里加一层解析兜底解析失败就重试一次重试还失败就返回格式错误。另一个技巧是用示例输出锚定格式。前面提到的示例输出部分实际效果很好。模型看到具体示例比看抽象描述更容易遵守格式。6.4 性能与成本优化建议技能多了之后每次调度都要把技能列表塞进提示词token 消耗不小。优化方向有几个。技能描述精简。只保留匹配必需的信息详细的参数说明可以放在单独文件里匹配阶段不加载。分层匹配。先用轻量模型或关键词做粗筛再用大模型精选。缓存匹配结果。相似的用户输入匹配结果往往一样可以缓存起来。我实测下来一个50技能的系统经过分层匹配优化后调度阶段的token消耗能降一半以上响应速度也明显提升。7. 技能生态的扩展玩法7.1 技能市场与共享机制SKILL.md的一个潜在价值是技能可以共享。因为它是纯文本、格式统一理论上可以建一个技能市场大家把自己写的技能发布上去别人直接下载就能用。想象一下你要做一个电商客服 Agent不用从零写订单查询退款物流这些技能直接从市场拉现成的改改接口地址就能跑。这会极大降低 Agent 开发的门槛。当然共享也带来质量参差的问题。需要一套评价和审核机制比如技能要有测试用例、要有使用量统计、要有用户评分。这些是生态层面的东西但方向是清晰的。7.2 非技术人员参与技能编写SKILL.md用 Markdown 写非技术人员也能看懂、能改。这意味着产品经理、运营可以直接参与技能优化不用每次都找开发。我见过一个团队让运营同事负责维护技能描述里的触发示例因为他们最了解用户怎么说话。开发只负责工具实现和调度器。分工明确后技能迭代速度快了很多。这对团队协作模式是个不小的改变。以前 Agent 优化是开发的活现在变成了跨职能协作。技能文件成了大家共同的语言。7.3 技能与工作流的结合技能不只能被 Agent 调用也能嵌入工作流引擎。比如一个审批流程每个审批节点可以对应一个技能技能描述里写清楚审批规则和输出。工作流引擎读取技能定义自动生成审批界面和逻辑。这种结合让技能的价值超出了 Agent 本身。它变成了一种通用的能力描述标准哪里需要描述一个可执行的能力哪里就能用。8. 我踩过的坑和几条实在建议做了一段时间SKILL.md这套东西踩的坑不少挑几个印象深的说说。别一开始就追求大而全。我最初想设计一个覆盖所有场景的技能体系结果技能描述写得极其复杂匹配准确率反而下降。后来改成小步快跑先做三五个高频技能跑通了再扩效果好很多。技能描述要像写给新同事看。你想象一个新来的同事完全不了解业务只看你的技能描述能不能正确使用这个技能。如果他能看懂模型大概率也能。这个标准很实用。错误码体系早点统一。我早期每个技能自己定义错误码后来调度器处理失败时得写一堆 if-else。统一成一套基础错误码后兜底逻辑一下子清爽了。留好调试日志。技能调度是黑盒出问题时日志是唯一的线索。每次调度记录用户输入、候选技能、匹配结果、抽取的参数、执行结果。这些日志在排查问题时价值极高。定期清理僵尸技能。技能多了之后有些技能可能几个月没人用。定期统计使用量把没人用的技能归档保持技能列表精简匹配准确率也能维持。最后分享一个小技巧给每个技能写一个反例。在描述里加一句本技能不处理XX情况请使用YY技能。这个反例对减少误匹配特别有效比单纯描述我做什么更能划清边界。这套东西还在快速演进SKILL.md的格式、调度策略、生态工具都在变。但核心思路我觉得是稳的把 Agent 的能力标准化、可读化、可复用化。手搓 Agent 的时代不会完全过去但有了这套东西至少不用每次都从零开始了。