资讯动态

Agent Skills实战:从大提示词到结构化技能库的智能代理开发指南

发布时间:2026/10/13 23:21:35 来源:尧图企业网站定制
去年我在做 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-runnerdescription一句话概述说明技能做了什么以及典型触发场景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(encodingutf-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( nameextract(name), descriptionextract(description), keywordskeywords, when_to_useextract(when_to_use), when_not_to_useextract(when_not_to_use), executionextract(execution), instructioninstruction, ) 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(encodingutf-8)) visitor ComplexityVisitor() visitor.visit(tree) stats[path] visitor.scores except SyntaxError: continue for path, scores in sorted(stats.items(), keylambda x: max(x[1].values(), default0), reverseTrue): print(path, dict(sorted(scores.items(), keylambda x: x[1], reverseTrue)[: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_asciiFalse, indent2), encodingutf-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, keyscore, reverseTrue) 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_outputTrue, textTrue, timeouttimeout, cwdworkspace, ) return proc.stdout except subprocess.TimeoutExpired: return ERROR: 技能执行超时 except FileNotFoundError as exc: return fERROR: 缺少执行依赖 - {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(fartifacts/{key}.json).write_text( json.dumps(data, ensure_asciiFalse), encodingutf-8 ) def load_artifact(key: str): return json.loads(Path(fartifacts/{key}.json).read_text(encodingutf-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_k1) if matched and matched[0].name case[expect]: correct 1 print(f准确率: {correct}/{len(cases)})这个评测不能反映全貌但作为一轮迭代后的回归测试成本低、见效快。我基本每次改完技能库都跑一遍发现问题当场改累积起来效率很可观。6.3 技能库不是一次成型而是持续生长的最后想聊一下心态技能库更像一个活的体系不是搭建完就结束。我刚开始追求技能覆盖的广度恨不得一个仓库里塞上百个技能。后来发现大多数技能都无人调用真正高频的永远是那十几个核心技能。现在我的习惯是只把实际任务中反复出现的操作沉淀成技能冷门场景宁愿让模型自由发挥也不急着做成技能。同时每个技能我都会视情况维护一个“活跃度”指标定期清理连续多轮没有调用过的技能。和代码库一样技能库也需要精简不然代理每次做技能选择时反而会被大量无效选项干扰。我在实际的使用过程中还有一个观察技能系统做得再完善也只是稳定性的上限保障真正决定下限的仍然是技能内部的指令质量和脚本质量。如果你发现自己不断堆技能但任务成功率没有提升大概率问题出在已有的技能指令写得不够精确而不是缺新技能。这个时候应该回头打磨基础技能而不是继续扩张技能数量。agent-skills 这套体系对复杂任务代理的意义本质上就是把“让模型临场发挥”变成“让模型按图索骥”。只要你的场景里有稳定的、重复出现的子任务就值得把它们沉淀成技能。动手从头做一遍你会对“AI 代理为什么时而聪明时而犯傻”这件事有全新的理解。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