☰
第4讲:RAG 实验——检索策略与切片效果的量化评估
2026/11/4 12:11:11 网站建设 项目流程

一、RAG 实验的核心问题

RAG 系统中哪些东西值得实验? ──────────────────────────────────────────────────── 切片策略:固定长度 vs 语义切分 vs 递归切分 切片参数:chunk_size=256 vs 512 vs 1024 检索算法:BM25 vs 向量检索 vs 混合检索 Top-K:3 vs 5 vs 10 vs 20 重排序:有 vs 无,用什么模型 Embedding 模型:text-embedding-ada-002 vs bge-large 查询改写:原始 query vs 改写后 query 上下文窗口:塞多少 token 给 LLM

核心难题:RAG 的效果评估链条很长——切片→向量化→检索→重排序→上下文构建→LLM 生成,任何一个环节的改动都会传导到最终输出,如何定位到底是哪一步出了问题?


二、RAG 评估指标体系

RAG 评估金字塔 ──────────────────────────────────────────────────── ┌──────────┐ │ 端到端 │ ← 最终用户体验 │ 质量 │ (用户满意度/任务完成率) └────┬─────┘ │ ┌────▼─────┐ │ 生成质量 │ ← LLM 输出质量 │ │ (相关性/准确性/完整性/无幻觉) └────┬─────┘ │ ┌────▼─────┐ │ 检索质量 │ ← 检索到的文档质量 │ │ (命中率/精确率/召回率/MRR/NDCG) └────┬─────┘ │ ┌────▼─────┐ │ 切片质量 │ ← 文档切分质量 │ │ (信息完整性/边界合理性/冗余度) └──────────┘

关键原则:先优化底层(切片→检索),再优化上层(生成)。如果检索都搜不到正确答案,LLM 再怎么调 Prompt 也没用。


三、整体架构

┌──────────────────────────────────────────────────────────────┐ │ RAG 实验系统 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │数据集管理│ │切片策略 │ │检索配置 │ │实验编排 │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────┬───────────────────────────────────┘ │ ┌──────────────────────────▼───────────────────────────────────┐ │ 实验执行引擎 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │文档切片 │ │向量化 │ │检索执行 │ │上下文构建│ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────┬───────────────────────────────────┘ │ ┌──────────────────────────▼───────────────────────────────────┐ │ 评估系统 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │检索评估 │ │生成评估 │ │端到端评估│ │统计分析 │ │ │ │(命中率等)│ │(ROUGE等) │ │(用户模拟)│ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────────────────────────────────────────┘

四、核心数据结构

// ragexp/types.go package ragexp import ( "time" ) // ─── 评估数据集 ─── // EvalDataset 评估数据集 type EvalDataset struct { ID string `json:"id"` Name string `json:"name"` Samples []EvalSample `json:"samples"` CreatedAt time.Time `json:"created_at"` } // EvalSample 评估样本 type EvalSample struct { ID string `json:"id"` Query string `json:"query"` // 用户问题 RelevantDocs []string `json:"relevant_docs"` // 相关文档 ID 列表 GoldenAnswer string `json:"golden_answer"` // 标准答案 SourceDoc string `json:"source_doc"` // 来源文档原文 } // ─── 切片配置 ─── // ChunkConfig 切片配置 type ChunkConfig struct { Strategy string `json:"strategy"` // fixed, semantic, recursive, sentence ChunkSize int `json:"chunk_size"` // 切片大小(字符数) Overlap int `json:"overlap"` // 重叠大小 Separators []string `json:"separators"` // 分隔符(递归切分用) } // ─── 检索配置 ─── // RetrievalConfig 检索配置 type RetrievalConfig struct { Method string `json:"method"` // bm25, vector, hybrid TopK int `json:"top_k"` MinScore float64 `json:"min_score"` // 最低分数阈值 RerankModel string `json:"rerank_model"` // 重排序模型,空表示不重排 HybridWeight float64 `json:"hybrid_weight"` // 混合检索时 BM25 权重 (0-1) } // ─── 实验变体 ─── // RAGVariant RAG 实验变体 type RAGVariant struct { ID string `json:"id"` Name string `json:"name"` ChunkConfig ChunkConfig `json:"chunk_config"` RetrievalConfig RetrievalConfig `json:"retrieval_config"` EmbeddingModel string `json:"embedding_model"` } // ─── 评估结果 ─── // RetrievalEval 检索评估结果 type RetrievalEval struct { SampleID string `json:"sample_id"` Query string `json:"query"` HitRate float64 `json:"hit_rate"` // 是否命中相关文档 (0/1) Precision float64 `json:"precision"` // 精确率 Recall float64 `json:"recall"` // 召回率 MRR float64 `json:"mrr"` // Mean Reciprocal Rank NDCG float64 `json:"ndcg"` // Normalized Discounted Cumulative Gain RetrievedDocs []string `json:"retrieved_docs"` // 检索到的文档 ID LatencyMs int64 `json:"latency_ms"` } // GenerationEval 生成评估结果 type GenerationEval struct { SampleID string `json:"sample_id"` GeneratedAnswer string `json:"generated_answer"` ROUGE1 float64 `json:"rouge_1"` ROUGE2 float64 `json:"rouge_2"` ROUGEL float64 `json:"rouge_l"` BERTScore float64 `json:"bert_score"` Faithfulness float64 `json:"faithfulness"` // 忠实度(是否基于检索结果) AnswerRelevance float64 `json:"answer_relevance"` // 回答相关性 } // EndToEndEval 端到端评估 type EndToEndEval struct { SampleID string `json:"sample_id"` OverallScore float64 `json:"overall_score"` // 综合评分 0-1 Passed bool `json:"passed"` Details map[string]float64 `json:"details"` }

