ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从提示词堆叠到可复用技能体系:Agent技能层设计实战

从提示词堆叠到可复用技能体系:Agent技能层设计实战 从给智能体“塞指令”到建一套可复用的agent-skills我大概花了三个月才彻底转过弯来。如果你也正在做Agent相关的东西大概率经历过这个阶段一开始觉得让模型会做事就是把能力说明往系统提示词里堆堆到几千字以后行为开始漂移指令互相打架调一个bug会带出另外三个bug。后来我意识到问题不在提示词写得好不好而在没有一个“技能层”——也就是把能力拆成可命名、可注册、可调用、可编排的独立模块。这个技能层社区里一般就叫agent-skills。这篇文章就把我这段时间在技能体系设计上的完整思考、踩坑和落地方案写出来。适合正在做Agent应用的开发者、想从单轮对话进阶到复杂任务编排的产品经理以及所有对“怎么让模型稳定地做成一件事”感兴趣的人。1. 为什么需要一套技能体系从“会说话”到“会做事”的那道坎很多人的Agent场景是从聊天机器人起步的。用户问一句模型答一句偶尔从文档里检索一下。这个阶段不需要技能体系提示词写好就够了。但一旦要让Agent去完成真实业务动作——查库存、发工单、调接口、跑分析脚本——事情就变了。1.1 “把所有能力写进提示词”为什么走不通我见过不少团队的初版方案都是这样的把工具说明、使用规则、注意事项全部塞进system prompt。在小规模、工具数量少于五个的时候这个方案是能跑的。但到二十个工具以上的时候开始出现几类非常典型的问题。第一是上下文膨胀。每个工具描述平均按200个token算二十个工具就是4000个token。加上对话历史、检索片段一次请求的输入可能冲到一万多token延迟和成本都在肉眼可见地涨。第二是行为不稳定。工具一多模型经常在“该用哪个工具”上犹豫不决甚至出现幻觉工具名。第三是调试地狱。行为不对的时候你根本说不清是提示词没写清楚、工具本身有bug还是模型理解偏了。三者搅在一起排查效率极低。1.2 技能层和工具调用的本质区别你可能要说这不就是function calling吗有什么新鲜的。这里我想明确一个区别工具函数是“单个动作”的抽象而技能是“解决一类问题”的能力封装。举个例子一个名为get_weather的function是一个工具它做一件事调一个接口拿天气数据。但一个技能可能叫plan_travel_route它内部要判断当前城市、调用天气接口、查询交通方式、结合用户偏好综合成一段建议文本返回。前者是原子操作后者是原子操作的组合。在传统系统里这叫业务流程编排在Agent体系里我倾向于把“具备完整输入输出契约、能被模型识别和选择、内部可以拆成多个步骤或子调用”的模块统称为技能。从代码组织的角度看技能层带来的直接好处是关注点分离。提示词只负责说清楚“目标、约束和风格偏好”技能负责把所有领域逻辑封装在黑盒里。模型不需要关心这个技能内部是调了三个API还是跑了一个模型推理它只需要知道《在什么场景下、传什么参数、能得到什么结果》。1.3 技能体系要解决的四个核心问题我把技能体系的目标收敛成四个后面所有设计都围绕这四个展开。第一可复用性。同样的“提取结构化信息”这个能力应该能被财务分析场景和电商评论分析场景共用而不是各写一套提示词。第二可测试性。每个技能都应该能脱离完整Agent上下文单独跑通这样至少可以在单元层面验证它对不对。第三可编排性。复杂任务不是靠单个技能完成的而是多个技能按某种流程协作完成。技能体系要支撑串行、并行、条件分支这些基本组合方式。第四可治理性。一个Agent接入了三十个技能之后谁负责的、什么版本、调用了多少次、失败率多少这些都要能看见不能是一团黑。这四个问题任何一个解决不好Agent项目一上规模就会返工。2. 技能的最小构成一个技能的解剖实验把“技能”这个词落到具体的工程实现上它到底应该长什么样我参考了不少开源项目里的skills写法也自己折腾过几版最后沉淀下来的最小构成是五个部分元信息、描述、参数契约、实现体、验证规则。2.1 元信息给技能一个身份元信息是技能的身份证。我常用的字段包括name、version、author、tags、permissions和cost_hint。其中name必须全局唯一且语义清晰version建议遵循语义化版本规范permissions用来声明这个技能需要访问哪些资源cost_hint给的是这个技能运行一次的大致代价等级比如低/中/高供模型在多个可选技能之间做平衡。给技能打tags是一个容易被忽略但实战价值很高的习惯。没有标签体系之前我找一个技能要去翻目录有了标签之后按“finance”“web”“data”这样的维度一筛就出来了。它还方便做技能路由模型可以先按标签筛掉一大批不相关的技能再在剩下的里面做精确选择对减少误调用很有帮助。2.2 描述决定模型什么时候想起它描述是整个技能定义里最关键、也最容易被写烂的部分。模型不会像人一样把技能文档从头读到尾它是靠描述里的语义匹配来决定要不要调用某个技能的。写得太笼统类似“获取信息的高级函数”模型根本不知道什么时候适合用写得太具体把所有边界情况都罗列进去token开销大模型反而抓不住重点。我推荐一种写法叫“触发场景输入要点输出承诺”三段式。触发场景当用户的问题涉及查询本周订单状态、物流轨迹或预计送达时间时使用。 输入要点需要订单号若无订单号可尝试用用户手机号后四位匹配。 输出承诺返回结构化的订单状态JSON字段包含status、timestamp、description。这样写模型比较容易建立“场景到技能”的映射。相比之下“这个技能可以帮你查询订单”这种描述基本等于没写。2.3 参数契约和模型约定输入格式参数契约决定了模型能往技能里传什么。我推荐直接用JSON Schema来定义不管是Python、TypeScript还是其他语言都有成熟的库去校验和生成类型。一个典型的参数定义长这样{ type: object, properties: { order_id: { type: string, description: 订单号通常以ORD开头, pattern: ^ORD[0-9]{10}$ }, include_detail: { type: boolean, description: 是否返回商品明细默认false, default: false } }, required: [order_id] }参数设计里最容易翻车的点是“过度宽容”。如果你允许order_id为空、然后在技能内部做一堆兜底逻辑模型就会越来越懒经常不传关键参数。我现在的原则是关键参数一律必填宁可让技能报参数错误让上层去追问用户也不要默默猜一个默认值继续跑。猜错的代价远大于多一轮澄清的代价。2.4 实现体不止是函数还可以是子流程技能的实现体可以有两种形态。第一种是最常见的“函数式实现”就是一个函数里面写业务逻辑调用API或者查数据库。第二种是“提示词模板工具链”的实现技能内部还有小模型调用或者子工具的串联。这种复合式技能是agent-skills比较有特色的地方它在技能层封装了一段可复用的“思路”而不仅仅是“动作”。我举一个例子。一个叫“从会议记录里生成待办事项”的技能它的实现体可以是先用一个抽取模板从原始文本里抽任务条目再调用一个日历服务的API检查冲突最后用一个小模型把任务格式化成带优先级的清单。对上层Agent来说它只需要传meeting_transcript进去拿到todo_list出来内部多啰嗦都不用管。2.5 验证规则没有校验的技能就是定时炸弹最后是验证规则。技能返回给模型的数据在进上下文之前一定要过一层校验。实际中我遇到过太多次接口明明返回了数据但字段名和技能文档里写的不一样模型直接把原始JSON丢给用户用户体验极差。校验规则至少包含两部分结构性校验和合理性校验。结构性校验看返回值是否符合声明的Schema字段是否有缺失、类型是否对。合理性校验看数值范围、时间先后逻辑之类的业务约束。校验不通过的处理策略也要定好是重试一次、降级到另一个技能还是直接返回错误说明。不定义兜底策略这个技能就不算完整。3. 技能库的组织方式从“有技能”到“好用的技能库”单个技能设计好了接下来就是规模化的问题。我在项目里管理的技能最多的时候到了六十多个如果没有一套清晰的库组织方式光找技能和维护技能就能消耗掉大量精力。3.1 文件系统与注册中心的取舍技能库的载体有两种主流做法一种是文件系统优先每个技能一个目录目录里放定义、实现、测试另一种是注册中心优先技能在运行时注册到一个中心服务里通过API动态拉取。文件系统派的好处是透明、易读、适合团队协作代码评审直接看diff就行。注册中心派的好处是灵活支持动态上下线、灰度、权限控制适合大规模平台化场景。我的建议是项目早期用文件系统就够了在目录结构上把规范定好等到技能数量超过五十个、或者要支持多个Agent共享一套技能库的时候再上注册中心。直接从注册中心起步往往是在给自己的项目加不必要的复杂度。我常用的目录结构是这样skills/ travel_planner/ skill.json main.py tests/ README.md order_query/ skill.json main.py tests/ README.md每个技能目录里skill.json存放元信息和参数契约main.py是实现tests放测试。这个结构足够简单又能保证每个技能自包含。多个人同时开发互不干扰。3.2 命名规范与冲突处理命名这事看似琐碎踩坑之后才知道重要。我之前有个同事给技能起名叫query结果项目里三个地方各有一个query一个查订单、一个查库存、一个查积分。模型按名字匹配的时候经常串日志里看到query被调用你根本不知道是哪家的。我现在的规范是“领域-动作-对象”三段式domain_action_object。比如commerce_query_order、inventory_check_stock、loyalty_get_points。这样做的好处是从技能名就能看出它属于哪个领域、干什么用、对象是什么基本不需要额外查文档。冲突处理上除了依赖命名规范避免语义冲突系统层面还要保证技能名的全局唯一性。注册的时候做一次查重重复的直接拒绝注册不要搞“同名覆盖”这种机制——同名覆盖在调试阶段非常坑你以为是A行为实际跑的是B逻辑排查一圈回来发现是注册顺序导致的。3.3 版本管理技能也有语义化版本技能是会被多个上层Agent引用的改了它的行为影响面和改一个微服务接口差不多。我建议每个技能都带上version字段遵循MAJOR.MINOR.PATCH的语义化规则。行为向后兼容的修改升MINOR比如加了新的可选参数、优化了返回结果里的某个描述。不兼容的修改升MAJOR比如改了必填参数、改了返回字段结构。修bug没有改接口的升PATCH。升级策略上上层Agent默认锁定MINOR版本段内的小版本只有显式升级才会跨MAJOR。这样可以在稳定性和迭代速度之间取得比较好的平衡。我还养成了一个习惯每个技能在skill.json里写明changelog哪怕只是简单几行也能在排查问题的时候省下大量考古时间。3.4 技能的可观测性让每次调用都有迹可循技能库规模一大没有可观测性等于盲飞。我给每个技能都埋了一套标准的调用日志包含技能名称、版本、入参摘要、出参摘要、耗时、成功标记、失败原因、调用方标识。集中存到日志平台里按技能名和时间维度做聚合查询。有了这些数据之后能直接回答几个关键问题哪些技能被高频调用、哪些技能几乎没人用、哪个技能失败率超标、哪次版本升级之后失败率上升了。我建议每个技能至少做到失败时能追踪到具体的入参和异常栈成功时能知道它产出了什么、耗了多少资源。这是技能治理的基础也是我做技能淘汰和优化最重要的依据。4. 技能编排的几种模式把技能组合成真正的“行为”单个技能只能做一件事Agent要完成复杂任务必须把技能编排成工作流。这一章讲三种基本编排模式外加一种兜底策略都是我在实战里真跑通过的方案。4.1 串行管道模式上一站的输出是下一站的输入串行是最常用的编排模式适合流程固定的强流程任务。比如“员工入职办理”这个场景先验证员工身份信息再创建账号再开通权限组最后发送欢迎邮件。每一步都依赖前一步的结果顺序不能乱。工程实现上我倾向于把串行流程写成一个显式的流程配置或者用一个编排框架控制而不是让模型自由发挥选择下一步。因为强流程任务里步骤是确定的让模型去编流程反而增加了不确定性。比较稳的写法是这样flow SkillFlow( steps[ (identity_verify, {employee_id: {input.employee_id}}), (account_create, {verified_info: {steps.identity_verify.output}}), (permission_assign, {account: {steps.account_create.output.account_id}}), (welcome_email_send, {to: {steps.account_create.output.email}}), ] ) result flow.run(context)这里的关键点是显式声明依赖关系并把上一步的结构化输出有选择地传给下一步。注意“有选择”这三个字不代表把上游输出整个塞给下游那样上下文和参数都会膨胀我一般会在每步之间做字段映射和裁剪。4.2 条件分支模式让模型只做决策不做执行不是所有流程都线性很多任务中间有分叉。比如用户想“帮我处理一笔退款”那得先判断订单状态未发货的走取消退款流程已发货的走退货退款流程已完成的走售后申请流程。这个判断逻辑如果让模型自由发挥结果可能每次都不一样。我的做法是拆分用一个“意图/状态识别”技能它的输出是几个离散的结论选项然后由一个确定性路由逻辑基于结论选项选择后续分支。模型只负责它擅长的事情——理解自然语言、做分类判断它不擅长的事情——在多个分支里做精确控制流——交给确定性代码。条件分支的配置看起来类似这样flow SkillFlow( steps[ (order_status_detect, {order_id: {input.order_id}}), ], branches{ unshipped: [(refund_cancel, {})], shipped: [(return_apply, {})], completed: [(after_sale_apply, {})], } )这个模式的精髓在于把模型的能力边界画清楚了。不要让模型直接输出“我应该走哪个分支”而是让模型输出一个结构化的状态然后由代码决定分支走向。确定性交给系统智能性留在技能内部两者各司其职。4.3 并行扇出模式做信息收集和聚合的最优解有些任务天然并行。比如用户问“帮我整理一下这几个城市本周的天气并推荐适合出行的目的地”就需要同时查多个城市的天气。串行查一遍太慢效率低并行才能体现价值。并行扇出模式适合的场景有这些特征子任务之间互不依赖、子任务的结果可以独立产出、最终只需要一个合并结果。工程实现上可以用异步任务池把每个子技能作为独立任务分发出去等所有任务完成后再交给一个汇总技能做合并。results await asyncio.gather( weather_query.run(city上海), weather_query.run(city杭州), weather_query.run(city南京), ) summary summary_skill.run(results)并行模式里比较需要注意的是资源控制和失败处理。全部并发可能导致外部API限流我通常会给并发度设一个上限比如最多同时跑五个。失败处理上并行任务里如果有一个失败了是整体重试还是只重试失败的那一个我现在的策略是允许部分任务失败失败的任务单独重试一次如果还不行就在汇总结果里标记该子项失败避免整条流程因为单个子任务挂掉而全部无法返回。4.4 失败恢复与回退编排层必须有的逃生舱再好的技能编排也会遇到failure。外部API超时、返回数据格式不对、模型调用报错多种多样。编排层如果没处理失败Agent就会卡死或者给用户一段莫名其妙的错误信息。我建议在流程设计阶段就定义好每个步骤的失败策略至少包括重试、降级、中止三类。重试适合瞬时的网络抖动和限流要注意退避策略。降级适合有备用方案的场景比如主查天气接口失败换备用接口。中止适合强依赖步骤直接返回给上层给出明确错误说明。一个不可忽视的问题是部分成功状态的处理。比如并行扇出里三个子任务成功了一个、失败了两个流程应该怎么走。我在很多项目里看到的做法是“有失败就整体失败”这对用户体验很不友好。更合理的做法是把成功和失败的结果都传给汇总步骤汇总技能基于全部信息生成最终回复失败的子项明确告知用户“这部分暂时拿不到数据”而不是整段任务直接失败。5. 我在实际项目中打磨技能的几条心得前面讲的是方法论和结构这一章聊一些更偏“手感”的东西。这些心得不是从教科书上看来的是真实项目里踩坑踩出来的。5.1 技能的粒度大则僵小则碎技能粒度是设计时最让人纠结的问题。定得太粗一个技能里塞了太多逻辑稍微换个场景就没法复用只能在技能内部加一堆条件判断最后变成一个“什么都做但做不精”的大泥球。定得太细技能数量爆炸模型做技能选择时也容易眼花缭乱。我现在的经验标准是如果一个技能内部开始出现“如果需求是A走这里如果需求是B走那里”的分支逻辑而且分支之间差异不小那大概率就是粒度太粗了应该拆成两个技能。反过来如果一个技能的主逻辑代码不到二十行又只有一个入口那可能太细需要看它是否和其他技能经常一起出现——如果是就考虑合并成一个复合技能。5.2 description的措辞直接决定调用率同一个技能description写法不同模型的调用率可能差出一倍。我做过一个对照实验同一个天气查询技能A版本写“查询天气信息”B版本写“根据城市名查询实时天气、温度、湿度、风速和空气质量指数当用户提到天气、温度、要不要带伞、适不适合出门时使用”结果B版本的调用准确率明显更高。但这不等于描述越长越好。描述长到一定程度后收益递减而且会挤占上下文。核心还是把触发条件写具体把输出写明确。我在定稿每个技能的description之前会问三个问题用户说什么样的自然语言模型应该想起这个技能这个技能绝对不能处理什么场景它输出什么东西对用户或下一个技能有用答案写清楚就够了。5.3 输出裁剪不要把大块文本直接倒进上下文技能执行完返回结果是要传给模型继续推理的。如果技能返回了两百行的JSON或者一万字的报告全文模型处理起来很吃力上下文被占满后续对话质量也会下降。我处理这个问题的办法是在技能里内置一个“模型消费视角”的输出设计。即技能不只返回原始结果而是同时产出一份“精简摘要版”和一份“结构数据版”。摘要版供模型快速理解含义结构数据版供下游流程精确处理。比如爬虫技能抓完网页输出给模型的我会让它先做一个清洗和概括把页面里的正文提取出来、压缩到一定字数再进上下文。原始内容可以存起来等用户需要细节的时候再调。5.4 技能缓存重复问题别让模型反复算我观察过实际情况相当大比例的技能调用是在处理重复或高度相似的问题。同一个结果查了一遍又一遍成本和时间都浪费在重复劳动上。我在技能层加了一层可选的缓存机制以“技能名参数哈希”作为缓存键把技能的结构化输出缓存一段时间。缓存能带来非常直观的优化但也伴随一个风险数据实时性。订单状态这种场景缓存五分钟可能就过时了。我的经验是给每个技能显式声明一个cache_ttl字段值可以为零不缓存。实时性要求高的技能一律不缓存数据变化不频繁、查询成本高的技能可以设置一个合理的过期时间。再配合缓存命中日志定期看命中率就知道哪些技能适合继续开缓存、哪些技能该把TTL调短。5.5 日志里看到的真相技能设计失败的信号最后说一个让我印象深刻的反思。我之前一直觉得某个“综合查询技能”设计得挺好参数灵活、覆盖场景多直到看调用日志才发现这个技能的失败率高达百分之三十五而且大部分失败发生在参数解析环节。模型经常传不齐必填参数或者传了根本不在Schema里的字段。原因是这个技能的必填参数有五个对模型来说心智负担太重了。后来我把这个技能拆成三个更聚焦的技能每个技能的参数不超过三个必填字段失败率立刻降到了百分之七以内。这个例子给了我一个非常重要的原则如果某个技能的调用失败率长期偏高优先怀疑技能设计本身而不是责备模型不够聪明。多数时候是技能的契约设计得不够好超出了模型稳定处理的能力范围。观察日志、收集失败样本、迭代技能定义应该是一个持续进行的循环而不是做完一版就不管了。5.6 技能心智负担的量化估算顺着上面那个案例我后来总结出一个粗略的“心智负担”估算方法。单个技能的必填参数每增加一个模型准确填写的概率就会明显下降一截。我的经验数据是三个以内必填参数模型基本稳定五到六个必填参数出错概率陡增超过八个基本就是设计有问题必须靠分流或重组来拆解。这个规律本质上是模型在短上下文里做结构化输出的能力边界。要降低出错率与其在提示词里反复强调“请准确填写参数”不如从技能契约本身做减法。能通过上下文自动推导出来的字段就不要让用户提供能拆成两个子技能解决的复杂输入就不要在同一个技能里堆参数。复杂度不会消失但把它切到模型能力的舒适区里整体效果会好很多。基于这套规律我在组件技能库的时候还会定期做一次“技能体检”把调用频率、失败率、平均耗时、上下文占比排个序。低频高维护成本的技能要么合并要么下架高频高失败率的技能优先重构高频低失败率的技能是核心资产后续做任何改动都要加倍小心避免打破原本稳定的平衡。等到你手头的技能库开始形成这样的良性循环Agent的稳定性、复用性和可维护性都会有一个质的变化。那也是我从“写提示词的人”转变成“设计技能系统的人”的分水岭。如果你正准备把Agent能力往产品化的方向推进早一点把agent-skills作为一等公民来对待后面会省掉很多折腾。
返回列表