ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

服务框架接入检索增强,先分开检索和模型调用

2026/8/18 20:19:20 拓冰建站 浏览量
服务框架接入检索增强,先分开检索和模型调用 服务框架接入检索增强先分开检索和模型调用RAG 示例常把检索、上下文拼接和模型调用放在同一条链路里。本地能跑不等于可以承受真实并发先划清组件职责更重要在 Controller 里接到请求先调 Embedding API 拿到向量再用同步 Client 去 Vector DB 查 Top3 切片接着用StringBuilder拼一长串 Prompt最后发给大模型。这种 Demo 在本地跑单次测试一点问题都没有。可一旦部署到压测环境并发量刚抬到 200 QPS应用在 3 分钟内就会彻底瘫痪。Tomcat 线程池被卡死在等待向量库响应的 Blocking Socket 上内存中暴胀的上下文文本直接引发频繁 Young GC。非确定性的 LLM 交互必须使用确定性的软件工程架构去包裹。如果不能在 Spring Boot 内部将“上下文检索”、“文本压缩”、“向量计算”与“大模型交互”进行严格的物理解耦RAG 架构根本无法具备生产级可维护性。1. 玩具 Demo 到生产级 RAG 的崩溃死穴分析一个典型网关崩溃的内存 Dump 文件就能看出 Demo 架构在生产环境中的三大死穴同步阻塞传染性Embedding 计算和 Vector 检索往往需要 100ms~500ms 的 I/O 耗时。如果在 Web MVC 线程池中同步等待200 个并发就会瞬间耗尽 Tomcat 的 200 个默认 Worker 线程。Context 超长无控溢出向量数据库返回的切片文本长度不可控。多个切片直接拼接极易超出 LLM 的 Context Window 限制导致 API 直接报错400 Invalid Parameter: token length exceeded。缺乏故障降级防护一旦向量数据库连接池打满或网络抖动整个智能客服或知识库系统直接抛出 500 异常无法退回传统的关键词检索或硬编码兜底问答。解耦的核心在于建立一个包含信号量限流、滑动窗口压缩以及异步响应式调度的“最小确定性架构”。2. 生产级 RAG 组件职责拆分架构我们必须将 RAG 管道拆分为四个强隔离的组件每个组件只承担单一职责RAG Dispatcher / Orchestrator负责整个 Pipeline 的状态流转与超时控制。Vector Retriever Hybrid Searcher负责混合检索向量 BM25 关键词并实施熔断降级。Context Compressor负责 Token 计费评估与滑动窗口上下文截断。Reactive LLM Client基于 WebClient 实现非阻塞流式响应推送。具体的组件交互链路与降级路径如下3. 生产级解耦代码实现Spring Boot 自定义 Starter 组件在 Spring Boot 3.x 体系下我们通过响应式编程与信号量隔离机制实现高性能的 Context 压缩器与非阻塞调用管道。3.1 上下文滑动窗口压缩器package com.example.rag.core.compressor; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; Component public class ContextWindowCompressor { private static final int DEFAULT_MAX_CONTEXT_TOKENS 2048; /** * 根据估算 Token 数量对检索出的 Document 切片进行截断防范 Context 溢出 */ public String compressAndFormat(ListString rawDocuments, int maxTokenLimit) { int limit maxTokenLimit 0 ? maxTokenLimit : DEFAULT_MAX_CONTEXT_TOKENS; StringBuilder compressedBuilder new StringBuilder(); int currentTokenCount 0; for (String doc : rawDocuments) { // 粗略评估 CJK 字符与英文 Token 比率1 中文字符 ≈ 0.6 Token int estimatedTokens estimateTokenCount(doc); if (currentTokenCount estimatedTokens limit) { break; } compressedBuilder.append(【相关资料】: ).append(doc.trim()).append(\n\n); currentTokenCount estimatedTokens; } return compressedBuilder.toString(); } private int estimateTokenCount(String text) { if (text null || text.isEmpty()) { return 0; } // 简化的物理字符评估逻辑生产环境可替换为 JTokkit return (int) (text.length() * 0.75); } }3.2 包含信号量限流与降级的响应式 Orchestratorpackage com.example.rag.core.pipeline; import com.example.rag.core.compressor.ContextWindowCompressor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.concurrent.Semaphore; Service public class ProductionRagOrchestrator { private static final Logger log LoggerFactory.getLogger(ProductionRagOrchestrator.class); private final ContextWindowCompressor compressor; private final WebClient llmWebClient; // 保护远端 LLM API 的并发信号量最多允许 50 个并发请求同时在途 private final Semaphore concurrencySemaphore new Semaphore(50); public ProductionRagOrchestrator(ContextWindowCompressor compressor, WebClient.Builder webClientBuilder) { this.compressor compressor; this.llmWebClient webClientBuilder.baseUrl(System.getenv(LLM_BASE_URL)).build(); } public FluxString executePipeline(String userQuery) { return Mono.fromCallable(() - retrieveDocsWithFallback(userQuery)) .subscribeOn(reactor.core.scheduler.Schedulers.boundedElastic()) .timeout(Duration.ofMillis(800)) // 向量检索阶段强制 800ms 超时 .onErrorReturn(Collections.singletonList(系统告警知识库检索超时使用通用模型回答。)) .flatMapMany(docs - { String context compressor.compressAndFormat(docs, 1500); return streamLlmResponse(userQuery, context); }); } private ListString retrieveDocsWithFallback(String query) { try { // 模拟向量库检索过程如 Milvus 调用 // 生产代码中如果抛出异常进入 Catch 返回兜底空文本 return List.of(Spring Boot 3.2 支持 Virtual Threads 虚拟线程。, 可以通过 spring.threads.virtual.enabledtrue 开启。); } catch (Exception e) { log.error(Vector Store Retrieval Failed!, e); return Collections.emptyList(); } } private FluxString streamLlmResponse(String query, String context) { if (!concurrencySemaphore.tryAcquire()) { return Flux.just(当前咨询人数过多请稍后再试系统触发背压保护。); } String promptPayload String.format({\model\:\deepseek-chat\,\messages\:[{\role\:\system\,\content\:\%s\},{\role\:\user\,\content\:\%s\}],\stream\:true}, escapeJson(context), escapeJson(query)); return llmWebClient.post() .uri(/chat/completions) .header(Content-Type, application/json) .header(Authorization, Bearer System.getenv(LLM_API_KEY)) .bodyValue(promptPayload) .retrieve() .bodyToFlux(String.class) .doFinally(signalType - concurrencySemaphore.release()); } private String escapeJson(String input) { return input.replace(\, \\\).replace(\n, \\n); } }4. 单元测试与本地工程调试命令针对这套解耦架构不能等到发布到 K8s 后才测试其稳定度。我们可以利用 JUnit 5 与 Spring Boot BootStrap 进行组件隔离测试。在本地执行 RAG 管道单元测试观察检索超时降级路径# 1. 运行指定的 RAG 管道集成测试类输出详细日志 mvn test -DtestProductionRagOrchestratorTest -Dlogging.level.com.example.ragDEBUG # 2. 本地模拟模拟高并发请求测试验证信号量背压Concurrency Limit ab -n $REQUEST_COUNT -c $CONCURRENCY -p query.json -T application/json $SERVICE_BASE_URL/api/rag/chat通过 Spring Actuator 查看运行期组件指标口径# 验证并发信号量与 WebClient 连接池使用率 curl -s $SERVICE_BASE_URL/actuator/metrics/executor.active | grep value如果结果显示即使在 100 并发压测下响应时间依旧平稳且当 Mock 向量库延迟调高到 1000ms 时应用无缝触发了onErrorReturn的降级策略返回兜底内容这说明组件解耦达到了预期防线。5. 生产架构落地的三条硬性隔离规则线程池硬隔离检索组件Vector DB Client必须使用单独的boundedElastic线程池调度严禁占用 Web 容器主线程池防止向量库卡死拉垮整站。Context 长度双重闸门在进入ContextCompressor之前进行硬截断任何单条 Document 超过 2000 字符直接强行裁剪绝不把垃圾数据送入 Token 评估链。双轨降级机制主轨走向量检索备轨走本地 Redis 静态问答库或 ElasticSearch 关键字检索。当向量库连续报错 5 次触发 Sentinel 熔断器后管道自动切到备轨保证业务可用率维持在 99.9% 以上。