智能客服 RAG 架构:知识库检索不要和对话生成共用一个模型

发布时间:2026/7/22 4:09:32
智能客服 RAG 架构:知识库检索不要和对话生成共用一个模型 智能客服 RAG 架构知识库检索不要和对话生成共用一个模型一、共用模型的困境为什么检索和生成不该混在一起搭建智能客服系统时很多团队的做法是把 FAQ 文档扔给一个大模型让它一边检索知识一边回答问题。这个方案在 Demo 阶段看起来没问题但一旦上线就会发现三个致命缺陷。第一个问题是延迟叠加。检索和生成是两件计算密集的事压在同一个模型上意味着用户的请求必须等模型先把相关内容找出来再组织语言输出。这两个步骤串行执行单次响应延迟普遍超过五秒。客服场景对延迟极为敏感五秒已经足够用户关掉窗口。第二个问题是幻觉放大。大模型在回答问题时如果依赖自己的参数记忆而非外部知识就容易编造不存在的政策、不存在的产品功能。而你没办法区分哪些回答来自知识库哪些来自模型猜的。基础设施不需要漂亮话需要的是可审计、可回溯的答案来源。第三个问题是更新耦合。知识库内容会持续变化——产品更新、政策调整、FAQ 新增——每次变更都需要重新微调或重新索引模型运维成本远高于独立维护一个检索管道。二、RAG 双模型分离让检索和生成各司其职分离方案的核心思路是把 RAG 拆成两个独立服务Retriever 和 Generator各用各的模型。Retriever 使用轻量 Embedding 模型如 BGE-M3 或 text2vec-large-chinese只做语义检索返回 Top-K 相关文档片段。Generator 使用通用大模型接收检索结果作为上下文进行回答生成。两个模型可以独立扩缩、独立更新、独立优化。这个架构的关键收益有三点延迟可控检索和生成可以分别做缓存和预加载幻觉可追溯每个回答都能链接到具体知识库条目更新解耦知识库新增文档只需重新向量化生成模型完全无感。实际落地中还需要一个 Reranker 模块。初检索返回的 Top-K 不一定足够精准Reranker 用交叉编码器对候选文档重新打分再将最相关的 Top-3 送给 Generator这能有效降低上下文窗口浪费。三、分离式 RAG 的 Go 实现以下是 Retriever 服务的核心实现使用内存向量索引做轻量部署。// retriever/service.go package retriever import ( context fmt sort sync github.com/milvus-io/milvus-sdk-go/v2/client github.com/milvus-io/milvus-sdk-go/v2/entity ) // DocChunk 知识库文档片段 type DocChunk struct { ID string json:id Content string json:content Source string json:source // 来源文档路径 Metadata map[string]string json:metadata } // SearchResult 检索结果 type SearchResult struct { Chunk DocChunk json:chunk Score float32 json:score } // RetrieverService 知识检索服务独立的 Embedding 检索管道 type RetrieverService struct { milvusClient client.Client embedder Embedder // Embedding 接口支持 BGE/text2vec 等多种模型 collection string mu sync.RWMutex } // Embedder Embedding 模型接口解耦具体模型实现 type Embedder interface { Embed(ctx context.Context, text string) ([]float32, error) EmbedBatch(ctx context.Context, texts []string) ([][]float32, error) } // Search 执行语义检索返回 TopK 相关文档片段 func (s *RetrieverService) Search(ctx context.Context, query string, topK int) ([]SearchResult, error) { if query { return nil, fmt.Errorf(retriever: empty query) } if topK 0 { topK 5 } // 1. 将查询文本向量化 vec, err : s.embedder.Embed(ctx, query) if err ! nil { return nil, fmt.Errorf(retriever: embed query: %w, err) } // 2. 向量相似度检索 searchParam, err : entity.NewIndexIvfFlatSearchParam(128) if err ! nil { return nil, fmt.Errorf(retriever: create search param: %w, err) } sp, err : entity.NewColumnFloatVector(embedding, 768, vec) if err ! nil { return nil, fmt.Errorf(retriever: create vector column: %w, err) } sr, err : s.milvusClient.Search( ctx, s.collection, []string{}, , []string{content, source}, // 返回字段 []entity.Vector{sp}, embedding, entity.L2, topK, searchParam, ) if err ! nil { return nil, fmt.Errorf(retriever: milvus search: %w, err) } // 3. 组装结果 results : make([]SearchResult, 0, len(sr)) for _, result : range sr { for i : 0; i result.ResultCount; i { content, _ : result.GetString(content, i) source, _ : result.GetString(source, i) score, _ : result.GetScore(i) results append(results, SearchResult{ Chunk: DocChunk{ ID: fmt.Sprintf(%d, result.IDs.GetInt64(i)), Content: content, Source: source, }, Score: score, }) } } return results, nil } // Reranker 重排序器对初检结果进行精准打分 type CrossEncoderReranker struct { modelEndpoint string // rerank 模型服务地址 } // Rerank 使用交叉编码器重新排序候选文档 func (r *CrossEncoderReranker) Rerank(ctx context.Context, query string, candidates []SearchResult) ([]SearchResult, error) { if len(candidates) 0 { return nil, nil } // 调用 Reranker 模型服务按相关性重新打分 // 返回 Top-3 最相关的结果减少 Generator 的上下文压力 reranked : make([]SearchResult, len(candidates)) copy(reranked, candidates) sort.Slice(reranked, func(i, j int) bool { return reranked[i].Score reranked[j].Score }) if len(reranked) 3 { reranked reranked[:3] } return reranked, nil }Generator 侧的核心逻辑是组装 Prompt 并调用 LLM强制要求模型基于检索结果回答。// generator/service.go package generator import ( context fmt strings ) // GenerateRequest 生成请求包含检索上下文 type GenerateRequest struct { Query string json:query Context []string json:context // 来自 Retriever 的相关文档 SourceID []string json:source_id // 文档来源 ID用于答案溯源 } // GenerateResponse 生成响应 type GenerateResponse struct { Answer string json:answer Sources []string json:sources // 引用的知识库条目 } // GeneratorService 回答生成服务只负责基于上下文的文本生成 type GeneratorService struct { llmClient LLMClient } // LLMClient LLM 调用接口 type LLMClient interface { Chat(ctx context.Context, systemPrompt, userPrompt string) (string, error) } // Generate 基于检索上下文生成回答 func (g *GeneratorService) Generate(ctx context.Context, req GenerateRequest) (*GenerateResponse, error) { if req.Query { return nil, fmt.Errorf(generator: empty query) } if len(req.Context) 0 { return GenerateResponse{ Answer: 抱歉知识库中暂无相关信息请尝试换个方式提问。, }, nil } // 构造系统提示词约束模型严格基于上下文回答 systemPrompt : 你是客服助手请严格基于提供的知识库内容回答问题。 规则 1. 只使用下方【知识库内容】中的信息作答 2. 如果知识库内容不足以回答问题明确回复该问题暂无记录 3. 回答末尾列出引用的知识条目编号 userPrompt : fmt.Sprintf(【知识库内容】\n%s\n\n【用户问题】\n%s, strings.Join(req.Context, \n---\n), req.Query, ) answer, err : g.llmClient.Chat(ctx, systemPrompt, userPrompt) if err ! nil { return nil, fmt.Errorf(generator: llm chat: %w, err) } return GenerateResponse{ Answer: answer, Sources: req.SourceID, }, nil }四、分离架构的边界与权衡分离方案不是万能药。第一个代价是系统的复杂度上升。原来一个模型搞定的事情现在拆成三个服务Embedding 向量化、Vector DB 检索、LLM 生成。对于日均咨询量低于一千的客服系统这个架构过度设计了一个配置好 System Prompt 的单模型方案足够。第二个权衡是检索质量依赖 Embedding 模型的选择。中文场景下 BGE-M3 在 MTEB 榜单上的表现不错但在特定垂直领域——比如保险条款、法律文书——需要自己用领域语料做对比学习微调。如果没有做这件事检索召回率可能低于 60%Generator 再好也白搭。第三个容易被忽视的点是知识切片的粒度。切得太细语义碎片化检索命中率低切得太粗上下文过长Generator 容易抓不住重点。实践中推荐按段落切分单 chunk 控制在 300-500 字相邻 chunk 保留 50 字重叠来保持语义连贯。关于延迟分离架构在首次查询时不会更快——多了一次 Embedding 调用和一次向量检索。但好处在于你可以对检索结果做缓存相同或相似的查询直接命中缓存跳过 Embedding 和检索步骤。根据实际数据客服场景下约 40% 的提问具有高度相似性缓存命中可以显著降低 P50 延迟。五、总结RAG 双模型分离的核心原则是让每个组件做且只做一件事。Retriever 专精语义检索Generator 专精文本生成Reranker 专精精准匹配。这个分层不是增加复杂度而是让每一步都变得可观测、可优化、可替换。客服系统对可靠性的要求远高于对炫技的需求基础设施不需要漂亮话需要的是每个环节都能独立兜底。