agent-skills实战:从按需加载到技能路由,构建可复用AI Agent能力

发布时间:2026/10/28 3:09:35
agent-skills实战:从按需加载到技能路由,构建可复用AI Agent能力 1. 从“写死脚本”到“教模型用工具”agent-skills到底在解决什么问题最近一段时间Agent开发圈里讨论最多的一个词除了MCP就是agent-skills了。我最早看到这个概念是在Claude的Skills功能里后来各家框架都在往这个方向靠。很多人把它理解成“给大模型加几个工具函数”实际上远不止这么简单。先说说我为什么对这个东西特别上心。做了两三年Agent应用最头疼的一件事就是每个新任务都要重新设计一套prompt再写一堆工具函数然后反复调提示词让模型知道什么时候该调哪个函数。换一个场景、换一个模型前面的活儿基本白干。agent-skills的思路是把“完成某类任务的能力”打包成一个标准化的技能单元模型按需加载不用把所有逻辑都塞进system prompt里。1.1 一个真实的开发痛点帮你理解它的价值假设你要做一个代码审查Agent。传统做法是写一个超长的system prompt里面描述“你要审查代码的风格、正确性、安全性”然后挂上三五个工具函数比如read_file、run_lint、check_deps。模型每次请求都要把这一大坨喂进去token开销大而且模型经常在“该用哪个工具”这件事上犯迷糊。用agent-skills的思路就完全不同。你可以把“代码审查”本身定义成一个技能包括技能的目标描述、触发条件、执行步骤以及相关的脚本和配置。Agent启动时并不加载全部细节而是先读到一份技能清单根据用户任务决定要不要激活“代码审查”这个技能。激活后相关的指引、脚本、工具才进入上下文。这种“按需加载”的模式既省token又大幅提升了模型调用的准确率。1.2 它和Tools、Function Calling、MCP有什么本质区别很多人会问我直接用Function Calling不就行了或者干脆用MCP这里面的差别我一开始也是绕了很久才想明白。机制粒度是否带执行逻辑上下文开销典型用途Function Calling单个函数否由模型生成参数、外部执行低查询天气、计算器、数据库查询MCP服务/工具集否提供标准化调用协议中连接外部系统、文件系统、浏览器agent-skills技能/任务单元是包含步骤指引、脚本、示例按需加载代码审查、数据分析、文档撰写区别一句话就能说清Function Calling是“让模型能调用一个功能”MCP是“让模型能按协议连接一个系统”agent-skills是“让模型学会完成一类任务”。技能里面可以包含多个函数调用、可以包含决策步骤、可以包含领域知识和示例。它更像是一份“操作手册”加“工具箱”的合体。2. 核心设计拆解一个技能到底是什么为什么这么设计2.1 技能的最小单元不止是一个脚本社区里比较有共识的技能结构一般是这样一个目录skill-name/ ├── SKILL.md # 核心文件描述技能做什么、何时用、怎么做 ├── scripts/ # 可执行的脚本Python、Shell、Node等 ├── assets/ # 参考文档、模板、数据集 └── requirements.txt # 依赖声明可选其中SKILL.md是整个技能的灵魂。它不是一个普通的README而是给模型看的“说明书”。我会在文件开头写清楚技能名称、一句话摘要然后用一个段落描述使用场景和触发条件再列出执行步骤。重点在于这份说明书要让模型在没有额外人工帮助的情况下自己判断“现在我该不该用这个技能”以及“用了之后怎么按步骤走”。我刚开始写时犯的最大的错就是把SKILL.md写成了给人看的开发文档堆了一堆架构设计、模块划分模型看了半天不知道从哪里下手。后来我改成“先给一句话场景判断再给步骤清单最后给示例”效果立竿见影。2.2 description是技能的灵魂模型靠它做路由判断如果把整个技能结构比作一本书那么SKILL.md里的description摘要描述就是书的目录和序言。Agent在选择技能的时候通常不会把每个技能的完整内容都读一遍而是先读描述再决定是否加载详情。所以description有个“三要素”写法什么场景下用“当用户需要审查代码质量时”比“代码审查工具”好一万倍。输入是什么用户会提供什么需要从哪里读取信息。输出是什么技能执行完会返回什么结果。举两个对比差评A tool for code review. 好评Use this skill when the user wants to review or analyze source code for bugs, style issues, or security vulnerabilities. The skill scans the repository, runs linting checks, and returns a structured report with severity levels and recommended fixes.前者模型大概率会误用后者模型几乎不会判断错。这个技巧不仅适用于agent-skills任何基于LLM的工具描述都适用。2.3 上下文效率为什么技能不能全量塞进system prompt我用一个笨办法验证过上下文开销的问题。最初我把五个完整的技能文档全部拼接进system prompt每个技能包含步骤说明、示例、脚本说明加起来差不多一万多token。效果确实不错但成本上去得吓人而且每次请求都带着这一大堆重复内容响应速度也变慢了。后来改成“技能清单按需加载”的模式system prompt里只有每个技能的标题和一行描述总长不到800 token。模型收到用户请求后如果需要再由一个路由函数把对应的SKILL.md全文拉进上下文。实测token消耗降低了差不多七成模型选择技能的准确率反而提升了因为上下文干净了干扰项变少了。这个“清单-详情”的模式本质上跟人处理信息的方式一样先看目录再翻到具体章节而不是把整本书背下来。3. 实操从零实现一套可用的agent-skills运行时理论说再多不如直接撸代码。下面我会用一个极简的Python实现带大家走一遍完整的技能注册、选择、加载、执行流程。这套代码我实际在内部项目里跑过结构可以直接抄走扩展。3.1 定义技能元信息与加载器首先定义一个技能类用来统一管理每个技能的元信息、描述、脚本路径和执行入口。# skill_model.py from dataclasses import dataclass, field from pathlib import Path from typing import Callable, Optional dataclass class AgentSkill: name: str # 技能唯一ID description: str # 给模型看的触发描述决定路由准确性 trigger_keywords: list # 触发关键词辅助路由 skill_dir: Path # 技能目录 entrypoint: Optional[Callable] None # 执行入口函数 def load_description(self) - str: 从SKILL.md中读取完整说明按需加载 md_path self.skill_dir / SKILL.md if md_path.exists(): return md_path.read_text(encodingutf-8) return self.description这里最需要注意的是load_description()只在真正需要执行该技能时才调用而不是初始化时就全部读入内存。这样能确保“清单-详情”模式是真正落地的而不是形式上写了个类、实际还是全部加载。接着写注册和路由逻辑# skill_registry.py import re from typing import Dict, List, Optional from skill_model import AgentSkill class SkillRegistry: def __init__(self): self._skills: Dict[str, AgentSkill] {} def register(self, skill: AgentSkill): self._skills[skill.name] skill def list_skills(self) - List[str]: 返回技能清单轻量级用于注入system prompt return [ f- {name}: {skill.description.strip().splitlines()[0]} for name, skill in self._skills.items() ] def route(self, user_input: str, top_k: int 2) - List[AgentSkill]: 简单的路由策略基于关键词 描述相关性打分。 生产环境可换向量检索或让LLM直接选择。 scores [] for skill in self._skills.values(): score 0.0 for kw in skill.trigger_keywords: if kw in user_input: score 1.0 # 对description进行朴素的部分匹配 for term in re.findall(r[\u4e00-\u9fa5a-zA-Z], user_input): if term and term in skill.description: score 0.3 scores.append((score, skill)) scores.sort(keylambda x: x[0], reverseTrue) return [skill for score, skill in scores[:top_k] if score 0]这个路由逻辑我故意写得朴素方便你看懂原理。真实场景下我建议对用户输入做Embedding然后再和每个技能的描述向量做余弦相似度这种方案在技能数量超过20个时表现更稳定。不过无论哪种方案description的质量都比路由算法更重要描述写得烂用什么高级算法都白搭。3.2 把技能注入到对话上下文的两种方式技能选出来之后怎么给模型用我实践下来有两种主流的方式各有适用场景。方式一注入法适合已有Function Calling机制的框架把技能里的脚本包装成函数然后通过Function Calling机制将技能描述、参数Schema一起注册给模型。这样模型会在对话中自动确定“要不要用这个技能”以及“用什么参数调用”。实现上就是把AgentSkill.entrypoint包装成JSON Schemadef skill_to_function_schema(skill: AgentSkill) - dict: return { type: function, function: { name: skill.name, description: skill.load_description()[:800], # 截断避免超长 parameters: { type: object, properties: { task: { type: string, description: 用户交给技能的原始指令必要时包含路径、需求细节 } }, required: [task] } } }这种方式的好处是模型对各工具的选择是显式的行为可控出错容易排查。坏处是技能内部如果包含多步决策逻辑全塞进一个Function可能不够灵活需要把内部步骤拆成多个函数。方式二动态Prompt注入法适合通用型对话Agent如果技能本质上是“指导模型完成一系列步骤”而非“调用一个外部函数”那更适合把SKILL.md全文动态插进上下文。具体做法是先让LLM根据用户输入判断需要哪些技能从Registry中取回技能对象调用load_description()读取SKILL.md然后拼接到对话上下文中class SkillAwareAgent: def __init__(self, registry: SkillRegistry, llm_call, system_prompt: str): self.registry registry self.llm_call llm_call self.system_prompt system_prompt def run(self, user_input: str): # 第一步预路由得到候选技能 candidates self.registry.route(user_input) print(f[SkillAwareAgent] 命中技能: {[s.name for s in candidates]}) # 第二步加载技能详情 skill_docs \n\n.join( f {s.name} 技能说明 \n{s.load_description()} for s in candidates ) # 第三步注入上下文并调用LLM messages [ {role: system, content: self.system_prompt}, {role: user, content: f用户任务{user_input}\n\n可用的技能说明参考\n{skill_docs}} ] response self.llm_call(messages) return response这套流程的骨架哪怕你用的是LangChain、LlamaIndex或者自研框架都可以照搬中间的“路由加载动态文档”思路只是把llm_call换成相应的接口。3.3 技能内脚本的封装规范技能里如果包含可执行脚本需要在SKILL.md中明确声明调用方式和输出格式。我在公司内部定了一套简单的规范脚本必须从标准输入或命令行参数接收输入不许硬编码路径。脚本必须以JSON格式输出结果方便后续LLM读取。脚本失败时返回非零退出码并在stderr输出错误原因。举一个更新技能脚本的例子#!/usr/bin/env python3 # scripts/run_code_review.py import json, sys def main(): repo_path sys.argv[1] if len(sys.argv) 1 else . # ... 这里做真实的静态检查逻辑 ... report { errors: [example.py:12 未使用的变量], warnings: [], score: 86 } print(json.dumps(report, ensure_asciiFalse)) if __name__ __main__: main()然后SKILL.md里对应的执行段落写清楚## 执行步骤 1. 调用 scripts/run_code_review.py传入仓库路径。 2. 解析返回的JSON报告。 3. 将报告中的errors/warnings整理成给用户的结论并给出修复建议。模型看到这样的指引基本不会在“怎么用脚本”上面卡壳。4. 技能编写与调优文档里不会写的经验4.1 描述里的关键词和示例怎么写模型才认很多人在技能的description里堆砌“powerful”、“efficient”这种形容词对模型路由没有任何帮助。模型判断技能是否匹配靠的是名词和动词的语义重叠比如“review code”、“scan repository”、“check security”。这些具体词汇才是关键词。我的经验是写完description后自己提三个不同问法的问题看模型能不能正确路由。比如写了一个代码审查技能就要测试“帮我看看这段代码有没有问题”、“检查一下项目的依赖安全”、“我怀疑这个函数有bug可行吗”这三个都命中。如果有一个没中说明description里缺少对应的同义表达。可以专门在description最后加一句“常见触发问法示例”效果很好。还有一点要注意不要在description里写负面条件。比如“这个技能只在用户明确要求时使用其他情况不要使用”。模型对否定式的理解很不稳定实测下来正面表达比负面约束有效得多。4.2 技能内部脚本的输入输出约束脚本的输入输出如果不加约束调试的时候会让人崩溃。最典型的问题是两个第一个是路径问题。技能里如果涉及文件操作一定要在SKILL.md里写清楚“默认工作目录是仓库根目录所有相对路径基于该目录解析”。否则模型或者脚本经常在“从哪里读文件”上产生分歧。第二个是输出格式飘忽不定。有些技能内部脚本会输出中文和英文混合、带颜色控制字符的内容这些噪声会严重影响LLM对结果的解读。所以我在每个脚本里强制规定只能输出纯JSON所有日志走stderr。然后在SKILL.md里放一个输出示例{passed: true, issues: [], summary: 未发现问题}模型有条理地解析结果才不会在后续步骤里基于幻觉继续瞎编。4.3 技能依赖与版本管理技能多了之后依赖冲突是必然出现的。比如技能A要求requests2.28技能B要求requests2.31如果两个技能在同一个Python环境里共存总有一个要挂。我建议每个技能尽量做成“子解释器”或“独立容器”运行。简单点可以用venvrequirements.txt每个技能目录自带一个虚拟环境执行脚本时用对应环境的解释器来跑。复杂点可以上Docker但如果经营的是一个中小型项目Docker的运维成本会偏高用venv性价比更高。版本管理方面SKILL.md里应该显式声明依赖包的版本而不是“最新版”。我在生产环境踩过多次坑比如某个升级版的lint工具换了输出结构脚本解析直接崩了。锁定版本再配合定期的安全更新检查才是稳妥的做法。5. 常见问题与排查实录5.1 问题一技能满了但模型就是不调用这是最常见的坑。排查方向有两个第一个方向检查路由结果。把日志里路由模块的输出打出来看看用户输入是否命中了候选技能。如果route返回空说明description里的关键词和用户输入的重叠度太低这时候不需要调prompt而是改description。第二个方向检查模型是否“看到”了技能。如果技能是通过Function Calling注册的确认一下注册之后模型的tools参数里确实包含了该技能。有时候框架内部的注册顺序错了导致后面的工具覆盖了前面的模型当然就调不到。我遇到过一个很奇怪的情况技能清单是正确的路由也命中了但模型就是不调用。后来发现是技能的名称和描述里的用词不一致比如技能ID叫code_review但描述里通篇说scan_code。模型举棋不定最后干脆不选。把名称统一之后就好了。5.2 问题二加载完整技能后上下文爆炸按需加载虽然省了常驻token但单个技能如果写得过长一加载也是几千token。尤其是SKILL.md里放了大量示例代码一个技能动辄五六千token。解决方法有两个一是压缩SKILL.md把示例从“完整代码”改成“伪代码关键接口说明”。比如要展示怎么调用某个脚本只写命令行加一个输出示例即可不用贴整段脚本源码二是做好技能的“多级粒度”把大技能拆成子技能主技能里只保留路由描述和步骤概览具体子步骤由子技能承载。5.3 问题三多个技能描述语义重叠模型选错技能数量超过十个之后两个技能描述重叠的情况非常常见。比如“代码审查”和“代码格式化检查”语义上就很容易混淆。这种问题靠改description只能缓解不能根治。我更推荐的做法是在路由阶段加一道“意图区分逻辑”比如要求用户输入中出现了“格式”、“风格”、“PEP8”等词就偏向格式化检查出现“安全”、“漏洞”、“注入”则偏向安全审查。这套关键词规则比模型自己判断要稳定得多。把规则写在路由模块里而不是寄希望于模型在prompt里自我克制。问题排查方向推荐解法模型不调用技能路由命中情况 注册顺序优化description、对齐命名上下文爆炸SKILL.md篇幅与加载粒度压缩示例、拆分子技能技能选错/混淆描述语义重叠度路由加意图规则、关键词分流6. 按这套思路做下来我的一些实际体会agent-skills这种设计模式说白了就是把以前靠“人工写死流程”的Agent变成“模型按手册执行”的Agent。它不会让Agent立刻拥有超人能力但会让Agent的行为变得可解释、可维护、可复用。我在实际项目中最大的感受是写技能和写代码一样三分靠实现七分靠迭代。第一版技能一定是不完美的但没关系先跑通一个完整链路再根据日志和失败案例去优化description和步骤说明。大概迭代两到三个版本之后稳定性就会有一个明显的跃升。另外如果你做的Agent不依赖任何特定平台我建议把SKILL.md的格式设计得通用一些不要在文档里绑定特定的模型或框架。这样无论是换底层模型、换前端框架技能资产都能复用。我已经把团队里常用的十几个场景沉淀成了技能库新项目来的时候做得第一件事就是把这套技能库迁过去而不是重新开发。最后分享一个很实用的小技巧每一个技能里都应该内置一个“自测用例”章节里面写两三个示例输入和期望输出方便每次改动技能后快速回归。刚开始我觉得这活儿麻烦后来有一次改动导致技能返回结果格式变化跑了两天都没发现直到下游任务突然全挂才知道。加了自测用例之后改动前先跑一遍十分钟就能发现问题。这个小成本能帮你省掉无数个排查波动原因的夜晚。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询