跳转到内容

5.1 RAG 架构中的角色定位

Chroma 在 RAG 中不是主角,但它是不可或缺的"记忆中枢"——没有它,LLM 就是一个没有图书馆的学者


这一节在讲什么?

前面四章我们学了 Chroma 的基本操作、CRUD、Embedding、查询和过滤——这些都是工具层面的知识。但工具只有在具体场景中才能发挥价值,而 Chroma 最核心的应用场景就是 RAG(Retrieval-Augmented Generation,检索增强生成)。这一节我们要从架构层面理解 Chroma 在 RAG 系统中扮演什么角色、它与其他组件如何协作、以及它的能力边界在哪里。这些理解不仅帮助你正确使用 Chroma,也是面试中"RAG 架构怎么设计"这类系统级问题的回答基础。


RAG 的全链路拆解

RAG 的核心思想很简单:LLM 的知识是有限的(只包含训练数据中的信息),但用户的问题可能涉及任何领域的私有数据。与其把所有知识都塞进 LLM 的参数里(微调成本高、更新慢),不如在每次提问时,先从外部知识库中检索相关信息,再把检索到的信息作为上下文喂给 LLM,让它基于这些信息生成回答。

完整的 RAG 链路可以分为五个阶段:

┌─────────────────────────────────────────────────────────────────────┐
│  RAG 全链路                                                         │
│                                                                     │
│  ① 用户提问                                                         │
│     "我们公司的退款政策是什么?"                                      │
│     ↓                                                               │
│  ② Query Understanding(查询理解)                                   │
│     - 改写/扩展用户的原始问题                                         │
│     - 提取关键实体和意图                                              │
│     - 生成适合检索的 query                                           │
│     ↓                                                               │
│  ③ Retrieval(检索)← Chroma 在这里!                                │
│     - Query → Embedding Function → 查询向量                          │
│     - 查询向量 → HNSW 向量搜索 → Top-K 候选文档                       │
│     - 可选:Where 过滤 → 缩小候选范围                                 │
│     - 可选:Re-ranking → 精排 Top-K                                  │
│     ↓                                                               │
│  ④ Context Assembly(上下文组装)                                    │
│     - 将检索到的文档拼接成 prompt                                     │
│     - 添加系统指令(如"基于以下信息回答问题")                         │
│     - 控制总长度不超过 LLM 上下文窗口                                  │
│     ↓                                                               │
│  ⑤ Generation(生成)                                               │
│     - LLM 基于 prompt + 检索到的上下文生成回答                        │
│     - 可选:引用来源、置信度评估                                      │
│     ↓                                                               │
│  回答:"根据公司政策,购买后7天内可无条件退款..."                      │
└─────────────────────────────────────────────────────────────────────┘

Chroma 负责的是第③阶段——检索(Retrieval)。它是整个 RAG 系统的"记忆中枢",存储着所有可检索的知识,并在用户提问时快速返回最相关的文档片段。


Chroma 在 RAG 中的具体职责

职责一:知识存储

RAG 系统需要一个地方来存储结构化的知识——产品文档、FAQ、技术手册、用户历史等。Chroma 的 Collection 就是这些知识的容器。每条知识以 Document 的形式存储,包含原文(document)、向量(embedding)、元数据(metadata)和唯一标识(id)。

python
# 知识入库:把文档切分后存入 Chroma
def ingest_knowledge(collection, documents, source, category):
    """将文档入库到 Chroma"""
    chunks = recursive_chunk(documents, chunk_size=600, overlap=80)
    for i, chunk in enumerate(chunks):
        collection.add(
            documents=[chunk],
            ids=[f"{source}_chunk_{i}"],
            metadatas=[{
                "source": source,
                "category": category,
                "chunk_index": i,
                "total_chunks": len(chunks)
            }]
        )

职责二:语义检索

当用户提问时,Chroma 负责从知识库中快速找到最相关的文档片段。这个过程包括:将用户的查询文本编码为向量、在 HNSW 索引中搜索最近邻、返回按相似度排序的 top-K 结果。

python
# 语义检索:从 Chroma 中找到最相关的文档
def retrieve(collection, query, category=None, n_results=5):
    """从 Chroma 检索相关文档"""
    where = {"category": category} if category else None
    results = collection.query(
        query_texts=[query],
        where=where,
        n_results=n_results,
        include=["documents", "metadatas", "distances"]
    )
    return results

职责三:结构化过滤

通过 metadata 的 where 过滤,Chroma 可以在语义搜索的基础上叠加结构化约束——比如只搜索某个类别的文档、只搜索最新版本、只搜索特定语言的内容。这种"语义+结构"的混合查询模式是 RAG 系统中提高检索精度的关键手段。


Chroma 不负责什么

理解 Chroma 的能力边界同样重要。以下是 Chroma 在 RAG 架构中不负责的部分:

