上个月客服工单里有一条投诉让我印象特别深:用户问“门店这个月的优惠券核销差了12笔,麻烦帮我查一下”,我们的机器人先一本正经地念了一段优惠券定义,然后建议用户“联系运营同事复核”,全程没有任何可执行动作,用户当场炸了。这事不怪模型笨,怪我一开始的思路就错了——我试图用一个全能大模型包揽客服所有工作,结果高射炮打蚊子,还打不准。
后来我花了三周时间,基于Spring AI把客服系统重构成了多模型协作架构:意图识别走轻量模型,规则性问题走规则引擎,复杂业务咨询走大模型,各干各的活儿,再由统一调度层串起来。这套系统上线之后,答非所问率明显下降,高峰期响应也没那么飘了。这篇文章就围绕这套Spring AI多模型协作智能客服系统,从架构设计、代码接入、路由策略,到源码层面的Advisor机制和线上踩坑,完整记录一遍。如果你在Java技术栈里做客服、做智能问答,想把大模型接进Spring Boot项目,又不想被某一家模型厂商绑死,这篇值得收藏。
1. 单模型客服的三个失控现场,以及我为什么要做多模型协作
1.1 失控现场一:一本正经地编造流程
客服系统刚上线那会儿,我天真地认为“反正大模型什么都会,直接接一个最强模型就行了”。结果第一个版本就出了大问题:用户问对账问题,模型能流畅地编出一整套“对账流程”,从打开财务报表到逐笔比对,讲得头头是道,但那些步骤根本不存在于我们的系统里。用户照着做,自然做不通,然后投诉量直接翻倍。
这个现象在业内有个说法叫“幻觉”,但落到客服场景里,它比技术演示里的幻觉更致命。技术Demo里你问个历史人物,模型胡说一通,你笑笑就过去了;客服场景里用户是按操作指引去执行的,一句编造的操作步骤,浪费的是用户整段工作时间,消耗的是平台信任。单模型对这类高精度业务场景完全没有办法,因为它没有“知道自己不知道”的能力。
1.2 失控现场二:高频问题答得又慢又贵
餐饮SaaS客服里真正的高频问题,其实是“怎么改门店营业时间”“怎么设置菜品估清”“打印机不出单怎么办”这一类。这些问题答案相对固定,知识库里都有,甚至很多问题用规则匹配就能命中。但单模型架构把所有问题都送到大模型里理解一遍、生成一遍,成本和延迟都上来了。
实测下来,当时接的大模型单次回答平均要2秒到4秒,高峰期甚至到8秒。用户等不起,更关键的是成本顶不住。每天几万次对话,其中七成以上是简单重复问题,全部走大模型推理,一个月账单涨得我肉疼。那会儿我才意识到:在客服这种流量大、场景杂、容错低的场景里,“所有问题都丢给最强模型”是一种昂贵且低效的偷懒。
1.3 失控现场三:高峰期并发把对话全堵死
单模型架构还藏着一个并发问题。同一个ApiKey,所有业务共享,午高峰和晚高峰流量一上来,模型接口的响应时间直线恶化,一批对话直接超时。更头疼的是,有些用户在这个时段咨询的是“怎么退订会员”这种本可以用一句话回答的问题,也被堵在队列里等着大模型慢慢算,体验奇差。
我当时看着监控面板上那一排红色的超时告警,心里只有一个念头:必须拆。把简单问题和复杂问题拆开,把意图识别和内容生成拆开,让不同模型承担不同职责,谁也别拖累谁。
1.4 破局思路:按场景拆模型,而不是再加一个更大的模型
多模型协作的核心不是“多模型投票”或者“模型叠加”,而是一句话:让最合适的模型干最合适的事。意图识别用便宜快速的轻量模型,知识库检索命中后用独立流程,复杂业务咨询才调用大规模模型,最后所有结果汇到统一出口。
这个思路在工程上落地,光有模型还不够,还需要一个能编排这些模型的框架。我调研了一圈,最后选了Spring AI。原因很简单:我们是Java技术栈,项目本身就是Spring Boot服务,Spring AI作为Spring官方生态里的AI应用框架,天然能融入现有工程体系,而且它对各家模型做了统一抽象,以后想换模型厂商,只改配置不动业务代码。这才是长期能维护的架构。
2. 服务端架构:模型网关、会话管理、路由决策三层的拆分
2.1 整体请求链路一句话讲清楚
重构后的系统链路可以概括成一句话:渠道消息进来,先过会话管理层恢复上下文,再过意图识别层判断用户想干什么,接着路由决策层决定由哪个模型或规则来回答,最后结果原路返回。所有请求都经过同一个入口,这个入口我习惯叫它“模型网关”。
模型网关不是一个独立的微服务,在多数场景下它就是一个Spring Boot应用里的核心Service层。这样做的好处是部署简单、链路短,而且网关作为唯一入口,日志、限流、超时、熔断、成本统计这些横切逻辑都可以集中管理,不用散落到各个渠道的controller里。
2.2 整体模块划分
我按职责把系统拆成下面几块:
- 渠道接入层:对接小程序、App、企业微信等渠道,负责消息收发。
- 会话管理层:维护每个用户的多轮对话状态,记忆存Redis,重启不丢。
- 意图识别层:对用户输入做分类,输出意图标签和置信度。
- 路由决策层:根据意图标签、规则命中结果、模型健康状态,决定走哪个模型。
- 模型适配层:基于Spring AI的统一ChatModel抽象,对接智谱、通义等多家模型。
- 知识库检索层:对高频标准化问题做向量检索或关键词匹配,减少大模型无谓消耗。
这六块边界清晰,每一块都能单独测试、单独降级。比如知识库检索层挂了,系统还能退化成纯大模型问答;意图识别层挂了,规则匹配还能兜底。多模型协作架构最大的底气不是模型多,而是每个环节都有后备方案。
2.3 对外响应格式统一的必要性
所有模型返回给用户的内容,最后都要包一层统一的响应结构。这个设计一开始我嫌麻烦,后来发现太重要了。因为不同的模型、不同的规则分支,返回内容的形态差异很大:有的返回纯文本,有的返回JSON,有的返回Markdown。如果不统一封装,前端和各个渠道的对接成本会失控。
我的做法是定义一个ResponseResult,里面包含reply(回复正文)、intent(命中的意图)、source(回答来源:模型还是规则)、confidence(置信度)等字段。这样不管是哪个模型回答的,上游渠道拿到的都是同一份结构,后续做用户满意度分析、回答质量评估也都方便。
3. Spring AI接入智谱AI:依赖版本、配置和第一个可用的ChatClient
3.1 maven依赖与BOM版本管理
Spring AI接入的第一步就踩了坑,因为它的版本迭代太快了。早期用spring-ai-openai-spring-boot-starter,后来又有一堆第三方starter冒出来,groupId和版本号经常对不上。网上很多人搜“spring ai maven 智谱ai version”,搜到的答案五花八门,直接抄作业很容易翻车。
我的建议是:一定要用BOM统一管理版本,不要自己在dependency里写死版本号。项目里我是这样配置的:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId> </dependency> </dependencies>这里特别提醒一句:版本号以你项目实际拉取到的为准。Spring AI的版本号和Spring Boot的版本有对应关系,M系列对应Spring Boot 3.x的某个版本,正式版出来后命名规则还会变。最稳的做法是先去Spring AI官方仓库看一眼Releases页,再回项目里定版本。别图省事用latest,一次意外升级就能让你排查半天。
3.2 配置文件与模型参数
依赖加好之后,配置文件比较简单,api-key用环境变量注入,别硬编码进application.yml。我用的智谱配置是这样:
spring: ai: zhipuai: api-key: ${ZHIPU_API_KEY} base-url: https://open.bigmodel.cn/api/paas/v4 chat: options: model: glm-4-flash temperature: 0.7 max-tokens: 1024智谱的glm-4-flash是个性价比很高的模型,响应快、便宜,非常适合意图识别和高频简单回答。复杂业务咨询我单独再配一个glm-4-plus的ChatModel,通过路由层按需切换。一个应用里同时配置多个ChatModel完全没问题,Spring AI的AutoConfiguration允许多个ChatModel通过@Qualifier区分。
3.3 定义一个带客服人设的ChatClient
Spring AI里最常用的门面是ChatClient,它的API风格和Spring WebClient一脉相承,用起来很顺手。我建议系统启动时就构建一个带人设的ChatClient:
@Configuration public class AiConfig { @Bean @Qualifier("customerServiceClient") public ChatClient customerServiceClient(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultSystem("你是餐饮SaaS平台的智能客服助手。回答要简洁准确,不要编造系统里不存在的功能;" + "涉及操作步骤时先说明前提条件;用户情绪激动时保持克制并引导转人工。") .build(); } }这个defaultSystem会在每次请求时自动拼进Prompt里,不用业务代码里反复传。构建一次、全局复用,正好符合客服系统的场景。调用也简单:
String reply = chatClient.prompt() .user(userMessage) .call() .content();如果你之前用的是OpenAI官方SDK或者各家厂商自己的SDK,第一次用ChatClient会觉得它薄得不像框架,但这恰恰是它的价值:它没有绑死在任何一家模型厂商的SDK上,ChatModel换一个实现,你的业务代码一行都不用改。
3.4 ChatClient不是SdkClient,而是你的应用会话门面
这里说一个容易误解的点:Spring AI的ChatClient不是简单地封装“发请求给模型”这件事,它承担了更多编排职责。你可以在prompt().advisors()里挂记忆加载、日志记录、敏感词过滤等横切逻辑,也可以指定在调用前把用户消息先存入记忆。它更像是一个“会话门面”,所有会话相关的动作都可以挂在它上面。
理解这一点之后,你再看Spring AI的官方文档就不会觉得乱。它其实就是在回答一个问题:如何用一套统一API,把模型调用、上下文管理、外部工具调用、RAG检索这些能力组织起来。在这个体系里,ChatClient是门面,ChatModel是底层实现,Advisor是拦截器,ChatMemory是记忆仓库,各司其职。
4. 意图识别与模型路由:小模型分类,大模型干活
4.1 用glm-4-flash做意图分类
多模型协作的关键在路由,路由的前提是意图识别。我把意图识别单独做成一个服务,使用轻量模型glm-4-flash,通过结构化输出直接拿到意图标签。
Prompt设计很讲究,不是随便问一句“用户想干嘛”就完事。我做了一个相对严格的分类Prompt:
你是客服意图分类器。请判断用户的诉求属于以下哪种意图,只输出JSON格式: {"intent": "单个人机协作意图", "confidence": "0到1之间的置信度"} 可选项: ORDER_QUERY:订单查询 REFUND:退款退货 PROMOTION:优惠券/活动咨询 DEVICE_FAULT:设备故障报修 MANUAL_HANDOFF:要求转人工 OTHER:其他 用户问题:{question}之所以限定可选意图,是因为我们知识库里就覆盖这几类高频问题,范围太宽反而会让分类模型糊涂。实际跑起来,glm-4-flash对这个六分类任务准确率能到90%以上,单次调用基本在300毫秒到800毫秒之间,成本可以忽略不计。这个速度是glm-4-plus这类大模型做不到的。
4.2 规则引擎前置,不要什么都丢给模型
模型分类虽然准,但不是所有场景都要先经过模型。大量请求其实是包含强关键词的,比如“发票”“退款”“打印机”,这些用规则匹配就够了,又稳又省。我在意图识别层做了一个前置规则引擎:
public Optional<Intent> matchByRule(String question) { if (question.contains("发票")) return Optional.of(Intent.INVOICE); if (question.contains("退款") || question.contains("退货")) return Optional.of(Intent.REFUND); if (question.contains("打印机") || question.contains("不出单")) return Optional.of(Intent.DEVICE_FAULT); if (question.contains("转人工")) return Optional.of(Intent.MANUAL_HANDOFF); return Optional.empty(); }规则引擎命中就直接走对应流程,不再调用模型。没命中才送到意图识别模型。这样架构上形成“规则兜底模型,模型兜底未知问题”的层层降级关系,既保住了绝大多数高频问题的稳定性和速度,又不会漏掉长尾问题。
4.3 路由决策表与委托调用
拿到意图之后,路由决策层根据一张“路由决策表”选择具体的执行目标。我实现得比较简单,就是一个枚举到ChatClient的映射:
public enum RouterTarget { INTENT_CLASSIFY(clientA), REFUND(clientB), ORDER_QUERY(clientC), COMPLEX_QA(clientD), DEFAULT(clientD); }每个意图对应的目标不一定是一个大模型,它可以是另一个获取订单数据的工具函数,也可以是知识库检索链路。这里我自定义了一个RouteTarget概念,本质上就是“把不同的意图委托给不同的处理单元”,处理单元内部再决定是调函数、查知识库、还是调大模型。
委托调用的代码大概长这样:
public String route(intent, String conversationId, String question) { RouterTarget target = routerTable.get(intent); return target.chatClient().prompt() .user(question) .call() .content(); }需要注意的是,同一个用户的多轮对话,不同意图之间是可能互相切换的。比如用户前面在问订单,后一句突然问“那退款多久能到账”,这时候系统要把意图从ORDER_QUERY切到REFUND,同时保留前文订单号的上下文。所以我特意在路由时不锁定单一意图,而是对每一轮输入都重新做意图识别,结合历史上下文决定是否切换。
4.4 超时熔断与兜底降级策略
路由层再往下,是一个必须提前想清楚的问题:如果某个模型挂了,或者响应超时,系统怎么办?我在网关层做了三重兜底:
- 规则兜底:意图识别超时时,把所有可选项里的规则匹配结果直接用上,命中不了就走人工。
- 模型降级:glm-4-plus超时或报错时,自动降级到glm-4-flash,保证用户至少能拿到一段可用回答。
- 人工兜底:所有自动回答都不可靠时,话术切换为“当前咨询量较大,已为您转接人工客服”,并把完整会话上下文带到工单系统。
这套兜底策略用代码写起来其实不复杂,核心是统一封装一个RouterExecutor,内部用超时控制和异常捕获,保证无论哪个环节出问题,上游拿到的都是可控结果,而不是一堆异常堆栈。
5. 多轮记忆的重构:从MessageWindow到RedisChatMemory
5.1 默认MessageWindowChatMemory为什么不够用
客服系统没有多轮记忆就是残废,用户上一句报了自己的门店编号,下一句你就忘记了,用户只会觉得你在敷衍。Spring AI默认提供了MessageWindowChatMemory,它的用法很简单,核心是维护一个滑动窗口,超过窗口大小就丢弃最早的消息。
但直接用它有两个问题。第一,窗口是内存态的,服务重启、水平扩容时,会话记忆全丢。第二,它只按条数截断,不区分消息优先级,如果窗口设得大,Token消耗就大;设得小,早期关键信息(比如门店编号)会被过早挤掉。在客服场景里,用户往往会在对话过程中陆续给出多个关键实体,用简单FIFO截断并不合适。
5.2 自定义ChatMemory:把记忆搬到Redis
我的做法是自定义一个ChatMemory实现,把记忆异步存到Redis里,同时支持按窗口取最近N条。核心就三个方法,add、get、clear:
@Component public class RedisChatMemory implements ChatMemory { private static final String KEY_PREFIX = "chat:memory:"; private final StringRedisTemplate redisTemplate; public RedisChatMemory(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Override public void add(String conversationId, List<Message> messages) { String key = KEY_PREFIX + conversationId; for (Message message : messages) { redisTemplate.opsForList().rightPush(key, JsonUtils.toJson(message)); } // 控制List长度,防止无限膨胀 redisTemplate.opsForList().trim(key, -50, -1); } @Override public List<Message> get(String conversationId, int lastN) { String key = KEY_PREFIX + conversationId; List<String> range = redisTemplate.opsForList().rightRange(key, -lastN, -1); return range.stream().map(JsonUtils::fromJson).collect(Collectors.toList()); } @Override public void clear(String conversationId) { redisTemplate.delete(KEY_PREFIX + conversationId); } }然后把自定义Memory注册进ChatClient的构建过程:
ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(redisChatMemory)) .build();这里会涉及一个接口签名在不同版本里的差异问题,不同Spring AI版本里Message和ChatMemory的接口可能略有调整,但只要核心的add/get/clear语义不变,自定义实现就能兼容。实在遇到编译问题,翻一下当前版本的源码接口,照着实现就行。
5.3 长会话的上下文压缩策略
Redis记忆解决了持久化,但解决不了另一个问题:一个特别长的会话,历史消息全量拼进Prompt,Token成本爆炸,而且模型处理长上下文时首字延迟也会明显上升。我用了“窗口+摘要”的组合策略:
- 最近6轮消息,直接保留原文。
- 6轮之前的消息,由模型生成一段压缩摘要,只保留用户提供的关键实体(门店编号、订单号、诉求)和未解决的问题。
- 对话结束时,把摘要持久化到数据库,下一次会话直接以摘要开场。
摘要压缩我用的是glm-4-flash,专门写一个SummarizePrompt,只取关键信息,不保留寒暄和重复追问。实测下来,一个原本50轮的会话,压缩后上下文Token用量能减少80%,多轮追问的准确率反而有提升,因为模型不再被大量无关历史记录干扰。
6. Advisor机制源码级拆解:Spring AI的拦截器到底怎么工作
6.1 Advisor和Spring AOP的对应关系
如果你熟悉Spring AOP,看Spring AI的Advisor会非常亲切。它本质上就是AOP思想在AI调用链上的实现:在ChatClient发起模型调用前后,插入一批横切逻辑,比如加载历史消息、记录成本日志、做敏感词过滤、追加RAG检索结果。
Spring AI的Advisor接口设计上借鉴了Spring AOP的Advice概念,核心是before、after、around三个方法,其中around方法里可以拿到下一个Advisor的引用,形成一个责任链。看起来就像一个微型的AOP拦截器链,每一层可以决定是直接放行、修改请求,还是修改响应。
这个设计很聪明。因为AI应用里的横切关注点实在太多了:记忆注入、上下文增强、日志埋点、成本统计、安全审查、结果校验,如果这些逻辑全部散落在业务代码里,系统很快就没法维护了。Advisor把这些问题集中起来,按Order顺序执行,业务代码反而保持着干净。
6.2 内置Advisor:MessageChatMemoryAdvisor与问答增强
Spring AI内置了几个常用Advisor,实际项目里基本够用了。最常用的是MessageChatMemoryAdvisor,它负责两件事:调用前从ChatMemory里取出历史消息,拼进Prompt;调用后把当前轮的用户消息和模型回答写回记忆。这里有一个容易被忽略的细节:MessageChatMemoryAdvisor加载历史消息的时机,一定要发生在其他需要读取上下文的Advisor之前,所以Order要排在最前面。
另一个很常用的是QuestionAnswerAdvisor,它是RAG场景下的核心组件。它会把用户问题转换成向量查询,从VectorStore里检索出相关文档片段,然后在调用模型前把这些片段拼到Prompt里,让模型基于检索结果回答。落实在代码上就是一行:
.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))这个Advisor的价值在于,它让“知识库检索+生成”这个流程变成了声明式配置,不用你自己在业务代码里手动编排检索和拼接Prompt。你只要把向量库配好,挂上这个Advisor,RAG链路就跑通了。
6.3 自定义一个“统一成本统计”Advisor
内置Advisor之外,我强烈建议自定义一个成本统计Advisor。客服系统的成本管理是必须做精细化的,否则月底账单会教会你做人。我的实现思路是,在around方法里记录调用前后耗时、模型名称、Token用量,然后输出到日志系统:
public class CostLogAdvisor implements Advisor { private final MeterRegistry meterRegistry; public CostLogAdvisor(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } @Override public AdvisedResponse around(AdvisedRequest request, AdvisorChain chain) { long start = System.currentTimeMillis(); AdvisedResponse response = chain.next(request); long costMs = System.currentTimeMillis() - start; // 从request和response中提取模型、token信息,不同版本API位置可能不同 String model = request.llmOptions().getModel(); int inputTokens = response.tokens().getInputTokens(); int outputTokens = response.tokens().getOutputTokens(); meterRegistry.counter("ai.cost", "model", model).increment(inputTokens + outputTokens); log.info("[AI Cost] model={}, inputTokens={}, outputTokens={}, costMs={}", model, inputTokens, outputTokens, costMs); return response; } }注意不同Spring AI版本里AdvisedRequest和AdvisedResponse的字段位置确实一直在变,有时候是直接get,有时候要context().get()。但原理都一样:你在chain.next(request)前后可以拦截到完整链路,这就是Advisor最核心的用法。把这种通用逻辑做成Advisor挂到ChatClient上之后,业务代码里再也不会出现一行成本统计代码。
6.4 源码层面的执行顺序细节
再往源码里挖一层,ChatClient内部对Advisor链的执行顺序大致是:构建Prompt时先按Order执行before逻辑,然后调用ChatModel.call,拿到响应后按相反顺序执行after逻辑,最后返回给业务代码。如果你的Advisor依赖了记忆数据,而记忆加载Advisor排在你后面,那你读到的一定是空历史。所以排Order时有一个经验:记忆类Advisor在最前,安全类Advisor在最前,业务包装类Advisor尽量靠后。
如果将来排查某次Prompt里为什么没有带上历史上下文,先不要怀疑模型,先检查Advisor的Order。激素水平的错误往往就藏在一条没打日志的Advisor链上。
7. 线上运行半年后的踩坑清单与参数调优
7.1 连环排查:一次“无法处理”的线上故障
这套系统上线运行半年,整体稳定,但中间出过一次让我印象深刻的故障。现象是用户集中反馈“机器人突然只会说‘抱歉我暂时无法处理,请稍后再试’”,日志里没有任何报错。排查链路我完整走了一遍。
第一步,从网关日志里定位到“无法处理”这个回复来自兜底分支,说明上游某个环节超时了。第二步,看监控面板,发现意图识别接口的P95耗时从平时的600毫秒飙到了6秒。第三步,查智谱的调用配置,发现默认的超时时间是3秒,3秒内没返回就直接走了兜底。第四步,继续追为什么意图识别变慢,最后发现是有个Batch任务在相同ApiKey下批量跑报表总结,把分配给意图识别的并发额度占满了。
解决措施有三条:意图识别模型用独立ApiKey并单独限流,避免被其他业务挤占;意图识别调用超时从3秒调到5秒,给轻量模型多一点容忍度;兜底话术从“无法处理”改成“已为您转接人工客服”,绝不把用户晾在一边。这次之后我养成了一个习惯:每个依赖外部模型的服务都得有独立的超时、重试、降级预案,这个思想后来被我写成了团队内部的AI服务接入规范。
7.2 版本与依赖的兼容性问题
Spring AI目前还在快速迭代,我在接入过程中至少遇到过三次版本引发的编译错误。最典型的是spring-ai-zhipuai这个starter在不同版本里groupId不一样,早期版本还叫别的名字。另一个坑是Spring AI的BOM引入了之后,会连带升级Spring Boot的某些基础组件,如果项目里其他中间件对Spring Boot版本敏感,就可能被牵连。
我的建议是:项目里所有AI相关依赖统一交给spring-ai-bom管理,版本号只改BOM一处;升级Spring AI前先在Git分支上升级、跑全量测试,确认没影响再合主干;生产环境用固定版本,别追最新。这套系统到现在用的还是我当时锁定的版本,只有确定有重大功能改进时才考虑升级。
7.3 各业务场景的模型参数推荐
不同业务场景对生成风格和Token量的要求差别很大,我最终调的参数如下:
| 场景 | 使用模型 | temperature | max-tokens | 超时 |
|---|---|---|---|---|
| 意图分类 | glm-4-flash | 0.1 | 64 | 3s |
| 高频规则回答 | glm-4-flash | 0.3 | 256 | 3s |
| 业务咨询问答 | glm-4-plus | 0.6 | 1024 | 10s |
| 工单摘要生成 | glm-4-flash | 0.3 | 256 | 3s |
| 复杂问题兜底 | glm-4-plus | 0.7 | 2048 | 10s |
意图分类的temperature调到0.1,是因为分类任务要的是稳定输出,越低越好。业务问答0.6是因为客服回答不能太机械,也不能太飘,0.6是个平衡点。max-token方面,客服回答一般不需要超过1024个token,设得太高一是浪费钱,二是会让模型啰嗦化。
7.4 最后的两个实践建议
根据我这段时间的实操经验,还有两条很具体的建议想分享。
第一条:用户明确说“转人工”时,千万别硬留。一旦意图识别命中MANUAL_HANDOFF,直接走人工转接流程,把完整上下文带到工单系统,让人工客服看到用户已经问过什么、系统回答过什么。多模型协作再强,也不要试图用一个模型去安抚一个已经炸了的用户,这是客服系统里最重要的一条原则。
第二条:每一次调用都要留下可审计的日志。用户如果投诉“机器人回复错了”,你要能快速定位到那一轮对话用了哪个模型、传了什么上下文、输出了什么内容。这要求从网关层开始就给每次对话分配一个traceId,贯穿整个链路。我在实践里就是靠traceId把渠道消息、意图识别结果、路由决策、模型响应串起来的,排查问题的效率完全不一样。
这套基于Spring AI的多模型协作智能客服系统,从最初的单模型失控到现在各司其职,中间踩了不少坑。但回过头来看,Spring AI提供的模型抽象和Advisor扩展机制,确实让整个重构过程顺了很多。如果你也想在Spring Boot里做类似的AI应用,我建议先别急着写业务,先把你的路由决策想清楚,把记忆方案定好,再动手接模型。架构稳了,接什么模型都只是配置的事。