最近有做 Agent 开发的朋友问我,说感觉自己写的提示词越来越长,模型反而越来越容易跑偏,到底有没有更工程化的组织方式。我给他的回答很简单:去看看现在讨论度很高的 “skills” 机制。
“skills” 不是什么专利概念,也不是某一家产品的专属功能,而是 Agent 能力组织方式的一次通用进化。它把一条条散落的提示词,连同配套脚本、参数声明、示例输出和触发条件,打包成一个个独立的“能力单元”。一个 Agent 可以挂载几十个彼此独立、按需调用的技能,而不是把全部指令硬塞进一个巨大的系统提示词里。
这篇内容不绑定任何特定平台背景,不管你在用什么 Agent 框架,只要它支持技能加载,这套方法论基本通用。适合正在做智能体应用、内部工具自动化,或者想把 AI 工作流沉淀成可复用资产的人。我会用一套实际可跑通的日志分析技能作为主线,带你过一遍技能目录怎么建、说明书怎么写、脚本怎么配套、踩过的坑怎么排。
1. 从 prompt 工程到技能工程:为什么大家都在聊 skills
1.1 prompt 越长,Agent 越“回”不到点子上
最早我调试 Agent 的方式很朴素:把所有能想到的规则、工具说明、输出格式全部写进系统提示词,指望模型一次读完全部约定,然后完美执行。刚开始任务简单还行,等业务复杂起来,问题就来了。
举个例子,一个客服会话分析 Agent,既要判断用户情绪,又要检索订单信息,还要生成总结工单。我起初把所有功能说明、输出模板、调用限制全部堆在一个 3000 字的 system prompt 里。结果模型经常在“生成总结”的任务里突然跑去“检索订单”,甚至在应该输出 JSON 的时候输出一长串无关解释。
后来我意识到一个核心问题:大模型的能力边界并不取决于提示词总字数,而在于每个任务指令能否在恰当的时机被准确唤醒。指令越多,互相稀释越严重。这就好比一个工具箱里所有工具没有分格分类,找一把螺丝刀要翻半天,还容易拿错。skills 机制解决的正是这个痛点:把工具分格分类,用标签和触发条件让 Agent按需取用。
1.2 技能是 Agent 时代的“插件系统”
我们说一个“技能”,本质上是由四部分组成的封装单元:元数据、指令文本、工具配置、示例校验。
- 元数据:技能名、版本、触发描述、依赖环境。
- 指令文本:告诉模型“你要做什么、怎么做、做到什么程度”。
- 工具配置:配套脚本、API、外部命令的调用方式。
- 示例校验:输入输出样例,以及常见异常的兜底处理。
这就像浏览器插件:浏览器本身只提供基础能力,具体功能靠插件按需加载。Agent 也一样,模型本身只负责推理对话,真正干活的还是这些被封装好的技能。
我在自己的项目里验证过这种设计的好处。之前一个数据清洗流程,前前后后在多个对话里重复执行,每次都靠重新写一遍 prompt 来指挥,既慢又不稳定。后来我把数据清洗做成了技能,挂载到 Agent 上,之后只需要说“帮我把这份 CSV 去重补全一下”,模型就会自动识别到对应技能,调用脚本执行,再返回结构化结果。整个过程的体验完全不同。
1.3 哪些场景适合先引入技能机制
不是所有任务都需要技能化。我建议优先把下面几类任务改造成技能:
- 重复执行的固定流程:比如日志分析、日报生成、数据清洗、格式转换。
- 需要外部工具配合的任务:调用脚本、查询接口、操作文件系统,这类任务纯粹靠 prompt 很难稳定。
- 多人共享的标准操作:团队内部经常要做的统一动作,比如监控告警归类、发布检查清单。
- 对输出格式有严格要求的任务:必须输出 JSON、Markdown、特定表格结构的场景。
不太适合的技能化场景也有:高频实时交互(比如用户在聊天窗口和 Agent 来回聊天)、需要严格人类审批的操作、短平快的问答。这类任务保持轻量配置反而更灵活。
我的经验是,第一版技能库不要贪多,选那类“做了能省一小时”的流程型任务。技术上是把提示词工程升级为能力工程,本质上更像是在给 Agent 建一套自己的功能清单。
2. 一个可落地的 skills 目录体系:结构与配置
2.1 推荐目录结构和文件职责
一套清晰的目录结构,是技能工程的地基。我不太建议把所有技能文件堆在一个扁平目录里,那样后期维护容易乱套。我常用的结构是这样:
my-skills/ ├── SKILLS.md ├── log-anomaly-analyzer/ │ ├── SKILL.md │ ├── scripts/ │ │ ├── analyze_logs.py │ │ └── config.yaml │ ├── examples/ │ │ ├── sample_input.log │ │ └── sample_output.md │ └── assets/ │ └── patterns.md ├──>--- name: log-anomaly-analyzer version: 1.2.0 description: 分析服务端错误日志和访问日志,统计错误级别分布、TOP N 接口失败量、异常堆栈聚类,并生成 Markdown 格式报告。 when_to_use: 当用户提供日志文件路径或粘贴日志文本,且任务目标是排查错误、分析失败原因、统计异常趋势、生成日志分析报告时使用。注意:不要用于处理非日志类的普通文本分析,比如聊天记录分析或合同审查。 input_args: - name: log_file type: string required: true description: 日志文件路径,支持 .log/.txt/.json - name: top_n type: integer required: false default: 10 - name: severity type: string required: false default: ERROR enum: [DEBUG, INFO, WARN, ERROR, FATAL] output_format: markdown depends_on: - python3 --- # 日志异常分析技能 ## 角色定义 你是日志分析助手,只负责阅读日志并输出结构化报告...模型在决定调用哪个技能时,主要读的就是 YAML 头里的描述和触发条件。正文部分则是调用之后要遵循的具体流程。两者职责不同,缺一不可,别把触发规则全写进正文里。
2.3 命名规范与触发设计
技能命名和触发设计,非常容易踩坑。
命名上,我建议用小写字母加短横线(kebab-case)。Log Anomaly Analyzer、日志分析这类命名,在不同系统里很容易出格式问题。短横线命名在所有命令行环境、Git 仓库、YAML 配置里都安全,也方便做文件路径引用。
触发设计上,最典型的问题有两个:描述太宽,或者描述太窄。
描述太宽,比如“处理日志”,模型可能把任何日志相关任务都往这个技能上调,哪怕用户只想简单问一句“今天有没有 warning”这种轻量问题。描述太窄,比如“读取 /var/log/app/error.log 文件并统计 ERROR 关键字数量”,模型遇到类似但路径不同的任务就不会调用,重新去写随机 prompt,导致行为飘忽。
正确的做法是给出任务类边界,而不是字面路径。拿日志技能举例,可以写“分析日志文件或文本片段中的异常信息,包括错误级别分布、失败接口聚合、根因线索提取”,这样既覆盖同类任务,又把非日志任务排除在外。
我还会在when_to_use里加反面澄清,这是很多人容易忽略的细节。写一句“不要用于处理聊天记录、合同文本或普通文章”,能显著减少误触发。
2.4 版本管理与依赖隔离
当技能数量多起来,版本管理就变得重要。
我采用的是类似代码库的管理方式:技能库用 Git 管理,每个技能子目录独立变化,技能的version字段手动维护。在SKILLS.md索引表里,给每个技能记录当前版本号,一旦有重要行为变化,升级版本号并清理变更日志。
技能之间的依赖也需要明确。比如日志分析技能依赖 Python 环境和config.yaml里的正则模式,depends_on字段就是用来声明这些的。另一个场景是技能之间的调用:主技能可能调用子技能。这种依赖如果不声明清楚,部署环境一变就全崩了。
依赖隔离的建议是,技能脚本尽量使用相对路径读取配套资源,重要的外部依赖避免直接写在全局环境中,最好用虚拟环境或者容器方案隔离。妄图“一个环境通吃所有技能”的做法,后期会付出代价。
3. 实操:从零创建“日志异常分析”技能
3.1 目标与方案选型
纸上谈兵没什么用,我以一个真实场景为例,带你完整建一个技能:日志异常分析。
我的原始需求很朴素:每天要人工查看生产环境的错误日志,统计出错最多的接口、错误级别分布、异常堆栈的相似片段,然后写一份日报。这是典型的重复劳动,而且人工统计容易漏。
目标明确为:输入一个日志文件路径,输出一份 Markdown 报告,内容包括错误级别分布、失败 TOP N 接口、典型错误样例、根因线索。
技术选型上,我选择 Python 3 和系统自带正则,不引入额外重量级框架。原因很简单:日志分析的逻辑不复杂,Python 的标准库足够覆盖。等将来需要并发处理、接入更多数据源,再逐步替换也不迟。
3.2 写主文件 SKILL.md
我完整写一份SKILL.md,你可以直接参考修改:
--- name: log-anomaly-analyzer version: 1.2.0 description: 分析服务端日志中的异常信息,统计错误级别分布、TOP N 接口失败量、异常堆栈聚类,输出 Markdown 报告。 when_to_use: 当用户提供日志文件路径或日志文本,且目标是排查错误、分析失败原因、统计异常趋势、生成日志报告时使用。不要用于聊天记录、合同、文章等非日志文本。 input_args: - name: log_file type: string required: true description: 日志文件绝对路径,支持 .log/.txt/.json - name: top_n type: integer required: false default: 10 - name: severity type: string required: false default: ERROR enum: [DEBUG, INFO, WARN, ERROR, FATAL] output_format: markdown depends_on: - python3 --- # 日志异常分析技能 ## 任务说明 当你接收到日志分析请求时,按以下流程处理: 1. 读取日志文件,忽略空行和非标准日志行。 2. 识别日志中的级别关键字:DEBUG、INFO、WARN、ERROR、FATAL。 3. 默认筛选 ERROR 及以上级别,除非用户另行指定。 4. 根据请求的接口 URI 聚合同类错误。 5. 从每条错误日志中提取关键信息:时间戳、错误类型、接口、错误短句。 6. 输出 Markdown 格式报告。 ## 输出模板 - 错误级别分布 - TOP N 失败接口 - 典型错误样例 - 可能的根因线索 ## 注意事项 - 日志读取出现编码问题时,跳过无法解析的行,不要中断。 - 没有匹配任何级别关键字的行,不纳入统计。 - 如果日志文件不存在或为空,明确告知用户,不要编造统计结果。 ## 示例 输入日志片段: [2025-05-01 10:11:22] ERROR /api/order/create 500 连接超时 [2025-05-01 10:13:04] ERROR /api/order/create 500 数据库连接池耗尽 [2025-05-01 10:15:00] WARN /api/pay/notify 200 重试次数偏高 报告片段: ### 错误级别分布 - ERROR: 2 - WARN: 1 ### TOP 接口 - /api/order/create : 2 条这里的正文部分,对模型来说是一份带具体步骤的说明书。我特意加入“不要编造统计结果”这条限制,是因为模型在没有数据的时候倾向于用幻觉补全,这是实测中非常常见的问题。
3.3 配套脚本设计
技能不能只有提示词,得有一个真正能独立运行的程序。配套脚本我用 Python 实现,核心逻辑分为三块:日志解析、异常聚合、报告渲染。
#!/usr/bin/env python3 """日志异常分析技能配套脚本""" import argparse import re from collections import Counter SEVERITY_LEVELS = re.compile(r"\b(DEBUG|INFO|WARN|ERROR|FATAL)\b") API_PATTERN = re.compile(r"(?:GET|POST|PUT|DELETE|PATCH)\s+(/[^\s,;]+)") def parse_log(file_path, severity_filter): records = [] with open(file_path, "r", encoding="utf-8", errors="ignore") as fh: for line in fh: line = line.strip() if not line: continue match = SEVERITY_LEVELS.search(line) if not match: continue level = match.group(1) if severity_filter and level != severity_filter: continue records.append({"level": level, "text": line}) return records def aggregate(records, top_n): level_counter = Counter(r["level"] for r in records) api_counter = Counter() api_samples = {} for r in records: api_match = API_PATTERN.search(r["text"]) if api_match: uri = api_match.group(1) api_counter[uri] += 1 if uri not in api_samples: api_samples[uri] = r["text"] return level_counter, api_counter.most_common(top_n), api_samples def render_report(level_counter, top_apis, api_samples): lines = ["## 日志异常分析报告", ""] lines.append("### 1. 错误级别分布") for level, count in level_counter.most_common(): lines.append(f"- {level}: {count}") lines.append("") lines.append("### 2. TOP N 失败接口") for uri, count in top_apis: lines.append(f"- `{uri}`: {count} 条") lines.append(f" - 样例: `{api_samples[uri][:200]}`") lines.append("") lines.append("### 3. 可能的根因线索") lines.append("- 结合高频错误短句聚类,定位共性链路。") return "\n".join(lines) def main(): parser = argparse.ArgumentParser(description="Log anomaly analyzer") parser.add_argument("-i", "--input", required=True, help="log file path") parser.add_argument("-t", "--top", type=int, default=10, help="top N") parser.add_argument("-s", "--severity", default="ERROR") parser.add_argument("-o", "--output", help="output markdown file") args = parser.parse_args() records = parse_log(args.input, args.severity.upper()) level_counter, top_apis, api_samples = aggregate(records, args.top) report = render_report(level_counter, top_apis, api_samples) if args.output: with open(args.output, "w", encoding="utf-8") as fh: fh.write(report) print(f"[done] report written to {args.output}") else: print(report) if __name__ == "__main__": main()这段代码我刻意保持精简。errors="ignore"是处理日志乱码时的防御性手段;API_PATTERN抓取常见 REST 接口路径;collect阶段同时记录每个接口的样例日志,这样报告不只是干巴巴的数字,还能看到具体错误长什么样。
注意一点,脚本不做过多业务假设。日志格式千奇百怪,技能说明书里写的那些字段,脚本不一定都能解析出来。最稳的做法是脚本尽量通用,具体业务逻辑放在配置或者更高级的分析步骤里。
3.4 加载与验证
写完脚本和说明书,把它挂到 Agent 上,我习惯先用命令行直接验证脚本本身,再做端到端测试。
验证脚本:
python3 scripts/analyze_logs.py -i examples/sample_input.log -t 5这一步如果没有报错,说明脚本逻辑没大问题。接下来在 Agent 配置里登记技能目录,然后测试触发。
我会准备几条不同的用户请求来验证触发稳定性:
- “帮我分析一下这份日志:/tmp/app.log”
- “统计生产环境最近一天的错误,按接口排序”
- “这个日志文件里有哪些 WARN 级别信息?”
第一条和第二条都应该能触发日志分析技能,但第三条可能会触发失败,因为它在语义上偏向“查询”,而不是“分析”。遇到这种边界情况,我会修改when_to_use,补充“统计 WARN/INFO 信息也属于该技能范围”。
端到端测试时,我还会让模型展示它执行了哪些步骤。如果模型处理中途改了逻辑,没有按说明书来,说明指令文本还需要更明确的约束。
3.5 结果检查与迭代
技能不是一次性开发完就行的,我会定期复盘它生成的报告质量。复盘维度有三个:
- 触发率:该调用的时候,有没有被正确调用。
- 报告准确性:统计结果和人工核对的结果是否一致。
- 输出可用性:直接拿报告去发日报,还需要额外加工多少。
有一次我发现模型生成的报告里,接口 URI 把查询参数也带了进去,导致/api/order/list?page=1和/api/order/list?page=2被当成两个接口。修正方式是在脚本里对 URI 去掉查询参数后再聚合。这种问题不跑真实数据是发现不了的。
技能更新之后,记得把version号往上提,然后在SKILLS.md里同步更新。如果不做这一步,后面排查问题时会分不清当前到底跑的是哪个版本。
4. 常见问题与排查技巧实录
4.1 技能没被触发,或者总调用错误技能
这是最常遇到的问题。现象是模型明明看到了用户需求,却没有去调用挂载的技能,而是自己现场组织一段逻辑来回复。
排查顺序我通常是这样的:
- 看
description是否覆盖了用户可能使用的自然语言表述。比如用户说“看看日志里啥报错”,你的描述里只有“分析日志文件路径”,可能就不命中,需要补充“查看报错、排查异常”这类近义表达。 - 看
when_to_use是不是太窄或者太宽。太窄就把边界放宽,太宽就加反面排除。 - 看技能是否和其他技能冲突。比如有两个技能都描述“处理日志”,模型就很容易猜错。这种情况建议合并成一个技能,或者显著区分触发条件。
- 看上下文里是否堆了太多技能,导致模型“选择困难”。实测中,Agent 上下文里技能数量超过 15 个以后,误触发概率会明显上升。解决方案是按用户场景动态加载技能,而不是把所有技能一次性全塞进去。
这里有一个判断技巧:如果模型总是能正确处理任务,只是没有调用技能,问题多半出在描述层面;如果模型调用技能后行为异常,才需要去查脚本和指令正文。
4.2 参数解析失败与工具调用报错
模型在调用技能时,需要按照input_args的声明传入参数。但大模型不会像程序一样严格遵守类型约束,经常出现把数字传成字符串、漏传必填项、传了非法枚举值等情况。
我在脚本里做了三重防护:
- 参数类型做防御性转换,比如
top_n传成字符串时,解析前先int()并捕获异常。 - 设置默认值,
top_n不传就默认 10。 - 对
severity做枚举校验,不在合法范围内就回退到ERROR。
另外一个有效的思路:让技能接收一个 JSON 文件路径,而不是在工具调用参数里传一堆散字段。比如分析任务需要“日志路径+时间范围+过滤条件”时,把这些打包成一个 JSON,脚本读取后统一处理。这样显著减少模型传参时的错误率。
4.3 技能循环调用,进入死循环
在技能协作场景中,模型可能陷入一种循环:主技能调用子技能A,子技能A执行时又去调用主技能,或者两个技能互相调用停不下来。如果平台没做步数限制,这会白白消耗大量 token。
我在配置技能协作时定了几条硬规则:
- 每个技能默认不允许调用自己。
- 主技能和子技能之间设计为单向依赖,H2 范围内不能出现环形引用。
- 在 Agent 平台层面,一般有最大调用步数限制,我会默认设置一个较小的值,比如 6 步。
排查循环问题时,重点看平台提供的调用链路日志。通常能看到技能A、技能B反复交替出现,这时候直接在对应子技能的触发描述里加上“该技能不应该再次触发日志分析主技能”之类的限制即可。
4.4 更新技能后行为没变化
有时候我改了SKILL.md或者更新了脚本,但重新测试时发现 Agent 行为和之前一模一样。第一反应不是脚本没改对,而是检查有没有缓存。
很多 Agent 框架在加载技能时会缓存解析结果,尤其是 YAML 头信息。如果你直接替换了文件,框架可能还拿着旧的解析结果。解决方法是看平台有没有清理缓存的命令,或者主动重启加载进程。更彻底的办法是在技能版本号里加一个递增标识,比如1.2.0改成1.2.1,然后强制重新加载。
另一个容易忽略的点:脚本更新之后,之前已经部署到容器或远程环境里的旧版本还留在那里,运行的依然是旧脚本。验证前先确认执行路径,别改完本地就跑测试,而线上跑的还是老版本。
4.5 技能质量自检清单
我给自己整理了一份技能上线前自检清单,每次新增或修改技能都过一遍。列出来给你参考:
| 检查项 | 说明 |
|---|---|
| 描述是否能被目标用户表述覆盖 | 把用户可能说的话写进测试集 |
| 是否有反面澄清 | 明确哪些任务不该用该技能 |
| 参数声明是否完整 | 类型、必填、默认值、枚举值 |
| 脚本是否有防御性处理 | 编码、异常输入、空数据 |
| 输出格式是否稳定 | 能直接被下游消费 |
| 是否有示例数据和期望输出 | 用于回归测试 |
| 依赖是否声明 | 环境、脚本、配置 |
| 版本号是否正确维护 | 和索引一致 |
| 是否配置权限最小化 | 技能只访问必要数据 |
| 是否有循环调用风险 | 检查技能依赖图 |
技能质量差的典型特征是:看着能跑,但换个环境、换个输入风格就不稳定。自检清单的价值,就是把这些不稳定因素提前掐掉。
5. 一些实操体会
说了这么多,最后分享几个我在实践中的真实感受。
第一,一套技能库的早期核心目标不是追求数量,而是追求稳定性。你就是把 3 个常用技能打磨到“谁用都说准”,也比做 30 个用不上、还互相干扰的技能强得多。我见过很多团队热情高涨地规划了几十个技能,最后维护成本高到吓人,反而拖垮了整个 Agent 项目。
第二,SKILL.md是一件需要持续迭代的“半产品”。初始化版本永远不是最好的版本。每次跑真实数据、看真实输出后,都会发现触发条件该调整、描述该补充、示例该丰富。我自己的经验是,一个技能至少经过三轮真实数据打磨,才算达到“合格”状态。
第三,技能和代码一样,要纳入团队的管理规范。哪次改了脚本、为什么改、影响哪些下游任务,这些信息不记录清楚,一个月后就会变成一团乱麻。我一般会在技能目录里放一个简短的CHANGELOG.md,每次改动加一行说明。
最后一个小技巧:你的第一个技能,别挑复杂的。找一个“数据转 Markdown 表格”或者其他特别简单的转换型任务,先跑通从编写到加载、从触发生成到校验结果的完整链路。等你吃完这一遍流程,剩下的思路就顺了。技能工程这条路,本质上是在给 AI 建一套可维护的能力仓库,前期慢一点,后面会省下非常多的时间。