不负责的部分由谁负责原因
查询改写/扩展LLM 或专用模型需要理解用户意图,不是向量搜索的职责
文档切分应用层(ingest pipeline)切分策略取决于业务需求
Prompt 组装应用层需要考虑 LLM 的上下文窗口和指令格式
文本生成LLMChroma 是数据库,不是生成模型
Re-rankingCross-Encoder 模型需要更精细的语义匹配
对话历史管理应用层 + Chroma Memory Collection需要区分短期和长期记忆

Chroma 的定位是专注做好检索这一件事。它不试图成为端到端的 RAG 框架——那是 LangChain、LlamaIndex 等工具的职责。Chroma 的优势在于它的 API 简洁、零配置启动、Python 原生集成,使得你可以快速搭建一个可工作的检索层,然后根据需要逐步添加查询改写、Re-ranking、对话管理等高级功能。


检索质量如何影响生成质量

RAG 系统有一个根本性的特征:检索是生成的上限。如果检索阶段没有找到正确的文档,LLM 再强也无法生成正确的回答——它要么编造答案(幻觉),要么坦白说"我不知道"。反过来,如果检索到了正确的文档,即使 LLM 能力一般,也能基于文档内容生成合理的回答。

┌─────────────────────────────────────────────────────────────┐
│  检索质量与生成质量的关系                                     │
│                                                             │
│  检索正确 + LLM 强 → ✅ 高质量回答                           │
│  检索正确 + LLM 弱 → ⚠️ 回答可能不够流畅,但信息正确          │
│  检索错误 + LLM 强 → ❌ 流畅但可能包含幻觉(更危险!)        │
│  检索错误 + LLM 弱 → ❌ 回答错误或"我不知道"                 │
│                                                             │
│  结论:检索质量是 RAG 系统的地基                              │
│  → 优化 RAG 的第一步永远是优化检索,而不是换更大的 LLM        │
└─────────────────────────────────────────────────────────────┘

这意味着在 RAG 系统的优化中,你应该把大部分精力放在检索层——选择合适的 embedding 模型、设计合理的 metadata schema、调整切分参数、引入 re-ranking。这些优化对最终效果的影响,往往比从 GPT-3.5 升级到 GPT-4 更大。


检索与生成的接口契约

Chroma(检索层)和 LLM(生成层)之间的接口契约是:检索到的文档必须包含足够的信息来回答用户的问题。这个契约对 Chroma 的使用方式有几个直接影响:

  1. 切分粒度要合适:每个 chunk 必须包含足够的上下文让 LLM 理解其含义。如果切得太碎,LLM 看到的只是孤立的片段,无法生成连贯的回答。

  2. Metadata 要完整:检索结果中的 metadata(特别是 source 和 chunk_index)应该传递给 LLM,让它能在回答中引用来源——"根据《用户手册》第 15 页的内容..."比一个没有出处的回答更可信。

  3. Top-K 数量要适中:太少可能遗漏关键信息,太多会引入噪声并超出 LLM 的上下文窗口。3~5 条是大多数场景的最佳范围。

  4. 距离阈值要设定:不是所有检索结果都值得传给 LLM。如果最相似的文档距离也很远(比如 cosine > 1.5),说明知识库中可能没有相关信息,此时应该让 LLM 回答"我没有找到相关信息"而不是强行生成。

python
def retrieve_with_threshold(collection, query, max_distance=1.0, n_results=5):
    """带距离阈值的检索:过滤掉距离过远的结果"""
    results = collection.query(
        query_texts=[query],
        n_results=n_results,
        include=["documents", "metadatas", "distances"]
    )

    filtered = []
    for i in range(len(results['ids'][0])):
        if results['distances'][0][i] <= max_distance:
            filtered.append({
                "document": results['documents'][0][i],
                "metadata": results['metadatas'][0][i],
                "distance": results['distances'][0][i]
            })

    return filtered

完整的 RAG 查询流程示例

让我们把前面学的所有知识整合起来,实现一个完整的 RAG 查询流程:

python
import chromadb
from chromadb.utils import embedding_functions

