news 2026/10/14 8:21:16

Agent Skills实战:如何为大模型装配可复用的技能模块

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent Skills实战:如何为大模型装配可复用的技能模块

你是不是也遇到过这种场景:模型明明很聪明,但让它干一件具体的事,比如“统计一下这批文本里出现次数最多的成语”,它就给你绕圈子,一会儿输出JSON,一会儿念一段说明,就是不给一个稳定的结果。你换了提示词、调了温度、加了一堆约束,效果还是不理想。问题不在模型,而在你没有给模型一个“可复用的技能”。

“agent-skills”这个项目,做的就是把这件事规范化。它不是某个模型的能力,也不是一个提示词模板,而是一整套给AI Agent装配“技能模块”的机制:技能怎么定义、怎么写元信息、怎么被模型发现、怎么注入提示词、怎么执行、怎么排错,全都有一套清晰的约定。核心思路很简单:把复杂任务的执行经验固化下来,用一套标准化格式封装成能被模型自动识别的能力包。

这篇文章会结合我实际跑通的完整流程,把技能设计、目录结构、调用机制、排错经验一次讲清楚。适合正在做AI应用开发的工程师、在研究智能体落地的产品经理,以及所有想把大模型从“聊天工具”变成“干活员工”的人。

1. 先想明白:Agent Skills到底在解决什么问题

1.1 为什么大模型应用需要“技能”这一层抽象

先提一个问题:你已经能通过函数调用(Function Calling)让模型调用外部工具,那“技能”和“工具”有什么区别?这个不搞清楚,后面全白搭。

工具是原子操作,比如“调用翻译API”“读取文件”“发一封邮件”。一个工具只做一件事,参数明确、边界清晰。但真实世界的任务从来不是单个工具能搞定的。比如“帮我写一篇产品评测,格式参照以前的文章风格”,这个任务需要模型理解评测框架、知道写作风格模板、可能需要触达历史文章库、还要把最终结果整理成特定结构——这已经不是一次函数调用能解决的问题了,而是一组逻辑的串联。

技能(Skill)就是夹在“单次函数调用”和“完整工作任务”之间的那层封装。它把完成一种任务所需的指令、参数定义、处理逻辑、输出格式打包成一个独立的模块。模型识别到任务类型后,加载对应技能,按技能里写好的SOP一步步执行。

我把这三者的关系用生活类比解释一下:工具是工具箱里的螺丝刀和扳手,技能是“换一个水龙头”的作业指导书,工作流则是“把卫生间翻新一遍”的完整施工方案。螺丝刀告诉你“我能拧螺丝”,指导书告诉你“先关水阀再拆旧件”,施工方案才关心整体怎么调度。

有了技能这一层,模型不需要每次都在上下文里塞满长篇规则。它只需要知道“我有哪些技能可用”,真正执行时再加载对应技能的详细说明。这对token成本和响应稳定性都是质变级的优化。

1.2 工具、技能、工作流:三者的边界和配合关系

很多人一开始分不清这三者,实际协作时会犯两类错误:要么把所有逻辑都塞进工具定义里,让工具变成一个巨大的黑盒;要么把所有指令都写在系统提示词里,把上下文撑爆。

我的经验是用三层结构来切分:

层级典型问题粒度可复用性
工具(Tool)“如何得到一个文件的大小”原子操作,单一职责跨技能复用
技能(Skill)“如何分析一份日志并输出异常摘要”一个完整任务的处理套路跨场景复用
工作流(Workflow)“每天定时巡检服务器并推送报告”多个技能按流程编排跨项目复用

工具层不需要“智能”,它只要稳定执行;技能层才开始有“套路”的成分,包含怎么做、按什么顺序做、什么情况下要停下来;工作流层则关注调度、重试、分支判断。

在agent-skills这套机制里,技能不重新发明工具,而是引用现有工具。一个技能内部可以调用多个工具,并且规定它们的调用顺序和结果处理方式。这就像一份菜谱:菜谱不生产锅和铲,但规定了你先炒鸡蛋还是先炒番茄。

1.3 这个项目的关键设计目标

