资讯详情

资讯详情

大模型Agent重构:从临时拼接到按需组合的微应用架构实战

大概是去年年中我接手维护一个早期用临时拼接思路搭起来的客服Agent系统。刚上线那会儿大家都很兴奋——它能查订单、能退换货、能回答产品问题。谁也没想到三个月后整个项目组最怕的不是模型幻觉而是改Prompt时不知道动了哪块逻辑会引发连锁反应。一个说查一下物流模型可能调用错工具也可能把几天前的会话记忆跟当前查询搅在一起。你问大模型有没有错它说没错客户那边已经炸了。后来我用了很长一段时间去拆这部分逻辑慢慢把架构从一个大Prompt塞满所有功能的临时拼接重构成按需组合的能力单元——其实就是把Agent拆成若干小型、边界清晰、能独立注册和调度的微应用。这篇文章就把这一路踩过的坑、想明白的原理、以及最后沉淀下来的一套方法完整写出来。如果你也在做大模型Agent尤其是已经感觉到多加一个工具就要重新调一遍所有Prompt的恶心那这篇文章大概率能帮你少走不少弯路。1. 临时拼接的Agent为什么注定走不远——先看清楚失控现场我不太想上来就讲架构图。先聊一段真实经历这些失控信号你应该不陌生。1.1 一段典型临时拼接Agent的真实生命周期某团队最初的Agent实现很朴素一个系统Prompt里面写了产品信息、售后规则、订单查询步骤然后塞了几个function calling定义。刚开始效果好因为功能少模型很容易理解什么时候该调用哪个函数。然后业务方来了加个优惠券查询。于是继续往Prompt里堆规则往function列表里加定义。再加一个保价提醒再堆一层。系统还能跑但是有几个预兆已经不对劲——同一个场景出现了多个候选函数模型开始随机选错Prompt从2000字膨胀到8000字每次调试都要把整段上下文打印出来一个一个看模型到底理解了什么。走到这一步项目组通常有两个反应。第一种是多问模型几句把选择规则写得再细一点第二种是拆Prompt按路由分发到不同子Agent也就是每个业务域一个小Agent外面套一层路由。这两种做法都不是错的但都只解决了表面问题。尤其在Multi-Agent方案里如果子Agent之间没有清晰的边界和通信契约照样会变成大混乱拆成小混乱——子Agent可能同时去改同一个订单状态或者一个Agent的中间结果被另一个Agent当成最终答案用。这就是我总结的第一个观点临时拼接的核心问题不在模型而在缺少工程化的边界。1.2 失控的三个技术根源上下文膨胀、工具权限与依赖纠缠我在复盘时把失控的原因收敛到三个技术根源。根源一是上下文膨胀。每加一个技能Prompt里就多一段说明多一个function definition主模型每次调用都要重新接收全部定义。不仅浪费token更致命的是让模型的注意力被稀释。本来该精准调用查询物流的场景因为上下文里同时飘着十几个不相关的函数签名模型开始自由发挥。根源二是工具权限缺失。早期拼接模式下所有函数对模型来说都是可见且可调用的。模型没有能力判断这个操作在当前会话、当前用户、当前业务状态下是否被允许。结果就是模型在很正常的对话里突然调用了一个高风险操作比如修改收货地址而这一步根本不该由当前环节触发。根源三是依赖纠缠。订单信息、用户身份、Session状态散落在全局变量里A技能的副作用影响到B技能的前提条件。我曾见过一个项目物流查询Agent觉得自己改写了订单状态是完全可以的因为它在全局上下文里看得到这个状态——实际上那是另一个Agent才能改写的字段。这种纠缠在代码review里根本查不出来只有线上出问题才能暴露。1.3 为什么微应用成为必然方向上面三个根源指向同一件事Agent的能力应该像一组可独立维护、独立发布、独立组合的微应用而不是像团在一起的面团。所谓微应用不是要你把每个功能都做成一个HTTP微服务。它更像是一个严格封装好的能力单元——这个单元知道自己能干什么、不能干什么、输入输出长什么样、需要在什么前置条件下运行。Agent主流程则是组合层负责按当前用户意图和会话状态动态拉取相关能力单元、装配执行链、控制上下文可见范围。简单说从把全部功能塞给模型看变成只把当前任务需要的能力按需呈现给模型。这才是标题里按需组合的微应用之路的实质。2. 微应用架构的骨架能力单元、注册表与组合层概念说清楚之后我们落到工程实现。这一部分给出一个可落地的最小骨架。2.1 能力单元比工具更完整的封装粒度很多人会问这不就是function calling吗不是。function calling只是把函数签名暴露给模型这种最浅层的封装。能力单元在函数之上额外规定了五件事第一输入契约。每个能力单元必须声明自己的入参Schema最好用JSON Schema而且要明确规定哪些字段必填、哪些可选、取值范围是什么。不要在入参描述里写大致传一个订单号就行模型会给你传一个客户名字。第二输出契约。不光定义正常返回结构还要定义错误码。能力单元内部可能出现多种失败查无数据、权限不足、上游超时、参数非法。每种错误都要有稳定、结构化的表达这样组合层才能做分支决策。第三前置条件。比如查询物流这个能力前置条件是当前会话已绑定用户身份、订单号格式合法、且该订单归属当前用户。条件不满足时能力单元要能直接拒绝执行而不是硬着头皮调接口。第四副作用声明。这个能力会不会写数据库会不会改订单状态会不会发消息组合层要靠声明来决定是否给这个能力授权、是否需要在执行前后做审计。没有副作用的单元可以宽松调度有副作用的单元必须走严格的权限闸口。第五运行边界。包括超时时间、重试策略、是否允许并行、是否需要在隔离的上下文命名空间里执行。这决定了组合层在编排时的调度姿态。我还建议每个能力单元都单独维护负责人。别笑这在Agent架构里相当重要。临时拼接时期所有人共同维护一个Prompt的协作模式最终都会变成没人真正负责。能力单元化之后每个单元有独立代码仓库、独立测试、独立发版节奏谁调坏了谁背锅架构和团队组织是对齐的。2.2 组合层路由、编排与动态装配组合层是微应用架构里最核心的部分。它不承担具体业务逻辑只干四件事意图识别、能力发现、执行编排、上下文治理。意图识别组合层根据用户当前消息、多轮会话摘要、业务上下文判断用户想做什么。这一步可以本地用分类模型也可以直接让大模型给出结构化意图标签。对这个阶段我会让模型输出意图候选和置信度而不是让它直接决定调用哪个函数。意图和函数之间还隔着一层路由这层路由就是为了拦截模型乱选的空间。能力发现根据意图标签去能力注册表里查询候选能力单元。这里就体现按需了。系统只把命中的候选单元及其入参Schema注入主模型其他单元完全不进入上下文。这样无论你积累了多少个能力单元单次对话的上下文宽度都只跟当前场景相关。执行编排这一步决定多个能力单元怎么组合。顺序执行、并行执行、条件分支、循环重试都由编排层通过声明式配置完成。建议用工作流描述文件JSON或YAML来定义典型链路而不是让模型自由发挥。模型擅长理解意图但我们在早期实践中发现让模型自由选择工具组合成功率大约只有70%复杂链路跌得更狠用编排模板固化后成功率能稳定在90%以上。上下文治理每个能力单元都应该在受限的上下文片段里运行。订单查询单元的输入是用户ID订单号它不应该看到整个对话历史。组合层在调用能力单元之前要做信息的裁剪和脱敏只把必要数据传给单元。这一步没有做好就会出现物流查询Agent看到了用户地址然后顺手改掉这类越权行为。2.3 一个可以抄走的目录结构与接口协议我在团队里推行过一套目录结构参考了后端微服务的包划分思路工程师适应很快capabilities/ ├── order_query/ │ ├── schema.json # 入参出参定义 │ ├── preconditions.py # 前置校验 │ ├── executor.py # 核心执行逻辑 │ ├── side_effects.yaml # 副作用声明 │ └── tests/ ├── logistics_track/ └── after_sale/ orchestrator/ ├── router.py # 意图识别与候选能力发现 ├── planner.py # 执行编排逻辑 ├── context.py # 上下文裁剪与隔离 └── registry.py # 能力注册表每个能力单元的注册描述我建议至少包含这些字段字段说明示例name能力唯一标识order_queryversion语义化版本1.4.2intent_tags对应意图标签列表[order_query, query_order_status]input_schema入参JSON Schema见文件side_effects副作用级别read_only / write_own / write_globaltimeout_ms超时控制3000required_context所需上下文字段user_id, order_no这套目录配合代码仓库的CI检查可以保证每个能力单元在接入组合层之前接口定义是完整且经过测试的。3. 实操把单体Agent重构为可组合微应用的迁移路径接下来是最多人问的部分我已经有一堆拼接出来的Agent代码了怎么动手拆3.1 拆解前的边界识别与机会评估先把现有系统里所有的功能列出来。不要按代码目录列按业务能力和触发场景列。比如查订单详情是一个能力单元修改订单地址是另一个尽管它们共用了同一个订单数据表。列完之后做两个分类。第一个分类是只读能力与写能力。只读能力优先拆因为风险低、方便验证效果写能力要谨慎需要大量契约测试和权限设计。第二个分类是高频能力与低频能力。高频能力优先拆成独立单元因为收益最大——调参、测试、性能优化的边际成本会显著降低。这一步还要识别能力的共享依赖。如果两个能力单元都要读用户会话信息就把它抽成统一的上下文服务如果两个能力单元都要调用同一个上游接口就考虑做一个内部依赖接口而不是让两个单元各自封装。我见过一个典型错误上来就直接把函数拆开做成几十个Promise/调用块然后交给模型自由组合。这样做只会让架构从一个大Prompt变成一百个小Prompt组合爆炸更严重。边界识别必须先于代码拆分而且要按业务能力切不按代码函数切。3.2 接口对齐与契约测试重构的定海神针拆解之前就要把每个能力单元的接口定义先写好。我推荐的做法是契约先行先写schema.json和side_effects.yaml再实现executor逻辑。接口定义确定了代码拆分就是水到渠成的事。关键是契约测试要做到位。对于每个能力单元至少要覆盖以下几种用例正常入参能够返回约定结构字段类型、数量、嵌套关系正确。非法入参能返回稳定的错误码而不是抛出未捕获异常。前置条件不满足时能礼貌拒绝并返回明确原因。上游接口超时或失败时有降级策略并返回结构化错误。高并发或同一用户连续调用时行为可预期不会串session。契约测试最好做到CI里每次合并代码之前自动跑。我的一位搭档把这块称为Agent架构的安全带——没有它重构过程就是在冰面上开车。实操中还有两个容易忽略的细节。一是入参中模糊字段要不要放宽。比如用户说帮我查最后一个订单此时组合层未必能拿到完整订单号而是只能拿到一个候选订单列表。我建议在能力单元入参里增加一个候选订单数组字段让组合层把歧义结果交给能力单元解析而不是在组合层用暴力逻辑猜。能力单元内部就可以根据订单状态、时间、金额等信息做二次判断。二是错误码的粒度。别只用一个ERROR覆盖所有情况。至少区分INVALID_PARAM、PERMISSION_DENIED、NOT_FOUND、UPSTREAM_TIMEOUT、UPSTREAM_ERROR、RATE_LIMITED。组合层看到NOT_FOUND可以换一个候选能力看到PERMISSION_DENIED则直接终止当前链路并告知用户看到UPSTREAM_TIMEOUT可以考虑重试或降级。错误码区分度直接决定编排逻辑的复杂度。3.3 渐进式替换不搞一刀切大重写我强烈不建议停掉线上系统然后花一个月重构完再上。正确姿势是同存双跑逐批迁移。具体操作保留原有Agent主流程作为默认路由但把高频单项能力例如物流查询单独拆成能力单元在组合层里做一个开关让线上请求先切换到新能力单元观察准确率、响应时长、用户投诉率。对比数据持续一两天没问题就继续迁移下一项有问题则开关回滚影响面极小。这里要特别注意新旧路由的流量分配策略。最开始做1%灰度后面逐步放大到10%、50%、100%。如果旧流程用的是大模型自由调用函数而新流程用的是编排模板两者的判断标准会不同必须建立统一的评估集。建议人工标注100条高频场景样本定期拿新旧流程同时跑一遍对比输出结构和业务正确性。渐进式迁移还有一个好处团队有充足时间积累能力单元的契约测试和经验不至于在重构期间手忙脚乱。4. 按需组合的战术细节动态加载、上下文隔离与可观测性这一部分是我实际运行中花费最多时间的地方。架构图画得再漂亮落到运行时的细节上才会发现坑。4.1 动态按需加载别让模型每轮都看一遍全部工具列表很多团队在使用function calling时习惯把几十个函数全部传给模型。不管当前对话需不需要模型每轮都要看见所有候选。这在无状态演示里没什么但放到真实产品尤其是有复杂业务规则的产品里就非常要命——模型注意力被稀释token消耗变大工具冲突概率升高。按需加载的核心逻辑分三层。第一层意图过滤器。组合层先根据用户消息做一次快速意图识别圈定可能相关的能力单元集合比如3-5个。第二层上下文相关度排序。把候选单元按照和当前对话主题的匹配度排序取top N。第三层动态入参注入。只把匹配上的单元的入参Schema塞给主模型其他单元的Schema不进入本轮上下文。听起来简单但有个很现实的坑意图识别本身是一个模型调用它有延迟和错误率。为了降低这一层的风险我会在意图识别前加一层轻量规则预过滤。比如用户消息中包含订单号字段就强制把order_query加入候选包含运费字样加入freight_calc。规则是保底模型是增强两层一起用稳定性会高很多。这个方案是规则兜底模型判断的组合工程上落地最稳。动态加载还要考虑热更新问题。能力单元在运行中可能发布新版本组合层需要有一个机制来控制新旧版本的切换。建议注册表里统一维护引用计数允许用户请求隐藏在历史链路上的单元继续运行完但新请求默认走新版本。这样能避免正在执行的链路突然被替换成新版逻辑导致的不可预期行为。4.2 上下文隔离每个能力单元活在独立沙箱里临时拼接架构下所有函数共享全局上下文这是很多诡异Bug的源头。微应用架构必须在上下文层面做隔离。我采用的做法是每个请求进入组合层时先创建一个以request_id为主键的隔离上下文对象。它划分为三层请求基础层用户ID、会话ID、时间戳等、业务数据层订单列表、商品信息、地址信息等、能力单元私有层某个单元执行过程中临时产生的中间结果。能力单元只能读写业务数据层中它被授权的那部分key不能访问私有层更不能私自改基础层。实现上有两个层次。轻量做法的方案是给每个能力单元传入一个context对象提供get和set接口键名统一前缀。重一点的做法是把每个能力单元放到独立进程或独立容器里通过内部RPC通信。后者隔离性好但运维成本高大多数场景下前者足够。隔离的真正收益在看效果时才会体现。之前物流查询Agent顺手改了订单状态之类的越权问题本质原因是全局上下文可写。隔离之后单元A根本看不到状态字段想改也没法改。另外大模型能力单元内部的对话历史也要隔离。对查询类单元一般只需要给它最近几轮和当前能力相关的消息片段不需要把整段历史都给它。你可以为每个能力单元定义一个上下文修剪策略比如order_query只保留用户最近一单的摘要信息不需要完整聊天记录。4.3 可观测性没有编排日志调试等于盲人摸象Agent架构有一个特别烦人的特性同一个问题模型这次走链路A下次可能走链路B。没有完整的trace出了问题完全无从下手。我的做法是组合层每做一次路由决策都写入结构化日志每个能力单元执行完也输出一条带request_id、单元名、版本号、入参摘要、出参摘要、耗时、错误码的记录。日志聚合的时候以request_id为组就能还原出一次完整请求从意图识别到最终回答的全链路。强烈建议在能力单元执行前后都做输入输出快照建议存到独立的日志系统而不是只打标准输出。因为线上问题排查时大家往往需要回看模型到底在那轮对话里看到了什么上下文、能力单元返回了什么数据。没有快照事后只能靠猜。还有一个非常值得做的事构建回放工具箱工具。把线上能够抽离的请求日志输入重放到本地测试环境对比新版能力单元和旧版能力单元的输出差异。这个工具不复杂但对回归测试帮助巨大。我记得有一次灰度新版订单查询单元回放测试直接抓出了两个版本对同一订单的状态字段解释不一致的问题很多问题我们是在上线前就拦截住了。5. 关于边界维护与微应用过度设计的个人体会架构演进到这一步回归一下眼前的大方向。微应用化不是万能药。它在带来清晰边界、独立迭代、按需组合优势的同时也一定会引入新的成本注册表维护、契约版本管理、编排层复杂度、以及团队协作流程变化。我见过有人把按需组合理解成函数粒度越小越好结果拆了几十个单元每个单元只有一段几行的逻辑编排层却要处理几十种组合策略。这种过度设计反而比临时拼接更痛苦。我个人的判断标准是如果一个能力单元在可见未来里只有一种调用方式且没有独立迭代节奏那就先别拆留着作为组合层内部函数。只有当它确实会被多个场景复用或者它自身有独立的业务规则和测试诉求才值得拆出来成为微应用单元。简单说拆的目的是复用和隔离风险不是为了拆而拆。还有一块容易被忽视能力单元与人之间的关系。临时拼接阶段大家可以互相帮忙改一段Prompt微应用化之后每个单元必须有明确负责人。负责人要对契约定义、测试覆盖、运行时行为全权负责。这个组织变化往往比技术变化更难推进比如有些负责人对于拆分到具体单元之前有疑惑但实际落地之后线上问题反馈链路反而更清晰——从前是整个Agent出问题了不知道找谁现在是订单查询单元异常直接钉对应的负责团队。说一条具体一点的经验灰度切换后一定要盯住两个指标用户侧的有效回答率和链路侧的单元失败率。有效回答率反映模型对编排模板的理解程度单元失败率反映下游接口的真实稳定性。这两个指标要同时看因为它们会互相掩盖问题。有一个项目曾出现有效回答率上升但单元失败率增加的情况表面成绩不错实际是编排模板更聪明了但它不停调用一个已经出现异常的下游接口。没有双层指标团队很容易误判健康度。最后想聊一下演进路径的心法。每次架构调整本质上是在回答同一个问题当系统规模变大之后怎么保持可预测这个特性。临时拼接的Agent之所以危险不是因为它跑不通而是因为它在一个可预测性极低的状态下运行——你永远不知道模型下一次会选择哪个工具、会看到什么上下文、会不会做越权操作。微应用组合架构真正加固的是可预测性路由逻辑是代码写的契约是测试保证的编排是声明式配置控制的模型被限定在可控的选择空间里。架构越接近这个状态你对系统的把握就会越强。如果你正在做Agent相关项目我的建议是别等到线上事故频发才开始拆。拿一个高频、只读、边界清晰的能力作为第一个微应用单元试点用一两周时间把注册、编排、灰度、监控这套链路跑通后面再推广到其他能力会轻松很多。这条从临时拼接到按需组合的路方向基本是确定的早走比晚走好。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →