完整 RAG 系统:端到端实战#

经过前三章的学习,我们掌握了 RAG 系统的所有零件:

章节

技术

作用

第一章

文档加载与切块

把长文档切成合适大小的片段

第二章

Embedding

把文本转成向量,捕获语义

第三章

ChromaDB

持久化存储向量,高效检索

本章把这三项技术组合成完整的 RAG 系统

文档语料库
    │
    ▼ chunk_text()
文本片段(chunks)
    │
    ▼ get_embeddings()
向量(embeddings)
    │
    ▼ collection.add()
ChromaDB 向量库
    │        ▲
    │        │ retrieve(query)
    ▼        │
相关片段(context)
    │
    ▼ rag_answer(question)
LLM 生成回答

完成本章后,你将拥有一个可直接用于生产的 RAG 骨架代码。

import chromadb
import litellm
litellm.drop_params = True
import json
import os
import re
import time
from dotenv import load_dotenv

load_dotenv()

MODEL = os.getenv("LLM_MODEL")
EMBED_MODEL = os.getenv("EMBED_MODEL", "openai/text-embedding-3-small")

print(f"LLM 模型:       {MODEL}")
print(f"Embedding 模型: {EMBED_MODEL}")
# gpt-5/o系列不支持自定义temperature值,统一用安全wrapper
def _c(**kw):
    _m = kw.get('model', MODEL)
    if any(_m.startswith(p) for p in ('openai/gpt-5','openai/o1','openai/o3','openai/o4')):
        kw.pop('temperature', None)
    return litellm.completion(**kw)
LLM 模型:       openai/gpt-5-mini
Embedding 模型: openai/text-embedding-3-small

核心辅助函数#

三个关键函数构成 RAG 的基础工具箱:

  • get_embedding:单条文本向量化

  • get_embeddings:批量向量化(效率更高)

  • chunk_text:带重叠的滑动窗口切块

为什么需要重叠(overlap)?
切块边界处的句子可能被截断,重叠区域确保边界信息不丢失。
例如 size=300、overlap=50 时,相邻两块共享 50 个字符。

def get_embedding(text: str) -> list[float]:
    """获取单条文本的 embedding 向量。"""
    response = litellm.embedding(model=EMBED_MODEL, input=[text])
    return response.data[0]["embedding"]


def get_embeddings(texts: list[str]) -> list[list[float]]:
    """批量获取 embedding,按 index 排序保证顺序一致。"""
    response = litellm.embedding(model=EMBED_MODEL, input=texts)
    items = sorted(response.data, key=lambda x: x["index"])
    return [item["embedding"] for item in items]


def chunk_text(text: str, size: int = 300, overlap: int = 50) -> list[str]:
    """滑动窗口切块,带重叠区域。
    
    Args:
        text: 原始文本
        size: 每块最大字符数
        overlap: 相邻块重叠字符数
    
    Returns:
        切块列表
    """
    if len(text) <= size:
        return [text.strip()]
    
    chunks = []
    start = 0
    step = size - overlap  # 每次前进的步长
    
    while start < len(text):
        end = min(start + size, len(text))
        chunk = text[start:end].strip()
        if chunk:
            chunks.append(chunk)
        if end == len(text):
            break
        start += step
    
    return chunks


# 验证 chunk_text
test_text = "A" * 650  # 650 个字符的测试文本
chunks = chunk_text(test_text, size=300, overlap=50)
print(f"原文长度: {len(test_text)} 字符")
print(f"切块数量: {len(chunks)}")
print(f"各块长度: {[len(c) for c in chunks]}")
print(f"重叠验证: 块0末尾50字符 == 块1开头50字符: {chunks[0][-50:] == chunks[1][:50]}")
原文长度: 650 字符
切块数量: 3
各块长度: [300, 300, 150]
重叠验证: 块0末尾50字符 == 块1开头50字符: True

第一节:构建知识库#

我们定义一个包含 5 篇中文文档的语料库,主题涵盖 LLM 核心概念。

构建流程:

  1. 对每篇文档调用 chunk_text 切块

  2. 批量获取所有块的 embedding

  3. 存入 ChromaDB,附带 doc_idchunk_idsource 元数据