我把agent-skills的设计目标归纳为四句话,这四句话也是我审视所有代码改动时反复对照的准绳:

  • 最小侵入:不绑架你现有的Agent框架。技能模块与主程序通过标准接口通信,你不需要为了用技能而重写整个业务系统。
  • 可插拔:技能以独立目录/仓库形式存在,新增一个技能就是新增一个目录,删除就是删除目录,主程序零改动。
  • 可观测:技能的加载、调用、输出、耗时全部有日志和指标。模型执行错了,你能定位到是“没触发技能”“技能内部报错”还是“参数传错了”。
  • 渐进式:允许一个技能从“一段纯提示词”开始,逐步演化为“带代码执行体的完整模块”,不用一开始就搞重量级框架。

这个设计思路很重要,因为很多Agent项目死掉就是因为一开始太复杂。你看那些成功跑起来的Agent应用,往往不是模型多么聪明,而是边界划分得清楚——什么归模型管,什么归技能管,什么归工具管,各司其职。

2. 技能的核心设计:目录结构、元信息与双模执行体

2.1 先搭一个标准的技能目录结构

在agent-skills里,一个技能就是一个自包含的目录。我建议的目录结构长这样:

skills/ ├── log_analyzer/ # 技能名:日志分析器 │ ├── SKILL.md # 技能元信息与使用说明(必须有) │ ├── executor.py # 程序化执行体(可选) │ ├── templates/ │ │ └── report_template.md # 输出模板(可选) │ └── assets/ # 辅助资源(可选) │ └── stopwords.txt ├── doc_explainer/ # 另一个技能 │ ├── SKILL.md │ └── executor.py └── registry.json # 技能索引文件(自动生成)

你可能会问,为什么技能目录要单独存在,而不是全部写在系统提示词里?

因为上下文长度是硬约束。一个技能如果展开成提示词可能要几百上千token,几十个技能就是几万token,模型Long Context也扛不住几百个技能的详细说明同时塞进去。把技能放在独立目录,通过索引做按需加载,其实就是给Agent做了“外部存储+检索增强”。模型只需要知道“这些技能存在、大概能干什么”,真正需要时再去取细节。

2.2 SKILL.md元信息:这是模型唯一稳定阅读的东西

技能目录里最重要的文件不是代码,而是SKILL.md。为什么?因为模型决策“要不要调用这个技能、怎么调用”,完全依赖SKILL.md里的描述。你代码写得再优雅,模型都看不见。

SKILL.md建议用Markdown + YAML Front Matter的结构,如下:

--- name: log_analyzer description: 分析日志文件,提取错误类型、出现频率,并输出结构化摘要。当用户要求“分析日志”“检查错误”“统计异常”时使用。 keywords: [日志, 错误分析, 异常统计] version: 1.2.0 author: ops-team license: MIT parameters: log_path: type: string description: 日志文件的路径 required: true example: "/var/log/app/error.log" time_range: type: string description: 分析的时间范围,格式为 ISO8601,例如 2024-01-01T00:00:00Z 到 2024-01-08T00:00:00Z required: false example: "2024-01-01T00:00:00Z/2024-01-08T00:00:00Z" before-you-begin: 检查日志文件是否存在且可读,如文件超过 500MB,先提示用户进行拆分或抽样。 --- # 日志分析技能 ## 执行步骤 1. 定位日志文件,确认权限可读。 2. 按时间范围过滤日志行。 3. 对过滤后的日志进行错误码提取与归类。 4. 统计每个错误码出现次数,并列出出现该错误码的时间分布。 5. 按报告模板输出结果。

这里有一个非常关键的细节:description 里一定要写清楚“什么时候用这个技能”。模型不是人,它不会“凭感觉”判断什么时候该用某个技能。如果你描述写得含糊,比如“一个用于日志分析的技能”,模型遇到日志相关问题时可能根本想不起来调用它;而如果你写“当用户要求分析日志、检查错误、统计异常时使用”,模型就有明确触发信号。

还有一个容易忽略的点:parameters 里每个参数都要写 example。模型在生成参数值时,对示例值的参考权重远比字段描述高。你给一个示例,它就大概率按这个风格传参;你不给,模型可能把布尔参数传成字符串,或者把路径拼接错。

