Agent Skills实战:从技能封装到路由编排的完整落地指南
发布时间:2026/9/17 21:47:31 锦皓数字建站

我最早开始认真整理 agent-skills不是因为看了哪篇论文而是因为一次能跑但没法用的原型翻车。那段时间我在做一个内部知识库问答代理用户丢一个需求进来代理要完成检索、摘要、生成报告三个动作。最初版全用提示词堆单轮表现不错可一旦任务链条拉长、多个需求混在一起模型就开始丢步骤、漏字段、把上一单的上下文串到下一单。后来我把每个动作拆成独立的技能模块把行为、输入输出契约、私有记忆和调用权限全部封装进技能包问题才真正稳定下来。这篇文章不聊空泛的Agent能力图谱就讲我在实际搭建 agent-skills 时沉淀下来的设计思路、落地链路和踩坑记录适合正在做代理应用、又不想止步于写写 prompt 的开发者。1. 我为什么开始整理 Agent Skills从一段失败的原型说起1.1 那次能跑但没法用的教训最开始的原型并不复杂一个路由层加几段长 prompt。用户输入后路由层判断意图拼上对应的指令模板再调用大模型返回结构化 JSON。演示时数据是干净的问题也简单看起来一切正常。可一放开真实流量大概第三天就陆续暴露问题用户会在同一条消息里夹带两三个任务模型偶尔就漏掉第二个某些问题在 A 会话里回答得很好在 B 会话里却因为历史消息干扰写了完全不同的格式更麻烦的是当任务需要先查询再生成时模型会把查询 SQL 里的拼接错误当成既定事实继续往下编报告。我复盘之后发现问题不在模型能力而在我们对能力的定义太粗糙。prompt 是静态文本它没有版本、没有接口、没有归属谁都可以改改了之后谁也不知道影响谁。而 agent-skills 的出发点就是把能力当作软件工程里的一等公民来管理一段可复用的行为有明确的输入输出、有版本、有依赖、有测试、可以被路由、可以被组合。1.2 技能、工具、提示词、工作流先给名词划清边界很多团队在谈论技能时其实说的是完全不同的东西。为了后面不产生歧义我先把名词的边界划清楚工具Tool单次的无状态函数调用比如查询天气执行 SQL读文件。它不关心调用之前发生了什么只处理当前这一次输入。提示词Prompt一段静态指令告诉模型怎么回答或怎么输出。没有生命周期修改成本低但也不可测试、不可组合。工作流Workflow有状态的固定流程比如先查库存→再算价格→再生成订单。流程写死在代码里灵活度低但可控。技能Skill位于这几者之上它是一个自包含的行为模块包含了怎么完成任务的方法论、可调用的工具、私有记忆、输入输出契约并且可以被显式路由和编排。我用一个生活化类比来理解它工具是一只手提示词是一句叮嘱工作流是一条流水线而技能是一个会干活的人。这个人知道自己该干什么、需要什么材料、用什么手段干、干完之后把什么结果交给你还能记住上一单的经验教训。1.3 技能设计的第一性原理有了边界我给自己定了三条原则后面所有设计都围绕这三条展开。第一技能必须可复用。同一个技能要能在不同会话、不同代理、不同任务里被反复调用不能跟某一次对话的上下文强绑定。第二技能必须有接口。它对外暴露一个清晰的调用约定内部怎么实现、怎么描述怎么做调用方不需要知道。接口的意义在于模型的自由发挥被收敛到了接口边界内接口之外的行为是确定性的。第三技能必须可观测。每一次调用消耗了多少上下文、调用了哪些工具、模型做了几步推理、是否成功都要有记录。没有观测你就不可能在真实环境里持续改进。这三条原则听起来简单但在实现时会产生很多细节问题接下来几章我逐个展开。2. 一个可复用的技能包到底长什么样2.1 技能清单与输入输出契约我习惯每个技能都包含四个文件描述文件skill.md、指令体instruction.md、输入输出 Schemaschema.json、示例集examples.jsonl。也可以把这些合并成一个 YAML 文件但分文件在团队协作时冲突更少。描述文件是整个技能的门面里面写清楚技能名称、适用场景、适用边界、典型的调用示例以及一句话说明什么时候不该用。这段描述非常重要因为路由层依赖它来判断是否把任务派给这个技能。写得模糊模型就会乱派单。输入输出 Schema 是技能和外部世界的契约我直接复用 JSON Schema 标准定义每个参数的名称、类型、枚举范围、必填项和互相依赖关系。这里我特别强调限制输入的枚举值与格式而不是只写给句话。比如生成周报技能的输入不能只有一个用户请求字符串而应该是 start_date、end_date、data_sources、report_format 这些结构化字段。字段越结构化模型越不容易跑偏。下面是一个简化但真实的 schema 示例{ name: weekly_report, description: 根据数据源生成周报。当用户要求汇总本周业务情况、分析趋势、输出周报文本时使用。, input_schema: { type: object, properties: { start_date: { type: string, format: date, description: 开始日期不含时区 }, end_date: { type: string, format: date, description: 结束日期 }, data_sources: { type: array, items: { type: string, enum: [orders, traffic, refund] }, minItems: 1 }, report_format: { type: string, enum: [markdown, html], default: markdown } }, required: [start_date, end_date, data_sources] }, output_schema: { type: object, properties: { report: { type: string, description: 最终报告正文 }, metrics: { type: object, description: 报告中的关键指标汇总 } }, required: [report] }, dependencies: [sql_query_runner, data_formatter] }2.2 怎么做的沉淀可执行指令的三层结构指令体是这个技能最核心的部分它决定了同样一次调用输出质量是 60 分还是 90 分。我尝试过很多写法最后稳定下来的是三层结构。第一层角色与目标。说明你这个技能在具体任务里扮演什么角色这一次调用要交付什么结果。不要写你是一个优秀的分析师这类玄学直接写你要基于 data_sources 字段指定的数据表输出一份面向业务负责人的周报周报必须包含趋势判断和异常说明。第二层执行步骤。用编号列出从输入到输出的中间步骤每一步都尽量以读取什么字段、执行什么动作、产出什么中间结果来描述。比如第 1 步读取 data_sources列出对应表名第 2 步调用 sql_query_runner 生成查询并执行第 3 步把查询结果格式化成 Markdown 表格第 4 步分析趋势并写结论。这样做的好处是模型在长任务中的决策点被拆小了每一步的错误都能在观测日志里定位。第三层质量约束与负面清单。明确写出哪些动作不能做哪些情况要停止并上报。比如当某个数据源连续三次查询失败时不要猜测数据直接返回失败状态并列出错误原因再比如报告中不允许出现未经验证的百分比。负面清单其实是给模型设置护栏效果比不停加 prompt 赞美它请谨慎好很多。2.3 记忆与经验技能内部的私有知识很多 agent 的记忆设计都是全局记忆所有技能共享一段历史。这在简单 Demo 里没问题但一遇到复杂业务就会出现上下文污染上一个任务的中间结论被模型误当成当前任务的先验条件。所以我在技能包里加了一个私有记忆区skill memory只有这个技能在执行时才能读写。私有记忆主要存三类东西本次执行的中间状态已经拿到哪些数据、生成了哪些中间结论。短期会话经验上次执行这个任务时模型采样到的方式比如上次客户把关注点放在退货率上这次生成报告时要额外突出退款趋势。失败记录上一次执行在哪一步失败过、原因是什么。下次执行时模型会先读取失败记录主动避开同样的坑。这个设计彻底改变了我对 agent 可靠性的看法。过去我们总想用一个超大的系统 prompt 把模型的全局行为都约束住结果上下文越长模型越容易丢失早期信息。而把记忆拆到技能内部后每个技能的上下文窗口控制在比较小的范围指令的精确度和稳定性都明显提升。2.4 上下文加载策略技能包与 LLM 上下文的结合这里有一个容易被忽略的工程细节并不是技能包里的所有内容都要一次性塞给模型。你可以把技能包拆成静态部分和动态部分。静态部分包括角色与目标、执行步骤、质量约束这些在调用时是固定的可以缓存不用每轮都重复构造 prompt。动态部分包括本次输入的参数、最新拉取到的数据、最近几轮会话中的关键消息、私有记忆中的失败记录这些才需要实时组装。我常用的做法是设置一个技能描述索引每个技能在注册时生成一段 30 到 50 个 token 的摘要描述路由层只看到这段摘要。只有当技能被真正选中并开始执行时才把完整的指令体、输入参数和所需记忆组装进模型的上下文。这种方式既保证了路由层的轻量又避免了上下文被技能包撑爆。3. 从代码到运行把技能注册进 Agent 的完整链路3.1 注册阶段让代理知道有哪些技能在代码层面我会给每个技能定义一个统一的结构体包含元信息、指令加载器和执行器然后通过一个注册函数把它们放进技能仓库。注册函数的职责不只是存一个字典它还会做三件事校验技能的 Schema 是否合法、检查依赖技能是否已存在、生成技能摘要描述并写入路由索引。示意代码如下dataclass class SkillMeta: name: str description: str input_schema: dict output_schema: dict version: str 1.0.0 dependencies: list[str] field(default_factorylist) class SkillRegistry: def __init__(self): self._skills: dict[str, SkillMeta] {} self._loaders: dict[str, Callable] {} def register(self, meta: SkillMeta, loader: Callable): for dep in meta.dependencies: if dep not in self._skills: raise ValueError(f技能 {meta.name} 依赖 {dep} 未注册) self._skills[meta.name] meta self._loaders[meta.name] loaderloader 是一个惰性加载函数负责在技能真正执行时把指令体、示例、私有记忆等资源加载进来。用惰性加载的原因很简单很多技能包含大量示例和长指令如果服务启动时全部加载到内存资源占用会非常高而且完全没有必要。3.2 路由与选择不是所有请求都值得进入技能路由层是我调试得最多、也最容易被低估的部分。最初版本我尝试让模型从完整技能列表里随意挑选结果经常选错。后来我改成两阶段选择第一阶段是规则过滤用关键词和简单的分类器把明显不匹配的技能排除掉把候选集缩小到 3 到 5 个第二阶段才是模型精排把这几个技能的摘要描述交给模型让它选择最合适的一个。这样设计的好处是既稳又省。规则过滤是确定性的不会因为模型状态波动而出错模型精排只面对少量候选决策成本低、准确率高。如果候选集只有一个甚至可以直接跳过模型省掉一次调用。还有一个边界情况要处理如果所有技能都不匹配怎么办我会在路由层设置一个 fallback 策略默认返回未找到合适技能并把用户请求原样转发给一个通用的问答技能而不是硬套一个不相关技能。经验是宁可让代理明确说我不会也不要让它牛头不对马嘴地硬答。3.3 执行与编排边界内的确定性和边界外的自由执行阶段的核心原则是确定性的事情交给代码不确定性的事情交给模型。比如参数校验、格式转换、数据查询、重试机制这些全部用代码完成不走模型。模型只负责两件事判断怎么拆解任务、以及生成需要语言创造力的内容。这也体现在技能内部的编排逻辑上。一个技能执行时会按照指令体的步骤走但每一步的走向也不是纯自由的。我在执行器里定义了一组 step handlers每个步骤对应一个 Python 函数。模型可以选择按顺序执行步骤也可以根据中间结果跳转或提前终止但所有步骤必须在预定义的集合内选择。换句话说模型不是一个完全自由的白板它是在一个有向的流程图上做路径选择自由度被约束在技能定义内部。这种设计在排障时特别有价值。因为每个步骤都有独立日志一旦输出质量下降我能立刻看到模型在哪一步做了偏差决策而不是面对一整段黑盒思考。3.4 观测、追踪、评估技能的体检报告没有观测技能优化就是盲人摸象。我在每次技能调用时都会记录一份调用追踪trace包含请求参数、路由决策依据、选择的技能名与版本、每一步执行耗时、模型 token 消耗、各步骤输出片段、最终输出、成功或失败标记、失败原因分类。日志数据汇总后我会重点关注三个指标技能命中率路由是否正确选择技能、执行成功率技能内步骤是否全部完成无报错、端到端满足率用户是否认为输出符合需求通常由下游反馈或人工抽样决定。这些指标不只是复盘用还直接决定技能的迭代节奏。如果命中率低说明技能摘要或适用范围写得不清楚要先调整路由描述如果命中率高但执行成功率高输出质量却差那问题大概率出在指令体或示例集上。指标会指导你改哪里避免每次出问题都去瞎调 prompt。4. 我在实测中踩过的三个高频坑4.1 坑一技能一泛化就失灵第一次做技能抽象的时候我总想把相似流程合并成一个万能技能想着通过参数来控制行为。结果技能描述变得又长又含糊路由层经常把它错误地选给毫不相干的任务执行时模型也因为指令太多而顾此失彼。后来我的处理方式很干脆宁可用十几个小技能也不用一个全能技能。判断标准是适用边界是否足够窄——如果一句话说不清什么时候该用这个技能那就说明边界不够清晰需要拆分。技能小不代表代码会重复公共逻辑可以抽到依赖技能里复用但对外暴露的技能必须是边界清晰的。4.2 坑二上下文污染导致同一个技能在不同会话里表现迥异前面提过全局上下文污染但在实际多人协作场景中还有一个更隐蔽的问题技能进化带来的隐性污染。团队里有人觉得某个技能输出不够详细就往指令体里加了一段要更详细地输出另一个人遇到相反问题又加了一段要简洁。加来加去指令体自相矛盾技能的稳定性断崖式下降。这个坑的根因是缺少版本管理和变更评审。现在我把技能的指令体当作代码来管任何修改必须有变更记录修改后必须跑一遍回归测试集才能发布新版本。发布后旧版本还要保留一段时间方便出问题时快速回滚。4.3 坑三拿个例当回归测试改一处崩一片最开始给技能写测试时我挑了几个经典成功案例存下来每次修改后跑一遍通过了就觉得没问题。结果有一回我优化了一个技能的输入处理逻辑示例全过了但一上真实流量发现某种历史格式的输入全部解析失败。为什么因为我的示例集里根本没有这种输入格式它是我后来临时加的兼容分支从来没有被测试覆盖过。与其事后后悔不如一开始就建立一个足够有代表性的测试集。我把历史真实调用中所有失败样本、边界样本、用户反馈差样本全部收集起来压进测试集分成两类必须通过的regression和辅助参考的reference。每一轮技能升级先跑必须通过的集合全部通过才允许发布。4.4 如何系统性地做技能回归测试技能回归测试和传统单元测试有很大区别它不是断言5 3 8而是判断这次输出是否满足用户需求带有主观性。我用的是两层评估法第一层是程序化断言检查输出 schema 是否合法、必填字段是否存在、枚举值是否正确、数量级是否合理第二层是模型评估用一个独立的评估模型对照用户请求和输出打出 1 到 5 分的质量分。两层结合的好处是程序化断言能捕获结构性问题模型评估能捕获语义问题。每轮发布时跑一遍完整测试集质量分低于阈值的样本会被单独标记让我在发布前知道这次改动可能影响哪些场景。5. 把技能库当产品做版本、依赖与多技能协作5.1 技能版本与依赖关系当技能库超过二三十个你会发现技能之间会有依赖链技能 A 调用了技能 B技能 B 又依赖了技能 C。这时版本管理就不是简单的单技能版本号问题而是要处理兼容矩阵。我的做法是给每个技能声明两种信息自身版本、依赖的最低兼容版本。注册时会校验依赖版本不满足就拒绝加载。发布时先生成一张依赖关系图逐层升级避免 A 依赖 C 的新特性但 C 还没升级导致的运行时错误。具体管理上我并没有引入太重的外部依赖就是用装饰器和注册表做了一个轻量依赖解析器。效果是当技能规模上来后集成测试反而比小规模时更稳定因为依赖关系被显式约束不会有人悄悄改坏一个被依赖的技能。5.2 技能之间的组合与编排单技能只能完成单领域任务真正复杂的用户需求往往要多个技能协作。比如生成本周运营报告可能涉及数据查询技能“指标计算技能“报告生成技能三个技能。我不太建议把这种协作逻辑全部交给模型自由发挥而是倾向于在技能内部显式声明子流程编排计划。编排计划的写法如下定义一个清单列出涉及哪些技能、每个技能输入取自哪里、输出交给谁、是否有条件分支。执行器拿到这个计划后按依赖顺序依次调用子技能。每个子技能执行完后结果写入一个共享的结果池供后续技能读取。这个结果池被严格限制在当前任务作用域内不会泄漏到其他任务。这样做之后编排的稳定性提升非常明显。过去模型自由调用子技能时经常出现重复调用、跳过前置步骤、参数对不上等问题改成显式编排计划后模型只负责在开头生成计划执行阶段由代码保证顺序和参数传递出错概率直接下降一个数量级。5.3 技能运行时的降级与恢复策略生产环境里不可避免会遇到外部依赖不稳定数据源查询超时、API 限流、模型服务抖动。技能库设计时必须考虑好降级策略。我给每个技能定义了三个档位正常模式完整执行所有步骤、精简模式跳过可选的增强步骤只保证核心输出、拒绝模式无法完成任务时明确告知用户失败原因。运行时检测到外部依赖异常自动降级而不是硬撑着继续执行然后返回一个错误结论。这里有一个小经验降级之前一定要记录原因。我用一个统一的状态字段标记降级原因比如data_source_timeoutmodel_rate_limit后续复盘时能看到降级是不是过于频繁。如果某个技能经常处于精简模式那就是信号需要优化外部依赖的稳定性而不是继续靠降级硬扛。6. 一些最后想说的实操体会技能库这件事本质上是在给模型能力做一个工程化的封装。模型本身仍然是那个基础模型但通过技能这套架构我们能把每一个具体任务的执行方式、边界、经验逐步沉淀下来而不是每次都依赖模型临场发挥。我个人现在维护一个大约四十多个技能的内部仓库靠的主要就是上面这套方法清晰的边界定义、标准化的技能包结构、两阶段路由、严格回归测试、显式的依赖和编排。如果刚开始接触 agent-skills建议先别急着追求技能数量。试着把你最常用的三个任务拆成技能包跑通注册、路由、执行、观测这一整条链路把每个技能的 schema 和指令体打磨到你觉得换一个模型也能稳定运行的程度再往外扩展。换一个模型也能稳定运行是我自己衡量技能质量的金线。一个技能如果只在某一个模型上表现好那可能只是它的提示词被这个模型记住了真正合格的技能应该靠指令结构和示例集驱动跟模型本身的风格解耦。最后再分享一个小技巧技能包里的示例集不必贪多但要保证每一条示例都是反例的良药。也就是说每加入一个示例它应该覆盖一个容易出错、但你不希望再犯的场景。这样积累下来示例集本身会变成一份非常好用的故障预防手册。别往示例里堆普通案例那是浪费 token模型学到的东西也极其有限。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。