# 定义语料库:5 篇关于 LLM 的中文文档
CORPUS = [
    {
        "doc_id": "doc_001",
        "source": "LLM基础教材",
        "text": (
            "大语言模型(Large Language Model,LLM)是基于 Transformer 架构的深度学习模型,"
            "通过在海量文本数据上进行预训练来学习语言规律。GPT 系列、Claude、Gemini 是当前最"
            "具代表性的大语言模型。这些模型参数量从数十亿到数千亿不等,训练数据来自互联网、书"
            "籍、代码等多种来源。预训练完成后,通过指令微调(Instruction Tuning)和人类反馈强"
            "化学习(RLHF)使模型更好地遵循人类指令。大语言模型的核心能力包括文本生成、问答、"
            "摘要、翻译、代码编写等,在众多自然语言处理任务上达到甚至超越人类水平。"
        ),
    },
    {
        "doc_id": "doc_002",
        "source": "RAG技术报告",
        "text": (
            "检索增强生成(Retrieval-Augmented Generation,RAG)是一种将信息检索与文本生成结"
            "合的技术框架。RAG 的核心思想是:在回答问题时,先从外部知识库中检索相关信息,再将"
            "检索到的内容作为上下文提供给语言模型,从而生成更准确、更有依据的回答。RAG 解决了"
            "纯 LLM 的三大痛点:知识截止日期(训练数据有时效性)、幻觉问题(模型可能编造事实)"
            "、领域知识不足(通用模型缺乏专业知识)。典型的 RAG 流程分为离线索引阶段和在线推理"
            "阶段。离线阶段将文档切块、向量化并存入向量数据库;在线阶段将用户问题向量化,检索"
            "最相关的文档块,拼接到提示词中让 LLM 生成回答。"
        ),
    },
    {
        "doc_id": "doc_003",
        "source": "Transformer论文解读",
        "text": (
            "Transformer 是 2017 年 Google 在论文《Attention is All You Need》中提出的神经"
            "网络架构。其核心创新是自注意力机制(Self-Attention),允许模型在处理序列中的每个"
            "位置时关注序列中所有其他位置,从而捕获长距离依赖关系。Transformer 由编码器"
            "(Encoder)和解码器(Decoder)组成。编码器将输入序列映射到连续表示,解码器根据"
            "编码器输出和已生成的词元逐步生成输出序列。多头注意力机制(Multi-Head Attention)"
            "让模型从不同角度理解输入,前馈网络(FFN)负责非线性变换,残差连接和层归一化保证"
            "训练稳定性。BERT 使用编码器结构,GPT 使用解码器结构,T5 使用完整的编解码器结构。"
        ),
    },
    {
        "doc_id": "doc_004",
        "source": "Embedding技术综述",
        "text": (
            "文本嵌入(Text Embedding)是将文本转换为高维向量的技术,使语义相近的文本在向量"
            "空间中距离相近。现代 Embedding 模型通常基于预训练语言模型微调而来,如 OpenAI 的"
            "text-embedding-3-small、Cohere 的 embed-multilingual 等。Embedding 的质量直接"
            "影响 RAG 系统的检索效果。常用相似度指标有余弦相似度(Cosine Similarity)和欧氏"
            "距离(Euclidean Distance)。余弦相似度关注向量方向而非长度,在文本检索中更常用。"
            "Embedding 维度通常在 384 到 3072 之间,维度越高表达能力越强,但计算和存储开销"
            "也越大。批量获取 Embedding 比逐条获取效率更高,生产环境应充分利用批量 API。"
        ),
    },
    {
        "doc_id": "doc_005",
        "source": "提示词工程指南",
        "text": (
            "提示词工程(Prompt Engineering)是设计和优化输入提示以引导大语言模型产生期望输出"
            "的技术。良好的提示词设计对模型性能有显著影响。常见技巧包括:少样本提示(Few-Shot "
            "Prompting)通过提供示例引导模型理解任务格式;思维链提示(Chain-of-Thought)让模"
            "型逐步推理,提升复杂问题解决能力;角色扮演提示通过赋予模型特定角色改变回答风格。"
            "在 RAG 场景中,系统提示词通常要求模型:仅根据提供的上下文回答,不要编造信息,无"
            "法从上下文中找到答案时明确告知用户。结构化输出提示可以让模型返回 JSON、表格等"
            "格式化数据,方便程序进一步处理。"
        ),
    },
]

