Agent Skills:从技能文档到行为契约的工程化实践

发布时间:2026/6/24 7:35:55

Agent Skills:从技能文档到行为契约的工程化实践 1. 从“技能文件”到“智能体行为中枢”Agent Skills 不是功能列表而是决策逻辑的载体你第一次在 GitHub 上看到一个 LLM 应用项目里有个叫skill.md的文件点开发现里面既不是 Python 代码也不是 JSON 配置而是一段段带标题、带参数说明、带示例输入输出的 Markdown 文本——你下意识觉得“这不就是个文档”结果跑起来才发现整个 Agent 的能力边界、它能接什么任务、怎么拆解用户一句话、甚至什么时候该调用外部 API、什么时候该查本地数据库全由这个看似“静态”的文件驱动。这就是Agent Skills的真实定位它不是功能说明书而是智能体Agent的行为契约与执行协议。它定义了“这个 Agent 能做什么”但更关键的是它定义了“这个 Agent 在什么条件下、以什么方式、带着什么上下文去做”。我最早在 Hermes Agent 和 DeepSeek Agent 的开源仓库里系统接触这套设计。当时团队正为一个金融投研助手做能力扩展原方案是每加一个新功能就硬编码一个函数、注册一个路由、写三份测试——两周加了 7 个技能光是函数签名对齐就出了 3 次类型错误。后来我们把所有技能抽象成统一结构用skill.md描述再通过一个轻量解析器注入运行时新增技能从“改代码→提 PR→等 CI→上线”压缩成“写好 skill.md → git push → 自动加载”。关键词Agent Skills在当前技术语境中已悄然完成一次语义跃迁2023 年初指代 LLM 提示词工程中“让模型学会某类任务”的技巧集合如“Chain-of-Thought 技巧”“Self-Consistency 技巧”2023 年中后期随 LangChain、LlamaIndex 等框架成熟开始指向可注册、可发现、可编排的“工具函数”Tool此时skill.md尚未出现开发者靠 Python docstring 或 YAML 注册2024 年至今skill.md成为事实标准它把“技能”从“一段可执行代码”升维为“一个可验证、可版本化、可跨框架复用的行为单元”。它不绑定语言Python/JS/Rust 均可实现、不绑定框架LangChain / LlamaIndex / 自研调度器均可消费、甚至不绑定执行环境本地 CLI / Web API / 移动端 SDK 都能加载。为什么是 Markdown不是 JSON不是 YAML这不是格式偏好问题而是工程权衡人类可读性优先产品经理要确认“这个技能是否覆盖了客服场景的退换货流程”他不该被缩进和引号困扰Git 友好git diff能清晰显示“新增了 refund_policy 参数”“修改了超时阈值”而 JSON/YAML 的 diff 常因空格、顺序、引号风格失效编辑门槛低实习生用 Typora 写完就能提交无需装 VS Code 插件或学 Schema 语法可扩展性强用 HTML 注释!-- skill:finance/stock_price --或自定义 frontmatter---\nscope: internal\nversion: 1.2\n---Markdown 解析器可轻松提取JSON/YAML 则需预定义全部字段。提示别被skill.md的后缀迷惑——它本质是技能元数据描述协议。.md是载体不是约束。你完全可以用skill.yaml或skill.json只要解析器支持。但社区选择.md是因为它天然平衡了机器可解析性与人可维护性这是过去十年 DevOps 工具链反复验证过的路径参考Dockerfile、Makefile、README.md 的成功逻辑。我见过太多团队卡在第一步把skill.md当作文档写结果开发时发现“文档里写的参数名和代码里不一致”“示例输出格式和实际返回不匹配”“没写错误码线上报错只能看日志”。根本原因在于他们没意识到skill.md是契约不是注释是接口定义不是使用说明。举个真实案例同花顺 Pine Script 团队曾向我们咨询如何将技术指标封装为 Agent Skill。他们原有指标是.pine文件含大量内置函数ta.sma()、request.security()。我们没让他们重写逻辑而是设计了一个skill.md描述协议# SMA 计算简单移动平均 ## 描述 基于指定周期计算收盘价的简单移动平均值支持多时间周期合成如 5 分钟 K 线计算 20 周期 SMA。 ## 输入参数 | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | symbol | string | 是 | — | 股票/期货代码如 SH600519 | | period | integer | 是 | — | 移动平均周期范围 2–200 | | timeframe | string | 否 | 1D | K 线周期支持 1D, 1H, 5M | ## 输出格式 json { value: 185.32, timestamp: 2024-06-15T14:30:00Z, source: tushare }示例调用{ symbol: SZ000001, period: 30, timeframe: 1H }错误码码含义排查建议E_SKILL_001symbol 格式错误检查代码是否含交易所前缀如SZ/SHE_SKILL_002period 超出范围周期必须为整数且 2≤period≤200这个文件交付后前端工程师直接按 input parameters 表格写表单后端工程师按 output format 写 DTO测试同学按 example call 写自动化用例运维按 error codes 配监控告警。**一份文件四端对齐零歧义落地。** 这才是 Agent Skills 的起点它让 LLM 应用开发从“拼凑提示词 调用函数”的手工作坊模式走向“定义契约 实现协议 自动编排”的工业化生产模式。 ## 2. 解剖 skill.md六个不可省略的字段缺一不可的工程闭环 很多团队写完第一个 skill.md 就急着跑通 Demo结果上线后三天内收到 17 个工单核心问题高度集中83% 的报错源于 skill.md 中某个字段缺失或描述模糊。这不是偶然——skill.md 的六个核心字段每个都对应一个工程环节的输入/输出契约。漏掉任何一个都会在下游引发雪崩式返工。 下面我以一个真实金融风控技能为例逐字段拆解其设计逻辑、常见错误及补救方案。这个技能名为 risk_score_v2用于实时评估用户贷款申请风险等级。 ### 2.1 名称与唯一标识# Risk Score v2 不是标题而是服务发现键 skill.md 的第一行 # Risk Score v2 看似只是 Markdown 标题实则是整个技能的**全局唯一标识符UID**。它参与三个关键环节 - **注册阶段**Agent 框架扫描目录时将 # 后文本作为 skill_id 存入技能注册中心 - **调用阶段**LLM 生成的 JSON Action 中name: Risk Score v2 必须与之完全匹配大小写、空格、标点均敏感 - **监控阶段**Prometheus 指标 agent_skill_duration_seconds{skill_idRisk Score v2} 依赖此 ID 聚合。 常见错误 - ❌ 使用中文或特殊符号# 用户风险评分V2 → 解析器报错 Invalid skill_id: contains non-ASCII chars - ❌ 版本混用# Risk Score 和 # Risk Score v2 并存 → 框架无法区分新旧版本调用随机失败 - ❌ 缺少版本号# Risk Score → 运维无法判断线上运行的是 v1 还是 v2故障排查无从下手。 正确做法 - ✅ 采用 kebab-case 命名# risk-score-v2推荐或 # risk_score_v2 - ✅ 版本号强制显式v1/v2/v2.1禁止 latest 或 stable - ✅ 在文件顶部添加注释说明变更!-- changelog: v2 adds income_source field, drops employment_years --。 注意skill_id 与文件名无关。你可以把 risk-score-v2.md 改名为 credit-assessment.md只要 # 行不变技能功能完全不受影响。这保证了“语义稳定物理可变”的工程灵活性。 ### 2.2 描述字段用“用户视角”写而非“开发者视角” ## 描述 字段常被写成技术说明“调用内部风控模型 API输入用户基础信息返回风险分值”。这毫无价值。真正有效的描述必须回答用户LLM 或终端使用者的三个问题 - **我能用它解决什么具体问题**场景锚定 - **它需要我提供哪些明确信息**输入预期 - **它会给我什么确定结果**输出承诺 反例开发者视角 “封装了 XGBoost 模型推理服务调用 /api/v1/risk/predict 接口。” 正例用户视角 “当用户提交贷款申请时根据其身份证号、月收入、负债总额、近 6 个月征信查询次数实时返回 0–100 的风险评分分数越高违约概率越大并标注高风险维度如‘负债过高’‘征信查询频繁’。适用于信贷审批、额度初筛、贷后预警等场景。” 这个描述的价值在于 - **LLM 可理解**它能据此判断何时该调用此技能如用户说“帮我看看这个客户的贷款风险” - **产品经理可验收**对照“适用于信贷审批…”确认是否覆盖业务需求 - **法务可审计**明确“实时返回”“0–100 分”符合金融监管对响应时效与结果可解释性的要求。 ### 2.3 输入参数表格即契约字段即接口 ## 输入参数 是 skill.md 中最易出错也最关键的字段。它必须是一个**严格定义的 Markdown 表格**且每一列都有不可妥协的含义 | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| - **字段**参数名必须与后端函数签名、API 请求体 key、数据库字段名**完全一致**包括大小写。例如 user_id 不能写成 userId - **类型**仅限基础类型 string/integer/number/boolean/array/object禁用 list/dict/json 等模糊表述 - **必填**是 或 否禁用 可选/recommended - **默认值**若为 否此处必须填具体值如 、0、false或 —表示无默认 - **说明**用“用户能懂的语言”解释**禁止技术术语堆砌**。例如 - ❌ “max_retries: 重试次数上限类型 int用于幂等性保障” - ✅ “max_retries: 如果网络请求失败最多自动重试几次填 0 表示不重试”。 真实教训我们曾因 ## 输入参数 表中 default_value 列写 null导致前端 JS 解析时 JSON.stringify({a: null}) 生成 {a: null}而后端 Python FastAPI 将 null 解析为 None触发非空校验失败。最终补救方案是在表格下方加一行 **类型转换说明**null 在 JSON 中等价于 NonePython、undefinedJS、nilRuby各语言实现需确保一致性。 ### 2.4 输出格式JSON Schema 的轻量替代方案 ## 输出格式 字段必须提供**完整、可复制的 JSON 示例**且该示例需满足 - 包含所有字段即使某些字段在特定条件下为空 - 数值类型用真实数字185.32非 {{value}} - 字符串用真实样例SH600519非 symbol_placeholder - 时间戳用 ISO 8601 格式2024-06-15T14:30:00Z - **禁止任何模板语法**如 Handlebars {{ }}、Jinja2 {% %}。 为什么不用 JSON Schema因为 Schema 太重 - 开发者要学 $ref、oneOf、additionalProperties - LLM 很难准确解析复杂 Schema 并生成合规 JSON - 前端表单生成器如 React JSON Schema Form对嵌套 Schema 支持不稳定。 而一个精心设计的 JSON 示例就是最直观的 Schema json { risk_score: 68, risk_level: high, high_risk_factors: [debt_to_income_ratio 0.7, credit_inquiries_last_6m 5], recommendation: 建议人工复核暂缓自动审批, timestamp: 2024-06-15T14:30:00Z, version: v2.1 }这个示例隐含了risk_score是整数68risk_level是枚举值high暗示还有low/mediumhigh_risk_factors是字符串数组recommendation是非空字符串timestamp是 ISO 格式字符串version是语义化版本字符串。LLM 在生成调用参数时会本能地模仿这个结构。这比写 20 行 Schema 更高效、更鲁棒。2.5 示例调用覆盖 80% 的边界场景## 示例调用不是“教你怎么用”而是穷举高频、典型、有代表性的输入组合。一个合格的示例表应覆盖正常主流程占 50%关键边界值如period: 2、period: 200占 30%常见异常输入如symbol: 、period: -1占 20%。反例只给一个理想情况{ symbol: SH600519, period: 30 }正例结构化覆盖场景输入 JSON说明主流程{symbol: SZ000001, period: 30}A 股主板股票30 日均线小盘股适配{symbol: BJ836077, period: 10}北交所股票10 日均线最小周期多周期合成{symbol: SH600519, period: 60, timeframe: 1H}基于 1 小时 K 线计算 60 周期 SMA错误输入{symbol: , period: 30}symbol 为空触发 E_SKILL_001越界输入{symbol: SH600519, period: 300}period 超出 200触发 E_SKILL_002这些示例直接成为单元测试的test_dataLLM 微调的 few-shot prompt前端表单的默认值来源客服培训的模拟话术库。2.6 错误码把“未知错误”变成“可运营事件”## 错误码字段是skill.md的安全阀。没有它所有异常都 fallback 到Internal Server Error运维只能看日志大海捞针。一个规范的错误码表必须包含三列| 码 | 含义 | 排查建议 |码全局唯一错误码格式E_SKILL_{三位数字}如E_SKILL_001便于日志聚合与告警含义一句话说明错误本质不暴露技术细节如不说“Redis 连接超时”而说“外部数据源暂时不可用”排查建议给一线运维/开发的可操作指引避免“请联系管理员”这类无效话术。真实案例某次risk-score-v2报E_SKILL_003“用户征信数据获取失败”排查建议写的是“1. 检查CREDIT_API_URL环境变量是否配置2. 执行curl -I $CREDIT_API_URL/health确认服务存活3. 查看credit-apiPod 日志中timeout关键字。”这条建议让 SRE 同学 3 分钟内定位到是 Kubernetes Service DNS 解析失败而非重启整个风控服务。提示错误码必须与后端代码中的raise SkillError(E_SKILL_001)严格一致。我们强制要求 CI 流程扫描所有skill.md中的错误码并与代码库中的SkillError枚举校验不匹配则构建失败。这是防止“文档与代码脱节”的最后一道防线。3. 从skill.md到可运行技能解析器、执行器与生命周期管理写好skill.md只完成了 30% 的工作。剩下 70%是让它真正活起来被发现、被加载、被调用、被监控。这背后是一套轻量但严谨的运行时机制我称之为Skills Runtime Layer。它不依赖 LangChain 或 LlamaIndex而是用不到 300 行 Python 代码实现已在多个生产环境稳定运行 18 个月。3.1 解析器把 Markdown 变成可编程对象解析器的核心任务是把skill.md的六个字段转换成内存中可操作的Skill对象。关键设计原则是不做任何假设只做严格校验。以下是我们解析## 输入参数表格的 Python 伪代码逻辑已简化生产环境使用mistune解析器def parse_input_params(md_content: str) - List[SkillParam]: # 1. 提取表格内容正则匹配 | 字段 | 类型 | ... | table_lines extract_table(md_content, ## 输入参数) params [] for line in table_lines[1:]: # 跳过表头 cols [c.strip() for c in line.split(|) if c.strip()] if len(cols) 5: raise ParseError(fInput param table row malformed: {line}) field, type_, required, default, desc cols[:5] # 2. 严格类型校验 if type_ not in [string, integer, number, boolean, array, object]: raise ParseError(fInvalid type {type_} for field {field}) # 3. 必填校验是/否 二值化 is_required required.strip() 是 # 4. 默认值处理— → None, → , 0 → 0 default_val parse_default_value(default) params.append(SkillParam( namefield, typetype_, requiredis_required, defaultdefault_val, descriptiondesc )) return params这个解析器的威力在于零容忍错误任何表格格式偏差、类型不合法、必填项缺失都在加载时抛出明确错误绝不静默失败类型即契约parse_default_value函数确保0被转为整数0false被转为布尔Falsenull被转为None杜绝字符串0传给期望int的函数可调试错误信息包含具体行号和原始内容开发者一眼定位问题。我们曾用此解析器扫描 200 个开源skill.md发现 63% 存在至少一处解析错误如type列写int而非integerrequired列写Y/N而非是/否。这证明没有解析器的skill.md只是漂亮文档有解析器的skill.md才是可执行契约。3.2 执行器隔离、超时、重试的黄金三角skill.md定义了“做什么”执行器决定“怎么做”。它必须解决三个核心问题隔离性一个技能崩溃不能拖垮整个 Agent可控性必须有硬性超时防止单个技能卡死整个流程韧性对网络抖动等瞬态错误应自动重试而非立即失败。我们的执行器采用“沙箱进程 硬超时 指数退避”策略def execute_skill(skill: Skill, input_data: dict) - dict: # 1. 输入校验用 Pydantic 模型验证 input_data 是否符合 skill.input_schema try: validated_input skill.input_model(**input_data) except ValidationError as e: return {error: E_SKILL_004, message: fInput validation failed: {e}} # 2. 启动子进程执行隔离崩溃 proc subprocess.Popen( [sys.executable, -m, skills.risk_score_v2, json.dumps(validated_input)], stdoutsubprocess.PIPE, stderrsubprocess.PIPE, timeoutskill.timeout_seconds # 硬超时OS 层面 kill ) try: stdout, stderr proc.communicate() if proc.returncode ! 0: # 3. 重试逻辑仅对特定错误码重试如网络超时、503 if ConnectionTimeout in stderr.decode() or 503 in stderr.decode(): return retry_with_backoff(execute_skill, skill, input_data, max_retries2) else: return {error: E_SKILL_005, message: Skill process crashed} return json.loads(stdout.decode()) except subprocess.TimeoutExpired: proc.kill() return {error: E_SKILL_006, message: Execution timed out}这个设计的关键细节子进程隔离risk_score_v2.py是独立脚本即使它import tensorflow导致内存爆炸也不会影响主 Agent 进程OS 级超时timeoutskill.timeout_seconds传递给subprocess.Popen由操作系统强制终止比 Pythonthreading.Timer更可靠智能重试只对可恢复错误网络、服务临时不可用重试对KeyError、TypeError等编程错误绝不重试避免掩盖 bug。经验skill.md中的timeout_seconds字段必须显式声明。我们规定所有技能默认超时 5 秒金融类技能如实时风控不得超过 2 秒数据分析类技能如批量报告可设为 30 秒。这个值写在skill.md里而非代码中确保 SLA 可审计、可变更。3.3 生命周期管理注册、发现、热更新、灰度发布一个技能从编写到上线需经历完整生命周期。skill.md是它的“出生证”而生命周期管理是它的“操作系统”。注册与发现Agent 启动时扫描skills/目录下所有*.md文件用解析器加载为Skill对象存入内存注册中心。同时它向 Consul 注册一个健康检查端点/skills/{skill_id}/health返回{status: up, version: v2.1}。这样Kubernetes 的ServiceMonitor或 Prometheus 就能自动发现所有技能实例。热更新我们不重启 Agent 来更新技能。当skill.md文件被inotify监听到修改时解析器重新加载该文件执行器切换到新版本的input_schema和timeout_seconds旧版本技能进程继续处理完正在运行的请求新请求全部路由到新版本。这实现了真正的“零停机更新”。某次紧急修复E_SKILL_002的边界校验从修改skill.md到全量生效耗时 12 秒。灰度发布对高风险技能如资金转账我们支持按user_id哈希灰度# skills/risk-score-v2.md --- canary: enabled: true traffic_percent: 5 hash_field: user_id ---执行器读取此配置对 5% 的user_id哈希值落在指定区间的请求调用新版本技能其余调用旧版本。效果立竿见影新版本上线 2 小时内通过对比灰度组与对照组的E_SKILL_003错误率确认修复有效随即全量。提示生命周期管理的精髓在于把skill.md从“静态文件”变成“活的配置”。它应该像 Kubernetes ConfigMap 一样可版本化、可审计、可回滚。我们要求所有skill.md必须纳入 GitOps 流水线每次git push都触发 CI 对解析、校验、冒烟测试的全流程验证。4. Agent Skills 的实战陷阱那些skill.md里不会写但会让你彻夜难眠的问题写skill.md很容易让skill.md在生产环境稳定运行很难。过去一年我帮 12 个团队排查过 Agent Skills 相关故障87% 的根因不在代码而在skill.md的“隐性约定”被打破。以下是三个最痛、最隐蔽、文档里绝不会提的陷阱附真实复现步骤与根治方案。4.1 陷阱一script标签的幽灵依赖——当skill.md里藏着未声明的 JS 模块搜索热词中反复出现failed to load module script: expected a javascript-or-wasm module script和the lang attribute of script is missing这绝非偶然。根源在于部分团队把skill.md当作“富文本编辑器”在里面插入script标签来实现动态逻辑。例如一个stock-alert.md技能为了“让用户自定义价格阈值”写了这样的 HTML 片段!-- 在 skill.md 的 描述 或 示例 区域 -- div idalert-config input typenumber idprice-threshold value100 button onclicksaveConfig()保存/button /div script function saveConfig() { const threshold document.getElementById(price-threshold).value; // 发送配置到后端... } /script问题在哪skill.md解析器只处理#、##、表格、代码块对script标签完全无视当前端页面渲染skill.md时如用marked.js这段 JS 会被执行但它依赖的saveConfig函数在全局作用域不存在因为skill.md是纯文本不打包 JS浏览器控制台报错ReferenceError: saveConfig is not defined但技能本身仍能调用——错误被掩盖直到某天用户反馈“配置按钮点不动”。根治方案零容忍script标签CI 流程加入grep -r script skills/检查命中即失败所有交互逻辑移出skill.md放入独立的前端组件如 Vue 的SkillConfig.vue通过skill_id动态挂载skill.md中的“示例”只保留纯 JSON禁用任何 HTML/JS。教训skill.md的使命是定义契约不是实现 UI。把它当作文档UI 就交给专业前端把它当作页面你就亲手埋下不可维护的雷。4.2 陷阱二SKILL.md与skill.md的大小写战争——Linux 与 Windows 的文件系统鸿沟热词中codebuddy无法导入skill.md高频出现真相令人哭笑不得开发者在 Windows 上用 VS Code 创建文件命名为SKILL.md全大写Git 提交后在 Linux 生产服务器上ls skills/看不到它——因为 ext4 文件系统区分大小写SKILL.md≠skill.md。更隐蔽的是VS Code 的文件树显示SKILL.md但右键“在终端中打开”却进入skill.md目录git status显示modified: SKILL.md但git add SKILL.md报错pathspec SKILL.md did not match any filesAgent 解析器遍历skills/*.md永远找不到SKILL.md技能“凭空消失”。根治方案文件系统无关化CI 流程强制标准化find skills/ -name *.md -exec rename s/(.*)\/(.*)\.md/$1\/\L$2.md/ {} \;将所有.md文件名转小写Git 配置core.ignorecase false让 Windows Git 客户端也区分大小写解析器升级glob.glob(skills/*.[mM][dD])同时匹配skill.md和SKILL.MD。我们曾因此故障停服 47 分钟。现在CI 流水线第一行就是# 检查大小写混乱 if find skills/ -regex .*\.[Mm][Dd] | grep -q [A-Z]; then echo ERROR: Uppercase .md files detected! Rename to lowercase. exit 1 fi4.3 陷阱三script setup的无声吞噬——Vue3 组合式 API 如何悄悄吃掉你的skill.md内容热词中vscode 中的vue script内容自动给我清理没有了和thymeleaf 无法使用layui的script模版{{指向同一个底层问题现代前端框架的模板编译器会主动解析并“优化” Markdown 中的script标签导致内容丢失。例如一个>python import requests # 调用技能 resp requests.post( https://api.example.com/skills/data-export-v1, json{query: SELECT * FROM users WHERE activetrue} ) print(resp.json())表面看是代码块但某些 Vue3 项目配置了 markdown-it-container 插件会把所有 script 标签内的内容当作“待执行 JS”在构建时尝试解析——而 requests.post(...) 不是合法 JS编译直接失败整个 skill.md 被清空。 **根治方案三层防御** - **第一层作者端**禁用 skill.md 中所有 script 标签代码示例一律用 Markdown 代码块python - **第二层CI 端**grep -r script skills/ grep -r .*\n.*requests\. skills/ 双重扫描 - **第三层运行时**解析器对 ## 示例调用 字段做白名单校验只允许 JSON/Python/Shell 代码块拒绝任何含 script 的 HTML。 最后一个经验所有陷阱的共性是把 skill.md 当作“自由文本”而忽略了它作为**契约文件**的严肃性。它应该像 API Swagger 文档一样接受 OpenAPI Spec 的严格校验。我们已将 skill.md 解析器集成到 SonarQube任何字段缺失、类型错误、格式违规都会在

相关新闻