第2周:Go RAG系统 - 完整概览

周目标

从文档解析到检索排序,实现一套生产级的RAG系统,而不是Python notebook。

交付物: v0.2 - 完整RAG Pipeline

整体架构

完整RAG Pipeline

graph LR
    subgraph Ingestion["📥 Ingestion"]
        Docs["📄 Raw Documents<br/>MD, TXT, PDF"]
        Parse["📝 Parser"]
        Chunker["✂️ Chunker<br/>1000-2000 tokens"]
        Meta["🏷️ Add Metadata"]
        JSONL["💾 chunks.jsonl"]
        
        Docs -->|Read| Parse
        Parse -->|Split| Chunker
        Chunker -->|Tag| Meta
        Meta -->|Export| JSONL
    end
    
    subgraph Embedding["🔢 Embedding"]
        EmbClient["🤖 OpenAI API"]
        Batch["Batch Process<br/>避免rate limit"]
        VectorDB["🗄️ Qdrant<br/>Vector DB"]
        
        JSONL -->|Read| Batch
        Batch -->|Call| EmbClient
        EmbClient -->|Save| VectorDB
    end
    
    subgraph Retrieval["🔍 Retrieval"]
        Query["👤 User Query"]
        VecSearch["📊 Vector Search<br/>cosine similarity"]
        KeySearch["🔑 Keyword Search<br/>BM25 or contains"]
        Merge["🔗 Merge & Dedupe<br/>union results"]
        Score["⚖️ Normalize Score<br/>0-1"]
        
        Query -->|Embed| VecSearch
        Query -->|Term| KeySearch
        VecSearch -->|Top 10| Merge
        KeySearch -->|Top 10| Merge
        Merge -->|Combine| Score
    end
    
    subgraph Ranking["🎯 Ranking"]
        Rerank["🏆 Rerank<br/>Cross-encoder"]
        TopK["🔝 Select Top 5"]
        Check["✓ Permission Check<br/>RBAC"]
        Final["✨ Final Chunks"]
        
        Score -->|Top 20| Rerank
        Rerank -->|Rescore| TopK
        TopK -->|Filter| Check
        Check -->|Allowed| Final
    end
    
    subgraph Generation["💬 Generation"]
        Format["📐 Format Prompt<br/>+ chunks"]
        LLM["🧠 LLM Generate<br/>with citations"]
        Parse2["🎯 Parse Output<br/>extract citations"]
        Validate["✅ Validate Citations<br/>chunk exists?"]
        Answer["📝 Final Answer<br/>+ citations"]
        
        Final -->|Context| Format
        Format -->|Send| LLM
        LLM -->|Output| Parse2
        Parse2 -->|Check| Validate
        Validate -->|OK| Answer
    end
    
    style Ingestion fill:#fff4e1
    style Embedding fill:#e1f5ff
    style Retrieval fill:#e1ffe1
    style Ranking fill:#ffe1f5
    style Generation fill:#f5e1ff

Hybrid Retrieval 权重组合

graph LR
    Query["Query: 'How to reset SSO?'"]
    
    Query -->|Embed| Vector["Vector Search<br/>top 10 by cosine sim"]
    Query -->|Term| Keyword["Keyword Search<br/>match 'SSO'"]
    
    Vector -->|Results<br/>with scores| VScore["V-Scores<br/>sso_guide: 0.85<br/>auth_faq: 0.78<br/>identity: 0.72"]
    
    Keyword -->|Results<br/>with scores| KScore["K-Scores<br/>sso_guide: 1.0<br/>sso_setup: 0.9<br/>password_reset: 0.3"]
    
    VScore -->|Normalize| VNorm["Normalized<br/>0.42, 0.39, 0.36"]
    KScore -->|Normalize| KNorm["Normalized<br/>0.5, 0.45, 0.15"]
    
    VNorm -->|Weight 0.6| Combined["Hybrid Score<br/>= 0.6 * V + 0.4 * K"]
    KNorm -->|Weight 0.4| Combined
    
    Combined -->|Sort| Final["Final Ranking<br/>1. sso_guide: 0.468<br/>2. sso_setup: 0.39<br/>3. auth_faq: 0.273"]
    
    Final -->|Top 5| Rerank["Rerank<br/>更精准"]
    
    style Vector fill:#e1f5ff
    style Keyword fill:#e1ffe1
    style Combined fill:#ffe1f5
    style Final fill:#fff4e1