print(f"语料库文档数: {len(CORPUS)}")
for doc in CORPUS:
    print(f"  {doc['doc_id']} [{doc['source']}] - {len(doc['text'])} 字符")
语料库文档数: 5
  doc_001 [LLM基础教材] - 271 字符
  doc_002 [RAG技术报告] - 292 字符
  doc_003 [Transformer论文解读] - 337 字符
  doc_004 [Embedding技术综述] - 356 字符
  doc_005 [提示词工程指南] - 285 字符
# 切块 + 向量化 + 存入 ChromaDB
kb_client = chromadb.Client()
kb_collection = kb_client.create_collection(
    name="llm_knowledge_base",
    metadata={"hnsw:space": "cosine"},
)

all_chunks = []      # 所有文本块
all_chunk_ids = []   # 每块的唯一 ID
all_metadatas = []   # 每块的元数据

print("── 切块结果 ──")
for doc in CORPUS:
    chunks = chunk_text(doc["text"], size=200, overlap=40)
    print(f"  {doc['doc_id']}: {len(doc['text'])} 字 → {len(chunks)} 块")
    
    for chunk_idx, chunk in enumerate(chunks):
        chunk_id = f"{doc['doc_id']}_chunk_{chunk_idx:02d}"
        all_chunks.append(chunk)
        all_chunk_ids.append(chunk_id)
        all_metadatas.append({
            "doc_id": doc["doc_id"],
            "chunk_id": chunk_idx,
            "source": doc["source"],
        })

print(f"\n总计 {len(all_chunks)} 个文本块")

# 批量获取 embedding
print("正在批量获取 embedding...")
t0 = time.perf_counter()
all_embeddings = get_embeddings(all_chunks)
t_embed = time.perf_counter() - t0
print(f"Embedding 完成,耗时 {t_embed:.2f}s")

# 存入 ChromaDB
kb_collection.add(
    ids=all_chunk_ids,
    embeddings=all_embeddings,
    documents=all_chunks,
    metadatas=all_metadatas,
)

print(f"知识库构建完成,共 {kb_collection.count()} 条记录")
── 切块结果 ──
  doc_001: 271 字 → 2 块
  doc_002: 292 字 → 2 块
  doc_003: 337 字 → 2 块
  doc_004: 356 字 → 2 块
  doc_005: 285 字 → 2 块

总计 10 个文本块
正在批量获取 embedding...
Embedding 完成,耗时 0.55s
知识库构建完成,共 10 条记录

第二节:RAG 检索函数#

retrieve 函数是 RAG 系统的「大脑」:给定用户问题,从向量库中找出最相关的文档块。

返回格式统一为字典列表,每项包含:

  • text:文档块原文

  • metadata:来源信息(doc_id、source 等)

  • distance:余弦距离(越小越相关)

def retrieve(
    query: str,
    collection: chromadb.Collection,
    top_k: int = 3,
    where: dict | None = None,
) -> list[dict]:
    """从向量库中检索与 query 最相关的文档块。
    
    Args:
        query: 用户查询文本
        collection: ChromaDB collection
        top_k: 返回最相关的前 k 条
        where: 可选的元数据过滤条件
    
    Returns:
        list of {text, metadata, distance}
    """
    query_embedding = get_embedding(query)
    
    kwargs = {
        "query_embeddings": [query_embedding],
        "n_results": min(top_k, collection.count()),
        "include": ["documents", "metadatas", "distances"],
    }
    if where:
        kwargs["where"] = where
    
    results = collection.query(**kwargs)
    
    return [
        {
            "text": doc,
            "metadata": meta,
            "distance": dist,
        }
        for doc, meta, dist in zip(
            results["documents"][0],
            results["metadatas"][0],
            results["distances"][0],
        )
    ]


# 示例检索
example_query = "Transformer 的自注意力机制是什么?"
retrieved = retrieve(example_query, kb_collection, top_k=3)

print(f"查询: \"{example_query}\"")
print("=" * 65)
for i, item in enumerate(retrieved, 1):
    print(f"\n[{i}] 来源: {item['metadata']['source']}  距离: {item['distance']:.4f}")
    print(f"    {item['text'][:80]}...")
