☰
Agent Skills实战:从大提示词到结构化技能库的智能代理开发指南
2026/10/13 23:21:24 网站建设 项目流程

去年我在做 AI 代理相关项目的时候,碰到了一个非常典型的瓶颈:一个看似简单的“整理代码、跑测试、出报告”任务,代理经常干到一半就跑偏,要么把中间产物当成最终答案,要么干脆忘了最开始的要求。我一度以为是模型能力不够,换了更强的模型,结果只是从“错得离谱”变成“错得没那么离谱”。后来真正解决问题的,不是什么玄学调参,而是把整个思路从“一个大提示词干到底”切换成了 agent-skills——也就是给代理预先构建一套结构化的、可复用的技能模块。这个变化带来的稳定性提升,比换模型明显得多。

如果你也在做智能代理类的开发,或者正在被“提示词越写越长但效果越来越飘”折磨,这篇内容应该对你有帮助。我会从设计思路、目录规范、核心实现到调试经验,把 agent-skills 这套方法完整拆开,尽量做到既能看懂原理,也能直接照着落地。

1. Agent Skills 的设计思路:把复杂任务拆成“可复用的原子能力”

1.1 从“大提示词”到“技能组合”

先说一个我踩过的坑。早期做代理任务,我的习惯是把所有要求塞进一个超长提示词:你先读这个目录,然后找到所有 TODO 标记,再按优先级排序,最后生成一个 Markdown 报告。听起来不复杂,但实际执行起来,代理经常会在“读目录”这一步就开始发散,去翻了一堆无关文件,或者把排序规则理解反了。核心问题在于:单次提示词越长,模型需要同时维护的上下文状态越多,中途出错和遗忘的概率就越高。

agent-skills 的解法很朴素:把任务拆成多个独立技能,每个技能只做一件清晰的事情。代理拿到任务后,先分析需要调用哪个技能,再按顺序执行。这样每个技能内部都是高度聚焦的,上下文负担小,出错面也窄。我当时第一个落地场景就是代码评审:把“发现代码里的潜在问题”做成一个技能,把“按严重程度分类”做成另一个技能,把“生成报告”做成第三个技能。组合起来之后,整个流程一下子变稳了。

1.2 一个技能的最小结构:描述、指令、执行体

我最终沉淀出来的技能结构其实非常简单,每个技能就是一个文件夹,里面最多放三样东西:

  • 技能描述(metadata):说明技能叫什么、干什么用、什么时候该用,给代理做决策时参考;
  • 指令文本(instructions):明确的执行步骤和注意事项,相当于这个技能内部的“操作手册”;
  • 执行体(scripts/templates):可选的脚本、模板、配置文件,供需要精确计算或访问外部资源时使用。

其中最重要的不是执行体,而是描述。很多初做技能库的人容易把精力全放在写脚本上,结果代理根本不知道该在什么时候调用这个技能。描述写得含糊,技能就变成了摆设。

举个例子,我最早给“代码格式化”技能写的描述是:“格式化指定目录下的代码文件”。听起来没问题吧?但代理在实际任务中很少会主动想到这个技能,因为它不知道该在什么场景、什么条件下调用。后来我把描述改成:“当用户要求整理代码风格、统一缩进、去除未使用的 import 时,使用本技能。支持常见语言,默认递归处理整个目录。如果用户只提到单个文件,需要先确认范围。”改动之后,代理的调用频率明显上升,而且调用时机的准确率也高了不少。

1.3 技能发现的机制:让代理自己“看到”技能

agent-skills 的一个关键设计是技能发现(skill discovery)。也就是说,代理在执行任务前,不是靠人把技能路径硬编码进提示词,而是模型先读取技能库的索引,再根据当前任务判断要调用哪些技能。

这种做法有两个明显好处。第一,技能库可以持续扩张,不需要每次新增技能都改主提示词。第二,代理能同时看到多个候选技能,自己组合出一条执行链路。举例来说,用户说“帮我检查这几个文件并且给出重构建议”,代理会在索引里同时看到“静态检查”“复杂度分析”“生成 Markdown 报告”三个技能,然后自主决定执行的先后顺序。

