news 2026/10/14 6:52:06

企业级智能客服系统架构实战:RAG、工具调用与记忆机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
企业级智能客服系统架构实战:RAG、工具调用与记忆机制

1. 企业智能客服系统的整体架构设计思路

1.1 为什么选择 RAG + 工具调用 + 记忆 + 流式 + 兜底这套组合

做企业级智能客服,最怕的不是模型不够聪明,而是答非所问、胡编乱造、上下文断裂、响应卡顿、异常无兜底。这五个问题任何一个没处理好,上线就是灾难。我前后参与过三个客服系统的搭建,踩过的坑基本都集中在这几个点上。这套架构的核心逻辑其实很朴素:用 RAG 解决知识准确性问题,用工具调用解决动态数据获取问题,用记忆解决多轮对话连贯性问题,用流式输出解决用户体验问题,用兜底策略解决异常和安全问题。

先说 RAG。企业客服的知识库通常包含产品文档、FAQ、售后政策、操作手册等,这些内容更新频率不低,如果每次更新都去微调模型,成本高且周期长。RAG 的思路是在用户提问时,先从向量库中检索出最相关的知识片段,拼接到提示词中让模型基于这些片段回答。这样做的好处是知识更新只需要更新向量库,模型本身不用动。但 RAG 也有它的局限,比如检索不到的时候怎么办、检索到多条冲突信息怎么排序、相似度阈值怎么定,这些都是实操中必须解决的问题。

再说工具调用。有些问题不是知识库能回答的,比如“我的订单到哪了”“帮我查一下上个月的账单”“这个工单现在是什么状态”。这些需要实时查询业务系统,RAG 无能为力。工具调用就是让模型在需要的时候主动发起一个函数调用,去查数据库或调接口,拿到结果后再组织语言回复用户。Spring AI 对工具调用的支持比较成熟,通过注解就能把 Java 方法暴露给模型。

记忆这块,很多人容易忽略。单轮问答看起来没问题,但用户一旦追问“那这个呢”“刚才说的那个再详细讲讲”,没有记忆的模型就懵了。记忆分短期和长期,短期记忆是当前会话的上下文,长期记忆是用户的历史偏好和关键信息。Spring AI 提供了对话记忆的抽象,但实际用的时候要注意内存管理和过期策略,不然会话一多内存就爆了。

流式输出不用多说,用户等 5 秒看到一整段回复,和逐字逐句蹦出来,体验差距是数量级的。Spring AI 对 SSE 和 Flux 的支持让流式实现变得简单,但要注意流式场景下的异常处理和兜底逻辑跟同步场景不太一样。

兜底策略是最容易被低估的部分。模型超时了怎么办、检索为空怎么办、工具调用失败了怎么办、用户问了敏感问题怎么办、并发量突增导致响应变慢怎么办。这些没有兜底,系统就是脆的。我的经验是,兜底要分层做:第一层是模型层面的兜底提示词,第二层是业务层面的降级策略,第三层是系统层面的熔断和限流。

1.2 这套架构适合什么样的团队和业务场景

这套方案不是万能药,它有比较明确的适用边界。如果你的客服场景是纯闲聊或者纯情感陪伴,那 RAG 和工具调用基本用不上,重点应该放在对话风格和记忆上。如果你的知识库非常小且几乎不变,那直接微调一个小模型可能更划算。但如果你的场景符合以下几个特征,这套架构就很合适:

  • 知识库有一定规模且需要频繁更新,比如产品迭代快、政策经常调整
  • 有相当比例的问题需要查询实时数据,比如订单、物流、账户状态
  • 多轮对话占比高,用户习惯追问和补充
  • 对响应速度和用户体验有要求
  • 需要处理异常情况和敏感内容

团队方面,需要至少有一个熟悉 Java 和 Spring 生态的后端开发,对向量数据库和嵌入模型有基本了解。如果团队里没人接触过 RAG,建议先花两天时间把检索流程跑通再往上叠其他能力。