查询: "Transformer 的自注意力机制是什么?"
=================================================================

[1] 来源: Transformer论文解读  距离: 0.4184
    Transformer 是 2017 年 Google 在论文《Attention is All You Need》中提出的神经网络架构。其核心创新是自注意力机...

[2] 来源: Transformer论文解读  距离: 0.5568
    er)和解码器(Decoder)组成。编码器将输入序列映射到连续表示,解码器根据编码器输出和已生成的词元逐步生成输出序列。多头注意力机制(Multi-Head ...

[3] 来源: 提示词工程指南  距离: 0.6744
    提示词工程(Prompt Engineering)是设计和优化输入提示以引导大语言模型产生期望输出的技术。良好的提示词设计对模型性能有显著影响。常见技巧包括:少...

第三节:RAG 生成函数#

rag_answer 函数串联检索与生成:

  1. 调用 retrieve 获取相关文档块

  2. 将文档块拼接成「上下文」

  3. 构建系统提示词,要求模型只根据上下文回答

  4. 调用 LLM 生成最终答案

系统提示词的关键设计:明确告知模型知识边界——上下文中没有的信息不要编造。

SYSTEM_PROMPT = """你是知识库助手,只根据提供的上下文回答用户问题。

规则:
1. 只使用「上下文」中的信息回答,不要添加上下文中没有的内容
2. 如果上下文中没有足够信息,直接回答「根据现有知识库,我无法回答这个问题」
3. 回答要简洁准确,引用具体信息时可以提及来源
4. 不要编造数据、引用或事实"""


def rag_answer(
    question: str,
    collection: chromadb.Collection,
    top_k: int = 3,
    verbose: bool = False,
) -> str:
    """完整的 RAG 问答流程:检索 → 构建提示 → 生成答案。
    
    Args:
        question: 用户问题
        collection: 知识库 ChromaDB collection
        top_k: 检索文档块数量
        verbose: 是否打印完整提示词
    
    Returns:
        LLM 生成的回答
    """
    # 步骤 1:检索相关文档块
    chunks = retrieve(question, collection, top_k=top_k)
    
    # 步骤 2:构建上下文
    context_parts = []
    for i, chunk in enumerate(chunks, 1):
        source = chunk["metadata"]["source"]
        context_parts.append(f"[文档{i} - 来源:{source}]\n{chunk['text']}")
    context = "\n\n".join(context_parts)
    
    # 步骤 3:构建用户消息
    user_message = f"""上下文:
{context}

问题:{question}"""
    
    if verbose:
        print("── 完整提示词 ──")
        print(f"[系统提示]\n{SYSTEM_PROMPT}")
        print(f"\n[用户消息]\n{user_message}")
        print("─" * 50)
    
    # 步骤 4:调用 LLM
    response = _c(
        model=MODEL,
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_message},
        ],
        temperature=0.1,  # 低温度确保回答稳定、忠实于上下文
    )
    
    return response.choices[0].message.content


# 演示:打印完整提示词
print("演示完整 RAG 提示词结构(第一次调用显示详情)")
demo_answer = rag_answer(
    "RAG 解决了哪些 LLM 的痛点?",
    kb_collection,
    verbose=True,
)
print(f"\n── 模型回答 ──")
print(demo_answer)
演示完整 RAG 提示词结构(第一次调用显示详情)
── 完整提示词 ──
[系统提示]
你是知识库助手,只根据提供的上下文回答用户问题。

规则:
1. 只使用「上下文」中的信息回答,不要添加上下文中没有的内容
2. 如果上下文中没有足够信息,直接回答「根据现有知识库,我无法回答这个问题」
3. 回答要简洁准确,引用具体信息时可以提及来源
4. 不要编造数据、引用或事实

[用户消息]
上下文:
[文档1 - 来源:RAG技术报告]
练数据有时效性)、幻觉问题(模型可能编造事实)、领域知识不足(通用模型缺乏专业知识)。典型的 RAG 流程分为离线索引阶段和在线推理阶段。离线阶段将文档切块、向量化并存入向量数据库;在线阶段将用户问题向量化,检索最相关的文档块,拼接到提示词中让 LLM 生成回答。

