
1. 为什么第一个 Agent Skill 必须从 SKILL.md 开始写——不是格式问题是思维锚点“把重复任务写成第一个 Agent SkillSKILL.md 怎么设计”这个标题里藏着一个被多数人忽略的关键真相SKILL.md 不是文档而是 Agent 的最小可执行契约。我带过二十多个跨行业 Agent 项目从电商客服自动归因、金融风控规则编排到高校教务系统课表冲突检测所有跑通的团队第一份真正落地的产出都不是 prompt、不是 workflow、更不是 config.yaml而是一份命名规整、结构清晰、能被任意 agent 框架直接加载的SKILL.md。它之所以必须是.md不是因为 Markdown 多好读而是因为它天然具备三个不可替代的工程属性人类可编辑、机器可解析、版本可追溯。你打开一个 Excel 表格改三行数据Git diff 显示的是二进制乱码但你在 SKILL.md 里加一句# 适用场景订单超时未支付自动催付Git commit 记录清清楚楚PR review 时同事一眼就能判断这个 skill 是否越权。这背后其实是 agent 开发中一个残酷现实90% 的失败不来自模型能力不足而源于 skill 边界模糊、责任不清、变更失控。我亲眼见过一个团队花两周调优 LLM 输出格式结果上线后发现真正卡住流程的是某个 skill 在处理“用户地址含生僻字”时没定义 fallback 逻辑导致整个 agent 执行链在第三步就静默终止——而这个问题本该在写 SKILL.md 的第一版就用## 异常兜底小节写死。所以当你问“怎么设计”本质是在问如何用最轻量的文本把一个活生生的业务动作压缩成 agent 能理解、能复用、能审计的原子单元。它不是说明书是接口协议不是笔记是服务契约不是学习材料是生产环境里的第一块路标。你写的不是 markdown是你和 agent 之间的第一份劳动合同。2. SKILL.md 的骨架不是模板是四层责任切片很多新手一上来就找“标准模板”抄个 header、description、input/output 就完事。结果写出来的文件agent 加载报错人看半天不懂测试用例写不出来。问题出在没理解 SKILL.md 的核心设计哲学它必须同时满足四类角色的阅读需求且每类角色只关心其中一层。我把这四层叫“责任切片”它们像洋葱一样层层嵌套缺一不可但每一层的写法、粒度、术语都完全不同。下面拆解真实项目中反复验证过的结构逻辑不是教条是血泪教训换来的分层指南。2.1 第一层机器可识别的元数据区YAML Front Matter这是 agent 框架启动时最先扫描的部分必须严格遵循 YAML 语法且字段名不能随意发挥。我见过最典型的错误是把version: 1.2写成version: 1.2导致框架解析为浮点数而非字符串后续做语义版本比对直接失败。正确写法如下--- id: order_timeout_reminder name: 订单超时未支付自动催付 version: 1.3 category: customer_service tags: [payment, reminder, timeout] author: zhangsancompany.com created_at: 2024-06-15 updated_at: 2024-07-22 status: production ---提示id字段是全局唯一标识必须小写下划线禁止空格和中文它是 agent 内部路由、日志追踪、权限控制的根 IDstatus只能是draft/review/production/deprecated四种框架会根据此值决定是否允许被 workflow 调用tags是未来做 skill 检索、智能推荐的基础别偷懒只写一个比如payment和reminder应该并存因为运营同学可能搜“催付”技术同学可能搜“支付”。2.2 第二层人类可理解的业务契约区H2 标题 自然语言这一层是给产品经理、业务方、测试工程师看的他们不关心代码只关心“这玩意到底干啥、在啥情况下干、干不好会怎样”。必须用完整句子禁用任何缩写或内部黑话。例如不要写“触发催付”要写“当订单创建超过 30 分钟且支付状态仍为‘待支付’时向用户发送包含订单号、剩余支付时限、一键跳转链接的短信提醒”。我坚持要求团队在这个区域写满三句话第一句说清楚触发条件时间、状态、数据源第二句说清楚执行动作发什么、给谁、含哪些关键字段第三句说清楚成功标准用户收到短信、短信内链接可点击、跳转后页面显示正确订单详情。这三句话就是后续所有自动化测试用例的原始输入。曾经有个项目因为这里只写了“通知用户”测试同学按“站内信”设计用例结果上线后实际走的是短信通道导致漏测了运营商网关超时场景凌晨三点告警炸群。2.3 第三层机器可执行的接口契约区H2 标题 结构化 Schema这一层是给开发、SRE、LLM Router 看的它定义了 skill 如何被调用、输入输出长什么样、每个字段的约束是什么。这里最容易犯的错是把 JSON Schema 当作文档写堆砌一堆type: string,required: [...]却忘了加最关键的example和description。没有 example 的 schema等于没写。正确示范## 接口定义 ### 输入参数Input | 字段名 | 类型 | 必填 | 描述 | 示例 | |--------|------|------|------|------| | order_id | string | 是 | 电商平台唯一订单号长度 16-32 位仅含数字与字母 | ORD20240722A8B9C0D1 | | user_phone | string | 是 | 用户手机号11 位纯数字需通过运营商号段校验 | 13800138000 | | timeout_minutes | integer | 否 | 自定义超时时长分钟默认 30范围 5-120 | 45 | ### 输出结果Output | 字段名 | 类型 | 描述 | 示例 | |--------|------|------|------| | sms_sent | boolean | 短信是否成功发出 | true | | sms_id | string | 短信网关返回的唯一回执 ID | SM1122334455667788 | | error_code | string | 错误码仅当 sms_sentfalse 时存在 | INVALID_PHONE |注意error_code的枚举值必须在此处穷举如INVALID_PHONE、SMS_QUOTA_EXCEEDED、GATEWAY_TIMEOUT不能写“其他错误见日志”。这是 agent 编排层做重试策略、降级开关的唯一依据。我们曾因漏写GATEWAY_TIMEOUT导致当短信网关抖动时agent 无法识别此错误盲目重试 3 次最终触发运营商风控限流。2.4 第四层可审计的运行保障区H2 标题 场景化清单这是给 SRE、安全合规、法务看的回答“这玩意上线后我怎么知道它没乱来、没越权、没泄露”。它不讲功能只讲边界、约束、证据。必须包含三项硬性内容数据权限声明、异常兜底路径、审计日志字段。例如## 运行保障 ### 数据权限 - 仅读取 orders 表的 order_id, user_id, created_at, status 字段 - 仅写入 sms_logs 表写入字段为 sms_id, order_id, user_phone, sent_at, status - **禁止访问** users 表的 id_card, bank_account 等敏感字段。 ### 异常兜底 - 当 user_phone 格式校验失败记录 ERROR_INVALID_PHONE 日志返回 sms_sentfalseerror_codeINVALID_PHONE**不抛出异常** - 当短信网关返回 503 Service Unavailable立即停止本次调用返回 sms_sentfalseerror_codeGATEWAY_UNAVAILABLE**触发告警 SKILL_SMS_GATEWAY_DOWN** - 当 timeout_minutes 超出 [5,120] 范围强制截断为 30记录 WARN_TIMEOUT_OUT_OF_RANGE 日志**继续执行**。 ### 审计日志 每次执行必须生成一条结构化日志包含以下必填字段 - skill_id: order_timeout_reminder - input_hash: 对输入参数做 SHA256 哈希保护原始手机号不落盘 - output_status: success 或 failed - duration_ms: 执行耗时毫秒 - trace_id: 全链路追踪 ID与上游 workflow 对齐这四层切片不是可选项是 SKILL.md 的刚性结构。少一层就意味着某类关键角色在协作中必然出现信息盲区迟早引发线上事故。我把它刻在团队 Wiki 首页“写不完四层不准提 PR”。3. 设计 SKILL.md 的五个致命陷阱与破局点光知道结构还不够。我在 Review 近三百份 SKILL.md 时发现有五个高频陷阱几乎每个新人至少踩中两个。这些不是语法错误而是思维惯性导致的设计缺陷必须用具体方法破局。3.1 陷阱一把 Skill 当成 Prompt 来写混淆“指令”与“能力”典型症状SKILL.md 里大段复制粘贴 system prompt比如你是一个专业的客服助手请用亲切友好的语气...。这是最危险的起点错误。Prompt 是给 LLM 的临时指令Skill 是 agent 的长期能力资产。前者随对话上下文漂移后者必须稳定可预期。破局点只有一个SKILL.md 里永远不出现任何语气、风格、人格化描述。它的职责是定义“做什么”和“怎么做”而不是“像谁一样做”。比如一个“生成商品推荐理由”的 skill其## 接口定义应该明确输入是product_list: [ {id, name, category, price} ]输出是reasons: [ {product_id, text} ]text 字段的约束是“不超过 50 字必须包含价格优势或品类独特性关键词”而不是“请用活泼可爱的语气写”。语气是上层 workflow 或 agent router 根据用户画像动态注入的不是 skill 本身的责任。我强制要求团队删掉所有be friendly、in professional tone类描述替换为可验证的文本长度、关键词覆盖率、情感极性阈值等量化指标。3.2 陷阱二输入输出搞“大而全”丧失原子性典型症状一个 skill 的 input 定义为user_data: object里面嵌套七八层 JSONoutput 返回一个巨长的 markdown 报告。这直接违背了 skill 的原子性原则——一个 skill 只解决一个明确、独立、可测试的业务子问题。破局点是“单职责 单数据源”十字检验法单职责该 skill 是否能用一句话说清“它唯一存在的理由”如果答案里有“并且”、“同时”、“还要”说明职责过载。单数据源该 skill 的所有输入字段是否全部来自同一个业务系统或数据库表如果user_name来自 CRMorder_amount来自 ERPcredit_score来自风控引擎那它就不是一个 skill而是一个 mini-ETL 流程应该拆成三个 skill 一个编排层。我们有个真实案例一个叫customer_health_score的 skill最初 input 包含 12 个字段来自 5 个系统。上线后每次数据源接口变更都要全量回归测试。后来按十字检验法拆成get_crm_activity、get_payment_history、get_support_tickets三个 skill每个只对接一个 API维护成本下降 70%且每个 skill 都能独立 AB 测试。3.3 陷阱三忽略“非功能需求”只写 happy path典型症状SKILL.md 里只有## 正常流程没有任何关于性能、安全、容错的约定。结果上线后一个本该 200ms 完成的查询 skill因未加缓存在大促时拖垮整个 agent 集群。破局点是强制添加## 非功能契约小节且必须量化。这不是可选项是上线准入红线。内容必须包含性能P95 响应时间 ≤ 300ms需注明压测环境4c8g 容器QPS50可用性支持 99.95% 月度可用率故障时自动降级为返回缓存数据安全所有输入字符串必须经过 XSS 过滤输出 JSON 必须 UTF-8 编码可观测性必须暴露 /metrics 端点提供skill_execution_total、skill_duration_seconds两个 Prometheus 指标。这些不是写给开发看的是写给 SRE 的 SLA 协议。我们曾因漏写性能指标导致一个 skill 在流量高峰时响应飙升至 2s但监控告警没触发因为没人定义“什么是慢”。3.4 陷阱四版本管理形同虚设靠人肉记忆典型症状version: 1.0写了一年没变或者v1.1、v1.2的 diff 全靠口头沟通。这在多团队协作时是灾难。破局点是“语义化版本 变更日志”双轨制。version字段必须严格遵循 SemVer 2.0MAJOR主版本接口不兼容变更如 input 字段删除、output 结构重构MINOR次版本向后兼容的功能新增如增加一个可选 input 参数PATCH修订版本向后兼容的问题修复如修正某个 error_code 的文案。更重要的是每次 version bump## 变更日志小节必须同步更新且格式固定## 变更日志 ### v1.3 (2024-07-22) - **BREAKING**: 移除 user_email 输入字段改由 user_id 关联获取关联 CRM 系统 - **ADDED**: 新增 send_channel 可选参数支持 sms/wechat/app_push - **FIXED**: 修复 timeout_minutes 为 0 时无限等待的死循环 bug这个日志不是给程序员看的是给 QA 看的测试范围指南也是给运维看的灰度发布 checklist。3.5 陷阱五脱离真实执行环境纸上谈兵典型症状SKILL.md 写得天花乱坠但没定义它在哪个 agent 框架里跑、依赖哪些底层服务、需要什么权限。结果开发拿到文档第一句话是“这个 skill 用什么框架实现”。破局点是在## 运行环境小节用表格锁定三大要素要素要求实例目标框架必须指定具体框架及最低版本LangChain 0.1.15或LlamaIndex 0.10.27依赖服务列出所有外部依赖及其 SLACRM API (99.9% uptime),SMS Gateway (P99 1s)权限要求声明所需最小权限集READ orders table,WRITE sms_logs table,EXECUTE http_client这个表格是开发环境搭建、CI/CD 流水线配置、生产部署审批的唯一依据。我们曾因没写明http_client权限导致 skill 在 sandbox 环境跑通上线后因权限不足被 agent runtime 拒绝加载回滚耗时 40 分钟。4. 从零开始手把手一个真实电商催付 Skill 的 SKILL.md 全过程光讲道理不够得看实操。下面以我上周刚交付的电商项目为例展示如何从一张白纸写出一份经得起生产考验的SKILL.md。全程不虚构所有参数、字段、逻辑均来自真实日志和监控数据。这不是教学 demo是现场作业记录。4.1 第一步锁定业务原点拒绝脑补一切始于业务方的一句口头需求“用户下单后 30 分钟没付款得提醒一下。” 这句话太模糊不能直接写进 SKILL.md。我拉着产品、运营、技术开了 15 分钟快会用四个问题把它钉死触发时机是“订单创建时间”还是“用户最后操作时间”→ 确认为created_at状态判定什么算“未支付”status pending还是payment_status IS NULL→ 查 DB schema确认为payment_status unpaid提醒渠道只发短信还是根据用户偏好选→ 运营拍板“首推短信因触达率最高后续再扩展”内容要素必须包含哪些信息订单号、剩余时间、跳转链接是刚需优惠券信息是锦上添花 → 确认前三者为must have。这四问的答案就是 SKILL.md## 业务契约的全部内容。没有这一步后面全是空中楼阁。4.2 第二步定义机器接口用表格代替段落基于上一步结论我立刻打开 VS Code新建SKILL.md先写死元数据和接口定义。注意这里所有字段名、类型、示例都来自真实数据库字段和 API 文档--- id: order_timeout_reminder name: 订单超时未支付自动催付 version: 1.0 category: customer_service tags: [payment, reminder, timeout, sms] author: liweiecommerce.com created_at: 2024-07-22 updated_at: 2024-07-22 status: draft ---## 业务契约 当订单创建时间超过 30 分钟且订单支付状态为 unpaid 时向订单关联的用户手机号发送一条包含订单号、剩余支付时限固定 30 分钟、一键跳转至支付页链接的短信提醒。成功发送即视为技能完成无需用户反馈。 ## 接口定义 ### 输入参数Input | 字段名 | 类型 | 必填 | 描述 | 示例 | |--------|------|------|------|------| | order_id | string | 是 | 订单唯一 ID来自 orders.id 字段长度 16-32 位 | ORD20240722A8B9C0D1 | | user_phone | string | 是 | 用户注册手机号11 位纯数字需通过号段校验 | 13800138000 | | payment_deadline | string | 否 | 支付截止时间戳ISO 8601用于计算剩余时间若为空则按 created_at 30m 计算 | 2024-07-22T15:30:00Z | ### 输出结果Output | 字段名 | 类型 | 描述 | 示例 | |--------|------|------|------| | sms_sent | boolean | 短信是否成功发出 | true | | sms_id | string | 短信网关回执 ID | SM1122334455667788 | | remaining_time_min | integer | 发送时计算的剩余支付分钟数向下取整 | 28 | | error_code | string | 错误码仅当 sms_sentfalse 时存在 | INVALID_PHONE |实操心得payment_deadline设为可选是因为上游 workflow 可能已计算好此值如从风控系统获取避免重复计算。remaining_time_min作为 output 字段是给上层 workflow 做“是否需要二次催付”决策的依据不是为了给人看而是为了机器判断。4.3 第三步补全运行保障把“万一”写进合同接着写## 运行保障。这里所有条款都来自我们短信网关的 SLA 文档和历史故障复盘## 运行保障 ### 数据权限 - 读取权限orders 表的 id, user_id, created_at, payment_status 字段 - 写入权限sms_logs 表的 sms_id, order_id, user_phone, sent_at, status, remaining_time_min 字段 - **禁止权限**users 表的 name, email, address 等 PII 字段。 ### 异常兜底 - user_phone 格式错误记录 ERROR_INVALID_PHONE 日志返回 sms_sentfalse, error_codeINVALID_PHONE**不重试** - 短信网关返回 HTTP 429限流立即返回 sms_sentfalse, error_codeSMS_QUOTA_EXCEEDED**触发告警 SKILL_SMS_QUOTA_ALERT** - order_id 在 orders 表中查无此记录返回 sms_sentfalse, error_codeORDER_NOT_FOUND**记录 WARN 日志不告警**可能是脏数据。 ### 审计日志 每次执行生成一条 JSON 日志字段包括 - skill_id: order_timeout_reminder - input_hash: sha256(order_id user_phone)原始手机号不落盘 - output_status: success 或 failed - duration_ms: 执行耗时从 DB 查询到网关返回 - trace_id: 继承自上游 workflow 的 trace_id - sms_cost_cny: 短信实际扣费金额用于成本核算注意sms_cost_cny是新增字段因为财务部门要求精确核算每条催付短信的成本。这再次印证——SKILL.md 是多方契约不是技术文档。4.4 第四步填充非功能契约与环境让运维敢上线最后补上硬性约束和环境要求这是上线前 SRE 最关注的部分## 非功能契约 - **性能**P95 响应时间 ≤ 250ms压测环境K8s Pod 2c4gQPS100DB 连接池 20 - **可用性**99.95% 月度可用率当 SMS Gateway 不可用时自动降级为记录日志不阻塞 workflow - **安全**所有 user_phone 输入必须通过 libphonenumber 库校验输出 JSON 必须 UTF-8 编码 - **可观测性**暴露 /metrics 端点提供 skill_execution_total{statussuccess|failed} 和 skill_duration_seconds_bucket 两个指标。 ## 运行环境 | 要素 | 要求 | |------|------| | **目标框架** | LangChain 0.1.15使用 RunnableLambda 实现 | | **依赖服务** | CRM API (v2.3, 99.9% uptime), SMS Gateway (v1.8, P99 800ms) | | **权限要求** | SELECT on orders, INSERT on sms_logs, HTTP GET/POST to SMS Gateway |4.5 第五步写变更日志为下次迭代埋点最后补上初始版本日志并预留扩展空间## 变更日志 ### v1.0 (2024-07-22) - **INITIAL**: 首次发布支持基础短信催付功能 - **TODO**: 下一版计划支持 send_channel 参数扩展微信服务号推送这份SKILL.md从敲下第一个---到最终定稿用时 42 分钟。它不是完美的但它足够清晰、足够具体、足够让开发、测试、运维、业务方在同一张纸上对齐。上线三天日均调用 12.7 万次P95 响应 186ms0 故障。这就是设计的力量。5. 常见问题速查表与独家避坑技巧在真实项目中总有些问题反复出现。我把它们整理成一张速查表附上只有踩过坑的人才知道的技巧。这不是 FAQ是生存指南。问题现象根本原因解决方案我的独家技巧Agent 加载 skill 失败报错invalid YAML front matterYAML 语法错误最常见是缩进不一致空格 vs Tab、冒号后少了空格、字符串含特殊字符未引号包裹用 VS Code 安装YAML插件开启实时校验Front Matter 中所有字符串值必须用双引号包裹技巧在团队 CI 流水线中加入yamllint检查yamllint -d {extends: relaxed, rules: {line-length: disable}} SKILL.md提前拦截 95% 的 YAML 错误测试用例总是 pass但线上执行失败SKILL.md 中的## 业务契约描述模糊测试同学按字面理解设计用例但实际业务逻辑有隐藏规则如“30 分钟”指自然分钟非工作日分钟## 业务契约必须用完整句子包含所有隐含条件每个句子必须能直接转化为测试用例的 Given-When-Then技巧要求测试同学用SKILL.md中的## 业务契约句子直接生成 cucumber feature 文件一行契约对应一个 scenario杜绝理解偏差多个 skill 之间数据传递混乱出现字段名冲突没有统一的数据契约规范各 skill 自行定义user_id、uid、customerId等别名在团队 Wiki 建立《全局数据字典》强制规定user_id为 16 位 UUID 字符串phone为 11 位纯数字字符串所有 skill 必须遵守技巧用jsonschema工具生成数据字典的 JSON Schema集成到 IDE 中输入字段时自动提示合法值和格式上线后发现 skill 执行太慢但本地测试很快未定义## 非功能契约中的性能指标也未在真实环境压测所有 skill 的## 非功能契约必须包含压测环境描述CPU/内存/网络/DB 配置和 QPS 要求上线前必须在 staging 环境用相同配置压测技巧在 skill 代码中内置performance_monitor(p95_threshold_ms250)装饰器超时自动上报不依赖外部 APM精准定位瓶颈skill 版本升级后旧 workflow 依然调用老版本导致行为不一致没有强制 workflow 绑定 skill 版本或框架不支持版本路由所有 workflow 的 skill 调用必须显式指定skill_idversion如order_timeout_reminder1.2框架必须支持按版本加载技巧在 CI 流水线中加入grep -r order_timeout_reminder workflows/ | grep -v 1.2自动检查是否有 workflow 未绑定版本阻断发布提示最后一个技巧是我们血的教训。曾因一个 workflow 漏写1.1自动调用1.0而1.0的remaining_time_min计算逻辑有 bug导致数千用户收到错误的“剩余 0 分钟”短信引发客诉。现在这条检查是发布流水线的 gatekeeper不通过不准上线。6. 我的个人体会SKILL.md 是 agent 世界的宪法序言写完这份 SKILL.md我合上笔记本盯着屏幕上的---符号看了很久。它看起来那么轻一段 YAML几段 markdown不到一千字。但我知道它承载的重量远超于此。它是我和 agent 之间的第一次握手是把混沌的业务需求翻译成机器可执行的精确语言的第一步。它不是终点而是所有后续工作的起点——workflow 编排、LLM 路由、监控告警、AB 测试、成本核算全都建立在这份薄薄的文本之上。我见过太多团队一上来就扎进代码调 prompt搭 pipeline结果跑通一个 demo 就欢呼却在规模化时被各种边界问题拖垮。而那些稳扎稳打的团队他们的第一个 PR永远是一份结构完整、责任清晰的 SKILL.md。这不是形式主义是工程敬畏。它强迫你把“我以为”变成“我确认”把“大概这样”变成“必须如此”。当你写下status: production的那一刻你签下的不是一份文档而是一份对稳定性、可维护性、可审计性的承诺。所以别急着写代码。先坐下来好好写你的第一个 SKILL.md。把它当成 agent 世界的宪法序言——简短但字字千钧。