2.3 技能执行体的双模设计

技能执行体有两条进路,我做agent-skills时强烈建议两条都支持。

第一种是“自然语言指令模式”,也就是技能没有实际代码,只有一段结构化的提示词SOP。模型看到SKILL.md后,按照里面的执行步骤自己一步一步来,每步的结果都是模型生成的文本或结构数据。优点是非常灵活,适合处理写文案、分析观点这类没有确定性算法的任务;缺点是不可控,模型可能跳步、漏步。

第二种是“程序化执行模式”,技能带一个executor.py(或其他语言的可执行模块),模型只需要提取参数并调用这个模块,模块负责真正干活。比如日志分析技能,解析日志、统计错误码这些步骤完全可以在Python里确定性完成,不依赖模型发挥。

在实际项目中,这两种模式经常混用。比如一个“代码审查技能”:首先用程序化方式拉取代码变更、计算改动文件列表,然后用模型逐项审查,最后再用程序化方式生成报告文件。Skill文件里同时有SOP指令和执行体入口,模型按需选择调用方式。

我做了一个简单的执行体接口参考,在你自己的技能里可以这样设计:

# executor.py from typing import Any def run(context: dict[str, Any]) -> dict[str, Any]: # context 里包含模型解析出的参数 log_path = context.get("log_path") time_range = context.get("time_range") # 核心逻辑... result = analyze_log(log_path, time_range) # 返回结构化结果,供模型整理成最终回复 return { "error_stats": result["error_stats"], "time_distribution": result["time_distribution"], "summary": result["summary"], }

注意一个问题:程序化执行体不要试图让模型自己修改代码。技能开发者在开发阶段写死逻辑,模型只负责传参和解读返回值。如果你让模型在执行过程中临时改代码,那等于把系统安全性和可维护性都交给了一个概率模型,早晚出事。

3. 实操跑通:把技能接入Agent主程序

3.1 技能注册与发现:让Agent知道自己有什么

技能装好了,主程序怎么知道有哪些技能?两种主流方案:全量注册和按需发现。

全量注册最简单,在启动时扫描skills目录,把每个SKILL.md的元信息加载到内存。这个方案的优点是实现容易、响应快,缺点是技能数量多了之后,把所有技能的元信息塞进上下文仍然是不小的开销。

按需发现更省token:系统维护一个技能索引,索引里只有技能名称、一句话描述、标签。主程序在每轮对话中,根据用户输入和已有上下文去匹配候选技能,只把候选技能的SKILL.md完整内容加载进上下文。这个过程类似检索增强生成(RAG),但检索对象是技能文档而不是知识片段。

我实际项目里采用的是折中方案:索引全量加载到系统层,但注入到模型上下文时只注入高相关内容。匹配方法一开始可以用简单的关键词命中,后来我换成了向量检索,效果明显提升。

代码示意如下:

import json from pathlib import Path SKILL_ROOT = Path("./skills") def load_skill_index(): registry = [] for skill_dir in SKILL_ROOT.iterdir(): if not skill_dir.is_dir(): continue skill_file = skill_dir / "SKILL.md" if not skill_file.exists(): continue meta = parse_front_matter(skill_file.read_text(encoding="utf-8")) registry.append({ "name": meta["name"], "description": meta["description"], "keywords": meta.get("keywords", []), "path": skill_dir, }) return registry def retrieve_skills(query: str, registry, top_k=3): # 这里可以用关键词匹配,也可以替换成向量检索 scored = [] for item in registry: score = 0 for kw in item["keywords"]: if kw in query: score += 2 if item["name"] in query: score += 1 scored.append((score, item)) scored.sort(key=lambda x: x[0], reverse=True) return [item for s, item in scored[:top_k] if s > 0]

这套逻辑我建议所有做Agent的团队都上一个,哪怕一开始只用关键词匹配。因为技能数量会快速增长,三五个技能时全量注入还撑得住,三五十个时再全量注入就是灾难。