五、切片策略实现与评估

// ragexp/chunker.go package ragexp import ( "strings" "unicode/utf8" ) // ─── 切片策略接口 ─── // Chunker 切片器接口 type Chunker interface { Chunk(text string, config ChunkConfig) []Chunk } // Chunk 切片结果 type Chunk struct { Index int `json:"index"` Text string `json:"text"` StartPos int `json:"start_pos"` EndPos int `json:"end_pos"` Tokens int `json:"tokens"` // 预估 token 数 } // ─── 固定长度切分 ─── type FixedChunker struct{} func (fc *FixedChunker) Chunk(text string, config ChunkConfig) []Chunk { var chunks []Chunk runes := []rune(text) size := config.ChunkSize overlap := config.Overlap pos := 0 index := 0 for pos < len(runes) { end := pos + size if end > len(runes) { end = len(runes) } chunkText := string(runes[pos:end]) chunks = append(chunks, Chunk{ Index: index, Text: chunkText, StartPos: pos, EndPos: end, Tokens: estimateTokens(chunkText), }) pos += size - overlap index++ } return chunks } // ─── 语义切分(按段落/句子) ─── type SemanticChunker struct{} func (sc *SemanticChunker) Chunk(text string, config ChunkConfig) []Chunk { // 1. 先按段落分割 paragraphs := strings.Split(text, "\n\n") var chunks []Chunk var current strings.Builder currentSize := 0 index := 0 startPos := 0 for _, para := range paragraphs { para = strings.TrimSpace(para) if para == "" { continue } paraSize := utf8.RuneCountInString(para) // 如果当前块加上这个段落会超过大小,先保存当前块 if currentSize+paraSize > config.ChunkSize && currentSize > 0 { chunkText := current.String() chunks = append(chunks, Chunk{ Index: index, Text: chunkText, StartPos: startPos, EndPos: startPos + currentSize, Tokens: estimateTokens(chunkText), }) index++ current.Reset() currentSize = 0 startPos += currentSize - config.Overlap } current.WriteString(para) current.WriteString("\n\n") currentSize += paraSize + 2 } // 最后一个块 if current.Len() > 0 { chunkText := current.String() chunks = append(chunks, Chunk{ Index: index, Text: chunkText, StartPos: startPos, EndPos: startPos + currentSize, Tokens: estimateTokens(chunkText), }) } return chunks } // ─── 递归切分 ─── type RecursiveChunker struct{} func (rc *RecursiveChunker) Chunk(text string, config ChunkConfig) []Chunk { separators := config.Separators if len(separators) == 0 { separators = []string{"\n\n", "\n", "。", ". ", ",", ", ", " ", ""} } return rc.recursiveSplit(text, config.ChunkSize, config.Overlap, separators, 0) } func (rc *RecursiveChunker) recursiveSplit(text string, size, overlap int, seps []string, depth int) []Chunk { if len(text) <= size || depth >= len(seps) { return []Chunk{{Text: text, Tokens: estimateTokens(text)}} } sep := seps[depth] var chunks []Chunk // 尝试用当前分隔符分割 parts := strings.Split(text, sep) var current strings.Builder for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } if current.Len()+len(part)+len(sep) > size && current.Len() > 0 { // 当前块已满,递归处理剩余部分 subChunks := rc.recursiveSplit(part, size, overlap, seps, depth+1) chunks = append(chunks, subChunks...) current.Reset() } else { if current.Len() > 0 { current.WriteString(sep) } current.WriteString(part) } } if current.Len() > 0 { chunks = append(chunks, Chunk{Text: current.String(), Tokens: estimateTokens(current.String())}) } return chunks } // ─── 切片质量评估 ─── // ChunkQualityEval 切片质量评估 type ChunkQualityEval struct { TotalChunks int `json:"total_chunks"` AvgChunkSize float64 `json:"avg_chunk_size"` MinChunkSize int `json:"min_chunk_size"` MaxChunkSize int `json:"max_chunk_size"` SizeStdDev float64 `json:"size_std_dev"` // 大小标准差(越小越均匀) InfoCompleteness float64 `json:"info_completeness"` // 信息完整性 (0-1) BoundaryQuality float64 `json:"boundary_quality"` // 边界质量 (0-1) RedundancyRate float64 `json:"redundancy_rate"` // 冗余率 } // EvaluateChunkQuality 评估切片质量 func EvaluateChunkQuality(chunks []Chunk, originalText string) *ChunkQualityEval { eval := &ChunkQualityEval{ TotalChunks: len(chunks), } if len(chunks) == 0 { return eval } // 1. 大小统计 sizes := make([]int, len(chunks)) totalSize := 0 for i, c := range chunks { size := utf8.RuneCountInString(c.Text) sizes[i] = size totalSize += size } eval.AvgChunkSize = float64(totalSize) / float64(len(chunks)) eval.MinChunkSize = min(sizes) eval.MaxChunkSize = max(sizes) // 标准差 variance := 0.0 for _, s := range sizes { variance += (float64(s) - eval.AvgChunkSize) * (float64(s) - eval.AvgChunkSize) } eval.SizeStdDev = sqrt(variance / float64(len(chunks))) // 2. 边界质量:检查是否在自然边界处分割 naturalBoundaries := 0 for _, c := range chunks { trimmed := strings.TrimSpace(c.Text) if len(trimmed) > 0 { // 检查是否以句号/换行结尾(好的边界) lastChar := trimmed[len(trimmed)-1:] if strings.ContainsAny(lastChar, "。.!!\n") { naturalBoundaries++ } } } eval.BoundaryQuality = float64(naturalBoundaries) / float64(len(chunks)) // 3. 冗余率:检查相邻 chunk 的重叠部分 overlapChars := 0 for i := 0; i < len(chunks)-1; i++ { overlapChars += overlapLength(chunks[i].Text, chunks[i+1].Text) } eval.RedundancyRate = float64(overlapChars) / float64(totalSize) // 4. 信息完整性(简化:检查关键段落是否被截断) // 实际生产中用更复杂的语义完整性检测 eval.InfoCompleteness = eval.BoundaryQuality * 0.7 + (1 - eval.RedundancyRate) * 0.3 return eval } func overlapLength(a, b string) int { // 简单实现:检查 a 的尾部与 b 的头部重叠 minLen := min(len(a), len(b)) for i := minLen; i > 0; i-- { if strings.HasSuffix(a, b[:i]) { return i } } return 0 } func estimateTokens(text string) int { // 粗略估计:中文约 1.5 字/token,英文约 0.75 词/token runeCount := utf8.RuneCountInString(text) wordCount := len(strings.Fields(text)) return int(float64(runeCount)*0.67 + float64(wordCount)*0.75) }