索引的规模我建议控制在 50 个技能以内,太多反而会增加模型的筛选负担。技能描述本身要在两到三句话内说清楚,这其实比写代码更难,因为你要站在模型的角度去想:它读到这段描述时,有没有足够的信号把它和同类技能区分开。

2. 技能系统的架构与目录规划:先把“地基”打好

2.1 一套可以直接抄的目录结构

技能库的物理组织方式会直接影响扩展性。我试过几种方案,最后稳定下来的是下面这种:

agent-skills/ ├── index.md ├── code-review/ │ ├── SKILL.md │ ├── review_rules.py │ └── templates/ │ └── report_template.md ├── test-runner/ │ ├── SKILL.md │ └── run_tests.sh ├── doc-generator/ │ ├── SKILL.md │ └── generate_docs.py └── common/ ├── file_utils.py └── logger.py

每个技能文件夹下都有一个 SKILL.md,里面是技能的元信息和指令。index.md 是总索引,汇总所有技能的摘要,方便代理快速扫描。common 目录放公共工具模块,避免每个技能重复造轮子。

这个结构的好处是:每个技能都是自包含的,可以独立测试、独立版本管理。我把这个目录直接放进 Git 仓库,每次新增技能都走一次代码评审,确保描述质量和脚本质量都过关。

2.2 SKILL.md 里的元信息怎么写才管用

我整理了一个自己常用的 SKILL.md 模板,具体字段如下:

字段作用建议
name技能唯一标识用连字符分隔,如 test-runner
description一句话概述说明技能做了什么,以及典型触发场景
keywords关键词列表帮助模型在做语义匹配时提高召回率
when_to_use具体适用场景写得更细,避免误调用
when_not_to_use不应该使用的场景这个字段被很多人忽略,其实是防跑偏的关键
execution执行方式纯指令 / 调用脚本 / 混合

attention 到when_not_to_use这个字段,我以前是不写的,后来发现没有它,代理会经常“勉强调用”。比如“归档旧文件”这个技能,我忘了写“如果用户只是需要删除文件,不要用本技能”,结果代理在遇到删除请求时也尝试归档,产生了不必要的副作用。加上排除性描述之后,误调用率下降了很多。

2.3 技能粒度:太大没用,太小累赘

技能粒度的把控可能是这个体系里最容易走极端的地方。粒度太大,比如一个“处理代码项目”的技能,本质上又变成了大提示词,毫无意义。粒度太小,比如“读取一个文件”“写一行日志”,代理为了完成一个简单任务要调用七八个技能,开销极大,而且链路越长,累积的上下文失真就越多。

我自己参照的粒度标准是:一个技能应该在一次执行中完成一个可以被验证的、有明确产出的子任务。比如“把代码格式化成 PEP8 风格”“运行指定测试文件并汇总失败用例”“根据 Jira 风格的标题生成周报”,这种粒度刚合适。它不需要代理做太多决策,但也不是一个机械操作。

这里额外说一句,技能内部的指令文本也不要写太长。我给自己定的上限是 400 字左右,超过这个长度,我会考虑是不是功能拆得还不够细。指令越短,模型执行时跑偏的空间越小。

3. 从零实现一个技能开发套件:动手第一步

3.1 用最小代码把技能跑起来

理论归理论,真正动手时,我建议先搭一个最小可用的技能加载框架,再逐步加功能。下面是我早期验证思路时写的一个很轻的加载器,核心目的只有一个:把 SKILL.md 读出来,解析成结构化的数据。