逐日进度

📌 Day 8: 文档解析与Chunking

你将学到:

  • 文档读取和解析(Markdown、TXT)
  • 智能分块(semantic vs fixed-size)
  • Metadata管理

关键代码:

// internal/rag/chunker.go
type Chunk struct {
	ID       string            `json:"id"`
	DocID    string            `json:"doc_id"`
	Title    string            `json:"title"`
	Section  string            `json:"section"`
	Text     string            `json:"text"`
	Source   string            `json:"source"`
	Metadata map[string]string `json:"metadata"`
}

type Chunker struct {
	chunkSize    int
	overlapSize  int
}

func (c *Chunker) ChunkDocument(doc *Document) []Chunk {
	// 1. 按\n\n分段落
	// 2. 如果段落太长,继续按sentence分
	// 3. 加metadata(来源、章节等)
	// 4. 生成ID(hash(content))
}

// 输出 chunks.jsonl
func WriteChunksToJSONL(path string, chunks []Chunk) error {
	// 每行一个Chunk的JSON
}

准备数据:

  • 10-20篇FAQ、Policy、Ticket文档
  • Markdown格式最佳

验证清单:

  • 能读取markdown文件
  • 能生成chunks.jsonl
  • 每个chunk有明确的ID和metadata
  • Chunk大小合理(1000-2000 tokens)

关键问题:

  1. 为什么要overlap?(连贯性)
  2. Metadata有什么用?(Day 10的权限过滤)
  3. 怎样生成稳定的ID?(用hash)

📌 Day 9: Embedding + Vector Store

你将学到:

  • Embedding API集成(OpenAI或其他)
  • Vector DB选择(pgvector vs Qdrant)
  • 批量embedding的效率

关键代码:

// internal/rag/embedding.go
type EmbeddingClient interface {
	Embed(ctx context.Context, texts []string) ([][]float32, error)
}

type EmbeddingRequest struct {
	Texts      []string
	Model      string
	RequestID  string
}

// internal/rag/vector_store.go
type VectorStore interface {
	Upsert(ctx context.Context, chunks []ChunkWithEmbedding) error
	Search(ctx context.Context, embedding []float32, topK int) ([]Chunk, error)
}

type PgvectorStore struct {
	db *sql.DB
}

func (vs *PgvectorStore) Search(ctx context.Context, embedding []float32, topK int) ([]Chunk, error) {
	// SELECT * FROM chunks 
	// ORDER BY embedding <-> $1 
	// LIMIT $2
}

数据流:

chunks.jsonl → 批量embedding(注意token限制) → 存入pgvector or Qdrant → 支持向量搜索

验证清单:

  • 能生成embeddings(用OpenAI或本地模型)
  • 批量处理避免rate limit
  • Vector DB可以搜索(余弦相似度)
  • 性能OK(<100ms)

关键问题:

  1. Embedding的维度是多少?(1536 for text-embedding-3-small)
  2. 怎样处理超长文本?(分批或截断)
  3. 检索多少个(topK)?(通常10-20)

📌 Day 10: Keyword Search + Hybrid Retrieval

你将学到:

  • 关键词搜索(BM25或simple contains)
  • Hybrid检索的merge策略
  • 得分归一化

关键代码:

// internal/rag/keyword_search.go
type KeywordSearcher interface {
	Search(ctx context.Context, query string, topK int) ([]Chunk, []float32, error)
}

// 简单实现:contains + TF-IDF
func (ks *SimpleKeywordSearcher) Search(ctx context.Context, query string, topK int) ([]Chunk, []float32, error) {
	// 1. 分词:query → ["word1", "word2"]
	// 2. 在chunks里搜索包含这些词的
	// 3. 计算相关度得分
	// 4. 排序、截断
}

// internal/rag/hybrid.go
type HybridRetriever struct {
	vectorSearcher   VectorSearcher
	keywordSearcher  KeywordSearcher
	vectorWeight     float32  // 0.6
	keywordWeight    float32  // 0.4
}