六、检索评估

// ragexp/retrieval_eval.go package ragexp import ( "math" "sort" ) // ─── 检索评估器 ─── // RetrievalEvaluator 检索评估器 type RetrievalEvaluator struct { dataset *EvalDataset } // NewRetrievalEvaluator 创建检索评估器 func NewRetrievalEvaluator(dataset *EvalDataset) *RetrievalEvaluator { return &RetrievalEvaluator{dataset: dataset} } // Evaluate 评估检索效果 func (re *RetrievalEvaluator) Evaluate( retriever func(query string, topK int) []RetrievedDoc, config RetrievalConfig, ) *RetrievalSummary { var evals []RetrievalEval for _, sample := range re.dataset.Samples { // 执行检索 docs := retriever(sample.Query, config.TopK) retrievedIDs := make([]string, len(docs)) for i, d := range docs { retrievedIDs[i] = d.ID } // 计算指标 eval := re.evaluateSample(sample, retrievedIDs, docs) evals = append(evals, eval) } return re.summarize(evals) } // evaluateSample 评估单个样本 func (re *RetrievalEvaluator) evaluateSample( sample EvalSample, retrievedIDs []string, docs []RetrievedDoc, ) RetrievalEval { relevantSet := make(map[string]bool) for _, docID := range sample.RelevantDocs { relevantSet[docID] = true } eval := RetrievalEval{ SampleID: sample.ID, Query: sample.Query, RetrievedDocs: retrievedIDs, } // 命中率 for _, id := range retrievedIDs { if relevantSet[id] { eval.HitRate = 1.0 break } } // 精确率和召回率 truePositives := 0 for _, id := range retrievedIDs { if relevantSet[id] { truePositives++ } } totalRelevant := len(sample.RelevantDocs) totalRetrieved := len(retrievedIDs) if totalRetrieved > 0 { eval.Precision = float64(truePositives) / float64(totalRetrieved) } if totalRelevant > 0 { eval.Recall = float64(truePositives) / float64(totalRelevant) } // MRR (Mean Reciprocal Rank) for rank, id := range retrievedIDs { if relevantSet[id] { eval.MRR = 1.0 / float64(rank+1) break } } // NDCG (Normalized Discounted Cumulative Gain) eval.NDCG = computeNDCG(retrievedIDs, relevantSet) return eval } // computeNDCG 计算 NDCG func computeNDCG(retrievedIDs []string, relevantSet map[string]bool) float64 { dcg := 0.0 idcg := 0.0 for i, id := range retrievedIDs { rel := 0.0 if relevantSet[id] { rel = 1.0 } if i == 0 { dcg += rel } else { dcg += rel / math.Log2(float64(i+1)) } } // 理想情况:所有相关文档都在最前面 numRelevant := len(relevantSet) for i := 0; i < numRelevant && i < len(retrievedIDs); i++ { if i == 0 { idcg += 1.0 } else { idcg += 1.0 / math.Log2(float64(i+1)) } } if idcg == 0 { return 0 } return dcg / idcg } // RetrievalSummary 检索评估汇总 type RetrievalSummary struct { TotalSamples int `json:"total_samples"` AvgHitRate float64 `json:"avg_hit_rate"` AvgPrecision float64 `json:"avg_precision"` AvgRecall float64 `json:"avg_recall"` AvgMRR float64 `json:"avg_mrr"` AvgNDCG float64 `json:"avg_ndcg"` PerQueryEvals []RetrievalEval `json:"per_query_evals"` } func (re *RetrievalEvaluator) summarize(evals []RetrievalEval) *RetrievalSummary { summary := &RetrievalSummary{ TotalSamples: len(evals), PerQueryEvals: evals, } if len(evals) == 0 { return summary } for _, e := range evals { summary.AvgHitRate += e.HitRate summary.AvgPrecision += e.Precision summary.AvgRecall += e.Recall summary.AvgMRR += e.MRR summary.AvgNDCG += e.NDCG } n := float64(len(evals)) summary.AvgHitRate /= n summary.AvgPrecision /= n summary.AvgRecall /= n summary.AvgMRR /= n summary.AvgNDCG /= n return summary } // RetrievedDoc 检索结果 type RetrievedDoc struct { ID string `json:"id"` Text string `json:"text"` Score float64 `json:"score"` Rank int `json:"rank"` }