1.3 十课收官的内容脉络

这个系列我规划了十课,从环境搭建到最终部署,每一课解决一个核心问题。第一课到第三课打基础,讲 Spring AI 的接入、向量库选型和 RAG 检索链路搭建。第四课到第六课讲进阶,包括工具调用的实现、记忆机制的设计、流式输出的处理。第七课到第九课讲保障,覆盖兜底策略、异常处理、性能优化。第十课做整体串联和部署上线。每一课都有可运行的代码和配置,不是纯理论。

下面这张表是十课的核心内容概览,方便你快速定位自己需要的部分:

课次核心主题关键产出
第一课环境搭建与 Spring AI 接入可运行的基础对话接口
第二课向量库选型与知识入库知识检索链路
第三课RAG 检索策略与提示词工程基于知识库的问答
第四课工具调用实现动态数据查询能力
第五课对话记忆设计多轮连贯对话
第六课流式输出处理SSE 流式响应
第七课兜底策略分层设计异常降级方案
第八课敏感内容与安全过滤内容安全机制
第九课性能优化与并发处理高并发稳定运行
第十课整体串联与部署上线完整可部署系统

2. RAG 检索链路的核心细节与实操要点

2.1 向量库选型:为什么我最终选了 PGVector 而不是专用向量库

向量库的选型是 RAG 的第一步,也是很多团队纠结的地方。市面上有 Milvus、Qdrant、Weaviate、Chroma、PGVector 等一堆选择。我前后用过 Milvus 和 PGVector,最终在企业客服场景下更倾向于 PGVector,原因有几个。

第一是运维成本。专用向量库需要单独部署和维护,多一个组件就多一份运维负担。PGVector 是 PostgreSQL 的扩展,如果你的业务系统本来就用 PG,那基本是零额外运维成本。第二是事务一致性。客服场景下知识入库和业务数据经常需要保持一致,PGVector 能跟业务表在同一个事务里操作,这点专用向量库做不到。第三是数据量。企业客服的知识库通常不会特别大,几万到几十万条文档片段是常见量级,PGVector 在这个量级下性能完全够用。如果你的知识库上千万条,那确实需要考虑专用向量库。

当然 PGVector 也有缺点,比如索引构建比专用库慢,高并发检索性能不如 Milvus。但客服场景的检索 QPS 通常不会特别高,这个缺点可以接受。

安装 PGVector 很简单,如果你用的是 Docker,直接拉带 pgvector 的镜像就行:

docker run -d \ --name pgvector \ -e POSTGRES_PASSWORD=yourpassword \ -e POSTGRES_DB=customer_service \ -p 5432:5432 \ pgvector/pgvector:pg16

进去之后启用扩展:

CREATE EXTENSION IF NOT EXISTS vector;

然后建表,我一般会建两张表,一张存原始文档,一张存向量片段:

CREATE TABLE knowledge_doc ( id BIGSERIAL PRIMARY KEY, title VARCHAR(500), content TEXT, category VARCHAR(100), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE knowledge_chunk ( id BIGSERIAL PRIMARY KEY, doc_id BIGINT REFERENCES knowledge_doc(id), chunk_text TEXT, embedding vector(1536), chunk_index INT, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX ON knowledge_chunk USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

注意:ivfflat 索引的 lists 参数跟数据量有关,一般建议 lists = 行数 / 1000,行数少于 100 万时 lists 取 100 左右比较合适。建索引之前最好先插入一部分数据,空表建索引没意义。

2.2 文档切分策略:固定长度切分是偷懒,语义切分才是正道

文档切分是 RAG 里最容易被忽视但影响很大的环节。很多人直接用固定长度切分,比如每 500 个字符切一刀。这样做的问题是经常把一句话或一个段落从中间切断,检索出来的片段语义不完整,模型理解起来也费劲。

我的做法是分层切分。第一层按文档结构切,比如按标题、章节、段落切。第二层对过长的段落再按句子切,保证每个片段语义完整。第三层控制片段长度,一般 300 到 800 个 token 比较合适,太短信息量不够,太长检索精度下降。

Spring AI 提供了TokenTextSplitter,可以按 token 数切分,比按字符数切分准确。但它的默认行为是纯按长度切,不保留语义边界。我一般会自己写一个切分器,结合段落和句子边界:

public List<String> splitDocument(String content, int maxTokens) { List<String> chunks = new ArrayList<>(); String[] paragraphs = content.split("\n\n"); StringBuilder current = new StringBuilder(); int currentTokens = 0; for (String para : paragraphs) { int paraTokens = estimateTokens(para); if (currentTokens + paraTokens > maxTokens && current.length() > 0) { chunks.add(current.toString().trim()); current = new StringBuilder(); currentTokens = 0; } if (paraTokens > maxTokens) { // 段落本身超长,按句子切 List<String> sentences = splitBySentence(para); for (String sentence : sentences) { int sentTokens = estimateTokens(sentence); if (currentTokens + sentTokens > maxTokens && current.length() > 0) { chunks.add(current.toString().trim()); current = new StringBuilder(); currentTokens = 0; } current.append(sentence); currentTokens += sentTokens; } } else { current.append(para).append("\n\n"); currentTokens += paraTokens; } } if (current.length() > 0) { chunks.add(current.toString().trim()); } return chunks; }

切分的时候还要注意重叠。相邻片段之间保留 10% 到 20% 的重叠,能避免关键信息刚好落在切分边界上导致检索不到。这个重叠量不用太大,50 到 100 个 token 就够了。

2.3 嵌入模型选择与批量入库

嵌入模型的选择直接影响检索质量。OpenAI 的 text-embedding-3-small 性价比很高,1536 维,价格便宜,效果在客服场景下够用。如果对成本敏感,可以用开源的 BGE 系列,但需要自己部署推理服务。Spring AI 对 OpenAI 的嵌入模型支持最好,直接配置就行:

spring: ai: openai: api-key: ${OPENAI_API_KEY} embedding: options: model: text-embedding-3-small

批量入库的时候要注意两点。第一是批大小,一次请求太多嵌入会超时,一般 50 到 100 条一批比较稳。第二是失败重试,网络抖动导致某批失败很常见,要有重试机制。我一般用 Spring Retry 做三次重试,间隔递增:

@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) public List<float[]> embedBatch(List<String> texts) { return embeddingModel.embed(texts); }

入库的完整流程是:读文档 -> 切分 -> 批量嵌入 -> 写入向量表。这个过程建议做成异步任务,不要阻塞主流程。可以用 Spring 的@Async或者消息队列来解耦。

2.4 检索策略:相似度阈值、TopK 和重排序

检索的时候有几个参数需要调。TopK 是返回最相似的 K 个片段,一般设 3 到 5 比较合适。设太小可能漏掉关键信息,设太大引入噪声。相似度阈值是过滤掉不相关的片段,一般设 0.7 左右,低于这个值的片段不参与回答。

但光靠向量相似度有时候不够。比如用户问“退款政策是什么”,向量检索可能返回一堆跟退款相关的片段,但其中有些是过时的政策。这时候就需要重排序。重排序可以用一个交叉编码器模型,对 TopK 结果重新打分,把最相关的排前面。Spring AI 本身不直接提供重排序,但可以自己接一个重排序服务,或者用简单的规则重排,比如按文档更新时间加权。

我一般会在检索后加一层过滤逻辑:

public List<Document> retrieveWithFilter(String query, int topK, double threshold) { List<Document> docs = vectorStore.similaritySearch( SearchRequest.query(query).withTopK(topK * 2) ); return docs.stream() .filter(doc -> doc.getScore() >= threshold) .sorted((a, b) -> { // 先按相似度,相似度接近时按更新时间 double scoreDiff = b.getScore() - a.getScore(); if (Math.abs(scoreDiff) > 0.05) { return scoreDiff > 0 ? 1 : -1; } return compareByUpdateTime(b, a); }) .limit(topK) .collect(Collectors.toList()); }

实操心得:相似度阈值不要设死,不同嵌入模型的分数分布不一样。建议先用一批测试问题跑一遍,看看正确片段的分数分布,再定阈值。我见过有人直接抄别人的 0.8,结果召回率低得可怜。

3. 工具调用与记忆机制的设计与实现

3.1 工具调用的适用场景与实现方式

工具调用解决的是“模型不知道但业务系统知道”的问题。客服场景下常见的工具包括:订单查询、物流跟踪、账户余额、工单状态、退换货申请等。这些信息不在知识库里,必须实时查。

Spring AI 的工具调用实现很简洁,用@Tool注解标记方法就行:

@Component public class CustomerServiceTools { @Tool(description = "根据订单号查询订单状态,返回订单的当前状态和预计送达时间") public OrderStatus queryOrderStatus( @ToolParam(description = "订单号,通常是10位数字") String orderId) { // 实际查询业务系统 return orderService.getStatus(orderId); } @Tool(description = "查询用户最近的工单列表") public List<Ticket> queryUserTickets( @ToolParam(description = "用户ID") String userId, @ToolParam(description = "查询条数,默认5条") int limit) { return ticketService.getRecentTickets(userId, limit); } }

然后在 ChatClient 里注册这些工具:

ChatClient chatClient = ChatClient.builder(chatModel) .defaultTools(new CustomerServiceTools()) .build();

模型会根据用户问题自动决定是否调用工具。比如用户问“我的订单 1234567890 到哪了”,模型会识别出需要调用queryOrderStatus,传入订单号,拿到结果后组织语言回复。

但这里有几个坑要注意。第一是工具描述要写清楚,模型靠描述来判断什么时候调用。描述太模糊模型不知道什么时候用,描述太详细又浪费 token。第二是参数校验,模型可能传错参数格式,工具方法里要做好校验和异常处理。第三是工具调用失败的处理,如果业务系统挂了,不能让整个对话卡死,要有降级回复。

3.2 工具调用的安全边界与权限控制

工具调用是把双刃剑,用好了很方便,用不好就是安全隐患。最典型的问题是模型可能被诱导调用不该调用的工具。比如用户说“帮我查一下别人的订单”,如果工具没有权限校验,模型可能真的去查了。

我的做法是在工具方法内部做权限校验,不依赖模型判断:

@Tool(description = "查询订单状态") public OrderStatus queryOrderStatus( @ToolParam(description = "订单号") String orderId, ToolContext context) { String currentUserId = context.get("userId"); Order order = orderService.getById(orderId); if (!order.getUserId().equals(currentUserId)) { throw new AccessDeniedException("无权查询该订单"); } return order.getStatus(); }

Spring AI 的ToolContext可以传递上下文信息,比如当前登录用户 ID。这样即使模型被诱导,工具层面也会拦住。

另外,涉及写操作的工具要特别谨慎。比如“提交退款申请”这种工具,最好加二次确认,或者只允许在特定意图下调用。我一般会把写操作工具单独分组,只在明确意图识别后才注册。

3.3 对话记忆的分层设计

记忆这块,我把它分成三层:会话级短期记忆、用户级长期记忆、全局知识记忆。

会话级短期记忆就是当前对话的上下文,用MessageWindowChatMemory就行,保留最近 N 条消息。N 一般设 10 到 20,太多会占用大量 token,太少又记不住上下文。

ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(20) .build();

用户级长期记忆是跨会话的,比如用户上次咨询的问题、用户的偏好、用户的基本信息。这些信息可以存在 Redis 或数据库里,每次会话开始时加载。实现方式可以是在系统提示词里注入用户画像,或者在检索时把用户历史作为额外上下文。

全局知识记忆就是 RAG 的知识库,这个前面讲过了。

三层记忆的优先级是:会话级 > 用户级 > 全局知识。当信息冲突时,优先用会话级的信息。比如用户这次说“我要退的是上个月买的那件”,那就以这次说的为准,不要被历史记忆干扰。

3.4 记忆的过期与清理策略

记忆不清理,内存迟早爆。会话级记忆一般设 30 分钟过期,用户级记忆设 30 天过期。过期策略可以用 Redis 的 TTL,也可以用定时任务清理。

我一般用 Redis 存会话记忆,key 是 sessionId,value 是序列化的消息列表,TTL 设 1800 秒。每次对话时刷新 TTL,这样活跃会话不会过期,不活跃的自动清理。

public void saveMemory(String sessionId, List<Message> messages) { String key = "chat:memory:" + sessionId; redisTemplate.opsForValue().set(key, serialize(messages), 30, TimeUnit.MINUTES); }

注意:序列化消息的时候要注意版本兼容,模型返回的消息结构可能随版本变化。建议用 JSON 序列化,并且加版本号字段,方便后续升级。

4. 流式输出与兜底策略的工程实现

4.1 流式输出的实现与异常处理

流式输出用 Spring AI 的stream()方法配合 SSE 就能实现。Controller 层返回Flux<String>,前端用 EventSource 接收:

@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> chatStream(@RequestParam String message, @RequestParam String sessionId) { return chatClient.prompt() .user(message) .advisors(new MessageChatMemoryAdvisor(chatMemory, sessionId)) .stream() .content() .onErrorResume(e -> { log.error("流式输出异常", e); return Flux.just("抱歉,系统暂时繁忙,请稍后再试。"); }); }

流式场景下的异常处理跟同步不一样。同步请求抛异常直接返回错误码就行,流式请求可能已经输出了一部分内容才出错,这时候不能直接返回错误,要在流里插入一个错误提示。onErrorResume就是干这个的。

还有一个坑是超时。流式请求如果模型响应慢,连接可能被网关断开。我一般会设一个总超时时间,比如 30 秒,超过就主动结束流并返回兜底话术。

4.2 兜底策略的分层设计

兜底我分三层来做。

第一层是提示词兜底。在系统提示词里明确告诉模型:如果检索到的知识不足以回答问题,不要编造,直接说“这个问题我暂时无法准确回答,建议您联系人工客服”。这能过滤掉大部分幻觉。

第二层是业务兜底。当检索为空、工具调用失败、模型超时等情况发生时,走预设的降级回复。比如检索为空时,可以回复“我暂时没有找到相关信息,您可以换个说法再问,或者转人工客服”。

第三层是系统兜底。当整个服务不可用时,返回一个静态的兜底页面或消息。这层一般由网关或前端处理。

兜底策略的配置我建议放在配置中心,方便动态调整:

fallback: empty-retrieval: "抱歉,我暂时没有找到相关信息,您可以换个说法再问,或者输入'转人工'联系客服。" tool-failure: "查询服务暂时不可用,请稍后再试。" model-timeout: "系统响应超时,请稍后再试。" sensitive-content: "您的问题涉及敏感内容,我无法回答,请咨询其他问题。"

4.3 敏感内容过滤与安全机制

客服系统面向公众,敏感内容过滤是必须的。过滤分输入和输出两端。

输入端过滤是在用户消息进入模型之前做检查,如果命中敏感词或敏感意图,直接返回兜底话术,不调用模型。输出端过滤是在模型回复返回之前做检查,防止模型生成不当内容。

实现方式可以用敏感词库加正则匹配,也可以用专门的分类模型。我一般用敏感词库做第一层快速过滤,再用一个轻量分类模型做第二层判断。敏感词库要定期更新,分类模型可以用开源的。

public boolean isSensitive(String text) { // 第一层:敏感词匹配 if (sensitiveWordFilter.contains(text)) { return true; } // 第二层:分类模型 return sensitiveClassifier.isSensitive(text); }

实操心得:敏感词过滤要注意误伤。比如“退款”本身不是敏感词,但“退款投诉”可能触发。建议敏感词分级,高敏感的直接拦截,低敏感的走人工审核或提示。

4.4 性能优化与并发处理

客服系统可能面临突发流量,性能优化不能等到上线才做。我一般从几个方面入手。

第一是连接池。数据库连接池、HTTP 连接池都要配好,避免请求堆积。HikariCP 的maximumPoolSize一般设 CPU 核数 * 2 + 磁盘数,但具体要看压测结果。

第二是缓存。嵌入结果、检索结果、常见问题的回答都可以缓存。嵌入结果缓存能避免重复计算,检索结果缓存能减少向量库压力。我用 Caffeine 做本地缓存,Redis 做分布式缓存。

第三是异步。工具调用、知识入库这些耗时操作尽量异步化,不要阻塞主流程。Spring 的@Async配合线程池就能搞定。

第四是限流。用 Sentinel 或 Resilience4j 做限流和熔断,防止单个用户或单个接口拖垮整个系统。

@Bean public RateLimiter rateLimiter() { return RateLimiter.of("chat", RateLimiterConfig.custom() .limitForPeriod(100) .limitRefreshPeriod(Duration.ofSeconds(1)) .timeoutDuration(Duration.ofMillis(500)) .build()); }

压测的时候重点关注 P99 延迟和错误率。P99 超过 3 秒就要优化,错误率超过 1% 就要排查。我一般用 JMeter 或 Gatling 做压测,模拟 100 并发、500 并发、1000 并发三档。

5. 常见问题与排查技巧实录

5.1 检索不准的排查思路

检索不准是最常见的问题,表现是模型回答跟知识库内容对不上。排查思路是逐层检查。

先看切分是否合理。把检索到的片段打印出来,看看是不是语义完整。如果片段被切得七零八落,那就是切分策略有问题。

再看嵌入是否正常。拿一个已知问题,手动算一下它跟知识库片段的相似度,看看分数分布是否合理。如果所有分数都很低,可能是嵌入模型选错了或者维度不匹配。

然后看检索参数。TopK 和阈值是否合适,可以调大 TopK 看看能不能召回正确片段。如果调大能召回但排序靠后,那就是重排序的问题。

最后看提示词。检索到了正确片段但模型没用,可能是提示词没引导好。在提示词里明确要求“基于以下知识回答”,并且给出引用格式。

5.2 工具调用失败的常见原因

工具调用失败一般有几个原因。一是工具描述不清晰,模型不知道什么时候调用。二是参数格式不对,模型传的参数跟方法签名不匹配。三是业务系统异常,工具方法内部抛异常。

排查的时候先看日志,Spring AI 会打印工具调用的请求和响应。如果模型根本没发起调用,那就是描述问题。如果发起了但参数不对,那就是参数描述问题。如果调用抛异常,那就是业务逻辑问题。

我一般会在工具方法里加详细的日志,记录入参和出参,方便排查:

@Tool(description = "查询订单状态") public OrderStatus queryOrderStatus(@ToolParam(description = "订单号") String orderId) { log.info("工具调用:queryOrderStatus, orderId={}", orderId); try { OrderStatus status = orderService.getStatus(orderId); log.info("工具返回:{}", status); return status; } catch (Exception e) { log.error("工具调用失败", e); throw new ToolExecutionException("查询订单失败:" + e.getMessage()); } }

5.3 流式输出中断的排查

流式输出中断的表现是用户看到一半内容就没了。原因可能是网络问题、网关超时、模型异常、代码异常。

排查的时候先看后端日志,有没有异常堆栈。再看网关配置,SSE 的超时时间是否够长。然后看前端,EventSource 的错误处理是否完善。

我遇到过一次是 Nginx 的proxy_read_timeout默认 60 秒,但模型响应慢的时候超过了这个时间,连接被断开。改成 300 秒就好了。

5.4 常见问题速查表

问题现象可能原因排查方向解决方案
回答与知识库不符检索不准检查切分、嵌入、TopK调整切分策略和检索参数
模型不调用工具工具描述不清查看工具调用日志优化工具描述和参数说明
流式输出中断网关超时检查 Nginx 和网关配置调大超时时间
多轮对话失忆记忆未生效检查 sessionId 和记忆配置确认记忆 Advisor 已注册
响应慢检索或模型耗时分段计时加缓存、异步化、限流
敏感内容漏出过滤不严检查过滤规则加强敏感词库和分类模型
并发高时错误率上升资源不足压测定位瓶颈扩容、连接池调优、限流

5.5 独家避坑技巧

第一个坑是嵌入模型的维度要跟向量表定义一致。我见过有人用 1536 维的模型,但表定义的是 768 维,插入的时候直接报错。建表之前一定要确认模型维度。

第二个坑是 PGVector 的索引要在数据插入之后建。空表建索引,后续插入的数据可能不走索引,检索性能很差。正确做法是先插入数据,再建索引,或者定期重建索引。

第三个坑是流式输出的时候不要用@Transactional。流式请求生命周期长,事务会一直持有连接,并发一高连接池就爆了。流式接口的事务要单独处理,或者干脆不用事务。

第四个坑是工具调用的超时要设短。业务系统如果挂了,工具调用可能等很久,把整个对话卡死。工具方法内部要设超时,比如 3 秒,超时就走降级。

第五个坑是记忆的序列化要注意版本。模型返回的消息结构可能随版本变化,序列化的时候加版本号,反序列化的时候做兼容处理,不然升级的时候历史记忆全丢。

6. 整体串联与部署上线的关键动作

6.1 从十课到一套可运行系统的串联

十课分开讲的时候每课都是独立的,但真正上线需要把它们串起来。串联的核心是定义好各层之间的接口和数据流。

用户请求进来,先过敏感内容过滤,然后加载会话记忆,接着走 RAG 检索,检索结果和记忆一起拼成提示词,调用模型。模型如果决定调用工具,就走工具调用链路,拿到结果后继续生成。生成过程用流式输出,同时做输出端敏感过滤。任何环节异常,走对应的兜底策略。

这个流程用 Spring AI 的 Advisor 机制能比较优雅地实现。Advisor 可以在请求前后插入逻辑,比如记忆加载、检索增强、敏感过滤都可以做成 Advisor。

ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory), new QuestionAnswerAdvisor(vectorStore), new SensitiveContentAdvisor() ) .defaultTools(new CustomerServiceTools()) .build();

6.2 部署上线的检查清单

上线之前有几件事必须确认。配置方面,API key、数据库连接、Redis 连接、向量库连接都要检查。性能方面,压测报告要达标,P99 延迟和错误率在可接受范围。安全方面,敏感过滤要生效,权限校验要到位。监控方面,关键指标要有埋点,日志要能追溯。

我一般会做一个上线检查清单,逐项确认:

  • 所有外部依赖的连接测试通过
  • 压测报告 P99 < 3s,错误率 < 1%
  • 敏感内容过滤测试用例全部通过
  • 工具调用权限校验测试通过
  • 兜底策略触发测试通过
  • 监控大盘配置完成
  • 日志采集和告警配置完成
  • 回滚方案准备就绪

6.3 上线后的持续优化方向

上线不是终点,是起点。上线后要持续关注几个指标:用户满意度、问题解决率、转人工率、平均响应时间、错误率。这些指标能反映系统健康度。

优化方向主要有几个。一是知识库持续更新,把用户问得多但回答不好的问题补充进去。二是检索策略调优,根据实际数据调整 TopK 和阈值。三是提示词迭代,根据 bad case 优化提示词。四是工具扩展,把更多业务能力接入。

我一般会每周看一次 bad case,挑出回答不好的对话,分析原因,针对性优化。这个习惯坚持下来,系统效果会越来越好。

6.4 一个完整的配置示例

最后给一个完整的配置示例,把前面讲的东西串起来:

spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.3 embedding: options: model: text-embedding-3-small datasource: url: jdbc:postgresql://localhost:5432/customer_service username: postgres password: ${DB_PASSWORD} hikari: maximum-pool-size: 20 minimum-idle: 5 data: redis: host: localhost port: 6379 timeout: 3000 chat: memory: max-messages: 20 ttl-minutes: 30 retrieval: top-k: 5 similarity-threshold: 0.7 fallback: empty-retrieval: "抱歉,我暂时没有找到相关信息,您可以换个说法再问。" tool-failure: "查询服务暂时不可用,请稍后再试。" model-timeout: "系统响应超时,请稍后再试。"

这套配置我在几个项目里用过,基本能覆盖中小规模客服场景。如果并发量特别大,需要把 Redis 和 PGVector 做集群,模型调用也要做负载均衡。

最后分享一个小技巧:上线初期建议开一个“影子模式”,把模型的回答和人工客服的回答都记录下来,对比分析。这样能快速发现模型的短板,针对性优化。等模型回答质量稳定了,再逐步放量。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:52:06

Spring AI 实战:yml 配置 + ChatClient 四步链式调用快速接入大模型

1. 从配置文件到对话窗口&#xff1a;Spring AI 到底解决了什么问题第一次看到“配好 yml 就能聊”这个说法&#xff0c;我其实是持怀疑态度的。在 Java 生态里摸爬滚打这些年&#xff0c;见过太多“配置即用”的承诺最后都变成了“配置三天&#xff0c;跑通三行”。但 Spring …

作者头像 李华
网站建设 2026/10/14 6:51:56

Spring Boot 3.4.x升级踩坑记录:配置迁移与自动配置兼容实践

升级 Spring Boot 3.4.x 的过程&#xff0c;我印象最深的一句话是&#xff1a;版本看着不大&#xff0c;坑倒是一个不少。当时我从 3.3.7 跳到 3.4.0&#xff0c;先是拿一个边缘服务试水&#xff0c;原以为替换坐标、改两个配置就能过&#xff0c;结果连启动都没撑过去。后来把…

作者头像 李华
网站建设 2026/10/14 6:51:52

Spring AI entity() 结构化输出实战:告别手撕 JSON 的类型安全方案

1. 从字符串拼接到类型安全&#xff1a;为什么我彻底放弃了手撕 JSON刚接触大模型应用开发那会儿&#xff0c;我写过一段现在想起来都脸红的代码。模型返回一段文本&#xff0c;我用indexOf找第一个{&#xff0c;用lastIndexOf找最后一个}&#xff0c;截出来丢给 JSON 解析库&a…

作者头像 李华
网站建设 2026/10/14 6:51:52

学术期刊在线发布全流程解析:从元数据到上线部署的关键操作

期刊的线上发布这事儿&#xff0c;看着就是挂个网页、发个通知&#xff0c;实际上背后的门道和功夫比大多数人想象的多得多。我做学术出版技术支持这几年&#xff0c;经手过不少期次的在线发布&#xff0c;这次看到某综合组学期刊的第2卷第4期正式上线&#xff0c;还是想跟同行…

作者头像 李华
网站建设 2026/10/14 6:51:52

Spring AI 实战:ChatClient 四步链式调用与 yml 配置详解

Spring AI 这两年在 Java 圈子里讨论度一直不低&#xff0c;但真正动手跑通一个能对话的 Demo&#xff0c;很多人卡在第一步——配置文件怎么写、依赖怎么引、ChatClient 到底怎么串起来。我前阵子在一个模拟项目里完整走了一遍这套流程&#xff0c;从零到能对话大概花了不到半…

作者头像 李华