
1. 从“一个prompt打天下”到技能化拆分我为什么重写Agent架构做Agent应用做到第三个月的时候我开始觉得哪里不太对劲。那会儿我们的智能客服机器人已经在线上跑了大半年。最开始的版本其实非常简单一个System Prompt里面塞了业务规则、话术模板、用户意图列表再加几个工具函数OpenAI的GPT-4直接调用函数就能干活。刚开始效果还不错用户问“怎么退款”能答问“订单在哪儿”也能答准确率大概在87%左右。但随着业务方不断提需求这个Prompt以肉眼可见的速度膨胀。今天加一个“如果用户情绪激动就先安抚再处理”明天加一个“跨境订单要区分保税仓和直邮仓”后天又来一个“会员积分过期前要提前7天提醒”。等我反应过来的时候System Prompt已经写到了4800多字模型一次请求的Token消耗翻了三倍响应延迟从1.8秒飙到了4.5秒。更棘手的是任务之间开始互相干扰。有一次我们上线了一个“发票开具”的新功能结果第二天“退货退款”的意图识别准确率直接从90%跌到了76%。运营跑过来问我怎么回事我查了半天才发现新写的发票规则里有一句“用户需要提供完整的抬头信息”模型把它当成了“任何人索要信息都要先索取抬头”的泛化指令。这不是个别Prompt写得不好而是架构层面的问题你试图让一个通用语言模型用一段纯文本承载所有业务逻辑逻辑之间的边界、优先级、依赖关系全部靠文字描述来隐式表达这本质上是在用草稿纸管理一座图书馆。后来我接触到了“技能化拆分”Skill-based Agent Design的思路才意识到问题出在哪里。核心理念是把Agent的能力从“一段超大文本”拆成“一组可独立定义、独立加载、独立验证的技能单元”。每个技能就是Agent的一项具体能力比如handle_refund_request、check_order_status、query_membership_points每个技能有自己的名称、触发条件、输入参数、执行逻辑和成功标准。这个思路其实和我们现在做微服务很像——你不会把整个后端逻辑写在一个Controller里为什么要让Agent把所有能力塞进一段Prompt里做了重构之后那个4800字的System Prompt缩小到了不到400字只剩一些全局性的行为准则语气、安全边界、对话终止条件所有业务能力全部下沉到技能层。新功能上线再也不用动主干Prompt加一个技能文件就行回归测试也从“全量对话回归”变成了“按技能维度抽样验证”。这篇文章我想把技能化拆分的完整方案、数据结构设计、动态加载机制、冲突仲裁逻辑和关键避坑经验都写出来。里面涉及到的代码示例我尽量用伪代码和实际可参考的片段方便你在自己的项目里直接落地。2. 技能的结构定义描述质量直接决定Agent决策上限技能系统的第一个核心问题是一个技能长什么样我见过很多团队的Skill定义基本就是给一个名字加一段描述。看起来没什么问题但实际上线之后你会发现名称和描述的设计直接决定了模型能不能在合适的时机选中这个技能、能不能正确地提取参数、能不能在多个相似技能之间做出正确选择。这块不下功夫后面花多少时间调prompt都是白搭。2.1 技能描述的双层结构from和when_relevant早期我写技能描述是这样的{ name: refund_request, description: 处理用户的退款申请流程 }然后在运行日志里就看到了各种离谱操作。有一次用户说“我不想要了能退货吗”模型没有调用refund_request而是调用了order_cancel——它觉得“退货”等于“取消订单”。还有一次用户问“我上个月买的洗衣液还能退吗”模型直接调了退款技能开始走退款流程完全没判断购买时间是否超出退换期。问题出在哪description太泛了。“处理退款申请”这句话只说明了“这个技能能干什么”没有告诉模型“什么时候该用它”“什么时候不该用它”“它和别的技能有什么区别”。后来我们引入了双层描述结构每个技能都有from和when_relevant两个关键描述字段字段作用举例name技能唯一标识动词开头小写短横线handle_refund_requestsummary一句话说明技能做什么供检索和概览处理用户的退款申请包括发起退款、填写退款原因、提交退款审核from技能的来源、约束、登记信息说明“这个技能从哪来、边界是什么”来自交易系统仅适用于已完成支付的订单退款金额不得超过实付金额when_relevant详细的触发条件说明“什么时候调用、什么时候不要调用”包含正面和反面示例当用户明确表达退款意向时调用用户仅询问退款政策时不调用线下门店购买商品不支持时调用关键就在这个when_relevant。AI模型在决定调用哪个技能的时候本质上是在做语义匹配它不是一个规则引擎不会按照if-else去匹配关键词它是在判断“当前的对话上下文和技能的触发条件在语义上有多接近”。所以when_relevant写得越具体、越有区分度模型的选择就越准。when_relevant里面我强烈建议包含正面触发条件和负面排除条件when_relevant | 用户明确表示想退款、退货、不要了、取消这笔交易。 用户询问退款到账时间或退款进度。 不相关: - 用户仅询问退款政策或退货规则 → 使用 answer_refund_policy - 用户想取消尚未发货的订单 → 使用 cancel_order - 用户反馈商品质量问题但未提退款 → 使用 handle_quality_complaint这样写完之后模型的选择失误率有了非常明显的下降。“退货”和“取消订单”的混淆场景在我们加了反面示例之后基本就绝迹了。2.2 参数定义凡是模型需要动态填充的必须显式声明技能参数的坑比描述还隐蔽。我们的退款技能一开始的parameters定义得非常简单只写了字段名和类型{ parameters: { order_id: { type: string, description: 订单编号 } } }结果模型经常把order_id填成一个用户随口说的“我前两天买的那单”而不是真正的六位数字订单号。系统的下游接单服务一查查不到直接抛异常。后来我们把参数Schema升级成了完整的JSON Schema规范每个字段都写明格式、约束、可选必选、关联字段并在描述里加入了“如果用户没主动给出精确值必须先询问不允许编造”的提示。{ parameters: { order_id: { type: string, pattern: ^[A-Z0-9]{6,16}$, description: 订单号格式为大写字母数字长度为6至16位。如果用户未提供完整订单号必须先引导用户提供完整订单号不允许猜测或编造。, required: true, example: AB123456 }, refund_reason: { type: string, enum: [不想要了, 商品破损, 商品与描述不符, 其他], required: false } } }这里有个特别重要但很多人不知道的细节模型在调用工具时会倾向于“完成参数填充”以完成任务呼叫如果某个必填参数的格式不对它有时候会硬凑一个“看起来差不多”的值出来。所以参数Schema里的description一定要写得像给一个细心但死板的新员工看的操作手册把“不允许做什么”也说清楚。另外当参数之间存在互斥关系或依赖关系时建议在参数的描述里直接写明。例如“如果选择了退款到原支付方式则不需要填写退款银行账号只有选择线下退款时才需要提供银行卡号”。不写的话模型有概率同时填两个互斥字段下游服务就会因为数据不一致而报错。2.3 技能的元信息与版本标记还有一块容易被忽略的是元信息。我在每个技能里都会带上owner负责人团队、version语义版本号、tags标签、last_updated最后更新时间。这些字段本身模型不关心但对人来说非常关键技能出问题的时候日志里能直接看到是哪个团队维护的技能拉人排查不需要再翻代码提交记录。技能迭代的时候version能让你快速确认线上跑的到底是哪个版本特别是灰度发布阶段。tags则可以辅助技能检索比如refund、order、payment这些标签在你做技能召回的时候能叠加一层过滤条件。3. 技能注册与热更新把能力变成可插拔的模块结构定义好了接下来要解决的是“技能怎么被Agent发现”的问题。这就涉及技能注册机制和更新机制。这块如果做得不好Agent的执行效果会时好时坏而且坏得莫名其妙。3.1 目录约定与启动注册我们采用的方案是“目录即注册表”。每一个技能就是一个包含skill.yaml技能配置和executor.py执行器代码的目录放在统一的skills/目录下skills/ ├── handle_refund_request/ │ ├── skill.yaml │ └── executor.py ├── cancel_order/ │ ├── skill.yaml │ └── executor.py ├── answer_refund_policy/ │ ├── skill.yaml │ └── executor.pyAgent在启动的时候扫描整个skills/目录逐个读取skill.yaml加载到内存中的技能注册表里。这样做的好处是增删技能不需要修改Agent主程序的代码不需要注册中心服务只要按约定往目录里放文件就行了。skill.yaml的核心结构大概长这样name: handle_refund_request summary: 处理用户的退款申请包括发起退款、填写退款原因、提交退款审核。 from: 交易系统。仅适用于已完成支付且未超出售后时效的订单。退款金额不超过实付金额。 when_relevant: 用户明确表达退款意向、询问退款进度、要求退回货款时调用。 不相关: 用户仅询问退款政策时使用 answer_refund_policy 用户想取消未发货订单时使用 cancel_order 用户仅反馈质量问题未提退款时使用 handle_quality_complaint。 version: 1.2.0 owner: customer-service-backend tags: [refund, order, payment] runtime: handler: executor.RefundRequestExecutor timeout_seconds: 103.2 动态热更新与版本切换技能上线之后迭代是必然的。但如果每次改技能都要重启Agent服务这个系统基本没法用。所以热更新机制必须在一开始就做进去。我们实现了一种“校验后替换”的热更新流程Agent用一个后台线程持续监听skills/目录的文件变更事件。检测到skill.yaml或executor.py变化后先在临时目录加载新版本执行Schema校验名称唯一性、参数合法性、依赖完整性。校验通过后将技能注册表里的原子指针从旧版本切换到新版本。校验不通过的变更会被拒绝加载注册表继续保留旧版本并输出错误日志。这个“原子指针替换”的思路是从Nginx热更新配置学来的——先完整加载新配置确认没问题再切换指向。如果加载一半发现错误不会影响线上正在使用旧版本的请求。热更新还有一个容易被忽略的问题正在执行中的技能实例怎么办假设用户正在走退款流程退款请求已经发了一半你热更新了退款技能的实现。如果你粗暴地把旧版本的执行器停掉用户的退款流程就会中断。我们的做法是给每个技能实例打一个版本快照。执行实例在启动时记录它使用的技能版本整个执行过程都调用那个版本的实现不受热更新影响。只有当“版本的运行实例数为0”的时候才允许物理卸载旧版本。这种做法在微服务领域叫优雅停机在技能系统里同样适用。3.3 一个容易被忽略的坑上下文裁剪技能热更新还有一个必须要处理的问题就是“Agent对话上下文中已注入的技能描述”如何跟随更新。我们最初实现的技能注入方式是每次对话开始前把所有技能的summary和when_relevant塞进System Prompt。这样做的一个副作用是如果技能已经热更新了但当前对话还在进行中模型里缓存的那份技能描述还是旧的。于是会发生这样的情况技能已经修好了但用户在当前对话里问的问题还在按照旧逻辑判断导致明明已经修复的问题又被用户撞上一次。后来我们改成了“按Token预算动态注入”的方案不是把全部技能一股脑塞进去而是根据当前对话的意图借助一个小的嵌入检索器从技能库中召回最相关的3到5个技能只把它们的描述注入上下文。这个方案既解决了上下文过长的问题也让技能更新的生效变得更快——因为每次对话都是独立的检索和注入用的都是最新的技能描述。4. 技能选择机制从全量注入到语义召回前面讲到热更新的时候已经涉及了技能选择机制这里展开说一下。技能系统能不能真正落地完全取决于一件事模型能不能在正确的时间调用正确的技能。技能库里有100个技能但一次对话上下文中能携带的描述Token是有限的你怎么选出最可能相关的几个技能给模型选错了模型当前上下文里没有这个技能它就只能瞎编。4.1 方案对比全量注入、规则匹配与语义召回我测试过三种技能选择方案方案实现方式优势劣势全量注入所有技能描述全部塞进System Prompt简单零额外检索开销Token消耗巨大技能超过20个后效果明显变差且易发生误选规则匹配基于关键词黑名单/正则触发可解释性强精确场景很好用无法覆盖长尾表达规则维护成本高语义召回用Embedding模型对技能描述和对话上下文做相似度检索泛化能力好长尾表达也能召回可控性较高需要额外引入向量化服务需要有足够的优质描述文本实测下来我们的技能库在25个技能的时候全量注入的准确率还有79%左右模型尚可应付当技能库扩张到60个技能后全量注入的准确率掉到了61%。语义召回方案的准确率稳定在88%附近而且是在技能库超过60个的情况下。我也试过“规则匹配语义召回”的混合方案先用规则锁定高确定性场景再用语义召回兜底长尾场景。这个方案效果最好准确率接近92%但实现复杂度也最高。我自己比较推荐的落地路径是先做语义召回把技能描述的质量提上来跑一阵子观察召回命中率再考虑是否需要叠加规则层。直接从规则方案起步大概率会因为长尾表达维护不过来而痛苦。4.2 语义召回的具体实现语义召回的具体实现不复杂。技能描述summarywhen_relevant提前向量化存入向量库。每次对话开始时把用户的最近一轮消息建议是最近两轮不要只取最后一轮因为用户上半句往往携带关键实体做向量化用余弦相似度找回Top K个技能。import openai import numpy as np SKILL_EMBEDDING_DB {} # skill_name - np.ndarray def retrieve_relevant_skills(user_message, top_k5): message_embedding get_embedding(user_message) scored [] for skill_name, skill_embedding in SKILL_EMBEDDING_DB.items(): similarity cosine_similarity(message_embedding, skill_embedding) scored.append((skill_name, similarity)) scored.sort(keylambda x: x[1], reverseTrue) return [name for name, _ in scored[:top_k]] def get_embedding(text): resp openai.Embedding.create(inputtext, modeltext-embedding-3-small) return np.array(resp[data][0][embedding])这块代码只是基础骨架。实际落地的时候有几个优化点嵌入模型要与业务语言匹配。如果你的Agent服务的是中文用户但Embedding模型以英文语料为主召回效果会打折扣。我们最终选择了对中文支持较好的Embedding模型并且用了一批业务里的真实用户问题做了微调召回率才从78%提到86%。这一步容易被忽略但影响很大。召回结果里要保留两个副选技能。我在取Top K的时候会多取两个作为备选即Top 5 2在主选技能执行失败或者被用户否定时Agent有机会切换到备选技能。有一次用户连续两次说“不是这个意思”Agent就是在备选技能里找到了正确的回应方向要不然那次对话就直接崩了。技能描述和用户问法之间的措辞差异是召回的主要失败源。比如技能描述里写的是“退费”用户说的是“把我的钱弄回来”Embedding模型如果训练语料覆盖不到这种口语化表达就很可能召回到错误的技能。所以我在when_relevant里特意加入了“用户可能这么说”的部分收录各种真实用户表达的同义改写。这一步做好召回幸福指数立刻上升。4.3 技能选择时的冲突仲裁技能多了之后除了召回问题还会出现“多个技能看起来都合适”的情况。这时候就需要冲突仲裁机制。我把冲突场景分成三类意图重叠型用户说“商品有问题帮我退了”既命中了report_defective_product也命中了handle_refund_request。这时候应该先调用前者记录商品问题再走退款而不是直接开退。互斥类型用户说“我先问一下退款是原路返回吧”这时候模型不该直接调用退款执行技能而应该调用退款政策解答技能。本质上是“询问”和“执行”两种行为的区别。依赖前置型用户要求退款但订单还没发货。这时候应该先执行cancel_order而不是handle_refund_request因为状态机里“未发货订单”没有退款流程只有取消流程。我们解决冲突的思路不是把所有判断都交给模型而是引入了一个技能编排层Skill Orchestrator。每个技能可以声明conflicts_with、requires_previous_skill和preempts等编排元数据。技能编排层在模型选完技能之后做一次规则检查如果发现当前上下文里存在更优先的技能就拦截并替换掉模型的选择。name: handle_refund_request preempts: [cancel_order] # 当用户明确说“退款”但订单已发货时此技能优先 conflicts_with: [answer_refund_policy] # 与退款政策解答互斥模型只能二选一 requires_previous_skill: [verify_user_identity] # 必须先做实名校验才能执行退款这套机制上线后退款场景里“身份没校验就发起退款申请”的错误几乎清零了说明编排层起到了很好的剪枝作用。这里面有个很重要的设计哲学不要试图让模型自己是全局决策者设一道确定的规则闸门把系统性风险挡住模型只处理灵活的语义理解部分。这也是把AI能力嵌入生产系统时一个非常通用的方法论。5. 技能链编排与依赖关系单个技能不是整个故事讲完单个技能的选择和仲裁还有一个更高级的话题当用户的需求横跨多个技能时Agent怎么把它们编排成一条完整的执行链拿我们最常用的一个场景来说用户说“我是企业会员我的货还没收到但我看到订单状态已经签收了”。这个需求实际上涉及至少四个技能check_order_status查看订单现在的物流状态verify_user_identity确认用户身份和会员等级因为企业会员有不同售后入口initiate_package_inquiry发起物流异常问询escalate_to_manual_service如果问询不成功转人工单个技能再强串不起来就是一堆孤岛。5.1 显式编排 vs 隐式编排我在项目里试过两种编排方式显式编排在技能定义里写明它依赖哪些前置技能、执行完能产出什么中间结果。Agent在决策的时候不是一次性选一个技能而是根据目标状态回溯需要的步骤。name: initiate_package_inquiry requires_previous_skill: [verify_user_identity, check_order_status] produces: - inquiry_ticket_id # 生成一个物流问询单号隐式编排不显式声明依赖完全靠模型在上下文中自行推理决定下一步调用哪个技能。这种方式的优点是灵活模型会随机应变缺点是太不可控在多步任务中非常容易丢上下文或跳步。实测下来我的观点是对于状态流转明确、步骤固定的业务比如电商售后、工单处理、订单变更用显式编排打底定义好依赖关系图对于一个开放式的创意任务比如写文章、做PPT大纲才适合用隐式编排让模型自由发挥。换句话说编排的自由度应该和业务的确定性成反比——越确定越要给它上轨道。5.2 中间数据传递技能间的共享上下文多个技能串起来执行避不开数据传递问题。第一个技能产出的结果比如order_id、user_id、is_member第二个技能要用这些中间数据存在哪里我们在Agent内部维护了一个执行上下文Execution Context以Key-Value结构保存供所有技能读写class AgentContext: def __init__(self): self.store {} def set(self, key, value): self.store[key] value def get(self, key, defaultNone): return self.store.get(key, default) def snapshot(self): return dict(self.store)每个技能执行前可以声明它需要从上下文读哪些键执行后声明它写入了哪些键。编排层根据这个声明做依赖检查——如果一个技能需要的键还没有被任何前置技能写入就表明编排有问题需要拦截并报告错误。# 执行链示例 skill.check_order_status reads: [order_id] writes: [order_exist, order_status, receiver_name] skill.initiate_package_inquiry reads: [order_exist, order_status] writes: [inquiry_ticket_id]这个声明式依赖还有一个隐藏好处可以做并行的技能组。如果两个技能读取的键交集为空、写入的键互不重复它们就没有数据竞争可以并行执行。我们在一个批量用户状态查询场景里用这个方式把响应时间从串行的8秒压到了并行的3秒。这个优化一开始没在计划里纯粹是数据流设计到位的自然产物。5.3 回调与分支技能链不是一条直线不要把技能链设计成“串行的一维数组”现实业务里会出现分支和回调。举一个实际发生过的例子用户发起退款退款技能执行到一半发现订单属于“赠品类目”不走常规退款流程而是走“赠送优惠券补偿”。如果编排层只支持线性的技能链这里就只能硬编码在退款技能的executor里把两个流程揉在一起逻辑会越来越乱。我们的解决方案是在执行上下文中增加branch_context技能在执行过程中可以通过返回一个switch信号让编排层重新路由class BranchDecision: def __init__(self, next_skill, context_overridesNone): self.next_skill next_skill self.context_overrides context_overrides or {} # 退款技能中的分支逻辑 if order.is_gift: return BranchDecision( next_skillissue_coupon_compensation, context_overrides{ compensation_type: coupon, compensation_amount: order.paid_amount } ) else: return BranchDecision(next_skillprocess_refund_payment)这种“技能返回分支信号”的方式比在编排层里硬编码if-else更灵活。技能自己是最了解自己所在业务域的实体它决定下一步跳转比外层编排层瞎猜要准确得多。6. 踩坑实录与调试建议这五个问题我花了最长时间最后说说那些文档里查不到、只有真正跑线上才会踩到的坑。这些经验是我和运维、算法、测试同事一起熬出来的列在这儿希望你不用再走一遍弯路。6.1 描述污染技能的when_relevant写了太多副作用我们的when_relevant一开始写了非常详细的业务规则比如“如果订单金额超过500且用户为企业会员则需要调用价格复核接口”这些细节写多了之后模型反而会变得犹豫不决甚至把规则运用在错误的技能选择上。后来我们明确了when_relevant的边界这个字段只回答“什么时候用这个技能”而不回答“怎么用这个技能”。“怎么用”属于executor内部的逻辑。就像简历上只写“我在上一家公司负责订单系统”不写“我每天定时用curl调用订单接口三次”。这听起来是常识但你不踩一次坑真的记不住。6.2 技能膨胀60个技能是分水岭技能数量超过60个之后语义召回的准确率会下降模型的选择抖动也会变严重。这不仅仅是检索问题也是技能之间相似度过高导致的决策混淆问题。我建议在技能体系构建的早期就做定期的“技能审计”合并那些语义高度重叠的技能比如把check_refund_progress和query_refund_status合并成一个query_refund_status只是参数里加一个progress_type。合并之后技能库从64个降到41个召回准确率提升了7个百分点实测的端上对话体验也稳定了许多。6.3 循环调用防护Agent会死循环而且死得非常优雅有一次线上事故Agent在一个循环里连续调用了十几次技能差点把下游系统的限流都打爆。原因是技能A产出结果后编排层让技能B执行B执行完后发现数据不满足去执行技能CC又把数据改回了一种让A重新触发的状态。我们在编排层加了三重防护最大执行步数任何多技能调度链最多执行8步包含参数重试超出即停止转人工。技能去重监视同一技能在10分钟内对同一个会话ID重复执行超过3次直接拦截。循环依赖静态检测启动时扫描技能依赖图检测是否存在环形依赖A前置B、B前置A有则拒绝启动并报错。6.4 没有轨迹可视化等于让运维盲人摸象技能系统上线后最容易被低估值的是可观测性。第一版我们只记了日志每次用户反馈“机器人回答得不对”开发和算法就要在几千行日志里翻半天效率极低沟通成本极高。后来我搭了一个技能调用轨迹视图每个会话可以看到用户输入了什么、模型召回了哪些技能、最终选了哪个技能、每个中间步骤花了多少毫秒、执行器返回了什么结果、有没有触发错误处理分支。有了这张图很多问题在10秒内就能定位。最小可行方案其实很简单打印一张结构化的执行记录表时间戳会话ID触发文本召回技能Top5分数选中技能执行结果耗时2025-06-01 10:12:03sess_9912我要退款answer_refund_policy(0.82), handle_refund_request(0.79), cancel_order(0.53)handle_refund_request成功2.1s6.5 灰度发布技能热更新也要走灰度流程技能热更新虽然技术上可以做到秒级生效但业务上绝不能秒级全量生效。有一次我们新版本退款技能的一个字段名写错了全量部署后所有退款请求都报参数错误。从那以后我坚定地认为技能更新必须走灰度流程。灰度方案不复杂把线上流量按会话ID哈希90%的流量继续走旧版技能10%流量走新技能版本。观察半小时的数据如果新版本的错误率低于1%、平均耗时没有上升再逐步把灰度比例提高到100%。这跟正常的服务发布流程没有本质区别只不过很多人把“技能文件”当成了配置而不是代码忽略了配置变更也是可能出事故的。就我个人经验来说Agent开发里最难调试的从来不是单个技能的代码逻辑而是技能与技能之间的边界划分和上下文信息流转。你不妨先在项目里把所有技能按“动词名词”的命名规范梳理一遍写清楚每个技能的when_relevant正反示例把3个核心链路的编排关系画在纸上再开始写代码。这套基本功打扎实了技能系统的地基就能帮你挡住后面大部分麻烦。