# skill_loader.py from dataclasses import dataclass from pathlib import Path import re @dataclass class Skill: name: str description: str keywords: list when_to_use: str when_not_to_use: str execution: str instruction: str script_path: str | None = None def parse_skill_file(skill_dir: Path) -> Skill: skill_file = skill_dir / "SKILL.md" text = skill_file.read_text(encoding="utf-8") # 粗糙的前置提取,先用正则把字段拆出来,后期再换 YAML def extract(field: str) -> str: match = re.search(rf"^{field}:\s*(.+)$", text, re.MULTILINE) return match.group(1).strip() if match else "" # 指令部分一般放在元信息之后 instruction_start = text.find("## Instructions") instruction = text[instruction_start:].strip() kw = extract("keywords") keywords = [k.strip() for k in kw.split(",") if k.strip()] return Skill( name=extract("name"), description=extract("description"), keywords=keywords, when_to_use=extract("when_to_use"), when_not_to_use=extract("when_not_to_use"), execution=extract("execution"), instruction=instruction, ) def load_skills(root: Path) -> list[Skill]: skills = [] for child in root.iterdir(): if child.is_dir() and (child / "SKILL.md").exists(): skills.append(parse_skill_file(child)) return skills

用正则做解析只是验证阶段的做法,实际项目我建议直接上 YAML frontmatter,字段规整,也不容易出错。下面这个示例就是我后来在项目里用的实际格式。

--- name: test-runner description: 运行测试文件并输出失败用例汇总,适用于用户要求验证功能或排查回归场景。 keywords: [pytest, unittest, 测试, 回归, 失败用例] when_to_use: 需要执行测试并获取结构化结果时。 when_not_to_use: 用户只是询问测试覆盖率或测试设计思路,不要求实际运行。 execution: script --- ## Instructions 1. 使用项目的默认测试命令,优先读取根目录下的配置文件; 2. 执行测试并捕获输出; 3. 解析失败用例,按模块分组汇总; 4. 输出结果为 Markdown 片段,包含失败原因与相关文件路径。

3.2 写第一个技能:代码静态评审

第一个技能我建议选一个你每天都会做的重复性任务。以我这边为例,我第一个正式技能是“静态代码评审”。原因是团队日常 review 大量代码,但人工看一遍效率低,我希望能让代理先做一轮自动化评审,把明显的认知负担先干掉。

这个技能的 SKILL.md 核心部分大致如下:

--- name: code-review-helper description: 对指定代码文件或目录做静态评审,关注潜在逻辑问题、重复代码、异常处理缺失。 keywords: [评审, code review, 静态检查, 逻辑缺陷, 异常处理] when_to_use: 用户要求对代码质量给出反馈,或提交 PR 前希望自动检查时。 when_not_to_use: 用户只要求执行测试或格式化,不涉及质量判断时。 execution: instruction --- ## Instructions 1. 遍历目标路径下所有源文件,忽略 node_modules、dist、build 等目录; 2. 对每个文件执行以下检查: - 是否存在未处理的异常分支; - 是否存在明显的重复代码片段; - 是否存在无用的变量或不可达分支; 3. 将所有发现按“严重问题/建议优化/风格提醒”三级分类; 4. 输出 Markdown 报告,每条问题附带文件名和行号范围。

实际执行下来,我发现一个容易被忽略的点:技能描述里写清楚“需要读取哪些文件、忽略哪些目录”,能大幅减少代理做无效扫描。很多代理跑偏,往往不是能力问题,而是它不知道“安全区”在哪里。

3.3 进阶:带 Python 脚本的技能

纯指令式技能适合简单场景,但需要精确计算或批量处理时,最好把脚本逻辑也一起打包。我举一个实际例子:统计项目里各模块的复杂度。这个任务靠模型凭空估算很容易翻车,直接写个脚本算 Cyclomatic Complexity 才是正解。

