不知道从什么时候开始,"给 Agent 写提示词"已经不能满足我了。真正让一个 Agent 从"能聊"变成"能干活",靠的往往是背后那一层看不见的agent-skills。这不是某个特定框架的专属名词,而是一套让模型调用外部能力、执行具体任务、复用复杂流程的方法论。我最近把几个项目里积累的脚本、工具调用、工作流全部重新梳理了一遍,最大的感受是:很多人的 Agent 不是能力不行,而是根本没有技能层。
这篇文章不会去复读某个官方文档,而是从我自己折腾的实际经验出发,聊聊 agent-skills 到底该存什么、怎么组织、怎么写描述、怎么调优,以及我在多次重构之后踩过的那些坑。无论你是刚接触 Agent 开发,还是已经在维护一套复杂的智能体系统,这篇文章应该都能给你一些可以直接落地的启发。
1. 先搞清楚:agent-skills 和提示词、函数调用、工作流的边界在哪
很多人第一次接触 agent-skills 时最迷惑的地方,就是它跟提示词(Prompt)、函数调用(Function Calling)、工作流(Workflow)到底有什么区别。我一开始也把这几个东西混在一起,结果就是技能文件里堆了一堆提示词片段,真正执行的时候又发现它跟工具函数脱节,整得四不像。
1.1 技能不是提示词,但它包含提示词
一个常见的误解是:agent-skills 就是把写好的 Prompt 存成一个文件,然后让 Agent 按需加载。这话说对了一半。
我在早期版本里确实这么干过——把一段精心写好的指令保存成 Markdown 文件,取名叫"数据分析助手.md",然后让 Agent 在需要时读取这个文件。结果是什么呢?Agent 确实会读,也确实会在对话里复述那套逻辑,但它并没有真正执行任何外部操作。它只是在用文字扮演一个数据分析师,而不是真的去跑脚本、读文件、生成图表。
后来我终于想明白了:技能的本质是能力封装,Prompt 只是它的"使用说明书"。一个完整的技能至少要包含:
- 触发条件:什么情况下该用这个技能,什么情况下不该用
- 执行逻辑:具体步骤,包括调用哪些脚本、读取哪些文件、执行哪些命令
- 参数接口:哪些输入是可变的,哪些是固定的
- 输出规范:最终应该返回什么格式的结果
所以说,如果你的 skills 目录里只有一堆 Markdown 提示词,那它只是一个"话术库",不是真正的技能库。技能的落脚点是执行,不是表达。
1.2 跟函数调用的关系:技能是函数的"使用上下文"
函数调用(Function Calling)解决的是"模型能不能调用某个 API",而 agent-skills 解决的是"模型在什么场景下、按什么顺序、怎么组合这些 API"。
拿我自己做的一个邮件处理技能来举例。底层我定义了两个函数:fetch_emails()和send_reply()。但如果只给 Agent 这两个函数,它并不知道什么时候该 fetch、什么时候该 send,更不知道怎么判断一封邮件是否重要、回复的语气应该怎么调整。
这时候技能的作用就体现出来了。我写了一个名为email_triage的技能,它明确了:
- 先调用
fetch_emails()拉取未读邮件 - 按发件人域名、邮件主题、是否包含附件三个维度做优先级评分
- 高优先级的直接生成回复草稿,调用
send_reply()发出 - 低优先级的只做摘要汇总,放在每日报告里
你看,函数只是零件,技能是装配图。Agent 有了装配图才知道怎么把零件组装成有价值的产品,而不是拿一堆零件当积木乱拼。
1.3 跟工作流的区别:技能是软件包,工作流是剧本
另外一个容易混淆的概念是工作流(Workflow)。在不少商业化 Agent 平台上,工作流被做成了可视化的画布,拖拽节点实现"如果-那么"的逻辑。那 agent-skills 跟它有什么不同呢?
我的理解是:工作流是固定的、线性的执行剧本,每一步怎么走、什么条件下跳转到哪里,都是预先设计好的,模型基本没有发挥空间。而技能更像一个软件包——它把执行逻辑封装好了,但内部有一定的灵活度,模型可以根据实际情况调整执行细节。
打个比方,工作流就像是餐厅后厨的标准作业程序:洗菜、切菜、炒菜、装盘,每一步都有标准。而技能更像是给你一份菜谱:它告诉你主料、辅料、大概的火候和时间,但具体怎么颠勺、什么时候加盐,全看你临场发挥。
所以在一个成熟的项目里,两者往往是配合使用的:技能负责封装"单点能力",工作流负责编排"整体流程"。如果你把工作流写死在一个技能文件里,那它就丧失了灵活性,Agent 会变成一个只会走流程的机器,遇到计划外情况就死机。
2. 我自己在用的技能库目录结构,以及为什么这样设计
前面聊了不少概念,这部分来点能直接抄作业的东西。我在最近的一个项目里重构了 agent-skills 的目录,踩过不少坑之后,目前稳定在下面这套结构上。
2.1 一套可落地的目录组织方案
agent-skills/ ├── core/ # 基础技能,任何场景都可能用到 │ ├── file_ops/ # 文件读写、格式转换 │ ├── search_retrieve/ # 信息检索、网页抓取 │ └── data_transform/ # 数据清洗、字段映射 ├── domain/ # 领域技能,按业务划分 │ ├── crm_followup/ # 客户跟进相关 │ ├── report_generation/ # 报告生成相关 │ └── qa_review/ # 质量审查相关 ├── workflows/ # 跨技能编排 │ ├── daily_summary/ # 每日汇总流程 │ └── alert_pipeline/ # 异常告警处理流程 ├── assets/ # 技能依赖的非代码资源 │ ├── templates/ # 模板文件 │ └── reference_docs/ # 参考资料文档 ├── registry.json # 技能注册表,记录技能元信息 └── README.md # 技能库总说明这套结构的核心思路是按复用频率分层。core里的技能通用性强,几乎可以跨所有场景复用,所以单独抽出来;domain里的技能跟具体业务强相关,变更频率高,需要跟 core 隔离开,避免改业务逻辑时误伤通用能力;workflows里的内容依赖 core 和 domain 的技能,属于编排层,放最后是合理的。
2.2 为什么我强烈建议你维护一个 registry.json
很多人写技能就真的只写技能文件,目录下散落着一堆xxx.md和xxx.py,用的时候靠猜,找的时候靠翻。我在项目早期就是这样,每次临时要加个技能都要全局搜索一遍,看有没有功能重复的。
后来我加了一个registry.json,把每个技能的关键信息登记在册。它的数据结构大致长这样:
{ "skills": [ { "name": "crm_followup", "path": "domain/crm_followup", "version": "2.1.0", "description": "基于客户近期互动记录生成定制化跟进建议", "trigger_conditions": [ "用户提到需要跟进客户", "检测到客户长时间未互动", "需要生成客户关怀内容" ], "main_entry": "run.py", "dependencies": [ "core/data_transform", "core/search_retrieve" ], "updated_at": "2025-01-15" } ] }这个 registry 带来的直接好处是:Agent 可以不用翻每个文件就能知道有哪些技能可用。它只要读一次 registry.json,就能建立起"场景 -> 技能 -> 入口文件"的索引。这比我一开始设计的"让 Agent 自己去翻目录"要高效得多,也大幅减少了模型在文件检索上浪费的 token。
另外一个意外收获是依赖关系变得清晰了。以前我总是忘了某个技能调用了哪些基础工具,改一个 core 里的函数就要跑一遍全量回归测试。现在dependencies字段直接就告诉我影响面,改动前能做到心里有数。
2.3 技能文件内部怎么组织
目录结构定了,单个技能内部也需要一套统一的规范。我目前用的模板是:
crm_followup/ ├── SKILL.md # 技能说明,给 Agent 看的 ├── run.py # 主执行脚本,模型会调用它 ├── schema.json # 输入输出参数定义 ├── utils.py # 辅助函数 ├── config.yaml # 运行时配置 └── tests/ └── test_run.py # 自测脚本为什么非要拆成这么多文件,而不是写在一个大文件里?原因有两个。第一,SKILL.md 和 run.py 的读者不同。SKILL.md 主要给大模型读,用自然语言描述触发条件和执行逻辑;run.py 直接被执行,用 Python 实现具体功能。混在一起会让两边的可维护性都变差。
第二,拆开之后可以按需加载。一个大文件全部加载进去会占用大量上下文窗口,拆开之后模型只需要先读 SKILL.md 和 schema.json,确定要执行时才去读 run.py 的内容。这对长流程、多步骤的 Agent 任务来说,token 省下来的数字非常可观。
3. 技能描述怎么写,Agent 才能真正用得上
一开始写 SKILL.md 的时候,我犯过一个特别幼稚的错误:把描述写成了"技能说明文档",大段大段地介绍功能特点和设计理念。结果就是 Agent 面对一个客户跟进的请求,死活想不起来要用这个技能。后来我才反应过来,技能的触发不需要形容词,需要的是判别式条件。
3.1 触发条件要写成"可被判定的状态",而不是模糊的场景
模糊的描述长这样:
本技能用于帮助用户更好地跟进客户,提升客户满意度和复购率。
模型看完这段话,根本不知道"现在到底该不该用这个技能"。它就算感觉"客户"两个字出现了,也没有把握——万一是别的场景呢?于是就不调用或者误调用。
现在的写法是:
触发条件:
- 输入消息中提及"客户""跟进""最近联系"等关键词
- 用户要求"生成跟进计划""看看哪些客户该联系了"
- 用户上传了客户互动记录文件,且未指定其他处理方式
这样模型就可以像走分支判断一样,逐一匹配当前输入,命中任何一条就直接调用。让技能的选择过程变成一个判定过程,而不是一个理解过程,是降低误用率的最有效手段。
3.2 执行步骤要显式列出,但不要剥夺模型的主观能动性
我见过两种极端的 SKILL.md。一种是把步骤写得巨细无遗,连"第一步,打开 config.yaml,第二步,设置 report_title 变量的值为"XXX""这种都写进去了,结果模型执行的时候一旦有一步对不上,整个流程就崩了。另一种是只写"按需处理",模型看完依然不知道怎么落地。
我现在的做法是:步骤写到"决策点"为止。比如crm_followup的核心步骤是这样写的:
- 从输入消息或默认路径读取客户互动记录
- 根据最近一次互动时间、互动频次、是否有未处理问题三个维度,将客户分为高优先级、中优先级、低优先级
- 对高优先级客户,从产品库中匹配相关产品信息,生成个性化跟进消息
- 对中优先级客户,生成标准关怀模板,仅替换客户名称和最近互动内容
- 低优先级客户只生成列表摘要,不发送任何消息
为什么会这样设计?因为分类标准是相对明确的,但具体生成内容的措辞应该留给模型发挥。如果连消息内容都规定死,模型生成的文本会非常僵硬;如果连分类逻辑都不写,模型会随机给客户定级,那业务逻辑就乱了。所以把确定的部分写成指令,把不确定的部分当作开放任务,这是技能设计里最核心的平衡。
3.3 schema.json 是给模型看的,不是给人看的
很多人写 schema.json 时习惯写一堆说明文字,比如"name": "客户姓名,注意这是必填字段,不能为空"。这些话对人类来说是友好的,但对模型来说反而是噪音。
我后来把 schema 精简到只保留必要信息:
{ "type": "object", "required": ["customer_id", "interaction_history"], "properties": { "customer_id": { "type": "string", "description": "唯一标识一个客户" }, "interaction_history": { "type": "array", "items": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "channel": { "type": "string", "enum": ["email", "phone", "wechat", "meeting"] }, "content": { "type": "string" } } }, "description": "按时间倒序排列的历史互动记录" } } }改动前后的核心区别是:description 里只写"用于决定什么"的信息,不写"人怎么填"的信息。模型读 description 是为了理解参数的业务含义,从而正确地向用户询问或提取信息,你不需要教它怎么填表。
4. 跑通一次技能调用:从注册到执行的完整链路
光有好的结构、好的描述还不够,技能最终要能在 Agent 运行时被真正调用起来才算数。这部分我以一个具体的执行过程为例,把从注册到运行的完整链路捋一遍,方便你照着搭。
4.1 注册环节:让 Agent 在茫茫技能库里找到你
技能注册的核心就是让 Agent 知道"现在有一个技能可以用,它的入口在哪,需要什么配置"。我这边run()函数入口的代码长这样:
# run.py 的主入口 import argparse import json from utils import parse_interactions, classify_priority from core.data_transform import normalize_customer_data def run(input_json: str) -> str: payload = json.loads(input_json) raw_records = payload.get("interaction_history", []) # 第一步:数据标准化 normalized = normalize_customer_data(raw_records) # 第二步:优先级分类 priority = classify_priority(normalized) # 第三步:按优先级组装输出 if priority == "high": output = compose_high_priority_message(normalized) elif priority == "medium": output = compose_medium_template(normalized) else: output = compose_low_priority_summary(normalized) return json.dumps({"priority": priority, "message": output, "status": "ok"})这段代码本身不复杂,需要说明的是输入输出的约定。我让所有技能都共用 JSON 字符串作为输入输出格式,这样模型侧只需要把一个 dict 序列化传给技能,技能内部再自行解析,完全不用关心底层是怎么通信的。
还有一个细节是执行结果的返回格式。我坚持统一返回一个{"status": ..., "data": ...}的结构,这样上游 Agent 可以统一做错误处理,而不是每个技能返回不同的字段。前期多花 5 分钟定这个规矩,后期省下的排查时间多到数不清。
4.2 执行环节:技能内部如何处理未知情况
我再强调一次,技能内部一定要有容错逻辑。模型调用技能时传入的数据未必规整,有时候缺字段,有时候格式不对,有时候传了一个根本不在枚举值类型里的参数。
我常用的容错手段有三层:
- 参数补齐:必填参数缺失时,尝试从最近的上下文里找合理值补上;找不到就返回明确的错误信息,而不是自己瞎猜。
- 类型转换:传入的类型不符合预期时,能转就转。比如
date传成了str格式,就用解析函数转成标准日期。 - 降级策略:主流程跑不通时,尝试用简化逻辑跑一个低配版。比如高优先级消息生成失败,就退化成发送一条标准模板消息,保证用户至少有一个兜底输出。
这三层里,最容易被忽略但也最影响体验的是第三层。没有降级策略的技能,经常因为一个小异常就整体失败,然后 Agent 只能向用户道歉。有了降级策略,技能至少还能产出结果,哪怕质量不是最优,也比空手而归强。
4.3 输出环节:让结果可被审计和追溯
技能执行完后返回给模型的数据,不应该只是最终结果,还应该包含一个审计信息字段。我在返回的 JSON 里加了trace,记录每一步处理了哪些输入、做了哪些决策:
{ "status": "ok", "data": { "priority": "medium", "message": "亲爱的客户..." }, "trace": { "input_count": 15, "classified_high": 3, "classified_medium": 7, "classified_low": 5, "steps_executed": ["normalize", "classify", "compose"] } }为什么要加 trace?因为你迟早会遇到"模型明明调用了技能,但结果不对"的排查场景。没有 trace 你只能干瞪眼,不知道是哪一步出了问题。有了 trace,你至少能定位到是数据标准化阶段出了问题,还是分类规则太激进,或者模板替换逻辑有 bug。可追溯性决定了你的技能体系能不能在出问题时快速自愈。
5. 调优和避坑:我在实际项目中总结的几条关键经验
技能库搭起来容易,真正难的是让它在长周期运行里保持稳定。下面这几条经验,都是我付出真金白银的代价换来的。
5.1 失控的"技能蔓延",比没有技能更可怕
技能库有个非常阴险的增长模式:一开始只有 3 个技能,运行得好好的;后来业务同学说"能不能顺便加个报表功能",于是加了第 4 个;再后来另一个业务线说"我们也想用 Agent",于是又加了 5 个领域技能。半年之后,技能库膨胀到了 30 多个,其中三分之一从来没人调用过,另外三分之一功能相互重叠。
这个现象我给它起了个名字叫"技能蔓延"。解决它的办法不是靠自觉,而是靠机制。我从第三个月开始,强制每个技能必须填写 registry.json 里的trigger_conditions和dependencies字段,并且每月做一次"无效技能清理"。凡是注册后 30 天内零调用的技能,直接移到archived/目录,不删除但也不参与加载。归档操作会显著降低模型的选择成本,也逼着我在设计新技能之前先看看有没有旧技能能复用。
5.2 技能描述的更新频率,往往比代码更新频率还高
代码的逻辑可能半年不变,但技能描述我几乎是每两周就要微调一次。原因很简单:模型的语境理解会随着你项目里其他提示词的变化而变化。比如某个月你新加了很多关于"客户分群"的上下文,模型看到"分析客户"这个词时就更倾向于往分群那边靠,原来的技能触发条件可能就不够精准了。
所以我养成了一个习惯:每次修改提示词系统或新增技能后,都要回过来看一眼已有技能的 trigger_conditions 是否还合理。这不一定是大改动,有时候仅仅是把一个不恰当的关键词删掉,就能挽回 20% 的技能调用准确率。
5.3 别把技能做成黑盒,模型需要能"看懂"你
最后一个想说的经验,可能听起来有点反直觉:技能的设计不能只考虑执行效率,还得考虑模型能否"理解"你这个技能到底想干嘛。
我曾经写过一个技能,内部用了非常复杂的语义相似度模型来给客户分类,效果确实不错,但有一天客户问了一个特别具体的问题:"这个客户为什么被分到高优先级?"Agent 只能回答说"系统根据综合评分判定",因为它也说不清楚。这种解释力不足的问题,在面向内部使用时尚可容忍,一旦技能的结果需要交付给外部用户或者接受审计,就完全行不通。
我在后面的版本里,把所有技能的关键决策逻辑都改成了规则可解释的方式。哪怕是用了机器学习模型,也会在描述里写明"主要考虑最近互动时间在 7 天内、互动次数 >= 3 次、存在未处理问题",让模型能够向用户清楚解释判断依据。这样做的代价是某些极端情况下分类不如纯模型的精度高,但换来的是整个系统在真实业务场景下的可用性大增。
这些就是我在 agent-skills 这条路上走得比较顺的几个关键点。如果你正准备搭建自己的技能库,我建议从最小的、最有把握的两个技能开始,先把目录结构和 SKILL.md 的规范跑通,再去扩充数量和复杂度。别想着一步到位设计一个完美体系——真正的技能库都是在实际调用和反馈迭代中长出来的,不是被设计出来的。