资讯动态

第8讲:RAG 生产化——Go 实现亿级向量召回 + 混合检索 + 重排序 Pipeline

发布时间:2026/10/14 3:21:06 来源:尧图企业网站定制
一、开篇RAG 的四个核心挑战2026 年RAG 已经是大模型落地的标配架构。但生产环境中简单的 Embedding → 向量检索 → LLM 生成 链条远远不够挑战现象生产级解法检索精度不足​Top-K 返回大量无关片段混合检索 重排序大规模性能瓶颈​千万级以上向量检索延迟飙升分片 HNSW 索引 GPU 加速查询意图漂移​用户问法多样直接检索效果差Query Rewrite HyDE上下文窗口浪费​召回的片段塞满 Context 但有效信息少动态剪枝 相关性过滤生产级 RAG Pipeline Query Understanding → Multi-Stage Retrieval → Reranking → Context Assembly → Generation二、整体架构用户查询 │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Stage 1: Query Understanding │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Query Rewrite│ │ Query │ │ HyDE │ │ │ │ (扩写/改写) │ │ Decompose │ │ (虚拟文档) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └──────────────────────────┬──────────────────────────────────────┘ │ 多条查询 ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Stage 2: Multi-Channel Retrieval │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 向量检索 │ │ BM25 关键词 │ │ 结构化过滤 │ │ │ │ (Dense) │ │ (Sparse) │ │ (Metadata) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ └────────────────┴────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ Fusion │ │ │ │ (RRF/CC) │ │ │ └──────────────┘ │ └──────────────────────────┬──────────────────────────────────────┘ │ Top-N 候选 ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Stage 3: Re-ranking │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Cross-Encoder│ │ LLM Reranker│ │ 规则过滤 │ │ │ │ (轻量级) │ │ (精确但慢) │ │ (去重/去噪) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └──────────────────────────┬──────────────────────────────────────┘ │ Top-K 精选 ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Stage 4: Context Assembly │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 窗口裁剪 │ │ 相关性排序 │ │ 引用标记 │ │ │ │ (Token 预算) │ │ (重要在前) │ │ (来源追溯) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └──────────────────────────┬──────────────────────────────────────┘ │ 优化后的 Context ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Stage 5: Generation │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ System Prompt│ │ Context │ │ 引用格式 │ │ │ │ 指令 │ │ 注入 │ │ 规范化 │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ └────────────────┴────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ LLM 生成 │ │ │ └──────────────┘ │ └─────────────────────────────────────────────────────────────────┘三、Stage 1: Query Understanding// rag/query.go package rag import ( context encoding/json fmt strings ) // QueryUnderstanding 查询理解结果 type QueryUnderstanding struct { OriginalQuery string json:original_query RewrittenQueries []string json:rewritten_queries // 改写后的多条查询 DecomposedParts []string json:decomposed_parts // 分解后的子问题 HypotheticalDoc string json:hypothetical_doc // HyDE 生成的虚拟文档 Intent string json:intent // 查询意图分类 Filters map[string]interface{} json:filters // 结构化过滤条件 } // QueryProcessor 查询处理器 type QueryProcessor struct { llm LLMClient } func NewQueryProcessor(llm LLMClient) *QueryProcessor { return QueryProcessor{llm: llm} } // Process 处理用户查询 func (qp *QueryProcessor) Process(ctx context.Context, query string) (*QueryUnderstanding, error) { result : QueryUnderstanding{ OriginalQuery: query, } // 1. 查询改写并行执行 rewriteChan : make(chan []string, 1) hydeChan : make(chan string, 1) intentChan : make(chan string, 1) go func() { rewrites, _ : qp.rewriteQuery(ctx, query) rewriteChan - rewrites }() go func() { doc, _ : qp.generateHyDE(ctx, query) hydeChan - doc }() go func() { intent, filters, _ : qp.classifyIntent(ctx, query) result.Intent intent result.Filters filters intentChan - intent }() result.RewrittenQueries -rewriteChan result.HypotheticalDoc -hydeChan -intentChan // 2. 如果是复合问题分解为子问题 if result.Intent complex { parts, _ : qp.decomposeQuery(ctx, query) result.DecomposedParts parts } return result, nil } func (qp *QueryProcessor) rewriteQuery(ctx context.Context, query string) ([]string, error) { prompt : fmt.Sprintf(你是一个查询改写专家。请将以下用户查询改写为 3 种不同的表述方式以提高检索召回率。 原查询%s 要求 1. 保持原意不变 2. 使用不同的措辞和句式 3. 适当补充同义词和相关术语 4. 每条不超过 50 个字 请输出 JSON 数组[改写1, 改写2, 改写3], query) resp, err : qp.llm.Chat(ctx, []Message{{Role: user, Content: prompt}}) if err ! nil { return []string{query}, nil // 降级返回原查询 } var rewrites []string if err : json.Unmarshal([]byte(extractJSON(resp)), rewrites); err ! nil { return []string{query}, nil } // 去重并限制数量 seen : map[string]bool{query: true} var result []string for _, r : range rewrites { r strings.TrimSpace(r) if !seen[r] r ! { seen[r] true result append(result, r) } } if len(result) 0 { result []string{query} } return result, nil } func (qp *QueryProcessor) generateHyDE(ctx context.Context, query string) (string, error) { // HyDE: Hypothetical Document Embeddings // 先生成一个假设的理想文档然后用这个文档去做向量检索 prompt : fmt.Sprintf(请根据以下查询生成一段理想的文档片段。这段文档应该包含能够完美回答该查询的内容。 查询%s 要求 1. 假设存在这样一篇文档它包含了回答该问题的所有必要信息 2. 文档风格客观、专业 3. 长度 100-200 字 4. 不要提及根据查询或假设等字样, query) resp, err : qp.llm.Chat(ctx, []Message{{Role: user, Content: prompt}}) if err ! nil { return query, nil } return resp, nil } func (qp *QueryProcessor) classifyIntent(ctx context.Context, query string) (string, map[string]interface{}, error) { prompt : fmt.Sprintf(分析以下查询的意图类别和可能的过滤条件。 查询%s 请输出 JSON { intent: factual|analytical|comparative|procedural|complex, filters: { date_range: null 或 {start: 2024-01, end: 2024-12}, category: null 或 技术文档|产品手册|FAQ, language: null 或 zh|en } }, query) resp, err : qp.llm.Chat(ctx, []Message{{Role: user, Content: prompt}}) if err ! nil { return factual, nil, nil } var result struct { Intent string json:intent Filters map[string]interface{} json:filters } json.Unmarshal([]byte(extractJSON(resp)), result) return result.Intent, result.Filters, nil } func (qp *QueryProcessor) decomposeQuery(ctx context.Context, query string) ([]string, error) { prompt : fmt.Sprintf(将以下复杂查询分解为多个简单的子问题。 查询%s 要求 1. 每个子问题应该可以被独立检索 2. 子问题之间可能有依赖关系 3. 保持信息的完整性 请输出 JSON 数组[子问题1, 子问题2, ...], query) resp, err : qp.llm.Chat(ctx, []Message{{Role: user, Content: prompt}}) if err ! nil { return []string{query}, nil } var parts []string json.Unmarshal([]byte(extractJSON(resp)), parts) if len(parts) 0 { parts []string{query} } return parts, nil }四、Stage 2: Multi-Channel Retrieval// rag/retrieval.go package rag import ( context fmt math sort sync ) // Document 文档 type Document struct { ID string json:id Content string json:content Metadata map[string]interface{} json:metadata Score float64 json:score Source string json:source // vector, bm25, filter } // VectorIndex 向量索引接口 type VectorIndex interface { Search(ctx context.Context, embedding []float32, topK int, filters map[string]interface{}) ([]Document, error) BatchSearch(ctx context.Context, embeddings [][]float32, topK int, filters map[string]interface{}) ([][]Document, error) } // BM25Index BM25 索引接口 type BM25Index interface { Search(ctx context.Context, query string, topK int) ([]Document, error) } // Retriever 检索器 type Retriever struct { vectorIndex VectorIndex bm25Index BM25Index embedder Embedder config RetrieverConfig } type RetrieverConfig struct { VectorTopK int json:vector_top_k // 向量检索返回数量 BM25TopK int json:bm25_top_k // BM25 返回数量 FinalTopK int json:final_top_k // 融合后最终数量 VectorWeight float64 json:vector_weight // 向量得分权重 BM25Weight float64 json:bm25_weight // BM25 得分权重 EnableRRF bool json:enable_rrf // 启用 RRF 融合 RRFDamping int json:rrf_damping // RRF 阻尼系数 } func DefaultRetrieverConfig() RetrieverConfig { return RetrieverConfig{ VectorTopK: 50, BM25TopK: 50, FinalTopK: 20, VectorWeight: 0.6, BM25Weight: 0.4, EnableRRF: true, RRFDamping: 60, } } // Retrieve 执行多通道检索 func (r *Retriever) Retrieve(ctx context.Context, queries []string, filters map[string]interface{}) ([]Document, error) { var allDocs []Document var mu sync.Mutex var wg sync.WaitGroup // 对每条查询分别执行向量检索和 BM25 检索 for _, query : range queries { wg.Add(2) // 向量检索 go func(q string) { defer wg.Done() docs, err : r.vectorSearch(ctx, q, filters) if err ! nil { return } mu.Lock() allDocs append(allDocs, docs...) mu.Unlock() }(query) // BM25 检索 go func(q string) { defer wg.Done() docs, err : r.bm25Search(ctx, q) if err ! nil { return } mu.Lock() allDocs append(allDocs, docs...) mu.Unlock() }(query) } wg.Wait() // 去重并融合 return r.fuseResults(allDocs), nil } func (r *Retriever) vectorSearch(ctx context.Context, query string, filters map[string]interface{}) ([]Document, error) { // 1. 生成查询向量 embedding, err : r.embedder.Embed(ctx, query) if err ! nil { return nil, fmt.Errorf(embed query: %w, err) } // 2. 向量检索 docs, err : r.vectorIndex.Search(ctx, embedding, r.config.VectorTopK, filters) if err ! nil { return nil, fmt.Errorf(vector search: %w, err) } // 3. 标记来源 for i : range docs { docs[i].Source vector } return docs, nil } func (r *Retriever) bm25Search(ctx context.Context, query string) ([]Document, error) { docs, err : r.bm25Index.Search(ctx, query, r.config.BM25TopK) if err ! nil { return nil, fmt.Errorf(bm25 search: %w, err) } for i : range docs { docs[i].Source bm25 } return docs, nil } // fuseResults 融合多路检索结果 func (r *Retriever) fuseResults(docs []Document) []Document { if len(docs) 0 { return nil } // 按文档 ID 分组 groups : make(map[string][]Document) for _, doc : range docs { groups[doc.ID] append(groups[doc.ID], doc) } var fused []Document for id, versions : range groups { if r.config.EnableRRF { // Reciprocal Rank Fusion score : 0.0 for _, v : range versions { rank : 0 if v.Source vector { rank int((1 - v.Score) * float64(r.config.VectorTopK)) } else { rank int((1 - v.Score) * float64(r.config.BM25TopK)) } score 1.0 / float64(r.config.RRFDampingrank1) } fused append(fused, Document{ ID: id, Content: versions[0].Content, Metadata: versions[0].Metadata, Score: score, Source: fusion, }) } else { // Weighted Score Fusion score : 0.0 for _, v : range versions { weight : r.config.VectorWeight if v.Source bm25 { weight r.config.BM25Weight } score v.Score * weight } fused append(fused, Document{ ID: id, Content: versions[0].Content, Metadata: versions[0].Metadata, Score: score, Source: fusion, }) } } // 按得分排序 sort.Slice(fused, func(i, j int) bool { return fused[i].Score fused[j].Score }) // 取 Top-K if len(fused) r.config.FinalTopK { fused fused[:r.config.FinalTopK] } return fused } // Embedder 嵌入器接口 type Embedder interface { Embed(ctx context.Context, text string) ([]float32, error) BatchEmbed(ctx context.Context, texts []string) ([][]float32, error) }五、Stage 3: Re-ranking// rag/reranker.go package rag import ( context fmt sort sync ) // Reranker 重排序器 type Reranker struct { crossEncoder CrossEncoder llm LLMClient config RerankerConfig } type RerankerConfig struct { EnableCrossEncoder bool json:enable_cross_encoder // 启用 Cross-Encoder EnableLLMRerank bool json:enable_llm_rerank // 启用 LLM 重排序 CrossEncoderTopK int json:cross_encoder_top_k // CE 处理的数量 LLMRerankTopK int json:llm_rerank_top_k // LLM 处理的数量 FinalTopK int json:final_top_k // 最终保留数量 RelevanceThreshold float64 json:relevance_threshold // 相关性阈值 } // CrossEncoder Cross-Encoder 接口 type CrossEncoder interface { Score(ctx context.Context, query string, documents []string) ([]float64, error) } func NewReranker(ce CrossEncoder, llm LLMClient, config RerankerConfig) *Reranker { return Reranker{ crossEncoder: ce, llm: llm, config: config, } } // Rerank 重排序 func (r *Reranker) Rerank(ctx context.Context, query string, documents []Document) ([]Document, error) { if len(documents) 0 { return nil, nil } // 1. Cross-Encoder 初筛 if r.config.EnableCrossEncoder r.crossEncoder ! nil { docs, err : r.rerankWithCE(ctx, query, documents) if err nil { documents docs } } // 2. LLM 精排 if r.config.EnableLLMRerank r.llm ! nil { docs, err : r.rerankWithLLM(ctx, query, documents) if err nil { documents docs } } // 3. 过滤低相关性文档 var filtered []Document for _, doc : range documents { if doc.Score r.config.RelevanceThreshold { filtered append(filtered, doc) } } // 4. 取 Top-K if len(filtered) r.config.FinalTopK { filtered filtered[:r.config.FinalTopK] } return filtered, nil } func (r *Reranker) rerankWithCE(ctx context.Context, query string, documents []Document) ([]Document, error) { // 只对 Top-N 进行 CE 打分 n : r.config.CrossEncoderTopK if n len(documents) { n len(documents) } candidates : documents[:n] contents : make([]string, len(candidates)) for i, doc : range candidates { contents[i] doc.Content } scores, err : r.crossEncoder.Score(ctx, query, contents) if err ! nil { return documents, err } for i : range candidates { candidates[i].Score scores[i] candidates[i].Source cross-encoder } // 合并剩余文档 result : append(candidates, documents[n:]...) sort.Slice(result, func(i, j int) bool { return result[i].Score result[j].Score }) return result, nil } func (r *Reranker) rerankWithLLM(ctx context.Context, query string, documents []Document) ([]Document, error) { n : r.config.LLMRerankTopK if n len(documents) { n len(documents) } candidates : documents[:n] // 批量打分 type scoredDoc struct { index int score float64 } results : make(chan scoredDoc, n) var wg sync.WaitGroup for i, doc : range candidates { wg.Add(1) go func(idx int, content string) { defer wg.Done() score, err : r.scoreWithLLM(ctx, query, content) if err ! nil { score 0.5 // 出错给中等分 } results - scoredDoc{idx, score} }(i, doc.Content) } wg.Wait() close(results) for result : range results { candidates[result.index].Score result.score candidates[result.index].Source llm-rerank } // 合并剩余文档 result : append(candidates, documents[n:]...) sort.Slice(result, func(i, j int) bool { return result[i].Score result[j].Score }) return result, nil } func (r *Reranker) scoreWithLLM(ctx context.Context, query, document string) (float64, error) { prompt : fmt.Sprintf(评估以下文档片段与查询的相关性输出 0.0 到 1.0 之间的分数。 查询%s 文档片段 %s 请只输出一个浮点数表示相关性分数。, query, truncate(document, 500)) resp, err : r.llm.Chat(ctx, []Message{{Role: user, Content: prompt}}) if err ! nil { return 0.5, nil } var score float64 fmt.Sscanf(resp, %f, score) return clamp(score, 0, 1), nil } func truncate(s string, maxLen int) string { runes : []rune(s) if len(runes) maxLen { return string(runes[:maxLen]) ... } return s } func clamp(v, min, max float64) float64 { if v min { return min } if v max { return max } return v }六、Stage 4: Context Assembly// rag/context.go package rag import ( fmt sort strings ) // ContextAssembler 上下文组装器 type ContextAssembler struct { maxTokens int reserveRatio float64 // 为 LLM 回复预留的比例 enableOrder bool // 是否按相关性排序 enableRefs bool // 是否添加引用标记 } func NewContextAssembler(maxTokens int, reserveRatio float64) *ContextAssembler { return ContextAssembler{ maxTokens: maxTokens, reserveRatio: reserveRatio, enableOrder: true, enableRefs: true, } } // Assemble 组装上下文 func (ca *ContextAssembler) Assemble(query string, documents []Document) (string, []Reference) { budget : int(float64(ca.maxTokens) * (1 - ca.reserveRatio)) usedTokens : 0 // 按相关性排序 if ca.enableOrder { sorted : make([]Document, len(documents)) copy(sorted, documents) sort.Slice(sorted, func(i, j int) bool { return sorted[i].Score sorted[j].Score }) documents sorted } var sections []string var refs []Reference for i, doc : range documents { estimatedTokens : estimateTokens(doc.Content) // 如果超出预算停止添加 if usedTokensestimatedTokens budget { break } // 添加引用标记 content : doc.Content if ca.enableRefs { refID : i 1 refs append(refs, Reference{ ID: refID, Document: doc, }) content fmt.Sprintf([%d] %s, refID, content) } sections append(sections, content) usedTokens estimatedTokens } context : strings.Join(sections, \n\n---\n\n) return context, refs } // Reference 引用 type Reference struct { ID int Document Document } // FormatReferences 格式化引用列表 func FormatReferences(refs []Reference) string { var sb strings.Builder sb.WriteString(\n\n**参考资料**\n) for _, ref : range refs { title : 未知来源 if ref.Document.Metadata ! nil { if t, ok : ref.Document.Metadata[title]; ok { title fmt.Sprintf(%v, t) } } sb.WriteString(fmt.Sprintf([%d] %s (相关度: %.2f)\n, ref.ID, title, ref.Document.Score)) } return sb.String() } func estimateTokens(text string) int { // 粗略估计中英文混合约 1.5 字符/token return len([]rune(text)) / 2 }七、Stage 5: Generator// rag/generator.go package rag import ( context fmt strings ) // GeneratorConfig 生成器配置 type GeneratorConfig struct { SystemPrompt string Temperature float32 MaxTokens int EnableCite bool // 启用引用 CiteFormat string // markdown 或 plain } // RAGGenerator RAG 生成器 type RAGGenerator struct { llm LLMClient config GeneratorConfig } func NewRAGGenerator(llm LLMClient, config GeneratorConfig) *RAGGenerator { return RAGGenerator{ llm: llm, config: config, } } // Generate 生成回答 func (g *RAGGenerator) Generate(ctx context.Context, query string, context string, refs []Reference) (string, error) { systemPrompt : g.buildSystemPrompt() userPrompt : g.buildUserPrompt(query, context, refs) messages : []Message{ {Role: system, Content: systemPrompt}, {Role: user, Content: userPrompt}, } resp, err : g.llm.Chat(ctx, messages) if err ! nil { return , fmt.Errorf(generate: %w, err) } // 追加引用列表 if g.config.EnableCite len(refs) 0 { resp FormatReferences(refs) } return resp, nil } func (g *RAGGenerator) buildSystemPrompt() string { if g.config.SystemPrompt ! { return g.config.SystemPrompt } citeRule : if g.config.EnableCite { citeRule 引用规则 - 当你引用某个资料时请在句末标注对应的引用编号例如 [1]、[2] - 如果一个观点来自多个资料标注多个编号例如 [1][2] - 不要在句子开头标注引用 } return fmt.Sprintf(你是一个专业的 AI 助手。请根据提供的参考资料回答问题。 核心原则 1. 只使用参考资料中的信息回答问题 2. 如果参考资料不足以回答问题请明确说明 3. 保持回答的准确性和客观性 4. 使用清晰、有条理的语言组织回答 5. 对于技术性问题提供具体的细节和步骤%s, citeRule) } func (g *RAGGenerator) buildUserPrompt(query string, context string, refs []Reference) string { var sb strings.Builder sb.WriteString(## 参考资料\n\n) sb.WriteString(context) sb.WriteString(\n\n) sb.WriteString(## 用户问题\n\n) sb.WriteString(query) return sb.String() }八、完整 Pipeline// rag/pipeline.go package rag import ( context fmt time ) // RAGPipeline 完整 RAG 流水线 type RAGPipeline struct { queryProcessor *QueryProcessor retriever *Retriever reranker *Reranker contextAssembler *ContextAssembler generator *RAGGenerator config PipelineConfig } type PipelineConfig struct { MaxQueryTokens int json:max_query_tokens EnableRewrite bool json:enable_rewrite EnableHyDE bool json:enable_hyde EnableDecompose bool json:enable_decompose EnableRerank bool json:enable_rerank ContextMaxTokens int json:context_max_tokens ContextReserveRatio float64 json:context_reserve_ratio LogLevel string json:log_level } func DefaultPipelineConfig() PipelineConfig { return PipelineConfig{ MaxQueryTokens: 512, EnableRewrite: true, EnableHyDE: true, EnableDecompose: true, EnableRerank: true, ContextMaxTokens: 4000, ContextReserveRatio: 0.3, LogLevel: info, } } // RAGResult RAG 结果 type RAGResult struct { Answer string json:answer References []Reference json:references Metrics RAGMetrics json:metrics } type RAGMetrics struct { TotalDurationMs int64 json:total_duration_ms QueryProcessMs int64 json:query_process_ms RetrievalMs int64 json:retrieval_ms RerankMs int64 json:rerank_ms GenerationMs int64 json:generation_ms RetrievedDocs int json:retrieved_docs UsedDocs int json:used_docs ContextTokens int json:context_tokens } // Run 执行完整 RAG 流水线 func (p *RAGPipeline) Run(ctx context.Context, query string) (*RAGResult, error) { start : time.Now() result : RAGResult{ Metrics: RAGMetrics{}, } // Stage 1: Query Understanding t1 : time.Now() understanding, err : p.queryProcessor.Process(ctx, query) if err ! nil { return nil, fmt.Errorf(query processing: %w, err) } result.Metrics.QueryProcessMs time.Since(t1).Milliseconds() // 构建检索查询列表 searchQueries : []string{query} if p.config.EnableRewrite { searchQueries append(searchQueries, understanding.RewrittenQueries...) } if p.config.EnableHyDE understanding.HypotheticalDoc ! { searchQueries append(searchQueries, understanding.HypotheticalDoc) } if p.config.EnableDecompose { searchQueries append(searchQueries, understanding.DecomposedParts...) } // Stage 2: Retrieval t2 : time.Now() documents, err : p.retriever.Retrieve(ctx, searchQueries, understanding.Filters) if err ! nil { return nil, fmt.Errorf(retrieval: %w, err) } result.Metrics.RetrievalMs time.Since(t2).Milliseconds() result.Metrics.RetrievedDocs len(documents) // Stage 3: Re-ranking t3 : time.Now() if p.config.EnableRerank len(documents) 0 { documents, err p.reranker.Rerank(ctx, query, documents) if err ! nil { // 重排序失败不影响后续流程 fmt.Printf(rerank warning: %v\n, err) } } result.Metrics.RerankMs time.Since(t3).Milliseconds() // Stage 4: Context Assembly context, refs : p.contextAssembler.Assemble(query, documents) result.References refs result.Metrics.UsedDocs len(refs) result.Metrics.ContextTokens estimateTokens(context) // Stage 5: Generation t4 : time.Now() answer, err : p.generator.Generate(ctx, query, context, refs) if err ! nil { return nil, fmt.Errorf(generation: %w, err) } result.Metrics.GenerationMs time.Since(t4).Milliseconds() result.Answer answer result.Metrics.TotalDurationMs time.Since(start).Milliseconds() return result, nil } // NewRAGPipeline 创建 RAG 流水线 func NewRAGPipeline( llm LLMClient, embedder Embedder, vectorIndex VectorIndex, bm25Index BM25Index, crossEncoder CrossEncoder, config PipelineConfig, ) *RAGPipeline { retrieverConfig : DefaultRetrieverConfig() rerankerConfig : RerankerConfig{ EnableCrossEncoder: crossEncoder ! nil, EnableLLMRerank: true, CrossEncoderTopK: 30, LLMRerankTopK: 10, FinalTopK: 10, RelevanceThreshold: 0.3, } generatorConfig : GeneratorConfig{ Temperature: 0.3, MaxTokens: 2048, EnableCite: true, CiteFormat: markdown, } return RAGPipeline{ queryProcessor: NewQueryProcessor(llm), retriever: NewRetriever(vectorIndex, bm25Index, embedder, retrieverConfig), reranker: NewReranker(crossEncoder, llm, rerankerConfig), contextAssembler: NewContextAssembler(config.ContextMaxTokens, config.ContextReserveRatio), generator: NewRAGGenerator(llm, generatorConfig), config: config, } } // LLMClient LLM 客户端接口 type LLMClient interface { Chat(ctx context.Context, messages []Message) (string, error) }九、使用示例// example/rag_example.go package main import ( context fmt log rag ) func main() { ctx : context.Background() // 创建 RAG Pipeline pipeline : rag.NewRAGPipeline( MyLLMClient{}, // LLM MyEmbedder{}, // Embedder MyVectorIndex{}, // 向量索引 MyBM25Index{}, // BM25 索引 MyCrossEncoder{}, // Cross-Encoder可为 nil rag.DefaultPipelineConfig(), ) // 执行查询 result, err : pipeline.Run(ctx, 如何使用 Go 实现一个高性能的 RAG 系统) if err ! nil { log.Fatal(err) } // 输出结果 fmt.Printf(回答\n%s\n\n, result.Answer) fmt.Printf(参考资料%d 篇\n, len(result.References)) fmt.Printf(耗时%dms (查询%dms, 检索%dms, 重排%dms, 生成%dms)\n, result.Metrics.TotalDurationMs, result.Metrics.QueryProcessMs, result.Metrics.RetrievalMs, result.Metrics.RerankMs, result.Metrics.GenerationMs) }十、关键要点Query Understanding 是 RAG 的第一道关卡​ — 改写、分解、HyDE 三条路并行显著提升召回率多通道检索比单一向量检索强 30%​ — 向量 BM25 结构化过滤互补RRF 融合比加权融合更鲁棒​ — 不需要调权重对不同规模的检索结果都稳定Cross-Encoder 重排序是性价比最高的精度提升手段​ — 只用它对 Top-30 打分成本可控LLM 重排序只用于 Top-10 的精排​ — 不要用 LLM 打全量太贵太慢Context Assembly 决定 LLM 能否正确利用信息​ — 裁剪、排序、引用标记缺一不可端到端耗时控制在 3 秒以内​ — 超过 5 秒用户就能感知到延迟十一、预告第 9 讲预告《大模型应用监控与可观测性OpenTelemetry Prometheus Grafana 全链路追踪》你将学到为什么 LLM 应用的可观测性比传统服务更重要OpenTelemetry 埋点Span / Trace / Metric 在 RAG 链路中的应用自定义指标Token 消耗、模型延迟、缓存命中率、重排收益告警规则成本异常、延迟飙升、召回率为零Go 实现的轻量级监控 SDK 开发之余的小工具推荐处理 Base64、JSON 格式化、JWT 解析、Crontab 计算、PDF 合并压缩这些碎片需求我常用一个纯前端本地工具箱zz365.top。所有计算在浏览器完成文件不上服务器关页即清。免费、无登录、无广告适合开发者当常驻标签页。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