# complexity_stats.py import ast import sys from collections import Counter from pathlib import Path class ComplexityVisitor(ast.NodeVisitor): def __init__(self): self.complexity = 1 self.scores = {} self.current_scope = None def _record_branch(self): self.complexity += 1 def visit_FunctionDef(self, node): old_scope, old_score = self.current_scope, self.complexity self.current_scope = node.name self.complexity = 1 self.generic_visit(node) self.scores[node.name] = self.complexity self.current_scope = old_scope self.complexity = old_score visit_AsyncFunctionDef = visit_FunctionDef def visit_If(self, node): self._record_branch() self.generic_visit(node) def visit_For(self, node): self._record_branch() self.generic_visit(node) def visit_While(self, node): self._record_branch() self.generic_visit(node) def visit_ExceptHandler(self, node): self._record_branch() self.generic_visit(node) if __name__ == "__main__": root = Path(sys.argv[1]) stats = {} for path in root.rglob("*.py"): if any(part in {"node_modules", ".venv", "dist"} for part in path.parts): continue try: tree = ast.parse(path.read_text(encoding="utf-8")) visitor = ComplexityVisitor() visitor.visit(tree) stats[path] = visitor.scores except SyntaxError: continue for path, scores in sorted(stats.items(), key=lambda x: max(x[1].values(), default=0), reverse=True): print(path, dict(sorted(scores.items(), key=lambda x: x[1], reverse=True)[:5]))

技能库里有了这种脚本,代理去跑的时候就不用猜了,结果准确率和可解释性都高。脚本本身也不需要写得多优雅,能稳定输出结构化结果就行。我更建议把脚本的入口参数尽量做得简单——只接收目标路径和可选参数,少交互,因为代理执行命令时不适合处理太复杂的交互流程。

4. 核心调度模块的实现:技能库背后的“大脑”

一个技能库如果没有调度机制,就只是一堆 Markdown 文件的集合。我在项目里把这部分称为“技能调度核心”,负责加载技能、解析任务、匹配技能、执行调用。这一节我拆开讲实现细节,你照着做就能搭出一条可用的链路。

4.1 技能加载与索引生成

技能调度核心的第一步是把技能库里的所有 SKILL.md 加载成内存对象,同时生成一份汇总索引。这份索引会作为系统提示词的一部分,交给代理做技能选择。

# skill_indexer.py import json from pathlib import Path from skill_loader import load_skills def build_index(root: Path) -> str: skills = load_skills(root) lines = ["## 可用技能列表", ""] for skill in skills: lines.append(f"- {skill.name}: {skill.description}") if skill.keywords: lines.append(f" 关键词: {', '.join(skill.keywords)}") lines.append(f" 避免在以下场景使用: {skill.when_not_to_use}") return "\n".join(lines) def save_index(root: Path, output: Path): skills = load_skills(root) data = [ { "name": s.name, "description": s.description, "keywords": s.keywords, } for s in skills ] output.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")

索引的长度和句子结构我会控制得比较统一。不要有的技能描述写了三行、有的只写五个字,模型在取舍时会给描述更长的技能不自觉加权,这是一个很隐蔽的偏好来源。

4.2 技能匹配:调用策略的选择

技能匹配有两条路线:一条是把索引塞进提示词,让模型自己决策;另一条是提前跑一个相似度匹配,把候选技能过滤到两三个,再让模型做最终决定。

早期我直接用第一条,简单,但技能多了之后提示词越来越长,选择和决策成本都上升。后来我改成混合策略:先用关键词匹配粗筛,再让模型做精排。

# skill_matcher.py from skill_loader import load_skills def rough_match(task_text: str, skills, top_k: int = 3): def score(skill): s = 0 for kw in skill.keywords: if kw.lower() in task_text.lower(): s += 2 if skill.name.lower().replace("-", "") in task_text.lower().replace("-", ""): s += 3 if skill.when_not_to_use and any(w in task_text for w in skill.when_not_to_use.split("、")): s -= 5 return s scored = sorted(skills, key=score, reverse=True) return [s for s in scored if score(s) > 0][:top_k]

注意我把when_not_to_use设成减分项,这是防止误调用的关键。很多误调用的问题,本质上是正向关键词拉高了候选排名,但缺少负向信号的约束。加了这个减分机制之后,效果立竿见影。

4.3 执行与安全:沙箱、超时与重试

技能不是都能顺滑执行的。脚本可能存在路径穿越、外部命令调用失败、甚至死循环的问题。我总结下来,三个安全措施必须有:工作目录隔离、超时控制、重试回退。