七、端到端实验运行器

// ragexp/runner.go package ragexp import ( "context" "fmt" "sync" "time" ) // RAGExperimentRunner RAG 实验运行器 type RAGExperimentRunner struct { dataset *EvalDataset chunkers map[string]Chunker embedders map[string]Embedder retrievers map[string]Retriever llmClient LLMClient } // Embedder 向量化接口 type Embedder interface { Embed(texts []string) ([][]float32, error) } // Retriever 检索器接口 type Retriever interface { Retrieve(query string, topK int) ([]RetrievedDoc, error) } // LLMClient LLM 调用接口 type LLMClient interface { Generate(ctx context.Context, prompt string, context string) (string, error) } // RunExperiment 运行实验 func (r *RAGExperimentRunner) RunExperiment(variant RAGVariant) (*ExperimentResult, error) { result := &ExperimentResult{ VariantID: variant.ID, StartedAt: time.Now(), } var retrievalEvals []RetrievalEval var generationEvals []GenerationEval for _, sample := range r.dataset.Samples { // 1. 检索阶段 docs, err := r.retrievers[variant.RetrievalConfig.Method].Retrieve( sample.Query, variant.RetrievalConfig.TopK, ) if err != nil { continue } retrievedIDs := make([]string, len(docs)) for i, d := range docs { retrievedIDs[i] = d.ID } // 2. 构建上下文 context := buildContext(docs) // 3. 生成回答 answer, err := r.llmClient.Generate(context.Background(), sample.Query, context) if err != nil { continue } // 4. 评估检索 relevantSet := make(map[string]bool) for _, id := range sample.RelevantDocs { relevantSet[id] = true } retEval := evaluateRetrieval(sample, retrievedIDs, relevantSet) retrievalEvals = append(retrievalEvals, retEval) // 5. 评估生成 genEval := evaluateGeneration(sample, answer, context) generationEvals = append(generationEvals, genEval) } result.RetrievalEvals = retrievalEvals result.GenerationEvals = generationEvals result.CompletedAt = time.Now() return result, nil } // ExperimentResult 实验结果 type ExperimentResult struct { VariantID string `json:"variant_id"` StartedAt time.Time `json:"started_at"` CompletedAt time.Time `json:"completed_at"` RetrievalEvals []RetrievalEval `json:"retrieval_evals"` GenerationEvals []GenerationEval `json:"generation_evals"` } func buildContext(docs []RetrievedDoc) string { context := "" for i, doc := range docs { context += fmt.Sprintf("[文档 %d]\n%s\n\n", i+1, doc.Text) } return context } // ─── 实验对比 ─── // ExperimentComparator 实验对比器 type ExperimentComparator struct { results []*ExperimentResult } // ComparisonReport 对比报告 type ComparisonReport struct { Variants []VariantReport `json:"variants"` Winner string `json:"winner"` } type VariantReport struct { VariantID string `json:"variant_id"` AvgHitRate float64 `json:"avg_hit_rate"` AvgRecall float64 `json:"avg_recall"` AvgMRR float64 `json:"avg_mrr"` AvgROUGE1 float64 `json:"avg_rouge_1"` AvgFaithfulness float64 `json:"avg_faithfulness"` AvgLatencyMs float64 `json:"avg_latency_ms"` CompositeScore float64 `json:"composite_score"` } func (ec *ExperimentComparator) Compare() *ComparisonReport { report := &ComparisonReport{} bestScore := 0.0 bestVariant := "" for _, r := range ec.results { vr := VariantReport{VariantID: r.VariantID} // 汇总检索指标 for _, re := range r.RetrievalEvals { vr.AvgHitRate += re.HitRate vr.AvgRecall += re.Recall vr.AvgMRR += re.MRR } n := float64(len(r.RetrievalEvals)) if n > 0 { vr.AvgHitRate /= n vr.AvgRecall /= n vr.AvgMRR /= n } // 汇总生成指标 for _, ge := range r.GenerationEvals { vr.AvgROUGE1 += ge.ROUGE1 vr.AvgFaithfulness += ge.Faithfulness } if n > 0 { vr.AvgROUGE1 /= n vr.AvgFaithfulness /= n } // 综合评分(可自定义权重) vr.CompositeScore = vr.AvgHitRate*0.3 + vr.AvgRecall*0.2 + vr.AvgMRR*0.1 + vr.AvgROUGE1 * 0.2 + vr.AvgFaithfulness*0.2 report.Variants = append(report.Variants, vr) if vr.CompositeScore > bestScore { bestScore = vr.CompositeScore bestVariant = vr.VariantID } } report.Winner = bestVariant return report }