func (hr *HybridRetriever) Search(ctx context.Context, query string, topK int) ([]Chunk, error) {
	// 1. 向量搜索topK个
	vectorResults, vectorScores := hr.vectorSearcher.Search(...)
	
	// 2. 关键词搜索topK个
	keywordResults, keywordScores := hr.keywordSearcher.Search(...)
	
	// 3. 合并 + 去重
	merged := merge(vectorResults, keywordResults)
	
	// 4. 归一化得分
	for i, chunk := range merged {
		normalizedScore := 
			hr.vectorWeight * normalize(vectorScores[chunk.ID]) +
			hr.keywordWeight * normalize(keywordScores[chunk.ID])
		merged[i].Score = normalizedScore
	}
	
	// 5. 排序 + 返回top K
	sort.Slice(merged, func(i, j int) bool {
		return merged[i].Score > merged[j].Score
	})
	return merged[:topK], nil
}

为什么Hybrid?

问题:"如何重启SSO?" 向量检索:会找到"身份认证"相关的章节(语义相似) 关键词检索:会精确匹配"SSO"这个词(易被遗漏) Hybrid结果:两者结合,既有语义又有关键词精度

验证清单:

  • 关键词搜索能工作
  • Hybrid检索能合并两个结果集
  • 得分归一化合理(0-1范围)
  • 性能OK(<200ms for both)

关键问题:

  1. Vector weight为什么是0.6?(可调,业务决定)
  2. 怎样去重两个结果集?(用ID或哈希)
  3. 如果关键词搜索返回0个,怎么办?(看vector结果)

📌 Day 11: Rerank + Citations

你将学到:

  • Reranker的作用(从10个候选里选5个)
  • 引用的强制化(prompt里要求cite chunk_id)
  • Citation验证

关键代码:

// internal/rag/reranker.go
type Reranker interface {
	Rerank(ctx context.Context, query string, candidates []Chunk) ([]Chunk, error)
}

// 简单实现:用LLM的cross-encoder或相似度重新打分
type LLMReranker struct {
	llmClient LLMClient
}

func (r *LLMReranker) Rerank(ctx context.Context, query string, candidates []Chunk) ([]Chunk, error) {
	// 对每个candidate计算与query的相关度
	// 用一个轻量级模型(可以用embedding的cosine相似度)
	// 重新排序,返回top 5
}

// RAG答案结构
type RAGAnswer struct {
	Answer    string     `json:"answer"`
	Citations []Citation `json:"citations"`
	Confidence string    `json:"confidence"`
}

type Citation struct {
	Source  string `json:"source"`      // 文档来源
	ChunkID string `json:"chunk_id"`    // 具体chunk ID
	Text    string `json:"text,omitempty"` // 引用的原文
}

// 生成答案时强制加citation
func (r *RAGEngine) GenerateAnswerWithCitations(ctx context.Context, query string, topChunks []Chunk) (*RAGAnswer, error) {
	// 组织prompt:
	// "根据以下参考文档回答问题。必须在答案中使用[chunk_id]标记引用。"
	
	response, err := r.llmClient.Generate(ctx, GenerateRequest{
		Messages: []Message{
			{Role: "system", Content: systemPrompt},
			{Role: "user", Content: fmt.Sprintf("参考文档:\n%s\n\n问题:%s", formatChunks(topChunks), query)},
		},
	})
	
	// 从response里解析出[chunk_id]标记
	citations := extractCitations(response.Content)
	
	return &RAGAnswer{
		Answer:     response.Content,
		Citations:  citations,
		Confidence: evaluateConfidence(citations),
	}, nil
}

func evaluateConfidence(citations []Citation) string {
	if len(citations) == 0 {
		return "low"  // 没引用 → 低信心
	}
	if len(citations) >= 2 {
		return "high"  // 多来源 → 高信心
	}
	return "medium"
}

验证清单:

  • Reranker能缩小范围(10→5)
  • 生成的答案带citation标记
  • Citation能解析和验证
  • 没有citation时confidence自动降低

关键问题:

  1. 为什么要rerank?(10个候选太多,LLM容易混乱)
  2. Citation格式怎样设计?([chunk_123]或footnote?)
  3. 如果LLM忘了加citation怎么办?(Prompt强化或后处理)

📌 Day 12: RAG Eval Dataset

你将学到:

  • 定义评估指标
  • 构建eval dataset
  • 自动化测试RAG质量

关键代码:

