Agent Skills实战:用技能包化解Prompt与工具混乱的工程指南
最近调试 Agent 的时候我发现团队里出现了一个很有意思的分歧有人把工具说明一股脑全塞进 System Prompt结果上下文越来越长模型反而不知道该调用哪个有人把每个小功能都写成独立 API最后维护成本比开发成本还高。说到底大家缺的不是工具而是把「某个领域的专业能力」打包成可复用单元的方法。这个单元就是现在社区里讨论很多的agent-skills。Agent Skills 并不是某个厂商的私有概念它正在成为 Agent 应用开发里的一种事实标准。简单说一个 Skill 就是一组围绕特定任务设计的提示词、脚本和依赖声明它能让 Agent 在需要时动态加载对应能力而不是把全部知识常驻在上下文里。这篇文章我会从一个实际可跑通的技能包入手讲清楚它的目录结构、加载逻辑、踩坑过程和治理方式适合正在做 Agent 应用、又觉得 Prompt 和工具越堆越乱的人参考。1. Skill 这个抽象层到底补上了哪块拼图1.1 没有技能抽象时Agent 是「什么都懂、什么都不会」我最早做 Agent 的时候思路非常简单把任务描述写清楚再把能用到的工具列表丢给模型让它自己选。跑 Demo 没问题一旦到了真实业务场景痛点立刻暴露。第一个痛点是上下文被工具说明占满。每个工具都要有名称、参数、返回格式、使用场景的描述。接上十几个工具之后光工具说明就可能占掉几千 token。模型既要理解用户需求又要在列表里翻找合适的工具推理质量肉眼可见地下降。第二个痛点是工具粒度难以统一。有人把「读 Excel 文件」做成一个工具有人把「读 Excel 并统计某列均值」做成另一个工具还有人反过来把「数据分析」做成一整个服务。粒度不一致导致 Agent 在任务拆解时经常选出错误的工具。第三个痛点是复用困难。这个项目里调通的工具换个项目又要重新写一遍描述因为描述和实现耦合在具体业务里没法独立迁移。Skills 解决的正是这几个问题把「知识、指令、代码」打包成一个独立目录通过少量元信息让 Agent 判断何时该用使用时动态挂载用完即走不常驻上下文。类比一下传统工具方式相当于把所有工具摆在桌上Agent 每次都要扫一遍Skill 方式相当于每个工具放在带标签的抽屉里Agent 看到任务先判断「这活儿该用哪个抽屉」然后才拉开抽屉。1.2 从「单一职责工具」到「复合技能」的粒度跃迁普通 function calling 里的工具通常只做一件事比如 get_weather、send_email。它是原子的、容易被描述的。但真实业务里的任务很少有这种单一性比如「整理会议纪要并生成待办」它至少包含转录文本清洗、要点提取、责任人分配、截止日期格式规范化甚至还要把结果同步到任务管理工具。如果把这四个步骤拆成四个原子工具Agent 每次都要按顺序调用四次任一步骤理解偏差都会让结果变形。Skill 允许你把这一整套流程的提示词、判断规则、代码脚本都放在一起只需要一个 description 告诉模型「这个技能是干什么的、适用于什么输入、产出的格式」。模型判断是否加载时只关注这一个描述而不是理解四个工具的协调方式。这就是为什么很多 Agent 框架开始引入「技能」这个概念——它在工具的原子性和业务任务的复合性之间补上了中间层。另外Skills 还改变了能力共享的方式。以前共享能力要共享源代码、部署服务、注册工具对方还需要理解你的调用约定。现在共享能力只需要共享一个目录里面包含说明和脚本对方的 Agent 运行时会自动读取并挂载。整个交付物从一个系统缩小到一个包这个变化对团队协作的影响是非常直接的。2. 一个 Skill 包的标准解剖从 SKILL.md 到可执行脚本2.1 目录结构三块内容各司其职目前社区里常见的 Skill 规范核心是一个包含说明文件、脚本和辅助资源的目录典型结构如下meeting-minutes-skill/ ├── SKILL.md ├── scripts/ │ ├── extract_actions.py │ └── format_minutes.py ├── requirements.txt ├── assets/ │ ├── minutes_template.md │ └── action_item_template.csv └── tests/ ├── test_extract_actions.py └── sample_transcript.txt其中SKILL.md是整个技能包的入口。Agent 运行时不会把整个目录都读进上下文一般只读取SKILL.md的前面一部分根据其中的描述字段决定是否需要加载更多内容甚至按需把scripts/下的代码交给执行器。所以SKILL.md的设计比普通文档要求高得多它既是说明书又是路由表。scripts/目录承担具体操作凡是确定性很强的工作格式转换、正则提取、数据清洗都应该放进脚本让代码完成而不是让模型用自然语言临场发挥。assets/放模板和静态资源tests/用来验证技能包在发布前能跑通。2.2 SKILL.md 的元信息其实是「触发条件」SKILL.md的开头通常有一段 YAML 格式的元信息里面最关键的两个字段是name和description。这一点很容易被低估——很多人把 description 写成论文摘要其实它应该是一段「为模型写的触发说明」。我用一个真实的对比来说明。普通写法可能是description: Handle meeting minutes and generate action items.。这个写法有问题模型不知道「什么场景应该激活这个技能」也不知道输入格式和输出格式更不知道哪些事情是它不该做的。更好的写法是--- name: meeting_minutes_process description: Use this skill when the user provides a meeting transcript or asks to generate meeting minutes from a conversation. Suitable for team meetings, client calls, and standups. The skill removes filler words, identifies decisions, extracts action items with owners and due dates, and returns a structured summary. Do not use for regular note-taking or personal to-do lists. ---这个 description 从三个维度约束了触发条件输入长什么样transcript、conversation、适用场景有哪些team meeting、client call、不适用场景有哪些regular note-taking。模型在意图匹配时这种描述比一句概括更容易命中也更不容易误触发。还有一个容易被忽略的点SKILL.md的正文是给模型看的人类指令但不应该被写成一篇长作文。它应该像一份给新同事的交接说明简洁、分块、明确边界。能放在脚本里的逻辑尽量不放正文里因为正文的每个字都会消耗 token脚本只在执行时消耗计算资源不占用上下文空间。2.3 依赖管理能少装一个包就少装一个包Skill 目录里的requirements.txt很容易变成一个「装饰品」。我见过不少人把 numpy、pandas、openpyxl 全部列上但实际脚本只用了一个csv标准库。Agent 运行时的依赖安装通常是有成本的安装包可能失败、可能超时、可能影响宿主环境而且每多一个第三方依赖就等于把 Agent 的一次工具调用变成一次不可控的运维操作。这个领域里值得坚持的一个原则是脚本优先用标准库实现不要为了省事引入大体积依赖。用一个例子说明我们要解析 CSV 格式的待办事项没必要引入 pandas标准库csv模块完全能胜任要做简单的文本模板渲染string.Template或者 f-string 就够了不必上 Jinja2。只有当任务真的涉及复杂数据处理时才考虑引入第三方库并且尽量选择纯 Python 实现、依赖少的库。提示如果技能包必须依赖第三方库建议在SKILL.md里明确写上「安装依赖前请检查运行环境」并在scripts/里提供缺失依赖的友好报错而不是让 Agent 拿着 traceback 乱猜。3. 实操手写一个「会议纪要与待办落单」技能包这里我直接给你一个能跑通的例子。这个技能包实现的是这样一个场景用户扔过来一段会议转写文本Agent 需要先清洗文本再抽取决议和行动项最后把行动项整理成带 owner 和 deadline 的表格。整个过程分成三层SKILL.md 负责告诉 Agent 如何组织流程脚本负责确定性的数据处理部分Agent 拿到脚本输出后再做最后的自然语言归纳。3.1 先写 SKILL.md把流程边界划清楚我不会让 Agent 直接处理原始转写文本而是定义两个动作先调用extract_actions.py得到结构化的 JSON再基于 JSON 由 Agent 自然语言输出给用户。这样设计的原因是行动项抽取的「确定性部分」比如识别包含「负责人」关键字的句子用正则和规则做更快、更稳定而「非确定性部分」比如把一段模糊表述概括成一条待办交给模型更合适。完整的SKILL.md正文可以这样写--- name: parse_meeting_actions description: Use when the user provides a meeting transcript or refers to a recorded call and wants meeting notes, action items, owners, or decisions extracted. Works for team syncs, client calls, and project reviews. Not intended for summarizing articles or writing emails. --- # Meeting Action Parser 1. Read the input transcript. 2. Clean the text with scripts/extract_actions.py. Pass the raw text to the script as a command-line argument, or write it to a temp file and pass the file path. 3. Read the JSON output from the script. It contains fields: - decisions: list of strings - actions: list of objects with owner, task, deadline 4. If no actions are found, do not invent them. Reply that no clear action items were identified. 5. Present the final minutes to the user in a concise markdown format. 6. Only include items that are supported by the transcript. If the transcript is short or informal, still run the script; the script handles sloppy text.这份说明最关键的一点是把「什么时候不要用」写清楚了用户只是想总结文章或者写邮件时不要启用这个技能。边界越清晰模型误触发的概率越低。3.2 脚本实现纯标准库一个函数一个职责然后是scripts/extract_actions.py。我尽量让它短小、只依赖标准库。这个脚本接收一段文本输出 JSON 到标准输出便于 Agent 直接解析。#!/usr/bin/env python3 Extract decisions and action items from a rough meeting transcript. import json import re import sys ACTION_OWNER_PATTERN re.compile( r(?:交给|负责人|owner|assign to|由)\s*([A-Za-z\u4e00-\u9fff][A-Za-z0-9_\u4e00-\u9fff]*), re.IGNORECASE, ) def split_into_sentences(text: str) - list[str]: # 按常见结束标点切分保留缩写的边界容忍度 raw re.split(r(?[。.!?])\s*, text.strip()) return [s.strip() for s in raw if s.strip()] def clean_sentence(sentence: str) - str: # 去掉口语废词保留语义完整 sentence re.sub(r\b(um|uh|like|you know|so|basically)\b, , sentence, flagsre.IGNORECASE) sentence re.sub(r\s, , sentence) return sentence.strip( ,。) def extract_decisions(sentences: list[str]) - list[str]: decisions [] for sent in sentences: if any(kw in sent for kw in (决定, 确认, 达成一致, decided, agreed)): decisions.append(clean_sentence(sent)) return decisions def extract_actions(sentences: list[str]) - list[dict]: actions [] for sent in sentences: if any(kw in sent for kw in (Action, 待办, 下一步, todo, 跟进, 负责)): owner match ACTION_OWNER_PATTERN.search(sent) if match: owner match.group(1) deadline due_match re.search(r(?:截止|due|by)\s*([0-9]{1,2}月[0-9]{1,2}日|[0-9]{4}-[0-9]{2}-[0-9]{2}), sent) if due_match: deadline due_match.group(1) actions.append({ owner: owner, task: clean_sentence(sent), deadline: deadline, }) return actions def main() - None: if len(sys.argv) 1: raw_text open(sys.argv[1], encodingutf-8).read() else: raw_text sys.stdin.read() sentences split_into_sentences(raw_text) result { decisions: extract_decisions(sentences), actions: extract_actions(sentences), } print(json.dumps(result, ensure_asciiFalse, indent2)) if __name__ __main__: main()这段脚本的意图很明确split_into_sentences先粗粒度切句clean_sentence负责去除口语废词extract_decisions和extract_actions用关键词加正则捕获负责人和截止日期。整个过程中 Agent 只负责「传入原文、读取 JSON、组织回复」不负责「手工找谁负责什么事」。3.3 挂到 Agent 运行时并验证命中脚本写好后把整个目录放到 Agent 的 skills 目录下。以当前主流 Agent 框架的惯例来说就是让运行时的 skills 加载路径指向这个目录然后启动一个交互会话输入下面的测试文本我们决定下周上线新版本。关于用户反馈的问题交给陈晨跟进下次迭代处理。王涛负责准备发布说明截止6月20日。还有一个行动项李琳需要更新测试用例。此时 Agent 应该通过SKILL.md的 description 判断需要加载parse_meeting_actions调用脚本并输出类似这样的结果{ decisions: [我们决定下周上线新版本], actions: [ {owner: 陈晨, task: 关于用户反馈的问题交给陈晨跟进下次迭代处理, deadline: }, {owner: 王涛, task: 王涛负责准备发布说明, deadline: 6月20日} ] }实测下来只要说明文件写得清楚、脚本本身没有异常模型几乎不会跳过这个技能。最容易出问题的地方反而是我不太注意的细节脚本运行目录和环境变量。如果 Agent 的执行器不知道技能包在哪个目录或者 Python 环境不对脚本会直接报错。所以技能包内部尽量用Path(__file__).resolve().parent定位资源文件不要依赖os.getcwd()。4. 跑通之后的第一道坎加载冲突、误触发与回退4.1 description 过于宽泛导致的「技能抢跑」技能写出来能跑通只是第一步。真正让我花时间调的都是边界问题。第一个常见问题是技能抢跑——用户明明在聊普通问题Agent 却因为某个技能的描述里包含常见词主动加载了那个技能。举个例子我有一次写了一个「竞品调研」技能description 里写了Use when the user talks about competitors, market, research...。结果用户问「你们团队最近有没有市场相关的培训资料」模型立刻加载了这个技能。原因很清楚market这个词同时在两个语境里出现。模型只看词面相似度不看语义边界。解决办法是强制在 description 里加负面条件而且负面条件要具体。与其写Do not use for general market discussions不如写Use only when the user requests a structured competitive analysis report with sections such as SWOT, pricing, feature comparison. Do not use for training material requests, internal knowledge search, or general questions about market news.负面条件越贴近真实误触场景拦截效果越好。4.2 同名技能的加载顺序和覆盖策略团队协作之后另一个问题浮出水面两个技能包可能同名。比如 A 同学写了一个report_generatorB 同学也写了一个report_generator加载路径里同时存在运行时的加载顺序不明确Agent 有时用 A 的、有时用 B 的结果完全不可控。我的处理习惯是三项约定第一技能包目录名全局唯一带团队前缀比如fin-report-gen和hr-report-gen避免通用名第二运行时遇到同名时以「先显式加载、后扫描发现」为优先级显式加载的永远覆盖扫描到的第三同一个技能包内部带version字段在SKILL.md元信息里写清楚遇到不兼容更新就升版本绝不在原目录上直接改。4.3 脚本执行失败时的回退策略Skill 里的脚本本质上也是代码代码就会遇到异常。异常处理不好整个 Agent 交互就崩了。我踩过的一个具体坑是脚本对输入文本做了假设比如假设一定能匹配到负责人结果用户给的转写文本全是泛称没有出现任何人名脚本返回了一个owner: 的空字段。模型拿到空值之后竟然自己编了一个负责人出来导致后续任务分配错误。这件事让我意识到脚本的输出也必须遵守一个原则默认真实拒绝臆造。于是我在脚本里做了调整当owner为空时不再输出空对象而是输出一个标记unassigned同时 SKILL.md 里明确写如果输出包含unassigned回复用户时需要指出「这项待办缺少负责人建议补充」而不是自作主张填一个名字。这个小小的约定让整个技能包的可靠性质变式地提升。注意技能包的失败处理不能只写在代码里必须同时体现在 SKILL.md 中。因为最终决定如何向用户解释错误的是模型不是脚本。脚本负责产生「真相」模型负责「解释真相」两者边界不能混淆。5. 把技能包当产品治理命名、版本、复用边界5.1 技能包命名与架构风格的统一当技能包数量超过二十个之后你需要一套可一致的裁量标准。我团队里现在执行的命名规则是domain-act-version例如hr-leave-query-v1、finance-expense-export-v2。三个部分的含义分别是领域、行为、主版本。这个命名有两个好处目录列表里按领域分组一目了然行为词约束了技能包的粒度——如果一个技能包里同时包含「查假期余额」和「发起退款审批」说明它越界了应该拆开。粒度问题值得单独说。我发现新手最容易把技能包写得太粗比如做一个data_analysis技能里面从读 CSV、画图表、做回归分析到生成周报全包了。这个技能包 description 写得再精准模型也很难判断用户到底想用它做哪一步。反过来太细也有问题比如calculate_sum这种技能用普通工具调用就够了没必要做成技能。我的判断标准很简单如果这个能力可以被一个不超过 30 行的原子函数描述清楚就不要做成技能如果它需要多步判断和多个子操作协作才考虑技能化。5.2 版本管理从 SKILL.md 的 frontmatter 开始技能包是文本加代码的组合所以版本管理应该同时覆盖两者。SKILL.md的最前面加一个version字段格式用MAJOR.MINOR.PATCH。PATCH 用于修复脚本 bug 和措辞调整MINOR 用于新增可选的子功能MAJOR 用于不兼容的流程变化或接口变更。这套规则和普通库的语义化版本规则一致最大的好处是运行时可以根据版本号判断兼容性比如某些技能只支持特定模型版本时可以加一个supported_models字段。这里有一个运维上的实操建议技能包目录放在独立 Git 仓库通过 tag 发布版本Agent 运行时固定加载某个 tag而不是直接拉 main 分支。我见过太多「直接拉最新代码」导致线上 Agent 行为突变的例子。Agent 的行为稳定性比功能迭代速度更重要因为用户无法像容忍普通软件一样容忍大模型应用「昨天还会的技能今天突然不会了」。5.3 衡量一个技能是否健康不是看调用次数最后分享一点个人经验。很多团队衡量技能包维护价值时只看调用次数。调用少就认为是废技能想删掉。我的经验是调用次数只能说明「曝光量」不能说明「命中质量」。更值得关注的三个指标是命中率Agent 决定加载该技能时是否真的解决了用户问题。可以在 SKILL.md 里要求模型在回复末尾附上!-- skill: name, status: success|fallback --标记这样能自动统计。误触发率加载了但任务其实不该用这个技能通常说明 description 写得不够精准。误触发高的技能要优先优化描述。失败回退率技能执行成功后模型仍没能给出合格答案说明技能边界和模型能力之间出现了断层。我见过一个「日历日程解析」技能调用次数一直很少但每次调用命中率都是 100%误触发率为 0。后来团队新人需要解析日历文本往里加了几个解析规则这个技能又活了。如果只看调用次数它早就被删掉了。所以不要用流量思维管理技能包要用「专业工具」思维——一个专业领域的工具平时用得不多用一次就得顶用这才是它存在的价值。我自己现在的习惯是每接入一个新领域先写一版粗略的技能包然后用一周时间持续记录 Agent 在哪类任务上「本来该加载却没有加载」、在哪类任务上「加载了却不该加载」。这些记录直接反馈到下一版 SKILL.md 的 description 里。技能包其实不是一个一次性的交付物它更像是一个不断被对话数据打磨的活文档。你花在边界描述上的每个字都会在之后的上百次调用里十倍地还回来。