Week 2 Day 13:Error Analysis - 苏格拉底教学

有了Day 12的eval数据,今天我们做侦探工作:系统为什么会答错?错在哪一步?怎么修?


第一部分:问题驱动

🤔 问题1:你的RAG系统答错了一个问题,你首先会做什么?

错误做法:

  • 立刻去调prompt,凭感觉修改
  • 换一个更贵的模型
  • 加更多文档进去

正确做法: 先诊断!搞清楚哪一步出了问题,才能对症下药。

引导问题:

  1. 答案错了,是因为检索到了错误的chunks?还是检索对了但LLM理解错了?
  2. 你怎么区分这两种情况?
  3. 如果同一类问题都答错,这意味着什么?

🤔 问题2:RAG的失败可以分成哪几类?

一个RAG请求经历了以下步骤:

用户问题 → 向量检索 → [Rerank] → 构造Prompt → LLM生成 → 输出答案

每一步都可能失败:

1. Retrieval Fail(检索失败) - 检索回来的chunks根本没有相关内容 - 正确的chunk存在但排名太靠后(K=5时排第10) - 问题表述和文档表述差异太大(词汇鸿沟) 2. Reasoning Fail(推理失败) - 检索到了正确chunks,但LLM没能正确理解 - LLM把多个chunks的内容混淆了 - 问题需要多步推理,LLM只做了一步 3. Output Fail(输出失败) - 答案内容正确但格式不对 - 答案过长或过短 - 引用了检索结果之外的知识(幻觉)

判断方法:

  • 如果 Precision@5 低 → Retrieval Fail
  • 如果 Precision@5 高但 LLMScore 低 → Reasoning Fail 或 Output Fail
  • 用LLM单独评估格式 → 区分 Reasoning 和 Output

🤔 问题3:找到问题根因之后,怎么决定优先修哪个?

引导问题:

  1. Retrieval Fail 影响了60%的case,Reasoning Fail 影响了20%,先修哪个?
  2. 改chunk size是个破坏性改动,有没有风险更低的方案?
  3. 修了之后怎么验证是真的变好了?

答案揭示:

  • 优先修影响范围最大的(通常是Retrieval)
  • 先尝试低风险改动(prompt优化、调K值)
  • 每次改动后重跑eval,用数据说话

第二部分:Failure Mode分类系统

完整的错误分类框架

Failure Mode ├── RETRIEVAL_FAIL │ ├── NO_RELEVANT_CHUNK // 文档里根本没有答案 │ ├── CHUNK_RANKED_LOW // 正确chunk在第K名之后 │ └── VOCABULARY_MISMATCH // 用户用词和文档用词不同 │ ├── REASONING_FAIL │ ├── MULTI_HOP_REQUIRED // 需要多步推理,LLM做不到 │ ├── CONTEXT_CONFUSION // 混淆了多个chunks的内容 │ └── HALLUCINATION // LLM编造了chunks里没有的内容 │ └── OUTPUT_FAIL ├── FORMAT_WRONG // 格式不符合要求 ├── INCOMPLETE // 答案不完整 └── TOO_VERBOSE // 答案过长,包含无关信息

第三部分:动手实现 - 错误分析工具

第一步:自动分类Failure Mode

// evals/error_analyzer.go
package main

import (
	"context"
	"encoding/json"
	"fmt"
)

// FailureMode 错误类型
type FailureMode string

const (
	FailureNone             FailureMode = "none"
	FailureNoRelevantChunk  FailureMode = "retrieval_no_relevant_chunk"
	FailureChunkRankedLow   FailureMode = "retrieval_chunk_ranked_low"
	FailureVocabMismatch    FailureMode = "retrieval_vocab_mismatch"
	FailureMultiHop         FailureMode = "reasoning_multi_hop"
	FailureContextConfusion FailureMode = "reasoning_context_confusion"
	FailureHallucination    FailureMode = "reasoning_hallucination"
	FailureFormatWrong      FailureMode = "output_format_wrong"
	FailureIncomplete       FailureMode = "output_incomplete"
	FailureTooVerbose       FailureMode = "output_too_verbose"
)

// ErrorAnalysis 单个case的错误分析
type ErrorAnalysis struct {
	EvalResult
	PrimaryFailure   FailureMode   `json:"primary_failure"`
	SecondaryFailures []FailureMode `json:"secondary_failures"`
	RootCause        string        `json:"root_cause"`
	SuggestedFix     string        `json:"suggested_fix"`
}