// evals/dataset.jsonl
{
  "input": "How to reset SSO?",
  "expected_answer": "...",
  "expected_sources": ["sso_setup.md"],
  "must_cite": true,
  "should_call_tools": ["search_knowledge_base"],
  "expected_confidence": "high"
}

// internal/eval/evaluator.go
type EvalResult struct {
	AnswerCorrect      bool
	CitationAccurate   bool
	RetrievalHitRate   float32
	ToolCallAccurate   bool
	ConfidenceCorrect  bool
	Latency            time.Duration
}

func (e *Evaluator) Eval(ctx context.Context, input string, expected EvalCase) *EvalResult {
	// 1. 调RAG系统
	answer := e.ragEngine.Query(ctx, input)
	
	// 2. 检查答案正确性(可用LLM judge)
	answerCorrect := evaluateAnswer(answer.Answer, expected.ExpectedAnswer)
	
	// 3. 检查citation准确性
	citationAccurate := verifyCitations(answer.Citations, expected.ExpectedSources)
	
	// 4. 检查retrieval命中
	retrievalHit := len(answer.Citations) > 0
	
	// 5. 检查confidence是否合理
	confidenceCorrect := validateConfidence(answer.Confidence, citationAccurate)
	
	return &EvalResult{
		AnswerCorrect:     answerCorrect,
		CitationAccurate:  citationAccurate,
		RetrievalHitRate:  float32(hitCount) / float32(len(testCases)),
		ConfidenceCorrect: confidenceCorrect,
	}
}

指标定义:

1. Answer Correctness - 答案是否正确(用LLM judge) 2. Citation Accuracy - citation的chunk是否真的支持答案 3. Retrieval Hit Rate - 是否检索到了相关文档 4. Tool Call Accuracy - 是否调用了正确的工具 5. Refusal Accuracy - 不知道的问题是否正确拒绝 6. Latency - 平均耗时 7. Cost - token消耗

验证清单:

  • 有10-20个eval case
  • 每个case定义clear expected output
  • Evaluator能自动跑所有case
  • 生成eval report(pass/fail + metrics)

关键问题:

  1. 怎样定义"答案正确"?(用LLM judge、语义相似度、exact match?)
  2. Citation accuracy的门槛是什么?(100%还是90%?)
  3. 什么样的答案应该拒绝而不是瞎编?

📌 Day 13: Error Analysis

你将学到:

  • 系统地分析失败原因
  • 根据错误类型做针对性优化
  • 数据-驱动改进

关键代码:

// internal/eval/error_analysis.go
type ErrorCategory string

const (
	ErrorRetrievalMiss   ErrorCategory = "retrieval_miss"      // 没找到相关文档
	ErrorWrongRerank     ErrorCategory = "wrong_rerank"        // Rerank把好结果排下去了
	ErrorMissingCitation ErrorCategory = "missing_citation"    // 答案没引用
	ErrorHallucination   ErrorCategory = "hallucination"       // 编造内容
	ErrorWrongTool       ErrorCategory = "wrong_tool"          // 调错工具了
	ErrorUnsafeAction    ErrorCategory = "unsafe_action"       // Guardrail拦截
	ErrorSchemaFailure   ErrorCategory = "schema_failure"      // 输出格式不对
)

type ErrorAnalysis struct {
	Category ErrorCategory
	Symptom  string  // 表现
	RootCause string // 根本原因
	Mitigation string // 修复方案
}

func AnalyzeFailures(results []EvalResult) map[ErrorCategory]int {
	// 1. 分类每个失败
	categories := make(map[ErrorCategory]int)
	
	for _, result := range results {
		if result.AnswerCorrect {
			continue  // 成功,跳过
		}
		
		// 根据症状推断错误类别
		if result.RetrievalHitRate == 0 {
			categories[ErrorRetrievalMiss]++
		} else if !result.CitationAccurate {
			categories[ErrorWrongRerank]++
		} else {
			categories[ErrorHallucination]++
		}
	}
	
	return categories
}

// 例:retrieval_miss的修复
// → 调大embedding模型
// → 改chunking策略
// → 加keyword search权重
// → 补充更多FAQ

修复工作流:

Eval失败 → 分类错误 → 查看具体case → 识别根本原因 → 修改配置/prompt/数据 → 重跑eval → 检查metrics变化