[文档2 - 来源:RAG技术报告]
检索增强生成(Retrieval-Augmented Generation,RAG)是一种将信息检索与文本生成结合的技术框架。RAG 的核心思想是:在回答问题时,先从外部知识库中检索相关信息,再将检索到的内容作为上下文提供给语言模型,从而生成更准确、更有依据的回答。RAG 解决了纯 LLM 的三大痛点:知识截止日期(训练数据有时效性)、幻觉问题(模型可能编造事实)、领域知识不足(通用模型缺乏专业知

[文档3 - 来源:Embedding技术综述]
量直接影响 RAG 系统的检索效果。常用相似度指标有余弦相似度(Cosine Similarity)和欧氏距离(Euclidean Distance)。余弦相似度关注向量方向而非长度,在文本检索中更常用。Embedding 维度通常在 384 到 3072 之间,维度越高表达能力越强,但计算和存储开销也越大。批量获取 Embedding 比逐条获取效率更高,生产环境应充分利用批量 API。

问题:RAG 解决了哪些 LLM 的痛点?
──────────────────────────────────────────────────
── 模型回答 ──
RAG 解决三大痛点:知识截止日期(训练数据有时效性)、幻觉问题(模型可能编造事实)、领域知识不足(通用模型缺乏专业知识)。(来源:RAG技术报告,文档1/文档2)

第四节:完整问答演示#

用三类问题测试 RAG 系统的边界:

类型

预期行为

知识库中有明确答案

准确回答,引用相关文档

知识库中有部分相关信息

基于已有信息作答,不过度延伸

知识库中完全没有相关信息

明确告知无法回答,不编造

test_questions = [
    {
        "question": "Transformer 架构是由谁提出的?主要创新是什么?",
        "type": "完全可回答",
        "note": "知识库 doc_003 有详细介绍",
    },
    {
        "question": "提示词工程在 RAG 中如何应用?",
        "type": "部分可回答",
        "note": "doc_005 有提示词工程,doc_002 有 RAG,但两者结合的内容有限",
    },
    {
        "question": "2024 年诺贝尔物理学奖得主是谁?",
        "type": "完全不可回答",
        "note": "知识库完全没有诺贝尔奖相关内容",
    },
]

for i, item in enumerate(test_questions, 1):
    print(f"\n{'='*65}")
    print(f"问题 {i} [{item['type']}]")
    print(f"Q: {item['question']}")
    print(f"备注: {item['note']}")
    print("-" * 65)
    
    answer = rag_answer(item["question"], kb_collection)
    print(f"A: {answer}")

print(f"\n{'='*65}")
=================================================================
问题 1 [完全可回答]
Q: Transformer 架构是由谁提出的?主要创新是什么?
备注: 知识库 doc_003 有详细介绍
-----------------------------------------------------------------
A: Transformer 由 Google 在 2017 年的论文《Attention is All You Need》提出(见文档1)。其主要创新是自注意力机制(Self-Attention),允许模型在处理序列中每个位置时关注序列中所有其他位置,进而捕获长距离依赖关系(见文档1)。

=================================================================
问题 2 [部分可回答]
Q: 提示词工程在 RAG 中如何应用?
备注: doc_005 有提示词工程,doc_002 有 RAG,但两者结合的内容有限
-----------------------------------------------------------------
A: 简要回答(仅基于提供的上下文):

- 在 RAG 流程中,首先离线将文档切块并向量化,在线将用户问题向量化并检索最相关的文档块,然后将检索到的文档拼接到提示词中交给 LLM 生成回答(参见:文档2)。  
- 提示词工程的常见手段(少样本提示、思维链提示、角色扮演提示等)用于设计和优化这些拼接后的提示,以引导模型按照期望的格式、推理步骤和风格输出(参见:文档1)。  
- 结合以上,RAG 的核心是把外部检索到的信息作为上下文提供给模型,提示词工程则负责如何组织、示例化和指引这些上下文,从而帮助解决知识时效、幻觉和领域知识不足等问题(参见:文档3;文档2 提到的相关痛点)。

=================================================================
问题 3 [完全不可回答]
Q: 2024 年诺贝尔物理学奖得主是谁?
备注: 知识库完全没有诺贝尔奖相关内容
-----------------------------------------------------------------
A: 根据现有知识库,我无法回答这个问题

=================================================================

第五节:评估检索质量#

RAG 系统的质量取决于两个环节:检索质量生成质量

本节聚焦检索质量评估,使用 Recall@K 指标:

$$\text{Recall@K} = \frac{\text{Top-K 结果中包含正确文档的查询数}}{\text{总查询数}}$$

Recall@3 = 0.8 表示 80% 的查询,正确答案文档出现在前 3 个检索结果中。

def evaluate_retrieval(
    test_cases: list[dict],
    collection: chromadb.Collection,
    top_k: int = 3,
) -> dict:
    """评估检索质量,计算 Recall@K。
    
    Args:
        test_cases: list of {question, expected_doc_id}
        collection: 知识库 collection
        top_k: 评估 Recall@K
    
    Returns:
        {recall_at_k, details}
    """
    details = []
    hit_count = 0
    
    for case in test_cases:
        question = case["question"]
        expected_doc_id = case["expected_doc_id"]
        
        retrieved = retrieve(question, collection, top_k=top_k)
        
        # 检查期望的 doc_id 是否出现在结果中
        retrieved_doc_ids = [item["metadata"]["doc_id"] for item in retrieved]
        hit = expected_doc_id in retrieved_doc_ids
        
        if hit:
            hit_count += 1
            hit_rank = retrieved_doc_ids.index(expected_doc_id) + 1
        else:
            hit_rank = None
        
        details.append({
            "question": question,
            "expected": expected_doc_id,
            "retrieved_ids": retrieved_doc_ids,
            "hit": hit,
            "hit_rank": hit_rank,
            "top_distance": retrieved[0]["distance"] if retrieved else None,
        })
    
    recall_at_k = hit_count / len(test_cases)
    return {"recall_at_k": recall_at_k, "k": top_k, "details": details}


# 5 个测试用例
eval_cases = [
    {
        "question": "大语言模型是如何训练的?",
        "expected_doc_id": "doc_001",
    },
    {
        "question": "RAG 的离线和在线阶段分别做什么?",
        "expected_doc_id": "doc_002",
    },
    {
        "question": "自注意力机制如何捕获长距离依赖?",
        "expected_doc_id": "doc_003",
    },
    {
        "question": "余弦相似度和欧氏距离的区别",
        "expected_doc_id": "doc_004",
    },
    {
        "question": "思维链提示词是什么技术?",
        "expected_doc_id": "doc_005",
    },
]

eval_result = evaluate_retrieval(eval_cases, kb_collection, top_k=3)

print(f"Recall@{eval_result['k']} = {eval_result['recall_at_k']:.2%}")
print()
print(f"{'#':<3} {'命中':<5} {'排名':<5} {'最小距离':<10} {'问题'}")
print("-" * 65)
for i, d in enumerate(eval_result["details"], 1):
    hit_str = "✓" if d["hit"] else "✗"
    rank_str = str(d["hit_rank"]) if d["hit_rank"] else "-"
    dist_str = f"{d['top_distance']:.4f}" if d["top_distance"] is not None else "-"
    print(f"{i:<3} {hit_str:<5} {rank_str:<5} {dist_str:<10} {d['question'][:30]}")
    if not d["hit"]:
        print(f"    期望: {d['expected']},实际检索到: {d['retrieved_ids']}")
Recall@3 = 100.00%

#   命中    排名    最小距离       问题
-----------------------------------------------------------------
1   ✓     1     0.2957     大语言模型是如何训练的?
2   ✓     1     0.4810     RAG 的离线和在线阶段分别做什么?
3   ✓     1     0.5923     自注意力机制如何捕获长距离依赖?
4   ✓     1     0.4899     余弦相似度和欧氏距离的区别
5   ✓     1     0.5055     思维链提示词是什么技术?

第六节:RAG vs 直接问 LLM#

RAG 最重要的价值是事实基础:模型的回答有据可查,而非凭空生成。

本节对同一个问题进行两种方式的回答对比:

  • 直接问 LLM:模型只能依赖训练数据,可能给出泛化或错误的答案

  • RAG 回答:模型基于知识库中的具体内容,回答更精准、可追溯

def direct_llm_answer(question: str) -> str:
    """直接调用 LLM,不提供任何上下文。"""
    response = _c(
        model=MODEL,
        messages=[{"role": "user", "content": question}],
        temperature=0.7,
    )
    return response.choices[0].message.content


# 选取一个在知识库中有具体信息的问题
comparison_question = "RAG 技术解决了大语言模型的哪三个具体痛点?请逐一说明。"

print(f"对比问题: {comparison_question}")
print("=" * 65)

print("\n【方式一:直接问 LLM(无上下文)】")
print("-" * 40)
direct_answer = direct_llm_answer(comparison_question)
print(direct_answer)

print("\n【方式二:RAG 回答(有知识库支撑)】")
print("-" * 40)
rag_ans = rag_answer(comparison_question, kb_collection)
print(rag_ans)

print("\n" + "=" * 65)
print("分析:")
print("  直接 LLM:回答来自训练数据,内容可能正确,但无法追溯来源")
print("  RAG 回答:内容源自 'RAG技术报告',可追溯、可验证")
print("  RAG 的优势在事实密集型、专业领域、需要时效性信息时最为突出")
对比问题: RAG 技术解决了大语言模型的哪三个具体痛点?请逐一说明。
=================================================================

【方式一:直接问 LLM(无上下文)】
----------------------------------------
简短回答:RAG(Retrieval‑Augmented Generation)主要解决三类痛点——知识覆盖与时效性、生成内容的可验证性/幻觉(hallucination)、以及模型上下文/规模与成本限制。下面逐一说明每个痛点、RAG 如何缓解、并给出要点与注意事项。

1) 知识覆盖不足与时效性(knowledge cutoff / domain gaps)
- 痛点:纯靠预训练参数的 LLM 知识固定于训练时,无法及时包含最新信息或特定领域的细节(法律条文、公司内部文档、专利数据等)。
- RAG 解决方式:在生成前检索外部文档库(网页、数据库、内部知识库、向量索引等),把相关片段拼接进 prompt,模型基于这些检索到的上下文生成答案,从而“动态”利用最新或领域特定信息。
- 举例:需要回答“某公司最新季度财报要点”时,先检索最新财报段落,再基于这些段落生成摘要,避免依赖旧知识。
- 注意/最佳实践:检索库需要及时更新;检索质量直接决定最终答案的正确性;要设计好检索策略(布尔、向量、混合)与 chunking 大小。

