
这事得从一个我踩得很惨的坑说起。之前维护一个 Agent 项目系统提示词里堆了十几个技能模板——报销单提取、合同比对、日报生成、SQL 转写……每个模板写得条理清晰、示例充足自认为相当专业。结果迭代到第五个版本Agent 开始频繁发疯让它抽发票它突然给你按合同摘要的格式输出让它写日报它把 SQL 审核的规则也一并带了出来。所有技能挤在一个 system prompt 里模型每次对话都要把这上万字符全读一遍上下文被塞得满满当当调一次接口贵不说响应还越来越慢。后来我改用 SKILL.md 这套机制把技能真正封装起来问题才彻底解决。这篇文章就把这段实战过程完整拆开来讲SKILL.md 到底是什么、它和提示词模板的本质区别在哪、怎么从零写一个能真跑的技能包、哪些场景适合继续用提示词、哪些场景必须做技能化改造。如果你正在做 Agent 开发、或者被提示词越长越难管折磨过这篇应该能帮你省下不少冤枉时间。1. 提示词模板思维在 Agent 时代撑不住的三件事——先说清楚痛点1.1 冷知识与热知识的混装问题提示词模板最舒服的写法是把所有东西揉在一起领域规则、调用规范、少样本示例、JSON 输出要求、甚至公司内部的口径偏好。看起来信息齐全但对 Agent 来说这是一种灾难性的组织方式。Agent 每一轮推理都会把系统提示词整体作为上下文输入模型对其中每一段文字分配的注意力权重是动态的它不会因为你把某段规则放在前面就真的优先遵守更不会因为你把某个技能写在第一优先级处就真的按第一优先级执行。我把这里的问题归纳为冷知识和热知识的混装。冷知识是确定性的事实规则比如发票代码通常由 12 位数字组成增值税专用发票需要同时提取税率和税额金额合计保留两位小数——这些在绝大多数情况下不会变它们适合作为可查证的静态资料。热知识则是当前这个任务应该怎么拆步骤什么情况下切换到哪个技能完成了之后该用什么形式汇报——这类内容依赖对话上下文实时变化需要模型动态判断。提示词模板的最大病根就是把这两类性质完全不同的内容放在同一个通道里同时喂给模型。冷知识占据了大量 token却未必在每轮对话里都被用到热知识被淹没在长篇大论中模型抓不住重点。SKILL.md 的思路则是把冷知识下沉到技能文件或脚本里让 Agent 在真正需要时才读取把上下文留给需要实时推理的热知识。1.2 确定性逻辑交给大模型自由发挥的失控感再深入一层提示词模板还有一个更隐蔽的问题当一项操作本身是确定性逻辑时你靠自然语言描述让模型尽量做对本质上是在赌模型的概率输出。举个例子你告诉模型提取发票金额时优先找价税合计字段找不到再找合计还找不到就找金额。这句话在常规输入下模型大概率能执行但遇到一张排版奇葩的电子发票、一个扫描歪斜的 PDF模型就开始自由发挥。关键是它发挥错了也不会给你报错只会淡然地输出一个错误结果反正 API 调用还是成功的管道里没人知道里面烂了。这种失控感在纯提示词方案里是无解的因为自然语言描述天生就有歧义空间模型会在边界场景里产生无数种合理的理解。代码则完全不同一段 Python 脚本定义了精确的解析顺序、异常分支和返回结构跑一遍就有确定的结果。SKILL.md 真正的杀手锏就在这里——它允许你把怎么做从自然语言里拿出来放进可执行的脚本里模型只负责判断要不要做和怎么理解结果具体的确定性操作交给代码。1.3 上下文长度的隐形税Agent 会话越长越明显第三个痛点最好理解但往往要到项目中期才会爆发。提示词模板一旦多了你的 system prompt 会从 2000 token 涨到 6000 token 再到 12000 token。很多人觉得模型上下文窗口有 200k这点不算什么但这里有个隐性成本链条每轮对话 system prompt 都会完整进入模型token 消耗翻倍地上涨响应延迟同步上升而且关键指令的信噪比被严重稀释。更麻烦的是Agent 任务往往有长会话特征。用户会来回补充条件、让 Agent 修改结果、要求重新解释。每新增一轮对话系统提示词里那些跟当前任务无关的技能仍在持续计费。我那个项目上线一周光 token 账单就涨了 40%。SKILL.md 解决这个问题的思路是按需加载技能文件放在外部存储里Agent 先根据用户意图做检索匹配只把命中的技能内容拉进上下文。没命中的技能不占任何 token等于给上下文做了一次精准的减脂。这才是长会话场景下真正可持续的架构。2. SKILL.md 不是新提示词格式而是一个技能包——认识它需要先放下提示词思维2.1 一个 skill 的本质是目录结构不是文本标签理解 SKILL.md 最关键的一步是把它从提示词这个认知框架里拽出来。它不是一个更规范、更结构化的提示词文件而是一整套技能包。什么叫技能包看目录结构最直观skills/ invoice-extractor/ SKILL.md scripts/ parse_invoice_pdf.py assets/ sample_invoice.png requirements.txt一个技能就是一整个目录。SKILL.md 是入口文件用自然语言描述这个技能是干什么的、什么情况下调用scripts 目录放可执行脚本承载确定性的核心逻辑assets 是辅助资源比如示例文件、模板图片requirements.txt 声明依赖环境。你可以把它理解成一个可被 Agent 动态加载的插件而不是一段要读给模型听的话。这个认知转换一旦完成后面所有设计都顺理成章。提示词模板的出发点是如何把话说清楚让模型理解我的意图SKILL.md 的出发点是如何定义一个能力单元让 Agent 按需调用并拿到确定性的结果。一个是修辞学问题一个是工程学问题。2.2 路由清单的关键字段SKILL.md 的 frontmatter 是整套技能的路由入口。我目前用得最顺的字段组合是这几项name技能的唯一标识务必全局唯一。检索系统一般用它做精确匹配。description一段概述说明技能解决什么问题、期望输出什么。这是给模型做语义检索时最重要的匹配依据。when_to_use清晰的触发信号最好包含什么场景适合用和什么场景绝对不要用两段。负面信号的杀伤力往往比正面信号还大。version技能版本号方便排查问题和做变更管理。dependencies需要的外部依赖清单。为什么when_to_use这么重要因为 Agent 的技能路由本质上是一次语义判断。正面描述写得再好模型也可能在模糊相似的任务上误触发但如果明确写了输入是结构化数据表格时不要使用本技能模型就多了一个硬性排除条件误触发率会明显下降。这一点在实践里效果非常显著我后面会专门讲。2.3 Agent 加载 SKILL 的完整生命周期从检索到执行再到产出一个技能从躺在磁盘上到为用户产生价值完整链路是用户发出请求后Agent 首先根据当前对话上下文对技能仓库做一次检索匹配这个环节主要读取每个 SKILL.md 里的 description 和 when_to_use 字段做打分比较命中之后Agent 才把对应 SKILL.md 的正文加载进上下文从中读取执行步骤和规则如果 SKILL.md 里声明了要调用脚本Agent 就在沙箱或本地环境里执行这些脚本脚本返回结构化结果后Agent 把结果整理、校验、再组织成给用户的最终回复。注意这个链路里两个关键设计。第一检索阶段只比对元信息不读取全文所以即使技能仓库里有几十个技能上下文压力也不会显著增加。第二脚本源码本身不需要进入模型上下文。SKILL.md 只需要告诉模型运行这个脚本会得到什么以及脚本报错该怎么处理模型通过 exit code 和输出文件来理解结果。这一层隔离既省 token又保证了核心逻辑的准确性和安全性。3. 手把手拆一个完整 SKILLPDF 发票关键信息抽取实战3.1 设计之前先想清楚错误的使用方式很多人拿到 SKILL.md 第一时间干的事就是把原来的提示词模板原封不动复制进去开头写你是一名发票信息抽取专家…中间列几个抽取规则结尾要求输出 JSON。这其实只完成了把大提示词拆成小文件并没有真正发挥 SKILL 的威力。一个合格技能的设计起点应该先画一条错误的使用方式。我设计发票抽取技能时先列了几条禁忌场景输入不是 PDF 或图片而是纯文本表格时不要用用户只问发票是什么这种常识问题不要用发票页数超过 20 页时先提醒用户分批处理而不是硬跑。这些约束写进 when_to_use 后Agent 的误判率明显降低。第二步才是考虑正确使用时该怎么办而这里的关键不是给模型讲抽取规则而是给它一条可执行的路径调用脚本、读输出、按需补充说明。3.2 SKILL.md 正文怎么写分五个区按执行链路排布下面是一个可以直接改用的 SKILL.md 骨架--- name: invoice_key_info_extractor description: 从 PDF 或图片格式的发票中提取发票代码、发票号码、开票日期、购买方信息、销售方信息、金额、税额、价税合计等关键字段输出 JSON 结构化结果。 when_to_use: 用户提供或指定了 PDF/图片格式的发票文件要求提取关键信息、自动录入系统、或核对金额字段时使用。如果输入已经是结构化文本表格或用户只询问发票相关常识不要使用本技能。 version: 1.0.0 dependencies: python3, pdfplumber, pytesseract --- # 发票关键信息抽取 ## 执行步骤 1. 检查输入文件存在且可读。如果是图片确认是否为清晰扫描件。 2. 运行脚本提取内容 bash python scripts/parse_invoice_pdf.py --input 文件路径 --output /tmp/invoice_result.json等待脚本执行结束。如果进程返回 exit code 0直接读取/tmp/invoice_result.json。如果返回非 0 退出码读取 stderr 中的错误信息按下方错误处理一节处理。最终回复用户时附上完整的 JSON 字段并对金额、税额、价税合计三个字段增加一句人工核对提示。关键字段说明invoice_code发票代码通常 10 或 12 位数字invoice_number发票号码8 位数字issue_date开票日期YYYY-MM-DDbuyer_name购买方名称seller_name销售方名称total_amount金额合计不含税total_tax税额合计total_with_tax价税合计以发票上数值最大的合计栏为准错误处理脚本报ModuleNotFoundError执行pip install -r requirements.txt后重试一次。脚本报PDFEncryptedError向用户索要密码说明无法处理加密文件。输出 JSON 中某个字段为空不猜测不编造保留 null 并提示用户该字段可能需要人工核对。五个区分别对应元信息区frontmatter、执行路径区步骤、领域规则区字段说明、异常处理区错误处理、边界约束区藏在 when_to_use 和错误处理里。这套结构之所以有效是因为它对应了 Agent 的执行本能——先判断该不该用再决定怎么跑跑完知道怎么校验出错了知道往哪个方向修。 ### 3.3 辅助脚本为什么必须存在大模型眼里的文档和 Python 眼里的文件不是一回事 有些朋友会问让大模型直接读 PDF 文本不行吗为什么非要写个 Python 脚本实测下来的答案是大模型直接读 PDF 在高噪声场景下非常不可靠。扫描件 PDF 在模型眼里是一堆没有文本层的图像直接读只能得到空白即使有文本层多栏排版、表格线、页眉页脚都会干扰模型的字段定位。而 Python 脚本可以用 pdfplumber 提取文本层用 pytesseract 做 OCR 兜底再用正则规则精准匹配发票代码、发票号码这些强模式字段——这些逻辑一旦写成代码结果就是确定的。 脚本承担确定性部分大模型负责理解与应答这是 SKILL 化最核心的分工。脚本的设计要点是明确的输入输出约定 python #!/usr/bin/env python3 import argparse import json import re import sys def extract_invoice_fields(text): fields { invoice_code: None, invoice_number: None, issue_date: None, buyer_name: None, seller_name: None, total_amount: None, total_tax: None, total_with_tax: None, } # 用正则匹配强模式字段 code_match re.search(r发票代码[:\s]*([0-9]{10,12}), text) if code_match: fields[invoice_code] code_match.group(1) # 更多字段解析逻辑... return fields def main(): parser argparse.ArgumentParser(descriptionInvoice extractor) parser.add_argument(--input, requiredTrue, helpInput PDF or image path) parser.add_argument(--output, requiredTrue, helpOutput JSON path) args parser.parse_args() try: # 调用 pdfplumber / OCR 提取文本 # text extract_text(args.input) # fields extract_invoice_fields(text) # 输出 JSON 到 args.output # print 状态信息到 stdout pass except Exception as e: print(fFATAL: {e}, filesys.stderr) sys.exit(1) if __name__ __main__: main()这个脚本的精髓是成功时输出 JSON 文件到指定路径失败时把错误信息写到 stderr 并以非零码退出。大模型通过 exit code 和 stderr 两类信号就能理解发生了什么完全不需要读懂脚本内部逻辑。这里有个容易被忽略的细节SKILL.md 应该告诉模型脚本能做什么而不是把脚本源码整个贴进上下文否则你做技能化节省下来的 token 又原封不动花回去了。3.4 在 Agent 环境下跑通并观察调用日志写完 SKILL 先别急着接业务找个最小测试用例跑一遍。我的做法是准备三张不同类型的发票一张数字化 PDF、一张扫描图片、一张故意模糊的样本。第一张验证常规路径第二张验证 OCR 兜底第三张验证错误处理逻辑。观察 Agent 的调用日志时重点看两件事。第一件是技能是否在正确的时机被触发用户说帮我看看这张发票能不能报销Agent 应该检索到 invoice_key_info_extractor用户说今天天气怎么样这个技能绝不能被加载。第二件是执行链路是否完整Skill 命中后Agent 是否按 SKILL.md 里的步骤运行了脚本、读取了 JSON、并在最终回复里包含了核对提示。如果日志显示 Agent 反复尝试把脚本内容塞进上下文说明你的 SKILL.md 里对执行方式的描述还不够明确需要补充一句直接运行脚本不要读取脚本源码。4. 提示词模板和 SKILL.md 的分界线什么时候只能上 SKILL什么时候提示词仍然够用4.1 优先用提示词的场景判断标准是是否需要历史状态与上下文感知技能化不是银弹有些场景用提示词反而更顺手。我自己的判断标准很简单任务是否需要感知丰富上下文、是否需要发挥模型的语义理解能力、是否是一次性或者低频的临时需求。举几个实际例子。客服话术风格的调整这就非常适合放提示词里因为语气更亲切一些解释问题时多铺垫一层这些要求本质上是模型语言能力的直接体现写成脚本反而绕远路。代码风格偏好也同理让模型变量命名遵循 PEP8、提交信息用 imperative mood这类行为约束直接写进提示词模型领会得很快。还有快速原型你只是想验证一个想法还没确定要不要长期维护此时用提示词做个临时技能成本最低。这些场景有一个共同特征它们调用的是模型的通用能力不涉及外部环境交互也不需要确定性输出。提示词模板的价值在于它是上下文感知的、动态的模型结合当前对话状态灵活发挥。只要你的技能需求没有超出大模型的通用能力边界就没有必要引入额外的工程复杂度。4.2 必须 SKILL 化的场景四个硬性信号反过来以下四个信号只要命中任何一个我都强烈建议直接做 SKILL 化。第一个信号是涉及文件系统或外部工具。需要读 PDF、写 Excel、调数据库、请求外部 API这类操作已经超出了模型能直接完成的范围必须有脚本承载。此时提示词只能描述意图真正做事的是代码那就直接按技能包来组织。第二个信号是需要保证输出格式稳定。比如你要把抽取结果接入下游财务系统每个字段都要严格符合 schema多一个空格都不行。这种情形靠模型尽力输 JSON风险太高必须在脚本层做校验失败就重来。第三个信号是同一技能被多个 Agent 或多个项目复用。提示词模板的复用方式是复制粘贴改一处全要跟着改很容易出现A 项目还是旧逻辑、B 项目已经新逻辑的失控状态。技能包放进 git 仓库统一版本管理谁用都是同一份这才能谈得上工程化。第四个信号是技能本身有较长的维护生命周期。只要你有预感这个逻辑会迭代、要加字段、要适配新格式就值得做技能化。因为它一旦成为独立的目录你就有了测试用例、变更日志、依赖声明——这些是任何正经软件资产都应该有的。用一张表直观对比维度提示词模板SKILL.md 技能包存在形态文本片段目录 文件 脚本加载方式全部常驻上下文按需检索加载确定性逻辑靠模型自觉脚本强制保证外部环境交互不支持原生支持可测试性较弱可单元测试、可回归验证复用与版本管理复制粘贴git 管理、版本隔离失败处理重新表述提示词exit code stderr 重试策略这张表不是暗示技能包全面优于提示词而是帮你做场景匹配。如果四个硬性信号一个都没命中用提示词完全不丢人命中两个以上硬扛提示词方案后面一定会返工。5. 提效背后容易被忽略的四个细节版本、依赖、环境与失败重试5.1 版本管理SKILL 是代码资产不是备忘录技能化改造之后很多人犯的第二个错误是只把 SKILL.md 当成格式化文本在用目录照样不建 git、改动不写注释、版本号永远停在 0.1。我吃过一次亏一个发票抽取技能改了 OCR 策略结果没有改版本号另一个项目引用了缓存版本整整一周都在用旧逻辑跑新接口最后对账才发现金额字段对不上。把每个技能目录放进 git 仓库frontmatter 里维护 version 字段每次行为变化必须升版本号。我还会在技能目录里放一个 CHANGELOG.md记录每次改了什么、为什么改。这个文件不只是给人类同事看的Agent 在排查异常时也会去读它——如果技能输出和用户预期不符Agent 可以先看 CHANGELOG 了解最近有哪些变更这对调试很有帮助。5.2 依赖声明与环境检测技能依赖环境一旦缺斤短两脚本必然报错。但 SKILL.md 相对提示词模板的好处是你可以把自愈式指令写进错误处理逻辑里。比如在 SKILL.md 中加入这样一条如果脚本报 ModuleNotFoundError读取 requirements.txt 执行 pip install 后重试一次。Agent 拿到这条指令就能自动处理环境缺包而不是直接投降。更稳妥的做法是在脚本开头加环境自检import importlib import sys REQUIRED_DEPS [pdfplumber, pytesseract, PIL] def check_dependencies(): missing [] for dep in REQUIRED_DEPS: try: importlib.import_module(dep) except ImportError: missing.append(dep) if missing: print(fMISSING_DEPS: {, .join(missing)}, filesys.stderr) sys.exit(2)这个设计强调了可观测性脚本缺依赖时stderr 里明确写了缺什么Agent 看一眼就能定位问题。比一句笼统的执行失败强得多。记住一个原则脚本的错误信息就是给 Agent 看的调试线索写清楚能让整个系统少走很多弯路。5.3 失败反馈比成功路径更需要写清楚一个成熟的 SKILL.md失败处理部分应该和成功路径同等篇幅。以下是发票抽取技能的错误处理对照表你可以直接抄走当模板错误现象观测方式Agent 应采取的修复策略ModuleNotFoundErrorstderr 中列出缺失模块读取 requirements.txt 安装依赖后重试一次PDFEncryptedError脚本以错误码退出stderr 含 encrypted停止重试向用户索要密码OCR 置信度低于阈值输出 JSON 中 conf_score 0.7提示用户文件可能不清晰建议提供原图关键字段全部为空JSON 中多个字段为 null升级到更完整的 OCR 流程或询问用户是否允许手动录入脚本运行超过 60 秒进程超时退出检查文件页数如超过 20 页则提示分批处理不要直接重试这些错误处理指令价值在于让 Agent 的错误修复行为有边界哪些错可以自动重试哪些错必须停下来问用户哪些错要换路径执行。如果你不写清楚Agent 会在同一个错误上反复撞墙浪费 token 和时间最后还给用户一个坏结果。6. 回到Agent 技能这件事本身skill 与 agent 的正确关系写到这里想聊一个很多开发者反复纠结的问题skill、agent、工具、记忆这些概念到底怎么区分我自己的理解是把 Agent 比作一个需要独立完成工作的人类员工工具是手里的锤子、螺丝刀这些基础硬件记忆是脑子里存的项目背景和用户偏好技能则是更换浴室水龙头这种完整的做事流程。水龙头怎么拆、需要哪些工具、拧到哪个位置算紧、漏水了怎么排查——这些组装起来才叫技能它不是单一工具也不只是一条记住用户家水龙头型号的记忆。所以 skill 和 agent 的关系应该是agent 是决策调度者技能是被封装的能力单元工具是能力的物理接口。一个没有 skill 的 agent就像只记住了很多知识点但手上没活儿的实习生——交流很顺畅一让动手就露怯。反过来一个没有 agent 调度的大量 skill就像仓库里堆满了一流的设备没人知道当下该用哪台、怎么组合使用。从学习路线上说我的建议是别一上来就冲 Agent 框架。先在提示词上把模型的脾气摸清楚——什么样的描述在什么模型上更可靠、上下文长度对不同能力的干扰有多大。再上手工具调用理解 function calling 的底层逻辑。最后再进入技能封装阶段把确定性的逻辑拆进脚本、把领域知识写成按需加载的文档。这三个阶段递进下来Agent 开发中很多为什么这样设计的问题你会有很自然的答案。至于 skill 和 agent 到底哪个更重要这不重要它们本来就是一体的两面agent 决定要不要做skill 负责把它做对。最后再分享一点我自己的体会。改造完发票抽取技能之后我做的第一件事是统计 token 节省量那个塞满技能的 system prompt 从 12000 token 降到了 2000 token响应速度肉眼可见地变快了。但更大的收获不是性能而是心态上的我不再担心改一个技能影响另一个技能因为每个技能都是独立目录、独立版本、独立测试改坏了回滚就行。这种安全感是指示词模板时代完全感受不到的。如果你也想动手试建议挑一个确定性强、高频重复的自动化任务先做技能化改造比如格式转换、信息抽取、报告生成这类——跑通一个再推广。你可能会发现一旦理解了技能是封装而不是描述这件事你会重新审视手头所有 Agent 项目里的提示词找出下一批值得技能化的目标。