// ErrorAnalyzer 错误分析器
type ErrorAnalyzer struct {
	llmClient LLMClient
	k         int
}

func NewErrorAnalyzer(llmClient LLMClient, k int) *ErrorAnalyzer {
	return &ErrorAnalyzer{llmClient: llmClient, k: k}
}

// Analyze 分析单个eval result的失败原因
func (a *ErrorAnalyzer) Analyze(ctx context.Context, result EvalResult) ErrorAnalysis {
	analysis := ErrorAnalysis{EvalResult: result}

	// Step 1: 判断是否是retrieval fail
	if result.PrecisionAt5 < 0.4 {
		// 检索质量差
		if result.RecallAt5 == 0 {
			// 根本没找到任何相关chunk
			analysis.PrimaryFailure = FailureNoRelevantChunk
			analysis.RootCause = "所有相关chunk都不在Top-K结果中"
			analysis.SuggestedFix = "检查chunk是否包含答案,考虑重新chunking或调大K值"
		} else {
			// 找到了部分,但排名靠后
			analysis.PrimaryFailure = FailureChunkRankedLow
			analysis.RootCause = fmt.Sprintf("相关chunk存在但MRR=%.3f,说明排名不够高", result.MRR)
			analysis.SuggestedFix = "考虑调整embedding模型或加入hybrid search"
		}
		return analysis
	}

	// Step 2: 检索还行,但答案质量差 → 推理或输出问题
	if result.PrecisionAt5 >= 0.4 && result.LLMScore < 3.0 {
		failure := a.classifyReasoningOrOutput(ctx, result)
		analysis.PrimaryFailure = failure
		analysis.RootCause = a.getRootCause(failure, result)
		analysis.SuggestedFix = a.getSuggestedFix(failure)
		return analysis
	}

	// Step 3: 没有明显问题
	analysis.PrimaryFailure = FailureNone
	return analysis
}

// classifyReasoningOrOutput 区分推理失败和输出失败
func (a *ErrorAnalyzer) classifyReasoningOrOutput(ctx context.Context, result EvalResult) FailureMode {
	prompt := fmt.Sprintf(`分析以下RAG系统的失败原因。

问题:%s
检索到的chunk IDs:%v
生成的答案:%s
参考答案:%s

问题是以下哪一类?
1. reasoning_hallucination:答案包含chunks中没有的内容
2. reasoning_multi_hop:需要多步推理,答案只回答了一部分
3. reasoning_context_confusion:混淆了多个文档的内容
4. output_incomplete:答案正确但不完整
5. output_too_verbose:答案过长,包含大量无关内容
6. output_format_wrong:内容正确但格式不对

只返回JSON:{"failure_mode": "上面的类别名", "explanation": "简短解释"}`,
		result.Query,
		result.RetrievedIDs,
		result.GeneratedAnswer,
		result.ExpectedAnswer,
	)

	resp, err := a.llmClient.Complete(ctx, prompt)
	if err != nil {
		return FailureReasoningHallucination() // 默认归类
	}

	var r struct {
		FailureMode string `json:"failure_mode"`
		Explanation string `json:"explanation"`
	}
	if err := json.Unmarshal([]byte(resp), &r); err != nil {
		return FailureHallucination
	}

	return FailureMode(r.FailureMode)
}

// FailureReasoningHallucination helper
func FailureReasoningHallucination() FailureMode { return FailureHallucination }

func (a *ErrorAnalyzer) getRootCause(f FailureMode, result EvalResult) string {
	switch f {
	case FailureHallucination:
		return "LLM生成了训练数据中的知识,而非基于检索结果"
	case FailureMultiHop:
		return "问题需要整合多个chunks的信息,LLM只处理了部分"
	case FailureContextConfusion:
		return "Prompt中多个chunks内容相似,LLM混淆了来源"
	case FailureIncomplete:
		return "Prompt可能缺少"请提供完整答案"的指令"
	case FailureTooVerbose:
		return "Prompt没有约束输出长度"
	default:
		return "未知原因"
	}
}