3.2 提示词注入策略:技能说明以什么形式给模型

技能说明注入到上下文,不是简单地把SKILL.md全文贴进去就完事的。你要考虑模型注意力集中的问题。

我常用的格式是这样组织系统提示词:

你是任务执行助手,可以调用以下技能。技能列表按相关性排序。 ## 技能:log_analyzer(日志分析) 当用户需要分析日志、排查错误、统计异常时使用此技能。 输入参数: - log_path(必需):日志文件路径 - time_range(可选):分析时间范围 执行要求:先检查文件是否存在,文件过大的时候提示用户拆分再分析。 ## 技能:report_writer(报告生成) 当用户需要输出格式化报告时使用此技能。 ...

关键是要把“触发描述”放在最前面,参数说明放在后面。模型的注意力机制对开头和结尾内容更敏感,触发描述放在开头能提高触发准确率。

还有一个细节值得提:技能调用不是只靠名字靠不靠谱。我见过有人给技能起名叫load_and_process_data,结果模型永远不调用,因为这个名字太泛了。技能名应当体现业务含义,比如parse_invoice_pdf、generate_changelog,这样模型即使在关键词匹配阶段也能凭直觉抓取。

3.3 一个最小可运行的技能调用全流程

这里给出一套可以直接跑通的最小实现,包含技能定义、索引、调用三步。这个示例是“文本统计技能”,非常基础但流程完整。

先看技能定义(SKILL.md):

--- name: text_stats description: 统计文本中的字数、句子数和关键词词频。当用户要求“统计字数”“分词统计”“文本体检”时使用。 keywords: [字数统计, 词频, 分割句子] parameters: text: type: string description: 待统计的文本内容 required: true example: "这是一段需要统计的文本。它包含两个句子。" top_n: type: integer description: 返回词频最高的前多少个词 required: false example: 5 --- # 文本统计技能 对输入文本执行以下操作: 1. 计算总字符数(不含空白)。 2. 按中英文标点拆分为句子。 3. 分词并统计词频。 4. 输出前 top_n 高频词。

再看程序化执行体(executor.py):

import re from collections import Counter from typing import Any def run(context: dict[str, Any]) -> dict[str, Any]: text = context["text"] top_n = int(context.get("top_n", 5)) char_count = len(re.sub(r"\s", "", text)) sentences = re.split(r"[。!?.!?]", text) sentences = [s for s in sentences if s.strip()] # 简易分词:按非中文非英文边界切分,项目里你可以接入 jieba tokens = re.findall(r"[\u4e00-\u9fa5]|[a-zA-Z]+", text) freq = Counter(tokens).most_common(top_n) return { "char_count": char_count, "sentence_count": len(sentences), "top_words": [{"word": w, "count": c} for w, c in freq], }

主程序调用时,流程是:识别用户意图 -> 检索技能 -> 注入技能说明到上下文 -> 模型返回参数 -> 调用executor.run() -> 模型基于返回值组织最终回复。你可以对比一下,如果不走技能机制,而是让模型直接算字数,它可能会用纯语言描述“这段文本大约有xx字”,但遇到复杂文本格式就容易数错。

我建议你做一次端口测试:准备十段格式各异的文本样本,比如带换行的、带英文单词的、带中文标点的,让不走技能的模型输出统计结果,再让走了技能的模型输出,你会直观感受到什么是稳定复现。

4. 踩坑实录:技能为什么没有被模型调用

4.1 描述写得太像文档,模型根本找不到触发信号

这个坑我踩得最深。早期我写技能描述,风格是“本技能提供文本统计能力,基于中文分词算法,支持多种输入格式”,看起来很规范对不对?但模型就是不调用。

后来我意识到问题:这段描述没有告诉模型“什么情况下触发我”。模型做决策时是在匹配用户请求和可调用能力,你的描述里没有“字数”“词频”这种和用户请求直接对应的词,它就很难把两者关联起来。

改法说起来也简单,把描述变成“当用户要求【统计字数】、【查看词频】、【文本体检】时使用本技能”。要让描述里出现用户可能说的话,而不是功能说明书。这个改动带来的是立竿见影的触发率提升。