验证清单:

  • 能分类失败的eval case
  • 每个Error都有symptom和root cause
  • 有优化方案清单
  • 修改后能看到metrics改进

关键问题:

  1. 怎样区分retrieval miss和hallucination?
  2. 如果很多错误都是"missing_citation",怎么办?(改prompt强制引用)
  3. 修改后怎样验证确实改进了?(对比baseline metrics)

📌 Day 14: RAG System Design Mock

系统设计题: Design a production RAG system in Go.

必须讲清的要点:

1. 需求澄清 - 知识库规模?(100 docs?1000?) - QPS要求?(10/s?100/s?) - 延迟SLA?(<200ms?) - 更新频率?(实时?日更?) 2. 数据Ingestion - 支持什么格式?(Markdown、PDF、HTML) - 增量还是全量更新? - 去重策略? 3. Chunking策略 - Fixed-size vs Semantic? - Chunk大小?(1000 tokens) - Overlap多少?(防止信息丢失) 4. Embedding - 用什么模型?(OpenAI、本地) - 批处理大小? - 缓存策略? - 更新频率? 5. Vector DB - PostgreSQL + pgvector还是Qdrant? - Index策略? - 多少replicas? 6. Hybrid检索 - 为什么需要?(语义+关键词) - 权重怎样调?(业务决定) - 性能影响? 7. Rerank - 用什么模型?(轻量级cross-encoder) - 候选数量?(10) - 最终返回多少?(5) 8. Citations - 怎样强制LLM引用?(系统prompt) - Citation验证?(确认chunk存在) - 如果LLM拒绝引用?(降低confidence或拒绝回答) 9. 可观测性 - Tracing(end-to-end latency) - Metrics(retrieval hit rate、citation rate) - Error tracking 10. 成本优化 - Embedding缓存?(相同query共用) - 模型选择?(用小模型做rerank) - Token压缩? 11. 扩展性 - 支持多知识库吗? - 支持权限过滤吗?(Day 19) - 支持热更新吗?

高分回答的关键:

  • ✅ 不是简单地"用OpenAI embedding + vector DB"
  • ✅ 要讲Hybrid的必要性和权重选择的依据
  • ✅ 要讲citation强制的prompt设计
  • ✅ 要讲eval dataset和metrics
  • ✅ 要讲故障恢复(embedding API超时、vector DB不可用)

📊 第2周学习成果检验

代码能力

  • 能实现chunker(semantic aware)
  • 能集成embedding API
  • 能设计vector store接口
  • 能实现hybrid retrieval
  • 能做rerank
  • 能强制citation

RAG理解

  • 理解embedding的局限(只看语义)
  • 理解关键词搜索的价值(精准性)
  • 理解citation的重要性(可信度)
  • 理解eval的方法论

Go特有

  • Batch处理的并发模式
  • 大数据集(chunks.jsonl)的I/O
  • Interface设计(EmbeddingClient、Reranker等)

🔗 Week 2代码关联

internal/rag/ ├── chunker.go (Day 8) ├── embedding.go (Day 9) ├── vector_store.go (Day 9) ├── keyword_search.go (Day 10) ├── hybrid.go (Day 10) ├── reranker.go (Day 11) ├── answer.go (Day 11) └── engine.go (汇总Day8-11) internal/eval/ ├── evaluator.go (Day 12) └── error_analysis.go (Day 13) evals/ └── dataset.jsonl (Day 12)

⏱ 推荐时间分配

日期 Task 时间
Day 8 Chunking实现 + 准备数据 3h
Day 9 Embedding + Vector DB 3h
Day 10 Keyword + Hybrid 3.5h
Day 11 Rerank + Citations 3h
Day 12 Eval Dataset + Evaluator 2.5h
Day 13 Error Analysis 2h
Day 14 系统设计题 + 整理 3h

周总计: 20小时


🎁 Week 2之后

  • 一个v0.2的RAG系统
  • 理解了information retrieval的细节
  • 有10-20个eval case验证质量
  • 能讲清"为什么不直接用embedding"

可以往resume里加:

Implemented a production RAG system with hybrid retrieval, reranking, and citation validation, achieving X% accuracy on custom eval dataset.

Week 2是整个项目的复杂度巅峰。Week 3会更多关于安全和工程化。

开始Day 8吧!🚀