func (a *ErrorAnalyzer) getSuggestedFix(f FailureMode) string {
	switch f {
	case FailureHallucination:
		return "在prompt中强调:只使用提供的文档内容,不要使用外部知识"
	case FailureMultiHop:
		return "改用chain-of-thought prompt,或拆分问题为子问题"
	case FailureContextConfusion:
		return "在每个chunk前加上来源标识符,prompt中说明区分来源"
	case FailureIncomplete:
		return "prompt中加入:请提供完整答案,涵盖所有要点"
	case FailureTooVerbose:
		return "prompt中加入字数限制或格式要求"
	default:
		return "人工审查此case"
	}
}

第二步:批量分析和统计

// evals/error_report.go
package main

import (
	"context"
	"fmt"
	"sort"
)

// FailureBreakdown 错误分布
type FailureBreakdown struct {
	Mode        FailureMode `json:"mode"`
	Count       int         `json:"count"`
	Percentage  float64     `json:"percentage"`
	AvgLLMScore float64     `json:"avg_llm_score"`
	Examples    []string    `json:"examples"` // 前3个例子的query
}

// ErrorReport 完整错误报告
type ErrorReport struct {
	TotalAnalyzed  int                `json:"total_analyzed"`
	FailingCases   int                `json:"failing_cases"`
	FailRate       float64            `json:"fail_rate"`
	Breakdown      []FailureBreakdown `json:"breakdown"`
	TopIssues      []string           `json:"top_issues"`
	Recommendations []string          `json:"recommendations"`
}

// AnalyzeAll 批量分析所有失败的cases
func AnalyzeAll(ctx context.Context, results []EvalResult, analyzer *ErrorAnalyzer) *ErrorReport {
	report := &ErrorReport{TotalAnalyzed: len(results)}

	var analyses []ErrorAnalysis
	modeMap := make(map[FailureMode][]ErrorAnalysis)

	for _, result := range results {
		// 只分析低分的case(LLMScore < 3.5或Precision < 0.4)
		if result.LLMScore >= 3.5 && result.PrecisionAt5 >= 0.4 {
			continue
		}
		analysis := analyzer.Analyze(ctx, result)
		analyses = append(analyses, analysis)
		modeMap[analysis.PrimaryFailure] = append(modeMap[analysis.PrimaryFailure], analysis)
		report.FailingCases++
	}

	report.FailRate = float64(report.FailingCases) / float64(len(results))

	// 计算breakdown
	for mode, list := range modeMap {
		var totalScore float64
		var examples []string
		for i, a := range list {
			totalScore += a.LLMScore
			if i < 3 {
				examples = append(examples, a.Query)
			}
		}
		report.Breakdown = append(report.Breakdown, FailureBreakdown{
			Mode:        mode,
			Count:       len(list),
			Percentage:  float64(len(list)) / float64(report.FailingCases) * 100,
			AvgLLMScore: totalScore / float64(len(list)),
			Examples:    examples,
		})
	}

	// 按数量降序排列
	sort.Slice(report.Breakdown, func(i, j int) bool {
		return report.Breakdown[i].Count > report.Breakdown[j].Count
	})

	// 生成TopIssues和Recommendations
	report.TopIssues = generateTopIssues(report.Breakdown)
	report.Recommendations = generateRecommendations(report.Breakdown)

	return report
}

func generateTopIssues(breakdown []FailureBreakdown) []string {
	var issues []string
	for i, b := range breakdown {
		if i >= 3 {
			break
		}
		issues = append(issues, fmt.Sprintf(
			"%s:%d个case(%.1f%%),平均LLM评分%.2f",
			b.Mode, b.Count, b.Percentage, b.AvgLLMScore,
		))
	}
	return issues
}

func generateRecommendations(breakdown []FailureBreakdown) []string {
	var recs []string
	for _, b := range breakdown {
		switch b.Mode {
		case FailureNoRelevantChunk:
			recs = append(recs, "优先级:HIGH - 扩充文档库或检查是否有内容缺失")
		case FailureChunkRankedLow:
			recs = append(recs, "优先级:HIGH - 尝试hybrid search或更好的embedding模型")
		case FailureHallucination:
			recs = append(recs, "优先级:MEDIUM - 强化prompt中的grounding指令")
		case FailureIncomplete:
			recs = append(recs, "优先级:LOW - 优化prompt中的输出格式要求")
		}
	}
	return recs
}