八、完整实验流程示例

// cmd/rag_experiment.go package main import ( "fmt" "ragexp" ) func main() { // 1. 准备评估数据集 dataset := &ragexp.EvalDataset{ ID: "qa_001", Name: "客服问答测试集", Samples: []ragexp.EvalSample{ { ID: "q1", Query: "如何退货?", RelevantDocs: []string{"doc_return_policy"}, GoldenAnswer: "您可以在收货后7天内申请退货...", }, { ID: "q2", Query: "密码忘记了怎么办?", RelevantDocs: []string{"doc_reset_password"}, GoldenAnswer: "点击登录页的'忘记密码'链接...", }, // ... 更多样本 }, } // 2. 定义实验变体 variants := []ragexp.RAGVariant{ { ID: "A", Name: "固定长度 512 + BM25 Top-5", ChunkConfig: ragexp.ChunkConfig{ Strategy: "fixed", ChunkSize: 512, Overlap: 50, }, RetrievalConfig: ragexp.RetrievalConfig{ Method: "bm25", TopK: 5, }, }, { ID: "B", Name: "语义切分 + 向量检索 Top-5", ChunkConfig: ragexp.ChunkConfig{ Strategy: "semantic", ChunkSize: 512, }, RetrievalConfig: ragexp.RetrievalConfig{ Method: "vector", TopK: 5, }, }, { ID: "C", Name: "语义切分 + 混合检索 Top-5 + 重排序", ChunkConfig: ragexp.ChunkConfig{ Strategy: "semantic", ChunkSize: 512, }, RetrievalConfig: ragexp.RetrievalConfig{ Method: "hybrid", TopK: 10, // 先检索更多 RerankModel: "bge-reranker", HybridWeight: 0.3, }, }, } // 3. 运行实验 runner := &ragexp.RAGExperimentRunner{ dataset: dataset, // 实际需要注入真实的 chunker/embedder/retriever/llm } var results []*ragexp.ExperimentResult for _, v := range variants { fmt.Printf("运行变体: %s (%s)\n", v.ID, v.Name) result, _ := runner.RunExperiment(v) results = append(results, result) } // 4. 对比结果 comparator := &ragexp.ExperimentComparator{results: results} report := comparator.Compare() fmt.Println("\n=== RAG 实验对比报告 ===\n") fmt.Printf("%-10s %-10s %-10s %-10s %-10s %-15s\n", "变体", "命中率", "召回率", "MRR", "ROUGE-1", "综合评分") for _, vr := range report.Variants { fmt.Printf("%-10s %-10.3f %-10.3f %-10.3f %-10.3f %-15.3f\n", vr.VariantID, vr.AvgHitRate, vr.AvgRecall, vr.AvgMRR, vr.AvgROUGE1, vr.CompositeScore) } fmt.Printf("\n🏆 优胜变体: %s\n", report.Winner) }