2) 生成幻觉与可验证性不足(hallucination / traceability)
- 痛点:LLM 有时会编造事实或给出无法追溯的断言,尤其在细节或引用场景中不可接受。
- RAG 解决方式:模型在生成过程中直接引用检索到的文档片段(并可返回来源链接/段落 ID),为答案提供证据链,使输出更可验证、可审计,并降低无中生有的概率。
- 举例:法律咨询或学术问答中,RAG 可以在回答后附上检索到的法规条文或论文段落与页码,便于用户核验。
- 注意/最佳实践:模型仍可能“曲解”或错误引用检索文本,需要后处理(事实检验、置信度阈值、使用 reranker 或 extractive verification)并在界面上明确标注来源与可信度。

3) 上下文窗口/规模与成本限制(长文档、个性化与参数规模问题)
- 痛点:大规模知识库无法全部放入模型上下文窗口,训练或微调包含全部信息代价高昂且不可扩展;对个性化/多用户知识存储需求也难以直接用模型参数解决。
- RAG 解决方式:把知识库存放在外部索引(向量数据库/搜索引擎),按需检索相关片段拼入有限上下文,避免把大量信息写进模型参数或上下文,从而实现对海量文档的可扩展访问并降低推理成本。也便于为不同用户注入个性化上下文。
- 举例:要在客服场景中支持数百万条知识库条目,RAG 仅检索与当前对话相关的几条,既保持响应速度又支持大规模知识。
- 注意/最佳实践:需要设计高效的索引、缓存与检索策略以降低延迟;检索与生成分开会带来工程复杂度(同步、版本控制、权限管理);可结合轻量微调或提示工程提升生成对检索上下文的利用率。