// PrintReport 打印分析报告
func PrintReport(report *ErrorReport) {
	fmt.Println("\n========== 错误分析报告 ==========")
	fmt.Printf("分析总数:%d\n", report.TotalAnalyzed)
	fmt.Printf("失败案例:%d(%.1f%%)\n", report.FailingCases, report.FailRate*100)

	fmt.Println("\n---------- 错误分布 ----------")
	for _, b := range report.Breakdown {
		fmt.Printf("  %-40s %3d个  %.1f%%  LLM均分=%.2f\n",
			b.Mode, b.Count, b.Percentage, b.AvgLLMScore)
		for _, ex := range b.Examples {
			fmt.Printf("    - %s\n", ex)
		}
	}

	fmt.Println("\n---------- 主要问题 ----------")
	for i, issue := range report.TopIssues {
		fmt.Printf("  %d. %s\n", i+1, issue)
	}

	fmt.Println("\n---------- 改进建议 ----------")
	for _, rec := range report.Recommendations {
		fmt.Printf("  * %s\n", rec)
	}
}

第三步:改进策略实施

// 改进策略1:调整Prompt减少幻觉
func buildGroundedPrompt(query string, chunks []string) string {
	context := ""
	for i, c := range chunks {
		context += fmt.Sprintf("[文档%d]\n%s\n\n", i+1, c)
	}

	return fmt.Sprintf(`你是一个严格基于文档的问答助手。

重要规则:
1. 只使用以下文档中的信息回答问题
2. 如果文档中没有相关信息,回答"根据现有文档,无法回答此问题"
3. 不要使用文档之外的任何知识
4. 回答简洁,不超过200字

参考文档:
%s

用户问题:%s

回答:`, context, query)
}

// 改进策略2:检测词汇鸿沟
func detectVocabMismatch(query string, chunkTexts []string) bool {
	// 简化实现:检查query中的关键词是否在chunks中出现
	queryWords := tokenize(query)
	allChunkText := ""
	for _, ct := range chunkTexts {
		allChunkText += ct + " "
	}

	matchCount := 0
	for _, word := range queryWords {
		if len(word) > 2 && contains(allChunkText, word) {
			matchCount++
		}
	}

	// 如果少于30%的关键词匹配,可能有词汇鸿沟
	return float64(matchCount)/float64(len(queryWords)) < 0.3
}

func tokenize(text string) []string {
	// 简化:按空格分词
	words := []string{}
	current := ""
	for _, ch := range text {
		if ch == ' ' || ch == ',' || ch == '。' || ch == '?' {
			if current != "" {
				words = append(words, current)
				current = ""
			}
		} else {
			current += string(ch)
		}
	}
	if current != "" {
		words = append(words, current)
	}
	return words
}

func contains(text, word string) bool {
	return len(text) > 0 && len(word) > 0 &&
		// 简单子字符串匹配
		(func() bool {
			for i := 0; i <= len(text)-len(word); i++ {
				if text[i:i+len(word)] == word {
					return true
				}
			}
			return false
		})()
}

// 改进策略3:Hybrid Search(向量+关键词)
type HybridSearchConfig struct {
	VectorWeight  float64 // 向量检索权重
	KeywordWeight float64 // 关键词检索权重
	K             int
}

// RRF (Reciprocal Rank Fusion) 合并两个排名列表
func RRF(vectorRanks, keywordRanks []string, k int) []string {
	scores := make(map[string]float64)

	for i, id := range vectorRanks {
		scores[id] += 1.0 / float64(60+i+1) // RRF标准公式
	}
	for i, id := range keywordRanks {
		scores[id] += 1.0 / float64(60+i+1)
	}

	// 按分数排序
	type kv struct {
		id    string
		score float64
	}
	var ranked []kv
	for id, score := range scores {
		ranked = append(ranked, kv{id, score})
	}
	sort.Slice(ranked, func(i, j int) bool {
		return ranked[i].score > ranked[j].score
	})

	result := make([]string, 0, k)
	for _, item := range ranked {
		if len(result) >= k {
			break
		}
		result = append(result, item.id)
	}
	return result
}

第四步:混淆矩阵可视化

// evals/confusion_matrix.go
package main

import "fmt"

// ConfusionMatrix 二分类混淆矩阵(相关/不相关)
type ConfusionMatrix struct {
	TP int // True Positive:检索到且相关
	FP int // False Positive:检索到但不相关
	TN int // True Negative:未检索到且确实不相关
	FN int // False Negative:未检索到但应该相关
}

