一、开篇: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.RRFDamping+rank+1)
}
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 usedTokens+estimatedTokens > 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。所有计算在浏览器完成,文件不上服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。