补充几点限制与实务建议
- RAG 的效果高度依赖检索质量(检索错误会直接导致错误输出)。使用向量检索 + 精排(reranker)常能显著提高准确率。
- 仍有“模型忽视上下文”或“拼贴式引用导致语义错配”的问题,需要在 prompt 中明确指令(只基于检索文本回答/标注不确定项)。
- 考虑合规与隐私:外部检索库与日志需做好权限与审计。
- 常见改进:混合检索(sparse + dense)、文档分块优化、检索-生成迭代(retrieve → generate → verify → re-retrieve)以及返回证据与置信度分数。

如果需要,我可以基于你关注的具体场景(客服、法律、医学、企业内网等)给出更具体的 RAG 架构建议与检索/提示范例。

【方式二:RAG 回答(有知识库支撑)】
----------------------------------------
RAG 技术解决了三大痛点(来源:RAG 技术报告):

1. 知识截止日期(训练数据有时效性)  
   说明:训练得到的 LLM 可能只包含到某一时间点的知识,无法获得最新信息。RAG 在回答时先从外部知识库检索相关内容并提供给模型,利用检索到的最新文档作为上下文来弥补训练数据的时效性(见文档3)。

2. 幻觉问题(模型可能编造事实)  
   说明:纯生成式模型可能会“编造”事实。RAG 将检索到的真实文档片段拼接进提示词,令模型基于检索到的证据生成回答,从而降低幻觉发生的概率(见文档3、文档2 关于在线阶段检索并拼接文档的描述)。

