RAG 文档问答系统 — 项目设计
设计目标
构建一个可量化评估的企业级 RAG 系统,重点解决三个核心问题:
- 检索质量:如何从海量文档中精准找到相关内容(Recall + Precision)
- 生成忠诚度:如何确保 LLM 基于检索结果回答,而非编造(Faithfulness)
- 工程可靠性:如何在生产环境下稳定、低延迟运行
系统架构
┌─────────────────────────── 离线索引管线 ─────────────────────────────┐
│ │
│ 文档源 ─→ Loader ─→ Splitter ─→ Embedding API ─→ VectorStore │
│ (PDF/MD) (解析) (分块) (向量化) (存储+索引) │
│ │ │
│ └─→ BM25 Index (倒排索引) │
│ │
└────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────── 在线查询管线 ─────────────────────────────┐
│ │
│ Query ─→ Query Rewrite ─→ ┌─ Vector Search (Top-20) ─┐ │
│ (Multi-Query/ │ │ RRF │
│ Decomposition) └─ BM25 Search (Top-20) ──┘ Fusion │
│ │ │
│ ▼ │
│ Reranker (Top-5) │
│ │ │
│ ▼ │
│ Prompt Assembly + LLM │
│ │ │
│ ▼ │
│ Citation + Hallucination Check │
│ │ │
│ ▼ │
│ Response (Stream) │
│ │
└────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────── 评估管线 ─────────────────────────────────┐
│ │
│ TestSet(Q+A+Source) ─→ Pipeline ─→ Metrics(Recall, MRR, Faith.) │
│ │
└────────────────────────────────────────────────────────────────────────┘核心接口定义
Loader — 文档加载器
go
type Document struct {
ID string
Content string
Metadata Metadata
}
type Metadata struct {
Source string // 文件路径
Title string // 文档标题
Page int // 页码(PDF)
Section string // 章节标题(Markdown)
}
type Loader interface {
Load(ctx context.Context, path string) ([]Document, error)
SupportedExtensions() []string
}Splitter — 文档分块器
go
type Chunk struct {
ID string
Content string
Metadata Metadata
Index int // 在原文档中的顺序
}
type SplitterConfig struct {
ChunkSize int // 目标 chunk 大小(字符数)
ChunkOverlap int // overlap 大小
Separators []string // 分隔符优先级
}
type Splitter interface {
Split(doc Document) []Chunk
}Embedding — 向量化
go
type EmbeddingClient interface {
Embed(ctx context.Context, text string) ([]float64, error)
EmbedBatch(ctx context.Context, texts []string) ([][]float64, error)
Dimension() int
}VectorStore — 向量存储
go
type SearchResult struct {
Chunk Chunk
Score float64 // 相似度分数
SearchType string // "vector" | "bm25" | "hybrid"
}
type VectorStore interface {
Upsert(ctx context.Context, chunks []Chunk, embeddings [][]float64) error
Search(ctx context.Context, query []float64, topK int, threshold float64) ([]SearchResult, error)
Delete(ctx context.Context, filter map[string]string) error
}
type BM25Index interface {
Index(ctx context.Context, chunks []Chunk) error
Search(ctx context.Context, query string, topK int) ([]SearchResult, error)
}Retriever — 检索器
go
type RetrieverConfig struct {
TopK int
ScoreThreshold float64
UseRerank bool
UseMultiQuery bool
UseHybrid bool
}
type Retriever interface {
Retrieve(ctx context.Context, query string, cfg RetrieverConfig) ([]SearchResult, error)
}Generator — 生成器
go
type Answer struct {
Content string
Citations []Citation
Confidence float64
TokenUsage TokenUsage
}
type Citation struct {
ChunkID string
Source string
Section string
Score float64
}
type Generator interface {
Generate(ctx context.Context, query string, contexts []SearchResult) (*Answer, error)
GenerateStream(ctx context.Context, query string, contexts []SearchResult, onChunk func(string)) error
}Evaluator — 评估器
go
type EvalResult struct {
RecallAtK float64 // 检索召回率
MRR float64 // 平均倒数排名
Faithfulness float64 // 生成忠诚度(答案是否基于检索结果)
Relevance float64 // 答案与问题的相关性
}
type TestCase struct {
Question string
ExpectedAnswer string
ExpectedSources []string // 期望召回的文档来源
}
type Evaluator interface {
Evaluate(ctx context.Context, testCases []TestCase) (*EvalResult, error)
}关键算法设计
1. RRF(Reciprocal Rank Fusion)混合检索融合
go
// RRF 融合算法
// 将多路检索结果按统一分数排序
// 公式:RRF(d) = Σ 1/(k + rank_i(d)),k=60
func RRFMerge(results ...[]SearchResult) []SearchResult {
scores := make(map[string]float64) // chunkID -> 累计分数
const k = 60
for _, resultList := range results {
for rank, r := range resultList {
scores[r.Chunk.ID] += 1.0 / float64(k+rank+1)
}
}
// 按累计分数降序排列
// ...
}2. Multi-Query 查询改写
go
// 用 LLM 将原始查询改写为多个不同角度的查询
// 每个查询并行检索,结果 RRF 融合
func (r *MultiQueryRetriever) Retrieve(ctx context.Context, query string) ([]SearchResult, error) {
// 1. LLM 生成 3-4 个改写查询
queries := r.rewriteQuery(ctx, query)
// 2. 并行检索(goroutine + errgroup)
var allResults [][]SearchResult
g, gctx := errgroup.WithContext(ctx)
for _, q := range queries {
q := q
g.Go(func() error {
res, err := r.baseRetriever.Retrieve(gctx, q)
// ...
})
}
// 3. RRF 融合
return RRFMerge(allResults...), nil
}3. 幻觉检测 — 引用溯源
go
// 答案中的每个关键断言都必须有检索结果支撑
// 通过 NLI (Natural Language Inference) 思路判断
func (g *CitationGenerator) VerifyFaithfulness(answer string, contexts []SearchResult) float64 {
// 1. 将答案拆分为独立断言(claim)
claims := g.extractClaims(answer)
// 2. 对每个断言,检查是否被某个 context 支持
supported := 0
for _, claim := range claims {
for _, ctx := range contexts {
if g.isSupported(claim, ctx.Chunk.Content) {
supported++
break
}
}
}
// 3. Faithfulness = supported / total
return float64(supported) / float64(len(claims))
}查询处理流水线
用户输入 "RAG 系统怎么处理幻觉?"
│
▼ [Query Understanding]
判断查询类型:事实问答 / 对比分析 / 操作指导
│
▼ [Query Rewrite] (可选,复杂查询启用)
生成改写:
- "RAG hallucination mitigation techniques"
- "检索增强生成中的幻觉抑制方法"
- "如何验证 RAG 输出的准确性"
│
▼ [Hybrid Retrieval]
向量检索 Top-20 + BM25 Top-20 → RRF 融合 → Top-10
│
▼ [Rerank]
Cross-Encoder 对 (query, chunk) 逐对打分 → Top-5
│
▼ [Threshold Filter]
过滤 score < 0.6 的结果(不相关内容)
│
▼ [Prompt Assembly]
System: "你是知识库助手,只基于参考资料回答..."
Context: [chunk1] [chunk2] [chunk3]
Question: "RAG 系统怎么处理幻觉?"
│
▼ [LLM Generation] (streaming)
生成答案 + 标注引用 [1][2][3]
│
▼ [Post-processing]
- 引用溯源:[1] → source: "rag_notes.md", section: "幻觉抑制"
- 置信度评分:基于检索分数加权
- 拒答判断:若所有 context score < 阈值 → "信息不足,无法回答"
│
▼ [Response]
返回答案 + 引用列表 + 置信度性能与可靠性设计
延迟优化
| 环节 | 目标延迟 | 优化手段 |
|---|---|---|
| Embedding | < 200ms | 批量请求、连接池复用 |
| 向量检索 | < 50ms | HNSW 索引、预热缓存 |
| BM25 检索 | < 30ms | 内存倒排索引 |
| 重排序 | < 300ms | 仅对 Top-10 精排 |
| LLM 生成 | 流式 | SSE 流式输出,首 token < 1s |
可靠性
- 重试:Embedding/LLM API 调用失败 → 指数退避重试(最多 3 次)
- 降级:重排序服务不可用 → 跳过精排,直接返回粗排结果
- 拒答:检索结果全部低于阈值 → 明确返回"信息不足"
- 超时:context.WithTimeout 控制全链路超时(默认 30s)
可观测
- Trace:OpenTelemetry 全链路追踪,每个环节耗时可视化
- Metrics:Prometheus 暴露检索 QPS、延迟分位数、缓存命中率
- Logging:结构化日志,记录每次检索的 query → results → answer
评估方案
评测指标
| 指标 | 含义 | 计算方式 |
|---|---|---|
| Recall@K | 前 K 个结果中包含正确答案来源的比例 | 命中数 / 总测试数 |
| MRR | 正确结果首次出现的位置倒数平均 | 1/rank 的平均值 |
| NDCG@K | 考虑位置加权的排序质量 | DCG/IDCG |
| Faithfulness | 答案中有检索支撑的断言比例 | supported_claims / total_claims |
| Answer Relevance | 答案与问题的相关程度 | LLM-as-Judge 评分 |
测试集构建
json
{
"test_cases": [
{
"question": "RAG 系统中如何处理文档分块?",
"expected_answer": "常见方法包括固定长度分块、递归分割、语义分块...",
"expected_sources": ["rag_notes.md#chunking"],
"difficulty": "easy"
}
]
}评估命令
bash
# 运行全量评估
rag eval --testset=eval/testsets/rag_qa.json --output=eval/results/
# 对比两种配置
rag eval --config=configs/baseline.yaml --config=configs/hybrid.yaml --compare开发分期
Phase 1:核心管线(MVP)
目标:跑通"索引 → 检索 → 生成"最小链路
- 文档加载器(Markdown)
- 递归字符分块
- Embedding 客户端(OpenAI 兼容)
- 内存向量存储 + 暴力搜索
- 基础 Prompt 模板 + LLM 生成
- CLI:
rag index+rag ask
Phase 2:检索质量提升
目标:混合检索 + 重排序,Recall@5 达到 0.85+
- BM25 倒排索引
- RRF 混合检索融合
- Multi-Query 查询改写
- Cross-Encoder 重排序
- 评估框架 + 基础测试集
Phase 3:生成质量 + 工程化
目标:Faithfulness 达到 0.90+,生产可用
- 引用溯源 + 幻觉检测
- 流式输出(SSE)
- 查询缓存 + Embedding 缓存
- 置信度评分 + 拒答机制
- Qdrant 集成
- 全链路 Trace
Phase 4:Agent 集成
目标:RAG 作为 Agent Tool,支持复杂推理
- Function Calling Tool 接口
- 对话记忆管理
- Query Decomposition(问题分解)
- HTTP 服务模式(
rag serve)