class SimpleRAG:
    """最简 RAG 实现:检索 → 组装 → 生成"""

    def __init__(self, collection_name="rag_kb", persist_dir="./rag_db"):
        self.client = chromadb.Client(settings=chromadb.Settings(
            is_persistent=True,
            persist_directory=persist_dir
        ))

        ef = embedding_functions.SentenceTransformerEmbeddingFunction(
            model_name="paraphrase-multilingual-MiniLM-L12-v2"
        )

        self.collection = self.client.get_or_create_collection(
            name=collection_name,
            embedding_function=ef
        )

    def ingest(self, text: str, source: str, category: str = "general"):
        """入库文档"""
        chunks = self._chunk(text)
        ids = [f"{source}_chunk_{i}" for i in range(len(chunks))]
        metas = [{
            "source": source,
            "category": category,
            "chunk_index": i,
            "total_chunks": len(chunks)
        } for i in range(len(chunks))]

        self.collection.upsert(documents=chunks, ids=ids, metadatas=metas)
        print(f"✅ 入库 {len(chunks)} 个 chunk (来源: {source})")

    def query(self, question: str, category: str = None, n_results: int = 3,
              max_distance: float = 1.2):
        """RAG 查询:检索 → 组装 prompt → 返回上下文"""
        where = {"category": category} if category else None

        results = self.collection.query(
            query_texts=[question],
            where=where,
            n_results=n_results,
            include=["documents", "metadatas", "distances"]
        )

        # 过滤距离过远的结果
        context_parts = []
        sources = []
        for i in range(len(results['ids'][0])):
            dist = results['distances'][0][i]
            if dist <= max_distance:
                doc = results['documents'][0][i]
                meta = results['metadatas'][0][i]
                context_parts.append(doc)
                sources.append(f"{meta['source']} (chunk {meta['chunk_index']})")

        if not context_parts:
            return {
                "answer": None,
                "context": "",
                "sources": [],
                "message": "未找到相关信息"
            }

        context = "\n\n".join(context_parts)

        # 组装 prompt(这里只返回上下文,实际生成由 LLM 完成)
        prompt = f"""基于以下参考信息回答用户的问题。如果参考信息中没有相关内容,请回答"我没有找到相关信息"。

参考信息:
{context}

用户问题:{question}

回答:"""

        return {
            "prompt": prompt,
            "context": context,
            "sources": sources,
            "n_retrieved": len(results['ids'][0]),
            "n_filtered": len(context_parts)
        }

    def _chunk(self, text, chunk_size=600, overlap=80):
        """递归字符级切分"""
        chunks = []
        start = 0
        while start < len(text):
            end = start + chunk_size
            chunk = text[start:end]
            if chunk.strip():
                chunks.append(chunk.strip())
            start = end - overlap
        return chunks if chunks else [text]


# ====== 使用示例 ======
rag = SimpleRAG()

# 入库知识
rag.ingest(
    "退款政策:购买后7天内可无条件退款。退款流程:1.在订单页面点击申请退款;2.填写退款原因;3.等待3-5个工作日审核。注意:已拆封的数码产品不支持无理由退款。",
    source="user_manual_v2",
    category="after_sales"
)

rag.ingest(
    "安装指南:1.下载安装包;2.双击运行安装程序;3.选择安装路径;4.点击安装并等待完成。系统要求:Windows 10及以上,8GB内存。",
    source="install_guide",
    category="technical"
)

# RAG 查询
result = rag.query("数码产品能退款吗", category="after_sales")
print(f"\n检索到 {result['n_filtered']} 条相关文档")
print(f"来源: {result['sources']}")
print(f"\n--- 组装的 Prompt ---\n{result['prompt'][:300]}...")

输出:

✅ 入库 2 个 chunk (来源: user_manual_v2)
✅ 入库 1 个 chunk (来源: install_guide)

检索到 1 条相关文档
来源: ['user_manual_v2 (chunk 0)']

--- 组装的 Prompt ---
基于以下参考信息回答用户的问题。如果参考信息中没有相关内容,请回答"我没有找到相关信息"。

参考信息:
退款政策:购买后7天内可无条件退款。退款流程:1.在订单页面点击申请退款;2.填写退款原因;3.等待3-5个工作日审核。注意:已拆封的数码产品不支持无理由退款。

用户问题:数码产品能退款吗...

常见误区

误区 1:把 Chroma 当成端到端的 RAG 解决方案

Chroma 只负责检索,不负责查询理解、prompt 组装和文本生成。如果你需要完整的 RAG 能力,应该把 Chroma 与 LLM 结合使用,或者使用 LangChain/LlamaIndex 等框架来编排完整的 RAG 流程。

误区 2:检索结果越多越好

过多的检索结果会引入噪声,降低 LLM 的生成质量。3~5 条高质量的相关文档比 20 条半相关的文档效果好得多。设定距离阈值(max_distance)过滤掉不相关的结果,是提高 RAG 质量的有效手段。

误区 3:忽略检索失败的信号

当所有检索结果的距离都很远时(比如 cosine > 1.5),说明知识库中没有与用户问题相关的信息。此时应该让 LLM 回答"我没有找到相关信息",而不是强行基于不相关的文档生成回答——后者更容易产生幻觉。


本章小结

Chroma 在 RAG 架构中扮演"记忆中枢"的角色,负责知识存储、语义检索和结构化过滤三个核心职责。核心要点回顾:第一,RAG 的五阶段链路是"提问→查询理解→检索→上下文组装→生成",Chroma 负责第③阶段;第二,检索是生成的上限——优化 RAG 的第一步永远是优化检索,而不是换更大的 LLM;第三,检索与生成之间的接口契约是"检索到的文档必须包含足够的信息来回答问题",这要求切分粒度合适、metadata 完整、top-K 适中;第四,设定距离阈值过滤不相关结果,避免 LLM 基于错误信息产生幻觉;第五,Chroma 不负责查询改写、prompt 组装和文本生成,这些由应用层或 LLM 框架处理。

下一节我们将实现一个端到端的 PDF 文档问答 Demo——从 PDF 加载到切分入库到检索生成,完整走通 RAG 的全流程。

基于 MIT 许可发布