九、本讲坑位

#

坑位

现象

解决方案

1

评估数据集偏差​

测试集只覆盖特定领域,线上表现差

构建多样化测试集 + 定期更新

2

检索指标与用户体验脱节​

MRR 很高但用户不满意

加入端到端评估 + 用户反馈闭环

3

切片边界切断关键信息​

一段话被切成两半,检索不到完整语义

语义切分 + 适当 overlap + 边界检测

4

Top-K 过大导致上下文污染​

检索太多无关文档,LLM 被误导

先宽后窄 + 重排序 + 动态 Top-K

5

Embedding 模型漂移​

模型更新后向量分布变化,检索效果下降

固定 Embedding 版本 + 回归测试

6

混合检索权重难调​

BM25 和向量检索的权重凭感觉设

网格搜索 + 贝叶斯优化自动调参


十、本讲小结

┌─────────────────────────────────────────────────────────────┐ │ 第4讲 核心收获 │ │ │ │ ✅ 理解了 RAG 评估的三层金字塔(切片→检索→生成) │ │ ✅ 实现了三种切片策略(固定/语义/递归) │ │ ✅ 实现了切片质量评估(均匀性/边界质量/冗余率) │ │ ✅ 实现了检索评估指标(命中率/精确率/召回率/MRR/NDCG) │ │ ✅ 实现了端到端实验运行器 │ │ ✅ 实现了实验对比和优胜变体选择 │ │ ✅ 知道了 6 个常见坑位及其应对方案 │ │ │ │ 核心理念:先优化检索,再优化生成——检索是 RAG 的地基 │ └─────────────────────────────────────────────────────────────┘

下一讲预告:《模型实验——如何选择最适合你的 LLM》

从模型版本对比到参数调优,从延迟成本权衡到多模型路由策略,用数据说话而不是凭感觉选模型。


效率工具推荐:zz365.top
—— 在线代码运行、正则测试、JSON 格式化、时间戳转换、Base64 编解码等日常开发工具一站搞定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询