func BuildConfusionMatrix(results []EvalResult, k int) ConfusionMatrix {
	var cm ConfusionMatrix
	for _, r := range results {
		expectedSet := make(map[string]bool)
		for _, id := range r.ExpectedIDs {
			expectedSet[id] = true
		}

		retrievedSet := make(map[string]bool)
		top := r.RetrievedIDs
		if len(top) > k {
			top = r.RetrievedIDs[:k]
		}
		for _, id := range top {
			retrievedSet[id] = true
		}

		for id := range retrievedSet {
			if expectedSet[id] {
				cm.TP++
			} else {
				cm.FP++
			}
		}
		for id := range expectedSet {
			if !retrievedSet[id] {
				cm.FN++
			}
		}
	}
	return cm
}

func (cm ConfusionMatrix) Precision() float64 {
	if cm.TP+cm.FP == 0 {
		return 0
	}
	return float64(cm.TP) / float64(cm.TP+cm.FP)
}

func (cm ConfusionMatrix) Recall() float64 {
	if cm.TP+cm.FN == 0 {
		return 0
	}
	return float64(cm.TP) / float64(cm.TP+cm.FN)
}

func (cm ConfusionMatrix) F1() float64 {
	p, r := cm.Precision(), cm.Recall()
	if p+r == 0 {
		return 0
	}
	return 2 * p * r / (p + r)
}

func (cm ConfusionMatrix) Print() {
	fmt.Println("\n========== 混淆矩阵 ==========")
	fmt.Println("              预测相关    预测不相关")
	fmt.Printf("  实际相关:   TP=%-5d    FN=%-5d\n", cm.TP, cm.FN)
	fmt.Printf("  实际不相关: FP=%-5d    TN=%-5d\n", cm.FP, cm.TN)
	fmt.Println()
	fmt.Printf("  Precision = %.3f\n", cm.Precision())
	fmt.Printf("  Recall    = %.3f\n", cm.Recall())
	fmt.Printf("  F1 Score  = %.3f\n", cm.F1())
}

第四部分:常见问题根因速查表

现象 指标 根因 优先解法
根本找不到答案 Recall@5 = 0 文档缺失或chunk太大跨越了答案 补充文档;减小chunk size
找到了但排名靠后 Recall高/Precision低 Embedding不准确;查询和文档词汇差异大 hybrid search;query rewrite
检索OK但答案错 Precision高/LLMScore低 LLM幻觉;prompt不够strict 强化grounding prompt
答案正确但不完整 LLMScore=3 答案被截断;prompt未强调完整性 调max_tokens;改prompt
延迟超过5秒 LatencyMs > 5000 LLM调用慢;没有缓存 加缓存;用更小的模型
特定类别差 ByCategory分数低 该类别文档不够;embedding对该类型弱 补充该类别文档

第五部分:算法题

Daily Temperatures(单调栈经典题)

面试场景: 处理时序eval数据时,需要找到每次下次出现更高分数的位置

问题: 给定每天的气温,返回每天到下一个更暖和天的等待天数

// 单调栈解法 O(n)
func dailyTemperatures(temps []int) []int {
	n := len(temps)
	result := make([]int, n)
	stack := []int{} // 存索引,维护单调递减栈

	for i := 0; i < n; i++ {
		// 当前温度比栈顶更高,弹出并计算距离
		for len(stack) > 0 && temps[i] > temps[stack[len(stack)-1]] {
			idx := stack[len(stack)-1]
			stack = stack[:len(stack)-1]
			result[idx] = i - idx
		}
		stack = append(stack, i)
	}
	// 栈中剩余的索引,等待天数为0(默认值)
	return result
}

/*
思路:
- 单调栈维护"还在等待更高温度"的日期索引
- 遍历时,如果今天比栈顶更暖,就更新栈顶的答案
- 每个元素最多进栈出栈一次 → O(n)

例子:[73, 74, 75, 71, 69, 72, 76, 73]
答案:[1,   1,  4,  2,  1,  1,  0,  0]
*/

Min Stack(辅助栈)

面试场景: 实现一个能O(1)获取最小值的栈,用于追踪eval过程中的最低分数

问题: 实现MinStack,支持Push、Pop、Top、GetMin,全部O(1)

type MinStack struct {
	stack    []int
	minStack []int // 辅助栈,存每个状态下的最小值
}