4.2 参数设计不合理,模型传了错参数

第二个坑是参数类型设计。早期我把time_range设计成字符串,并且没有给示例,结果模型经常传成“昨天到今天的日志”,操作系统根本没法解析。后来我把参数格式明确成ISO8601字符串,还给example,错误率立刻降下来。

还有一种情况更隐蔽:必填参数没标required。模型发现参数可填可不填时,往往会偷懒不填,直接返回默认逻辑。这会导致技能执行结果不完整,但界面看起来又是“成功”的。排查难度很高,因为错误被吞掉了。

所以,参数设计有一条硬性原则:不是必填的参数就删掉,是必填的参数一定标required并提供example。宁可少一个花哨的可选参数,也不要一个含糊其辞的必填参数。

4.3 技能数量膨胀后,上下文质量急剧下降

项目跑了一个月后,我往registry里加了二十多个技能。问题出现了:即使按检索注入,模型也开始混淆技能之间的边界。

有个案例让我印象很深——模型在执行“合同关键条款提取”任务时,调用了“发票信息抽取”技能,因为两者的description都出现了“提取关键信息”这几个字。检索阶段排在前面的不一定是语义上最匹配的技能。

解决思路有两步。第一步,检索阶段引入向量相似度,而不是纯关键词权重。第二步,每个技能的description开头写“不要用于XXX场景”,明确负向边界。比如合同提取技能的描述里加一句“此技能不处理发票信息”。有时候负向提示比正向提示更有效。

4.4 错误排查速查表

我把这个技能系统运行以来最常遇到的五个问题整理成了表格。你在自测时如果遇到类似症状,可以直接对照:

症状可能原因排查步骤解决方案
模型从不调用技能描述没有触发关键词检查用户输入里是否出现描述中的词重写描述,加入用户常见说法
调用了技能但行为错乱SKILL.md执行步骤含糊检查执行步骤是否每步都可量化把“适当”“可能”等模糊词去掉
传参格式不对parameters缺少example看模型实际传参和期望传参差异补充示例值,标注格式
加载技能后上下文暴涨全量注入所有技能统计注入的token数改为按需检索top-k注入
两个技能互相抢占description边界重叠看检索排序结果加负向边界描述,丰富关键词

这张表你在项目上线前拿来自测一遍,能省掉很多线上排查时间。

5. 进阶玩法:技能组合、缓存与效果评测

5.1 技能组合:怎么让多个技能协同完成复杂任务

单个技能解决问题有限,真实业务场景往往是多个技能接力。我在agent-skills里实现了几种组合模式。

最基础的是顺序型。比如“生成周报”这个任务,先调用“数据汇总技能”拉取本周各渠道数据,再调用“报告撰写技能”把数据组织成周报。两个技能按顺序执行,前一个的输出作为后一个的输入。这种组合逻辑简单,适合管道型任务。

第二种是条件分支型。模型需要根据当前情境选择一个分支执行。比如“工单分类系统”——判断工单类型后走不同技能:退款类走“退款处理技能”,技术支持类走“技术答疑技能”。分支判断本身由模型做,技能负责执行各自分支的逻辑。

第三种是并行型。技能之间没有依赖时,可以同时触发多个技能再汇总结果。比如用户要求“帮我分析这份简历并匹配JD”,可以同时调用“简历解析技能”和“JD关键词提取技能”,最后汇总输出。并行能显著降低整体延迟,但成本会增加,因为多个技能同时在跑模型调用。

组合模式的设计原则:技能的组合编排尽量放在Agent层,不要塞进技能内部。一个技能最好只做一个完整子任务,组合逻辑由上层工作流控制。这样技能才能保持独立、可替换。

5.2 缓存与成本控制:不是每个技能都需要实时跑

我做了很多技能后发现,有些技能的返回结果是高度稳定的。比如“查询当前支持的语言列表”“获取静态配置项”这类技能,每次调用都返回一样的内容,再花token去跑模型就不划算了。

解决方案是给技能加缓存层。我在registry里给每个技能加了一个cache_ttl字段,默认为0表示不缓存。改成300就表示结果缓存5分钟,时间范围内直接命中缓存返回。

