资讯详情

Agent Skills实战指南:用可插拔技能模块替代失控的Prompt工程

📅 2026/10/13 23:15:11 | 华诺云谱 👁 阅读
Agent Skills实战指南:用可插拔技能模块替代失控的Prompt工程
这两年做Agent相关项目我最大的感受是单靠一个超大System Prompt把Agent调教成全能选手基本都会在半路翻车。语言模型本身擅长的是语言理解和生成但它不擅长那些需要确定逻辑、固定流程、精确计算的事。于是越来越多的团队开始把能力拆成一个个可插拔、可测试、可版本管理的模块业内一般管这个叫Agent Skills。这篇文章就围绕这个思路展开记录我在实际项目里沉淀的组件结构、路由机制、异常处理和避坑经验适合正在做Agent应用开发、想把LLM从对话玩具推向生产力工具的开发者参考。1. 技能化之前单体Agent的失控现场1.1 三条要素塞进一个Prompt后果是什么我先描述一个很常见的场景。某团队让我帮忙优化一个客服辅助Agent最初的设计很简单所有业务规则、话术模板、判断逻辑全部写进System Prompt然后让模型直接回答客户问题。前十几个问题效果尚可但一旦需求变多Prompt越来越长问题就来了。第一个问题是上下文膨胀。Prompt里塞了几千行说明模型在生成回答时注意力被大量规则分散重要约束反而被忽略。有次客户问退换货政策模型把七天无理由记成三天原因就是相关描述被埋在一大段业务说明里。我后来统计过那段Prompt大约有8000多Token但真正有效的约束不足10%。第二个问题是行为不可控。语言模型本质是概率生成同样一句指令今天能执行明天可能就换了一种措辞。更麻烦的是用户一旦用非常规说法提问模型就容易自由发挥答出连我自己都没见过的流程。第三个问题最致命能力无法沉淀。你在Prompt里调好的某一段技能想单独升级、单独测试、单独复用几乎做不到。换一个场景就要复制粘贴整个Prompt其中大部分内容还不相关。有一次要把同样的数据筛选能力搬运到另一个Bot里我整整花了两天去适配上下文只因为筛选规则和别的业务说明粘连得太紧。这让我意识到Agent的稳定产出不应该依赖于把什么都写进提示词而是要把可确定的操作从模型推理中剥离出来。1.2 技能化的本质从告诉模型怎么做到让模型调度别人做Agent Skills这套思路的核心改变在于不再要求模型自身掌握所有能力而是让模型知道遇到什么情况该调用哪个技能。技能本身是一段确定性代码一个明确的输入输出契约一份给模型看的使用说明。打个比方你可以教一个人做一百道菜但他一定会忘、会错、会发挥不稳定。更好的做法是给他一整套自动厨具每个厨具负责一道标准菜他只需要学会看菜单、按按钮。菜单是描述按钮是接口自动厨具就是技能执行器。模型的角色从厨师变成了调度员这比让模型自己当厨师可靠得多。我在某个跨平台数据整理项目里试用了这套思路。原本需要模型自己理解把CSV里的空值按规则填补、格式统一、再导出Excel现在拆成三个技能每个技能都有固定的输入参数和校验逻辑。模型只负责解析用户的意图并依次调用技能。结果模型的Token消耗直接下降了一半而且每个步骤的输出都能被独立校验出问题也不再需要重新“理解”整个任务。1.3 什么样的能力适合做技能什么样的不适合不是所有事都应该技能化。我自己会用一个判断标准该能力是否可以被明确描述、是否有固定输入输出、执行过程是否不需要即兴推理、是否有可验证的结果。满足这些条件的比如计算日期、查询订单状态、格式化数据、调用第三方接口、生成图表、发送通知都很适合做成技能。这些操作里模型的聪明反而是干扰项代码的确定性才是价值。反过来一些探索性任务不太适合进入技能体系头脑风暴、文本润色、策略建议、涉及长对话记忆的情感陪伴。这类任务依赖模型的语义能力强行技能化只会得到一个什么都想管但什么都管不好的虚胖模块。我的习惯是让技能只负责确定性的手脚让模型专心负责语言和决策的头脑。2. 技能组件长什么样目录、描述与输入输出契约2.1 一个标准技能目录的布局技能不是一个函数那么简单它是围绕一个能力组织起来的一组文件。我在项目里常用的结构是这样skills/ web-search/ SKILL.md search.py requirements.txt tests/ test_search.py ># Web Search ## 功能 在给定关键词的情况下检索网页内容返回标题、链接和摘要。 ## 适用场景 - 用户询问最新资讯、产品参数、人物动态 - 需要外部信息辅助回答的事实性问题 ## 不适用场景 - 不需要检索即可回答的通用常识问题 - 用户要求访问特定登录页面 ## 示例 输入: 2024年某会议的主题是什么 输出: - title: XXX, url: ..., snippet: ...关键在不适用场景这一段。很多路由混乱就是模型在边界情况下不知道该不该调用某个技能明确写清禁忌能大幅减少误调用。我自己还试过把高质量综合信息这类模糊词从描述里删掉因为它们只会让模型猜测不会产生确定的路由结果。2.3 输入输出Schema给模型画清楚的边界线技能必须有明确的输入输出契约。这个契约我说直白一点就是参数表哪些字段必填哪些可选字段类型是什么取值范围是什么输出结构是什么。一个典型的输入Schema长这样name: web_search description: 搜索网页并返回结果列表 inputs: query: type: string required: true max_length: 200 description: 搜索关键词 top_k: type: integer required: false default: 5 min: 1 max: 10 outputs: type: array items: title: string url: string snippet: string errors: - RATE_LIMITED - EMPTY_RESULTSchema的价值有两层。对外它让模型知道调用时最多能传几个参数、每个参数什么含义减少幻觉参数的发生对内它是运行时校验的依据。模型的参数提取再仔细也会有错如果校验层直接把非法参数拦截在技能执行之前很多奇怪问题根本不会出现。我在实际项目中试过不加Schema直接让技能函数裸奔结果模型偶尔会传一个根本不存在的参数进去函数内部报错还要靠日志反推效率极低。加上契约之后这类问题的排查时间几乎为零。还有一点需要注意技能的输出要结构化尽量避免让技能返回自然语言描述。我之前有个技能直接返回处理成功四个字的字符串结果下游根本不知道数据到底处理成了什么样也没办法程序化判断。改成返回JSON结构后状态字段、耗时、结果摘要一目了然调试和监控都轻松很多。3. 注册与路由Agent怎么知道该调用哪个技能3.1 注册中心所有技能的总目录技能分散在各自目录里还不够Agent运行时要快速找到合适的技能必须有一个集中的注册索引。这个索引本质上是一张表每个技能的名称、描述、版本、参数Schema、执行入口、可用状态。我之前维护过一个最简单的注册表核心字段如下字段示例说明nameweb_search唯一技能名用于程序调用description搜索网页并返回结果给模型看的短描述version1.2.0语义化版本entryskills.web_search.run代码入口tags[search, web]用于路由过滤statusactiveactive / deprecated / disabled注册表不需要多复杂本质上就是一个JSON或YAML文件。但它一定要有一个强制校验逻辑发布技能时检查名称是否重复、入口是否存在、Schema是否合法。这个检查如果省了后面路由时经常出现索引里有这个技能但代码根本找不到的尴尬。3.2 三种路由决策机制让Agent决定该用哪个技能目前我实践过三种机制各有适用场景。第一种是规则匹配。用正则或者关键词直接命中。比如用户输入里包含搜一下查查最新就命中web_search技能。这种方式最稳、最快但很死板只能在封闭域里用。适合那种技能数量少、用户意图非常明确的场景。第二种是向量召回。把每个技能的描述用Embedding索引起来用户请求同样做Embedding算相似度取TopK。这套方案适合技能数量多、描述又比较丰富的场景检索速度快且能容忍措辞变化。但它的召回结果只是候选不是最终答案相似度最高不代表意图正确。第三种是LLM自主选择。把技能列表作为上下文交给模型让它输出调用哪个技能。这个方案最灵活但延迟高、Token消耗大而且在技能数量多的时候容易出现选了一个看起来很相似但实际功能不对的情况。我目前项目里用的是混合路由流程是先做规则命中命中失败再做向量召回召回后把Top3候选交给LLM做最终裁决。这样的好处是既有规则的确定性又有LLM理解语义的兜底。一个简化的路由伪代码def route(intent: str, skills: List[Skill]): # 第一层规则精确命中 for sk in skills: for pat in sk.patterns: if re.search(pat, intent): return sk # 第二层向量召回候选 candidates vector_top_k(intent, skills, k3) # 第三层LLM终选 return llm_select(intent, candidates)3.3 路由歧义与技能优先级路由最头疼的是技能语义重叠。你在技能库里同时有删除文件和清空回收站用户说把临时文件清理掉模型很可能选错。我的应对办法是在注册表里增加一个priority字段并在描述里明确两个相近技能的边界。比如删除文件描述里写只删除指定路径下的文件不涉及回收站而清空回收站描述里写清空回收站中所有文件不可恢复。这样模型至少有一个二选一的判断依据。另一个技巧是给技能打标签。在路由层先按标签做粗筛再进入LLM选择可以显著减少候选集合。例如金融类的数据清洗和电商类的订单统计在标签上先区分开就不会同时出现在候选列表里互相干扰。标签体系越清晰路由准确率越高。4. 技能与记忆的粘合上下文怎么安全地进出技能4.1 技能不应该能访问所有对话历史刚做技能化时我给技能函数传了一个完整对话上下文想着反正数据多技能自己知道怎么取。结果技能经常被无关的闲聊内容干扰有时候甚至把客户名字当成搜索关键词。后来我彻底改变了思路技能只能拿到自己声明需要的参数其他一概不给。这个原则可以用一句话概括技能是工具不是同事不需要了解来龙去脉。让技能保持无记忆、少上下文反而能大幅降低输入噪音也避免隐私数据被无关技能读取。4.2 上下文协议前置条件与输入快照我习惯给技能调用设计一个轻量的上下文协议主要包含三样东西前置条件、输入快照、副作用声明。前置条件用来告诉技能在什么状态下才能执行。比如导出报表技能要求数据表必须已生成否则直接报错而不是拿一份空表去生成什么格式。输入快照是调用时刻关键状态的固化例如文件路径、用户ID、查询时间。这样后续执行时不会因为对话被插了别的内容而改变参数。副作用声明则是告诉调用方这个技能会修改外部状态。比如会写入数据库会发送一封邮件会删除本地文件。这种事必须显式声明否则Agent在自动执行链里可能连续触发几次不可逆操作等用户反应过来已经晚了。4.3 状态回写与技能串联多个技能一起工作时上一个输出要成为下一个输入这里最怕的是中间状态丢失。我用过的可靠做法是每个技能的输出都返回结构化的结果对象并且包含一段简短的执行摘要。摘要的作用不是让模型去读大段数据而是让模型知道这一步做完了拿到了什么。例如第一步定位文件技能返回{ status: ok, file_path: /tmp/data/2024.csv, summary: 找到csv文件路径已保存 }下一步处理这个文件时路由层会从上一步的结果对象里提取file_path字段传给下一个技能。这里的关键是中间数据不要放进聊天正文而是保存在一个独立的执行状态对象里。否则对话历史一长那些路径和临时数据会被模型当作闲聊内容处理造成不可复现的上下文丢失。我在项目里管这种状态对象叫工作台每轮对话结束后工作台把当前任务的临时变量持久化。这样即使某次调用失败了下次重试也能恢复现场而不是让用户从头再来。这个机制对多步技能串联尤其重要。5. 稳定性打磨技能调用中的异常、超时与降级5.1 技能会以哪些方式失败技能运行时的失败模式比想象中多。我整理了几类最常见的参数非法模型生成的关键词为空、类型不对、超过长度限制外部依赖失败第三方接口超时、返回非预期数据状态冲突前置条件未满足比如文件还没生成就去读权限不足写入目录无权限、访问受限资源被拒幂等性破坏同一个技能被重复调用产生了副作用叠加一个没有异常策略的技能库表面上“能跑”实际上一遇到这些问题就会把错误原样抛给模型让模型根据不完整的错误信息去瞎猜下一步。我见过最离谱的情况是技能报了Internal Error之后模型自己编造了一个再试一次就能成功的回复把用户硬生生带进死循环。5.2 异常分级可重试、可降级、不可恢复我给技能调用设计了一个简单的异常分级体系每一类错误都有对应的策略。异常类型典型原因处理策略可重试网络抖动、服务暂时超时自动重试3次指数退避可降级主技能不可用切换到备用技能或规则实现不可恢复参数校验失败、权限不足直接返回错误码由上层解释给用户有一个容易被忽视的点重试只适用于那些没有副作用的操作。对于发送邮件、扣减库存这类有副作用的技能重试可能造成重复执行我一般会在请求里携带一个RequestId后端做幂等判断。没有这层保护的重试机制从风险角度来说还不如不重试。实现一个通用异常策略的简化版本def call_skill(skill, params): for attempt in range(3): try: result skill.run(params) return result except TransientError as e: if attempt 2: raise time.sleep(2 ** attempt) # 指数退避 except PermanentError as e: return {status: error, code: e.code}这里的核心不是代码本身而是对错误分类的共识只有明确知道重试可能会好转的错误才值得重试其余错误尽量快速失败并给出清晰的错误码。5.3 技能可观测性每一步都有据可查技能库一旦超过五六个调试就变成了一场侦探游戏。你会遇到技能A明明该被调用却调用了技能B、用户说成功了但数据没写入这类问题。与其靠猜不如在调用链路上加好日志。日志至少应该包含请求ID、技能名称、版本、输入参数去掉敏感字段、输出摘要、耗时、错误码。例如req8f3a12 skillweb_search version1.2.0 input近期AI发布会 output_count5 cost1200ms statusok req8f3a12 skilldata_clean version0.3.1 inputfile_2024.csv output_rows1024 cost3500ms statusok有了日志之后复现问题时可以按请求ID把整个技能链路拉出来逐步定位是路由选错、参数传错还是执行器本身报错。我通常还会给技能加一个干跑模式不真正执行外部副作用只走完整链路并返回预定义结果。这个模式在测试路由和上下文粘合时特别有用能避免测试时误发邮件、误删数据。6. 实战复盘技能库从0到1的关键取舍6.1 别贪多三条技能就能跑通闭环我第一次搭建技能库时犯过贪多的毛病一口气规划了十几个技能结果一个月过去了还在做技能清单和描述优化。后来我只挑了三件最影响核心流程的事做成技能搜索、格式化输出、持久化。核心链路立刻跑通了后续再按实际需求逐个增加。这个顺序背后有个逻辑先把输入—处理—输出的完整闭环打通让Agent能完成一个端到端任务再谈技能数量。闭环保底之后新增一个技能只是在既有架构上加一条分支风险小很多。如果一开始就来一堆技能彼此之间的调用关系还没理清排查问题就会陷入一团乱麻。6.2 三个最容易踩的坑第一个坑是描述漂移。技能功能更新后SKILL.md没有同步改模型依然按旧描述路由结果用户请求被路由到错误的入口。后来我把更新技能代码必须同步更新SKILL.md写进了发布检查单每一版本都必须带着描述变更一起发布这个坑才算堵上。第二个坑是技能冗余。不同时间段用不同思路做了几个功能相近的技能比如格式化日期和日期转换描述又相似路由时经常打架。建议每隔一段时间做一次技能合并把功能重叠的技能整理成一个并在描述里明确边界。技能不是越多越好而是描述差异越清晰越好。第三个坑是Schema随意扩容。输入参数一加再加没有一个废弃机制导致老调用还在传旧参数、新调用在传新参数执行器要兼容一大堆历史逻辑。现在我的规则是破坏性变更必须升大版本旧版本可以保留下线但绝不悄悄改行为。6.3 一个能直接抄的发布检查清单每个技能发布前我会过一遍下面的清单也算给大家一个参考技能名称是否唯一描述是否包含适用场景和不适用场景输入Schema是否有必填、可选、类型、默认值是否有长度约束是否声明了副作用是否支持幂等或已加RequestId是否写了至少一个可重试和不可恢复的异常处理分支是否有日志输出是否包含请求ID、技能名、版本、耗时、错误码是否提供了干跑模式或测试用例SKILL.md是否与代码逻辑同步更新是否在注册表中正确登记版本号和标签这些检查项看着琐碎但每一条背后都对应着一次线上教训。我宁可发布慢一点也不愿意技能库里混进入口不明、行为不可查的模块。最后再分享一个小体会。每个新技能上线后尽量让它先挨骂再上量——先在受控场景下运行一段时间观察路由准确率和异常率再去推广到全部流量。这个习惯帮我挡下了很多本来要在线上爆发的低级问题。技能体系的建设没有终点它本质上是一个持续性治理工程越早把工程规范立起来后期就越省心。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