大概是去年年中,我接手维护一个早期用"临时拼接"思路搭起来的客服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_query |
| version | 语义化版本 | 1.4.2 |
| intent_tags | 对应意图标签列表 | ["order_query", "query_order_status"] |
| input_schema | 入参JSON Schema | 见文件 |
| side_effects | 副作用级别 | read_only / write_own / write_global |
| timeout_ms | 超时控制 | 3000 |
| required_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相关项目,我的建议是:别等到线上事故频发才开始拆。拿一个高频、只读、边界清晰的能力作为第一个微应用单元试点,用一两周时间把注册、编排、灰度、监控这套链路跑通,后面再推广到其他能力会轻松很多。这条从临时拼接到按需组合的路,方向基本是确定的,早走比晚走好。