# skill_executor.py import subprocess import sys def run_script(script_path: str, args: list[str], timeout: int = 30): try: proc = subprocess.run( [sys.executable, script_path, *args], capture_output=True, text=True, timeout=timeout, cwd="workspace", ) return proc.stdout except subprocess.TimeoutExpired: return "ERROR: 技能执行超时" except FileNotFoundError as exc: return f"ERROR: 缺少执行依赖 - {exc}"

在 cwd 上做隔离很重要。我会让每个技能在独立的临时工作目录里运行,避免脚本误读项目文件或者互相污染。超时时间根据技能类型动态配置:文件扫描类给 60 秒,简单计算类给 10 秒,千万不要一刀切。重试逻辑我建议只针对“依赖缺失”类的错误重试一次,如果依赖彻底安装失败,再重试也是浪费时间。

5. 常见问题与排查技巧实录:我在实战里踩过的坑

技能库用起来之后,你会迎来另一类问题:不是技能本身写错了,而是代理用错了。我把这段时间遇到的典型问题整理成了一份速查表,每个问题补上了我的排查思路。

5.1 技能“看不见”或“被无视”

现象:代理面对任务时没有调用任何技能,直接开始自由发挥。原因十有八九是索引生成的时机不对,或者索引没有出现在系统提示词当中。

我的排查顺序是:先确认索引文件是否生成成功,再确认生成后的内容是否包含目标技能的描述,最后确认系统提示词里的索引是最新版本而不是缓存的旧版本。曾经有一次我在本地测试全部正常,部署上去之后却一直不生效,最后发现是部署流水线里拉代码之后没有重新执行索引构建。这个问题卡了我大半天,说白了就是构建时序问题。

5.2 技能被“错误调用”

现象:用户问“这个函数能不能优化?”,代理却去调了“文档生成”技能。这类问题最烦人,因为不是一次性失败,而是每次都在不确定的时候抽风。

我的建议是优先检查when_not_to_use,把排除场景写具体,写狠一点。同时关注技能之间的描述差异性:如果两个技能的 description 里都出现了“代码”“检查”“优化”这类高频词,模型很难不混淆。我会把高频词打散,让每个技能拥有独特的触发词汇,比如“复杂度分析”技能突出“圈复杂度、嵌套层数、分支数量”,“重构建议”技能突出“坏味道、重复代码、过长函数”。

5.3 长任务执行到一半就断

现象:代理调用了技能 A、B,但在执行技能 C 之前,上下文已经溢出或者状态丢失了。这个问题在纯指令型技能里特别常见。

我的对策是给技能之间设计一种“中间产物传递”机制:技能 A 的输出存成文件,技能 B 在执行时读取该文件,而不是完全依赖上下文传递结论。这样即使模型在某个环节把上一步的结果简述丢了三成信息,技能 B 拿到的还是原始完整数据。下面是一个最基本的实现:

# context_store.py import json from pathlib import Path def save_artifact(key: str, data: dict): Path(f"artifacts/{key}.json").write_text( json.dumps(data, ensure_ascii=False), encoding="utf-8" ) def load_artifact(key: str): return json.loads(Path(f"artifacts/{key}.json").read_text(encoding="utf-8"))

用文件传递中间结果之后,长任务稳定性提升非常明显。代价是多了一点 I/O 开销,但对于最核心的可靠性收益来说完全值得。

5.4 技能执行结果格式不稳定

现象:同一个技能,有时候输出 JSON,有时候输出 Markdown,有时候是纯文本。这种不稳定性直接破坏下游处理流程。

纯靠“在指令文本里写‘请输出 JSON’”是不够的,模型对格式的理解取决于你是否给了一个具体的例子。我后来在 SKILL.md 的指令里直接附上了输出模板,用三个反引号把格式圈出来,并在模板里注释掉所有可能的歧义。有了模板之后,格式一致性基本能稳定到九成以上。

症状可能原因推荐排查动作
代理完全不使用技能索引未更新或未注入检查索引日志,确认提示词中技能列表存在
技能调用时机错误描述存在高频重叠词重写 description,增加独有触发词
执行结果格式乱指令缺少输出模板在 SKILL.md 中补充模板示例
长任务中断上下文信息逐级丢失中间结果落盘,降低跨技能上下文依赖
脚本运行报错环境依赖不一致在技能目录增加依赖说明文件,统一执行环境

6. 可观测性与持续迭代:怎么判断技能好不好用

6.1 给代理调用链路打上完整日志

技能系统能不能持续变好,取决于你有没有一套观测手段。我刚开始犯的错是只记录技能执行的成功和失败,忽略了“调用前模型是怎么做决策的”。后来我在调度核心的每个关键节点都埋了日志,至少记录四个方面:

  • 任务原文摘要:不用全量记录,但要记录特征向量或关键词,方便后续分析;
  • 粗筛候选技能:记录每个候选技能的评分明细;
  • 模型最终选择:记录最终命中的技能及其选择依据(如果模型输出可解释文本);
  • 执行结果摘要:记录技能输出长度、用时、是否报错。

有了这些日志,你可以轻易回答一个关键问题:当一个技能调用失败时,到底是“模型选错了技能”还是“技能本身实现有问题”。

6.2 离线评测:不依赖线上流量就能迭代

技能系统的迭代瓶颈不在写新技能,而在验证。等线上日志积累到一定规模,我会抽取一批典型任务样本,离线跑一遍完整的调度链路,得出成功率和误调用率两个指标,然后针对最差的问题迭代技能描述。

这里分享一个我常用的手工评测脚本思路:用一组固定的测试任务,每次修改技能库后跑一遍,对比指标变化。测试任务最好覆盖每个技能的正面场景和负面场景各一套,正面场景验证技能能被正确调用,负面场景验证技能不会被误调用。

# evaluate.py cases = [ {"task": "请检查这个目录下的Python文件并给出评审意见", "expect": "code-review-helper"}, {"task": "帮我运行test_case_01里的测试并汇总失败项", "expect": "test-runner"}, {"task": "把项目里所有类的继承关系整理成文档", "expect": "doc-generator"}, ] def evaluate(skills): correct = 0 for case in cases: matched = rough_match(case["task"], skills, top_k=1) if matched and matched[0].name == case["expect"]: correct += 1 print(f"准确率: {correct}/{len(cases)}")

这个评测不能反映全貌,但作为一轮迭代后的回归测试,成本低、见效快。我基本每次改完技能库都跑一遍,发现问题当场改,累积起来效率很可观。

6.3 技能库不是一次成型,而是持续生长的

最后想聊一下心态,技能库更像一个活的体系,不是搭建完就结束。我刚开始追求技能覆盖的广度,恨不得一个仓库里塞上百个技能。后来发现大多数技能都无人调用,真正高频的永远是那十几个核心技能。现在我的习惯是:只把实际任务中反复出现的操作沉淀成技能,冷门场景宁愿让模型自由发挥,也不急着做成技能。

同时每个技能我都会视情况维护一个“活跃度”指标,定期清理连续多轮没有调用过的技能。和代码库一样,技能库也需要精简,不然代理每次做技能选择时反而会被大量无效选项干扰。

我在实际的使用过程中还有一个观察:技能系统做得再完善,也只是稳定性的上限保障,真正决定下限的仍然是技能内部的指令质量和脚本质量。如果你发现自己不断堆技能但任务成功率没有提升,大概率问题出在已有的技能指令写得不够精确,而不是缺新技能。这个时候应该回头打磨基础技能,而不是继续扩张技能数量。

agent-skills 这套体系对复杂任务代理的意义,本质上就是把“让模型临场发挥”变成“让模型按图索骥”。只要你的场景里有稳定的、重复出现的子任务,就值得把它们沉淀成技能。动手从头做一遍,你会对“AI 代理为什么时而聪明时而犯傻”这件事有全新的理解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询