CACHE_TTL = {"language_support": 300, "config_reader": 60} def execute_with_cache(skill_name, payload): cache_key = f"{skill_name}:{json.dumps(payload, ensure_ascii=False)}" if CACHE_TTL.get(skill_name, 0) > 0: cached = cache.get(cache_key) if cached: return cached, True result, _ = raw_execute(skill_name, payload) if CACHE_TTL.get(skill_name, 0) > 0: cache.set(cache_key, result, ttl=CACHE_TTL[skill_name]) return result, False

这看起来是个很简单的改动,但线上token消耗有时候能降30%以上。尤其是那些被工作流重复调用的技能,缓存收益非常可观。

5.3 怎么衡量一个技能写得好不好

技能写完之后怎么验收?三个指标我觉得最重要。

第一个是调用触发率。统计所有用户请求中,本应触发该技能的请求有多少真正触发了。如果触发率低,问题大概率在description的触发信号不够。

第二个是参数正确率。从日志里拉取该技能被调用时的参数,人工或规则核验必填参数是否完整、格式是否正确。参数错误会直接导致技能执行结果偏离,但这个错误往往不会报异常。

第三个是输出符合度。检查技能的最终输出是否严格符合SKILL.md里定义的输出结构。可以用JSON Schema校验结构化输出,也可以用模型打分评估文本类输出。

我建议你把这些指标写到日志系统里,每次技能调用都记录一条结构化日志。没有日志,技能系统就变成黑盒,出了问题只能靠猜。有了日志,你可以随时回答“这个技能上周被调用了多少次、成功率多少、平均耗时多少”。

在我个人的实操体会里,运行“agent-skills”这套机制最大的收益不是某个技能本身多强大,而是让Agent的行为变得可预期了。以前模型面对一个任务时,思考过程像一团雾,你不知道它下一步要干嘛;有技能之后,模型的选择路径清楚,执行逻辑固定,调试的时候你盯的是技能目录和元信息,而不是猜模型心思。

最后分享一个我自己坚持的小习惯:给每个技能加一个“适用边界”说明。比如文本统计技能,我会在SKILL.md里写清楚“如果输入文本超过100万字,不要直接执行,先提示用户做分段处理”。这个边界说明在很多时候不触发,但一旦触发就能防止模型把简单任务变成灾难。做Agent开发,边界意识比聪明更重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 8:21:16

TCP RTO动态估算原理与实战调优指南

1. 项目概述:为什么RTO不是个固定数字,而是一场实时博弈“TCP的RTO值估算”这六个字,表面看是协议栈里一个冷门参数,实则牵动着每一次网页加载、每一条消息送达、每一帧视频缓冲的成败。我带过不少刚接触网络编程的开发者&#xf…

作者头像 李华
网站建设 2026/10/14 8:21:06

React Native跨平台口腔护理App开发实践

1. 项目背景与核心需求口腔护理类App在移动健康领域一直有着稳定的需求。随着鸿蒙系统的崛起和React Native技术的成熟,开发一个能同时覆盖iOS、Android和HarmonyOS三大平台的口腔健康管理工具变得可行且必要。这个"今日任务"功能模块,本质上是…

作者头像 李华
网站建设 2026/10/14 8:20:59

diagram-design:从可维护性出发的架构图设计原则与工具选型

1. 从“diagram-design”这个标题说起:它到底在解决什么问题第一次看到“diagram-design”这个标题,很多人会下意识觉得它是个画图工具,或者某个前端组件库。但如果你真正做过系统设计、写过技术方案、画过架构图,就会明白这个标题…

作者头像 李华
网站建设 2026/10/14 8:20:02

Servlet+Android+MySQL三端登录通信实战指南

1. 这不是“又一个登录Demo”,而是Web与移动端通信的最小可行骨架你可能在某次技术分享里听过这句话:“注册登录功能,是所有应用的‘Hello World’”。但真正动手搭过Servlet安卓MySQL三端联动的人会立刻笑出声——这根本不是一句轻飘飘的比喻…

作者头像 李华