3. 领域知识不足(通用模型缺乏专业知识)  
   说明:通用 LLM 在专业领域可能知识不足。RAG 通过从领域相关的知识库检索并提供专门文档作为上下文,使模型能利用外部领域资料生成更具专业性的回答(见文档3)。

=================================================================
分析:
  直接 LLM:回答来自训练数据,内容可能正确,但无法追溯来源
  RAG 回答:内容源自 'RAG技术报告',可追溯、可验证
  RAG 的优势在事实密集型、专业领域、需要时效性信息时最为突出

总结#

完整 RAG 流水线图#

┌─────────────────────────────────────────────────────────────┐
│                    离线索引阶段(一次性)                      │
│                                                             │
│  原始文档                                                    │
│     │                                                       │
│     ▼  chunk_text(size=300, overlap=50)                     │
│  文本块 [chunk_0, chunk_1, ..., chunk_N]                    │
│     │                                                       │
│     ▼  get_embeddings(chunks)  [批量调用 Embedding API]      │
│  向量  [emb_0, emb_1, ..., emb_N]                          │
│     │                                                       │
│     ▼  collection.add(ids, embeddings, documents, metadata)  │
│  ChromaDB 向量库(持久化存储)                               │
└─────────────────────────────────────────────────────────────┘
                          │
                          │  每次查询时
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                    在线推理阶段(每次查询)                    │
│                                                             │
│  用户问题: "RAG 是什么?"                                    │
│     │                                                       │
│     ▼  get_embedding(question)                              │
│  问题向量                                                   │
│     │                                                       │
│     ▼  collection.query(top_k=3) + 可选 where 过滤          │
│  相关文档块 [chunk_a, chunk_b, chunk_c]  +  距离分数         │
│     │                                                       │
│     ▼  构建提示词                                           │
│  System: 只根据上下文回答...                                │
│  User:   上下文: [chunk_a]\n[chunk_b]\n[chunk_c]           │
│          问题: RAG 是什么?                                  │
│     │                                                       │
│     ▼  litellm.completion(model, messages)                  │
│  最终回答(有事实依据,可追溯来源)                           │
└─────────────────────────────────────────────────────────────┘

本章核心代码结构#

函数

作用

关键参数

chunk_text(text, size, overlap)

文档切块

size=300, overlap=50

get_embeddings(texts)

批量向量化

EMBED_MODEL

retrieve(query, collection, top_k)

语义检索

top_k=3, where 过滤

rag_answer(question, collection)

完整 RAG 问答

MODEL, SYSTEM_PROMPT

evaluate_retrieval(cases, collection, k)

检索质量评估

Recall@K

下一步优化方向#

  1. Reranker:在 Top-K 检索后,用交叉编码器重新排序,提升精度

  2. 混合检索:结合 BM25(关键词)和向量检索,优势互补

  3. 查询扩展:用 LLM 生成多个查询变体,提高召回率

  4. 多轮对话 RAG:结合对话历史,支持追问和上下文关联

  5. 流式输出:使用 litellm.completion(stream=True) 实现打字机效果

  6. 引用追踪:在回答中标注具体来自哪篇文档的哪一段