func NewMinStack() *MinStack {
	return &MinStack{}
}

func (s *MinStack) Push(val int) {
	s.stack = append(s.stack, val)
	// 辅助栈:如果新值更小(或辅助栈为空),压入新值;否则压入当前最小值
	if len(s.minStack) == 0 || val <= s.minStack[len(s.minStack)-1] {
		s.minStack = append(s.minStack, val)
	} else {
		s.minStack = append(s.minStack, s.minStack[len(s.minStack)-1])
	}
}

func (s *MinStack) Pop() {
	if len(s.stack) == 0 {
		return
	}
	s.stack = s.stack[:len(s.stack)-1]
	s.minStack = s.minStack[:len(s.minStack)-1]
}

func (s *MinStack) Top() int {
	return s.stack[len(s.stack)-1]
}

func (s *MinStack) GetMin() int {
	return s.minStack[len(s.minStack)-1]
}

/*
关键思路:
- 辅助栈和主栈保持同步(同时push/pop)
- 辅助栈每次存入"到目前为止的最小值"
- 所以GetMin()直接看辅助栈顶 → O(1)

面试追问:如果同一个最小值push了很多次,辅助栈会膨胀吗?
答:是的,但空间是O(n),可以接受。
优化:只在新值<=当前最小值时才压辅助栈,pop时只有辅助栈顶=弹出值才同步pop。
*/

第六部分:自测清单

  • 能说出三种failure mode各自的诊断方法吗?
  • Precision@5高但LLMScore低,说明问题在哪一步?
  • 词汇鸿沟(Vocabulary Mismatch)如何检测和解决?
  • RRF算法的核心公式是什么?
  • 混淆矩阵的TP/FP/FN分别是什么?
  • 改进一个系统前,应该先做什么?(先量化现状!)
  • Daily Temperatures为什么用单调栈而不是暴力O(n^2)?
  • Min Stack的辅助栈策略是什么?

第七部分:实战作业

任务1:分类你的失败案例

  • 用Day 12的eval results,找出所有LLMScore < 3的case
  • 手动分类:每个case属于哪种failure mode?
  • 做一个简单统计:哪种failure mode最多?

任务2:定位最大问题

  • 如果Retrieval Fail最多:调整K值(从5到10),重跑eval看变化
  • 如果Reasoning Fail最多:修改prompt,加入强grounding指令,重跑eval
  • 记录:改了什么 → 哪个指标变化了 → 变化了多少

任务3:实现混淆矩阵

  • 完成 evals/confusion_matrix.go
  • 对你的eval results构建混淆矩阵并打印
  • 实现 RRF 函数(已提供模板)
  • 对比pure vector和hybrid的Precision@5差异

第八部分:常见问题

Q: 我的Precision@5很高(0.8),但LLMScore只有2.5,这怎么回事?

A: 典型的Reasoning Fail。说明检索做得不错,但LLM没能正确利用这些chunks。排查步骤:

  1. 打印几个低分case的实际生成答案
  2. 看答案是否包含了chunks里没有的内容(幻觉)
  3. 还是答案正确但不完整(output_incomplete)
  4. 针对性修改prompt

Q: 有些query本来就很难,任何系统都答不对,要怎么处理?

A: 这类"impossible queries"需要单独标记:

{"query": "...", "difficulty": "impossible", "reason": "知识库中无此信息"}

在计算metrics时可以过滤掉或单独统计,避免它们拉低整体分数。

Q: 改了chunk size之后,chunk ID全变了,原来的eval dataset就失效了?

A: 是的,这是个常见陷阱。解决方案:

  1. 用内容哈希作为chunk ID(chunk内容不变则ID不变)
  2. 在eval中用语义匹配而非ID精确匹配(判断retrieved chunk是否包含expected answer)
  3. 重新生成eval dataset(破坏性改动后不得不做的事)

第九部分:下一步预告

明天是Week 2最后一天(Day 14),我们会:

  1. 画出完整RAG系统架构图
  2. 做规模估算(1000万文档、每天1万查询)
  3. 做成本估算(embedding / LLM / storage)
  4. 练习面试场景:如何2分钟pitch一个RAG系统设计

准备问题:

  • 你们现在的系统能支持多大规模?
  • 1000万文档存向量,需要多少磁盘空间?
  • 每天1万次RAG查询,LLM成本大概多少?