从 0 到 1 搭建 RAG 智能小说问答系统

0 阅读32分钟

因为最近一直在看小说,所以就以金庸经典武侠小说《天龙八部》为示例,展示如何构建一个能够理解小说内容、回答人物关系、剧情发展等问题的AI智能问答系统。

本文章是我从零开始搭建一个完整的RAG(检索增强生成)智能小说问答系统的经验总结。

image.png

一、RAG 是什么?为什么需要它?

1.1 大语言模型的局限性

大语言模型(LLM)在自然语言处理领域取得了革命性突破,但在实际应用中存在三大核心局限性:

幻觉问题(Hallucination)

LLM 基于统计概率生成文本,当模型对某个问题缺乏足够知识时,会"一本正经地胡说八道"。这种现象被称为幻觉。其技术根源在于:模型的训练目标是预测下一个 token,而非追求事实准确性。当训练数据中缺乏相关事实时,模型倾向于生成"看起来合理"而非"真实"的内容。

典型案例

  • 询问不存在的书籍、论文、人物时,模型可能虚构出看似合理但完全虚假的信息
  • 在法律、医疗等专业领域,幻觉可能导致严重后果

知识截止(Knowledge Cutoff)

LLM 的知识完全依赖于训练数据,存在明显的时效性问题。GPT-4 的训练数据截止于 2023 年 4 月,这意味着:

  • 无法回答训练后发生的事件(如最新政策、新闻、技术发展)
  • 对于快速变化的领域(科技、金融、法律),知识可能已过时

领域知识缺失

通用 LLM 在特定领域的专业知识和私有数据方面存在先天不足:

场景问题
企业内部知识库员工手册、业务流程、技术文档等私有数据无法进入模型训练
行业专业知识医疗诊断标准、法律条文解读、金融合规要求等专业深度不足
实时数据数据库查询、API 调用结果等动态信息无法直接获取

image.png

1.2 RAG 的核心思想

RAG(Retrieval-Augmented Generation,检索增强生成) 是一种将信息检索与文本生成相结合的技术架构。其核心思想是:在生成回答之前,先从外部知识库中检索相关信息,然后将检索结果作为上下文,引导 LLM 生成更准确的回答。

RAG 的核心流程

RAG 系统的工作流程可以分为两个主要阶段:

索引阶段(离线):将知识库文档转换为向量并存储

  1. 文档加载:从各种数据源加载原始文档
  2. 文本切分:将长文档切分为适当大小的文本块
  3. 向量化:使用 Embedding 模型将文本转换为向量
  4. 存储:将向量存入向量数据库

查询阶段(在线):根据用户问题检索相关内容并生成答案

  1. 问题向量化:将用户问题转换为向量
  2. 相似度检索:在向量数据库中找到最相关的文档
  3. 上下文构建:将检索到的文档与问题组合
  4. 答案生成:LLM 基于上下文生成回答

image.png

三大核心组件

1. 检索器(Retriever):负责从知识库中找到与问题最相关的文档片段。

核心技术点:

  • 嵌入模型(Embedding Model):将文本转换为高维向量表示,语义相似的文本在向量空间中距离更近
  • 向量数据库:专为向量检索设计的数据库,支持高效的相似度搜索
  • 分块策略:将长文档切分为适当大小的块,平衡检索精度和上下文完整性

2. 上下文整合(Context Integration):将检索到的文档与用户问题组合成完整的提示词。这一步需要考虑:

  • 上下文长度限制:LLM 有 token 数量限制
  • 信息优先级:如何组织多个文档片段
  • 提示词工程:如何引导模型正确使用上下文

3. 生成器(Generator):LLM 基于增强后的提示词生成最终回答,核心优势:

  • 有据可依,减少幻觉
  • 可引用具体来源,增强可信度
  • 支持知识的实时更新(更新向量库即可,无需重新训练模型)

RAG 的核心价值

维度传统 LLMRAG
准确性依赖模型记忆,易产生幻觉有检索结果支撑,回答有据可查
时效性受训练数据截止日期限制更新知识库即可,无需重新训练
私有数据无法处理企业内部数据轻松集成私有知识库
可解释性难以追溯知识来源可引用具体文档出处
成本微调成本高、周期长部署简单、更新灵活

1.3 RAG vs 微调 vs 提示工程

在优化 LLM 应用时,主要有三种技术路线:提示工程(Prompt Engineering)、RAG、微调(Fine-tuning)。它们各有优劣,适用于不同场景。

详细对比分析

通过一个表格来看他们的区别:

维度提示工程RAG微调
核心能力引导模型输出格式和风格注入外部知识,增强事实准确性改变模型行为模式和专业能力
适用场景输出格式控制、任务指令化知识问答、文档分析、实时数据专业领域、特定任务、风格定制
知识更新无法更新知识实时更新(更新知识库)需要重新微调
私有数据不支持完美支持可支持但成本高
部署成本低(仅 API 调用)中等(向量库 + API)高(训练 + 部署)
技术门槛中等
响应延迟中等(检索开销)
幻觉控制效果有限效果显著效果有限

那我们又该如何选择呢??

image.png

组合策略:混合方案

在实际项目中,最佳实践往往是组合使用:

RAG + 提示工程(最常见):

  • RAG 负责知识检索
  • 提示工程控制输出格式、角色设定、安全边界

RAG + 微调

  • 微调让模型适应特定领域语言风格
  • RAG 注入最新知识和事实

三管齐下

  • 微调:定制领域专家角色
  • RAG:实时知识支持
  • 提示工程:精细控制输出

实践建议

场景推荐方案理由
企业知识库问答RAG + 提示工程需要实时更新知识,控制输出格式
法律文书生成微调 + RAG需要专业术语能力,同时需要法规检索
客服对话机器人RAG + 提示工程知识更新频繁,需要角色设定
代码生成助手微调 + RAG需要代码能力,可能需要检索 API 文档
数据分析报告提示工程主要是格式控制和指令遵循

1.4 RAG 的典型应用场景

场景一:企业知识库问答

企业内部积累了大量文档(员工手册、技术文档、流程规范等),员工查找信息效率低下。RAG 系统可以:

  • 新员工快速获取企业知识,降低培训成本
  • 统一知识传播,避免"口口相传"的信息失真
  • 7x24 小时可用,提升工作效率

典型实现:员工手册问答、IT 支持、流程查询

场景二:智能客服系统

传统客服需要培训大量人员,成本高且响应慢。RAG 智能客服可以:

  • 产品咨询:自动回答产品规格、价格、使用方法
  • 故障排查:基于知识库引导用户自助解决问题
  • 订单查询:结合业务系统 API 提供个性化信息

ROI 亮点

  • 减少 60-80% 的人工客服工作量
  • 平均响应时间从分钟级降至秒级
  • 答案一致性显著提升

场景三:文档分析与洞察

企业和研究机构需要处理大量文档,提取关键信息。RAG 可以:

  • 合同审查:快速定位关键条款,识别风险点
  • 研究分析:从大量论文/报告中提取关键信息
  • 竞品分析:自动对比分析多份产品文档
  • 合规检查:验证文档是否符合法规要求

场景四:专业领域助手

领域核心需求RAG 价值
医疗准确性、可追溯、合规提供有文献依据的建议,降低误诊风险
法律时效性、全面性、引用确保法规最新,提供案例参考
金融实时性、合规性、洞察结合实时市场数据,确保合规分析

二、RAG 系统架构总览

2.1 核心组件解析

RAG 系统包含五大核心组件,各司其职:

2.1.1 文档加载器(Document Loader)

文档加载器负责从各种数据源加载原始文档,是 RAG 系统的数据入口。

关键技术点

  • 编码检测:使用 chardet 库自动检测文件编码,解决中文文档乱码问题
  • 统一抽象:不同格式的文档最终都转换为统一的 Document 对象
  • 元数据保留:保留文件来源信息,便于追溯答案来源

支持的文档格式包括:TXT、Markdown、PDF、Word、HTML 等。

2.1.2 文本切分器(Text Splitter)

文本切分器将长文档切分为适合向量化的文本块(chunks)。切分策略直接影响检索质量。

为什么需要切分?

  1. LLM 有上下文长度限制,无法一次性处理超长文档
  2. 细粒度的文本块检索更精准,能找到真正相关的内容
  3. 向量检索在小文本块上效果更好,语义更聚焦

切分参数说明

参数作用最佳实践
chunk_size每个文本块的最大字符数500-1000 字符适合大多数场景
chunk_overlap相邻块的重叠区域设置为 chunk_size 的 10% 左右
separators分隔符优先级列表中文章节优先用句号、换行符

在5.3会详细讲解重叠模块

红色框内为重叠数据

2.1.3 Embedding 模型

Embedding 模型将文本转换为高维向量表示,是语义检索的核心。

我们可以选择大模型厂商提供的模型,我这里用的是硅基流动的:Qwen/Qwen3-Embedding-8B,大家可以自行选择

image.png

向量表示原理

文本转换为向量后,语义相似的文本在向量空间中距离更近。

举例子说明空间距离,例如,"机器学习"和"深度学习"的向量相似度会很高,而"机器学习"和"美食烹饪"的相似度会很低。

2.1.4 向量数据库(Vector Database)

向量数据库存储文档向量并支持高效相似度检索,我这里选择是:Chroma ,最大的优势是可以本地部署,无需额外安装,如果是企业级推荐使用 Milvus,功能更全面。

Chroma 的优势

  1. 轻量级:纯 Python 实现,无需额外依赖服务
  2. 持久化:支持本地磁盘存储,重启不丢失数据
  3. 易集成:与 LangChain 无缝对接
  4. 多 Collection:支持多书籍/多知识库隔离管理

2.1.5 大语言模型(LLM)

LLM 负责基于检索到的上下文生成最终答案,我这里选择的就是 deepseek,价格便宜,准确率也错。

image.png LLM 在 RAG 中的角色

  1. 理解问题:解析用户提问的意图
  2. 整合上下文:将检索到的文档片段与问题关联
  3. 生成答案:最后用户问题+检索到的上下文=输出准确、连贯的回答
  4. 引用来源:标注信息来源

2.2 数据流向

RAG 系统的数据流分为两个主要阶段:离线索引阶段在线查询阶段

离线索引阶段

索引阶段将原始文档转化为可检索的向量数据,工作流程如下:

  1. 文档加载:从文件系统或网络加载原始文档(txt、pdf、word....)
  2. 文本切分:将长文档切分为适当大小的文本块(50、100、200 等等)
  3. 向量化:使用 Embedding 模型将每个文本块转换为向量(使用第三方)
  4. 存储:将向量和原始文本一起存入向量数据库

image.png

在线查询阶段

查询阶段实现「检索 + 生成」的核心逻辑:

  1. 问题向量化:将用户问题转换为向量
  2. 相似度检索:在向量数据库中找到最相关的 Top-K 文档
  3. 上下文构建:将检索到的文档组合成上下文
  4. 答案生成:LLM 基于上下文生成最终答案

image.png

2.3 技术栈选型

我最终选择的是 FastAPI+LangChain+Chroma 组合,也是一个比较成熟的方案,以下是选择他们的理由。

LangChain:AI 应用开发框架

选择理由:

  • 模块化设计:文档加载、切分、向量存储、链式调用各司其职
  • 丰富生态:内置 100+ 文档加载器、50+ 向量库集成
  • 统一抽象:不同 LLM、Embedding 模型通过统一接口调用
  • 易于扩展:自定义 Chain、Tool、Agent 非常简单

Chroma:轻量级向量数据库

对比维度ChromaPineconeMilvus
部署复杂度零配置SaaS 服务需 Docker
本地开发✅ 原生支持❌ 需联网⚠️ 较重
持久化✅ 自动✅ 云端✅ 需配置
学习曲线
生产就绪中小规模大规模大规模

FastAPI:高性能 Web 框架

特性说明
异步原生async/await 原生支持,适合 I/O 密集场景
自动文档Swagger/ReDoc 自动生成,开发效率高
类型安全Pydantic 模型校验,减少运行时错误
性能优越基于 Starlette + Uvicorn,性能媲美 Go

技术栈全景图

image.png

三、环境搭建与项目结构

3.1 Python 环境准备

版本要求

本项目基于 Python 3.9+ 开发,推荐使用 Python 3.10 或 3.11 版本以获得最佳兼容性和性能。

创建虚拟环境

虚拟环境是 Python 项目的标准实践,它能隔离项目依赖,避免版本冲突。

# 创建虚拟环境
python -m venv venv

# 激活虚拟环境
# Windows (PowerShell)
venv\Scripts\Activate.ps1

# Linux/macOS
source venv/bin/activate

核心依赖

包名用途
fastapiWeb 框架,构建 REST API
langchainLLM 应用开发框架
langchain-openaiOpenAI 模型集成
chromadb向量数据库,支持持久化存储
pydantic-settings配置管理,基于类型注解

环境变量配置

项目使用 .env 文件管理敏感配置:

# OpenAI API 配置(必填)
OPENAI_API_KEY=your-api-key-here
OPENAI_BASE_URL=https://api.openai.com/v1

# 模型配置(可选)
MODEL_NAME=gpt-3.5-turbo
EMBEDDING_MODEL=text-embedding-ada-002

# 向量库配置(可选)
CHROMA_PERSIST_DIR=./chroma_db

重要.env 文件包含敏感信息,切勿提交到版本控制。


3.2 项目目录结构

python-rag-chroma/
├── app/                          # 应用核心代码
│   ├── main.py                  # FastAPI 应用入口
│   ├── config.py                # 配置管理
│   ├── models/
│   │   └── schemas.py           # Pydantic 数据模型
│   ├── routers/
│   │   └── api.py               # API 路由定义
│   ├── services/
│   │   ├── document_service.py  # 文档处理服务
│   │   ├── vector_store.py      # 向量存储服务
│   │   └── rag_service.py       # RAG 问答服务
│   └── static/                  # 静态资源
├── books/                        # 示例书籍数据
├── chroma_db/                    # 向量数据库持久化目录
├── .env                          # 环境变量
└── requirements.txt              # Python 依赖

四、构建 Web API 服务

将 RAG 系统封装为 Web API 服务,是实现生产部署和集成的关键步骤。下面介绍如何使用 FastAPI 构建一个功能完整的 RAG 智能问答 API 服务,包括文档上传、智能问答、文档管理等核心功能。

4.1 FastAPI 构建web服务

FastAPI简介

FastAPI 是一个现代、高性能的 Python Web 框架,非常适合构建 AI 应用的 API 服务。它的核心优势包括:

  1. 高性能:基于 Starlette 和 Pydantic,性能媲美 Node.js 和 Go
  2. 自动文档:内置 Swagger UI 和 ReDoc,无需额外配置即可生成交互式 API 文档
  3. 类型安全:基于 Python 类型注解,自动进行请求验证和响应序列化
  4. 异步支持:原生支持 async/await,适合处理 I/O 密集型的 AI 推理任务

应用入口设计

FastAPI 应用入口负责创建应用实例、配置中间件、注册路由和管理应用生命周期。实际代码位于 app/main.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from contextlib import asynccontextmanager
import os


@asynccontextmanager
async def lifespan(app: FastAPI)

...


# 创建 FastAPI 应用实例
app = FastAPI(
    title="RAG 智能问答系统",
    description="基于 LangChain + Chroma + FastAPI 构建的检索增强生成系统",
    version="1.0.0",
    lifespan=lifespan
)

# 配置 CORS 中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 注册路由
app.include_router(api.router)

# 挂载静态文件
_static_dir = os.path.join(os.path.dirname(__file__), "static")
app.mount("/static", StaticFiles(directory=_static_dir), name="static")

....

核心功能

  1. 生命周期管理:通过 lifespan 上下文管理器实现服务初始化和资源清理。应用启动时调用 api.init_services() 初始化所有服务实例,并自动检测是否需要加载默认数据。

  2. 中间件配置:CORS 中间件允许前端应用跨域访问 API,这是前后端分离架构的常见配置。

  3. 路由注册:通过 include_router 将 API 路由模块化,便于维护和扩展。

  4. 静态文件服务:挂载静态文件目录/static下,通过AI生成的html页面,提供 Web 界面访问。

配置管理

配置通过 Pydantic Settings 从环境变量加载,实现配置与代码分离。实际代码位于 app/config.py


from pydantic_settings import BaseSettings
from typing import Optional


class Settings(BaseSettings):
    """应用配置类,从环境变量加载配置项"""

    # OpenAI API 配置
    openai_api_key: str
    openai_base_url: str = "https://api.openai.com/v1"

    # 模型配置
    model_name: str = "gpt-3.5-turbo"
    embedding_model: str = "text-embedding-ada-002"

    # 向量库配置
    chroma_persist_dir: str = "./chroma_db"

    # 文档切块配置
    chunk_size: int = 500
    chunk_overlap: int = 50

    # 服务配置
    port: int = 8000

    class Config:
        """Pydantic 配置"""
        env_file = ".env"
        env_file_encoding = "utf-8"
        case_sensitive = False


# 全局配置实例,应用启动时加载
settings = Settings()

配置项包括:OpenAI API 密钥和地址、模型名称、向量库路径、文档切块参数、服务端口等。通过 .env 文件管理敏感配置,避免硬编码。

4.2 API 设计:上传文档、问答、文档管理

一个完整的 RAG 系统需要提供三类核心 API:文档上传、智能问答、文档管理。以下是 API 整体架构: image.png

API 端点概览

模块端点方法功能描述
核心/queryPOST智能问答
核心/uploadPOST上传文档
核心/healthGET健康检查
管理/admin/documentsGET/POST文档列表/添加
管理/admin/documents/{id}GET/PUT/DELETE文档 CRUD
管理/admin/collectionsGET/POSTCollection 管理

文档上传流程

文档上传采用异步后台处理模式,避免大文件上传时阻塞请求:

image.png

实际代码位于 app/routers/api.py

@router.post(
    "/upload",
    summary="上传文档",
    description="上传文档文件并自动向量化存储,支持 .txt、.md、.pdf 格式"
)
async def upload_document(background_tasks: BackgroundTasks, file: UploadFile = File(...)):
    """上传文档接口(异步后台处理)"""
    # 验证文件类型
    allowed_extensions = [".txt", ".md", ".pdf"]
    filename = file.filename
    file_ext = filename[filename.rfind("."):].lower()

.....

智能问答接口

智能问答是 RAG 系统的核心功能,接收用户问题并返回基于知识库的答案:

@router.post(
    "/query",
    response_model=QueryResponse,
    summary="智能问答",
    description="基于已上传文档进行检索增强问答"
)
async def query_documents(request: QueryRequest) -> QueryResponse:
    """根据问题检索相关文档并生成答案"""
    # 检查向量库是否就绪
    if not vector_store_service.is_ready():
        raise HTTPException(
            status_code=503,
            detail="向量库暂无数据,请先上传文档"
        )

    try:
        # 执行 RAG 查询
        result = rag_service.query(request.question, request.top_k)

        # 转换来源文档格式
        sources = [
            SourceDocument(
                content=src.get("content", ""),
                metadata=src.get("metadata", {})
            )
            for src in result.get("sources", [])
        ]

        return QueryResponse(
            answer=result.get("answer", ""),
            sources=sources
        )

    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"问答处理失败: {str(e)}"
        )

完整请求流程

image.png

启动服务

通过 uvicorn 启动 ASGI 服务:

# 开发模式(热重载)
uvicorn app.main:app --reload --port 8000

# 生产模式(多进程)
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4

五、构建文档处理管道

文档处理管道是 RAG 系统的第一道关卡,决定了知识库的质量上限,下面将深入探讨如何构建一个健壮、高效的文档处理管道。

5.1 文档加载器:支持多格式文档

文档加载器负责将各种格式的原始文档转换为统一的 Document 对象,是整个管道的起点。

5.1.1 统一的文档抽象

LangChain 提供了统一的 Document 数据结构:

from langchain_core.documents import Document

# Document 结构示意
document = Document(
    page_content="文档的文本内容...",  # 文本内容
    metadata={                         # 元数据
        "source": "example.pdf",       # 来源文件名
        "page": 1,                     # 页码(PDF)
    }
)

为什么需要统一抽象?

  1. 下游处理简化:无论 TXT、PDF 还是 Markdown,下游的切分器、向量化器都只处理 Document 对象
  2. 元数据传递:保留文档来源信息,便于追溯答案出处
  3. 可扩展性:新增格式只需添加对应的加载器,不影响现有逻辑

5.1.2 文本文件加载:编码检测的艺术

中文文档最常见的坑是编码问题。不同来源的文件可能使用 UTF-8、GBK、GB2312 等编码:

# app/services/document_service.py

import chardet
from langchain_community.document_loaders import TextLoader

def load_text_file(self, file_path: str) -> List[Document]:
    """加载文本文件,自动检测编码格式"""
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"文件不存在: {file_path}")

    # 读取二进制内容
    with open(file_path, "rb") as f:
        raw = f.read()

    # 使用 chardet 自动检测编码
    detected = chardet.detect(raw)
    encoding = detected.get("encoding") or "utf-8"

    # 使用检测到的编码加载文件
    loader = TextLoader(file_path, encoding=encoding)
    documents = loader.load()
    return documents

编码检测流程

image.png

5.1.3 PDF 文件加载:逐页解析

PDF 文档结构复杂,本系统使用 PyPDFLoader 进行逐页解析:

from langchain_community.document_loaders import PyPDFLoader

def load_pdf_file(self, file_path: str) -> List[Document]:
    """加载 PDF 文件,逐页解析为独立 Document"""
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"文件不存在: {file_path}")

    loader = PyPDFLoader(file_path)
    documents = loader.load()
    return documents

PyPDFLoader 的工作原理

image.png

5.1.4 上传文件处理:临时文件模式

Web 应用中,文件通过 HTTP 上传,需要先保存为临时文件再处理:

import tempfile

def load_uploaded_file(self, file_content: bytes, filename: str) -> List[Document]:
    """加载上传的文件内容"""
    file_ext = os.path.splitext(filename)[1].lower()

    # 创建临时文件保存上传内容
    with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as temp_file:
        temp_file.write(file_content)
        temp_path = temp_file.name

    try:
        # 根据文件类型选择加载器
        if file_ext == ".pdf":
            documents = self.load_pdf_file(temp_path)
        elif file_ext in [".txt", ".md"]:
            documents = self.load_text_file(temp_path)
        else:
            raise ValueError(f"不支持的文件类型: {file_ext}")

        # 添加文件名到元数据
        for doc in documents:
            doc.metadata["source"] = filename

        return documents
    finally:
        # 清理临时文件(防止磁盘空间泄漏)
        os.unlink(temp_path)

关键设计点

  1. try-finally 保证清理:即使解析失败,临时文件也会被删除
  2. 扩展名判断:通过文件名后缀区分文件类型
  3. 元数据注入:将原始文件名存入 metadata,便于后续溯源

5.2 文本切分策略:为什么切分?怎么切分?

文档加载后,长文本无法直接向量化存储——需要切分成适当大小的文本块(chunks)。

5.2.1 为什么必须切分?

原因一:模型上下文限制

模型上下文窗口相当于中文长度
GPT-3.5-turbo4K tokens约 2000 字
GPT-48K tokens约 4000 字
一本《红楼梦》-约 73 万字

即使是最强大的模型,也无法一次性装入整本小说。

原因二:检索精度考量

向量检索基于语义相似度,文档越长,语义越分散:

image.png 原因三:响应效率优化

检索 Top-K 文档作为上下文,文档块越小,能注入的信息片段越多:

假设 Token 限制 = 4000
Chunk Size = 500 字符  250 tokens
可注入文档数 = 4000 / 250  16 个不同片段

Chunk Size = 2000 字符  1000 tokens
可注入文档数 = 4000 / 1000  4 个片段

5.2.2 RecursiveCharacterTextSplitter 详解

本系统采用 RecursiveCharacterTextSplitter,其核心思想是递归使用分隔符层级

from langchain_text_splitters import RecursiveCharacterTextSplitter

self.text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,           # 目标块大小
    chunk_overlap=50,         # 块间重叠
    length_function=len,      # 长度计算方式
    separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""]
)

分隔符优先级设计思路

  1. \n\n(双换行):优先在段落边界切分,保持段落完整
  2. \n(单换行):段落内按行切分
  3. 。!?;(中文句末标点):按句子切分,保持语义完整
  4. (空格):词级切分
  5. ``(空字符串):字符级强制切分(最后手段)

5.3 切分参数调优:chunk_size 和 chunk_overlap 的权衡

5.3.1 chunk_size:块大小选择

chunk_size优点缺点适用场景
小(200-300)检索精度高,语义聚焦上下文碎片化,可能丢失关联问答、事实检索
中(500-800)平衡精度与上下文完整性需要调优 overlap通用 RAG 场景
大(1000+)上下文完整,适合长文理解语义分散,检索精度下降摘要生成、长文分析

实测数据对比(使用《天龙八部》测试):

chunk_size总块数平均块长度检索召回率生成质量评分
30015234298 字92.3%7.5/10
5009140495 字89.1%8.2/10
8005713792 字84.6%8.5/10

5.3.2 chunk_overlap:重叠区设计

chunk_overlap 控制相邻块之间的重叠字符数,保证跨边界语义的完整性。

为什么需要重叠?

重叠区域确保语义完整性。例如,一个关键概念可能跨越切分边界,重叠区域能保证至少有一个完整的语义块包含该概念。

image.png

用【天龙八部】这本小说的数据库来对照看会更清楚一些:

段落 1: image.png

段落 2: image.png

根据实际的拆分效果,可以对比出以下结果:

无重叠切分问题示例

原始文本: ……段誉道:“咱们大理国姓段的人成千上万,也不见得个个都会这点穴的法门。”……

  • Chunk A: ……段誉道:“咱们大理国姓段的人成千上万,也不见得个个都会这
  • Chunk B: 点穴的法门。”我不姓段,你叫我姓甚么”……

问题: 检索“点穴的法门”相关内容时,可能只命中 Chunk B,导致丢失了前半句的主语信息(大理国姓段的人),AI 无法准确回答是谁会或谁不会点穴。

有重叠切分解决方案
  • Chunk A: ……段誉道:“咱们大理国姓段的人成千上万,也不见得个个都会这点穴的法门。
  • Chunk B: 咱们大理国姓段的人成千上万,也不见得个个都会这点穴的法门。”我不姓段……

改进: 完整的语义(大理国姓段的人与点穴法门的关系)被保留在两个 chunk 中。无论检索到哪一个,都能获得完整的逻辑链条。

overlap 设置原则

chunk_size推荐 overlap比例
200-30020-5010-15%
500-80050-10010%
1000+100-20010-15%

本系统的默认配置

# app/config.py

class Settings(BaseSettings):
    # 文档切块配置
    chunk_size: int = 500      # 经过测试,适合小说类文档
    chunk_overlap: int = 50    # 约为 chunk_size 的 10%

5.4 完整处理流程

image.png

总结

文档处理管道是 RAG 系统的基础,核心在于:

  1. 文档加载:多格式支持 + 编码自动检测 + 统一 Document 抽象
  2. 文本切分:递归分隔符策略,优先保持语义完整性
  3. 参数调优:chunk_size 500-800 适合通用场景,overlap 约为 10%

六、构建向量存储服务

向量存储是 RAG 系统的核心组件,负责将文本转化为向量并支持高效的语义检索。下面将深入讲解如何基于 Chroma 构建向量存储服务,涵盖 Embedding 模型初始化、向量库管理、文档 CRUD 操作和相似度检索。

6.1 Embedding 模型初始化

Embedding 模型将文本映射到高维向量空间,使语义相似的文本在向量空间中距离更近。

6.1.1 向量化原理

image.png

关键概念

概念说明
向量维度常见 768、1024、1536 维,维度越高表达能力越强
语义相似度通过余弦距离、欧氏距离等度量
模型选择需平衡效果、成本、延迟

6.1.2 代码实现

本系统使用 硅基流动 的 text-embedding-ada-002 模型,兼容阿里云等国产 API:

# app/services/vector_store.py
# 作者: 
# 创建日期: 2026-03-11
# 职责: 管理 Chroma 向量数据库的初始化、存储和检索

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
from typing import List, Optional
import os

from app.config import settings


class VectorStoreService:
    """向量存储服务:封装 Chroma 向量数据库操作"""

    def __init__(self) -> None:
        """
        初始化向量存储服务
        创建 Embedding 模型和 Chroma 向量库实例
        """
        # 初始化 OpenAI Embedding 模型
        self.embeddings = OpenAIEmbeddings(
            openai_api_key=settings.openai_api_key,
            openai_api_base=settings.openai_base_url,
            model=settings.embedding_model,
            tiktoken_enabled=False,  # 禁用 tiktoken,直接传递文本
            check_embedding_ctx_length=False,  # 禁用长度检查
            chunk_size=25  # 阿里云 API 限制每批最多 25 个文本
        )

        # 向量库持久化路径
        self.persist_dir = settings.chroma_persist_dir

        # 确保持久化目录存在
        os.makedirs(self.persist_dir, exist_ok=True)

        # 向量存储实例(延迟初始化)
        self._vector_store: Optional[Chroma] = None

参数详解

参数作用配置原因
openai_api_keyAPI 密钥从环境变量加载,避免硬编码
openai_api_baseAPI 基础 URL支持阿里云等兼容 API
model模型名称ada-002 性价比最高
tiktoken_enabled=False禁用 OpenAI 分词器兼容非 OpenAI API
check_embedding_ctx_length=False禁用长度检查简化错误处理
chunk_size=25批处理大小阿里云 API 限制每批最多 25 条

6.2 相似度检索

6.2.1 检索原理

image.png 相似度度量:Chroma 默认使用余弦相似度,分数越小越相似。

分数范围相似度等级说明
0.0 - 0.3高度相关语义非常接近
0.3 - 0.5相关可作为上下文
0.5 - 0.7弱相关有一定关联
0.7 - 1.0不相关通常不应采用

6.2.2 检索方法实现

def similarity_search(self, query: str, top_k: int = 3) -> List[Document]:
    """
    语义相似度搜索
    :param query: 查询文本
    :param top_k: 返回的文档数量
    :return: 相似度最高的文档列表
    """
    return self.vector_store.similarity_search(query, k=top_k)

def similarity_search_with_score(self, query: str, top_k: int = 3) -> List[tuple]:
    """
    语义相似度搜索(带分数)
    :param query: 查询文本
    :param top_k: 返回的文档数量
    :return: (文档, 分数) 元组列表,分数越小越相似
    """
    return self.vector_store.similarity_search_with_score(query, k=top_k)

使用示例

# 基础检索
results = vector_store.similarity_search("林黛玉的性格特点是什么?", top_k=3)
for doc in results:
    print(f"内容: {doc.page_content[:100]}")
    print(f"来源: {doc.metadata.get('source', '未知')}")

# 带分数检索
scored = vector_store.similarity_search_with_score("贾宝玉和林黛玉的关系", top_k=5)
for doc, score in scored:
    print(f"分数: {score:.4f} | 内容: {doc.page_content[:50]}")

小结

本章核心要点:

  1. Embedding 初始化:配置 OpenAI 兼容 API,通过 tiktoken_enabled=Falsechunk_size=25 兼容阿里云 API
  2. 延迟初始化模式:通过 @property 装饰器实现按需加载,提升启动性能
  3. 文档 CRUD 操作:支持批量添加、单个文档增删改查,注意 Chroma 更新需要先删后加
  4. 相似度检索:分数越小越相似,0.3 以下为高度相关
  5. Collection 管理:支持多知识库隔离,通过切换 Collection 实现不同场景

后面将介绍如何将向量存储服务与 LLM 结合,构建完整的 RAG 问答系统。

七、构建 RAG 核心问答服务

下面将深入讲解 RAG(检索增强生成)的核心问答服务实现,包括检索流程\Prompt 设计、LLM调用以及来源引用等关键环节。

7.1 检索增强生成的完整流程

本RAG系统的核心思想是:在生成回答之前,先从知识库中检索相关文档,然后将这些文档作为上下文提供给LLM,从而生成更准确、更有依据的回答。

7.1.1 RAG 流程概览

一个完整的 RAG 问答流程包含以下步骤:

image.png

7.1.2 服务初始化与依赖注入

项目采用依赖注入模式,服务在应用启动时初始化:

# app/routers/api.py

# 服务实例(在应用启动时初始化)
document_service: DocumentService = None
vector_store_service: VectorStoreService = None
rag_service: RAGService = None

def init_services() -> None:
    """
    初始化所有服务实例
    在应用启动时调用一次
    """
    global document_service, vector_store_service, rag_service

    document_service = DocumentService()
    vector_store_service = VectorStoreService()
    rag_service = RAGService(vector_store_service)  # 注入依赖

依赖注入的优势

  1. 解耦:服务之间通过接口通信,便于替换实现
  2. 测试友好:可以注入 Mock 对象进行单元测试
  3. 生命周期统一管理:所有服务在应用启动时创建

7.2 Prompt 模板设计:让 AI 基于上下文回答

Prompt 是 RAG 系统与 LLM 交互的桥梁。一个好的 Prompt 模板能够引导模型生成更准确、更有依据的回答。

7.2.1 Prompt 设计原则

原则说明示例
明确角色告诉模型它的专业身份"你是一位专业的小说内容分析专家"
任务清晰明确说明要完成的任务"基于提供的上下文内容回答问题"
边界约束限制模型的回答范围"如果上下文中没有相关信息,明确说明"
格式规范指定期望的输出格式"回答要准确、简洁"

7.2.2 上下文构建策略

上下文(Context)由检索到的文档块拼接而成:

# app/services/rag_service.py

def query_with_context(self, question: str, top_k: int = 3) -> Dict[str, Any]:
    # 先检索相关文档
    relevant_docs = self.vector_store.similarity_search_with_score(question, top_k=top_k)

    # 构建上下文
    context_parts = []
    for doc, score in relevant_docs:
        context_parts.append(doc.page_content)

    # 使用分隔符连接各文档块
    context = "\n\n---\n\n".join(context_parts)

使用 \n\n---\n\n 作为分隔符,能够清晰区分不同文档块,帮助模型理解上下文边界。

7.2.3 Prompt 模板实现

# app/services/rag_service.py

prompt = f"""你是一位专业的小说内容分析专家,擅长从文本中提取关键信息并回答相关问题。

任务要求:
1. 严格基于提供的上下文内容回答问题
2. 如果上下文中没有相关信息,明确说明"根据现有资料无法回答该问题"
3. 回答要准确、简洁,引用原文关键段落支持你的答案
4. 使用中文回答,保持专业、客观的语气

上下文内容:
{context}

用户问题:{question}

请基于上述上下文,提供专业准确的回答:"""

关键设计点

  1. 角色定位:设定为"小说内容分析专家",引导模型以专业角度回答
  2. 约束条件:明确要求"严格基于上下文",避免模型"幻觉"
  3. 兜底机制:当没有相关信息时,要求明确说明而非编造

7.3 LLM 调用与回答生成

7.3.1 LLM 配置

项目使用 LangChain 封装的 ChatOpenAI:

# app/services/rag_service.py

from langchain_openai import ChatOpenAI
from app.config import settings

# 初始化 LLM 模型
self.llm = ChatOpenAI(
    openai_api_key=settings.openai_api_key,
    openai_api_base=settings.openai_base_url,  # 支持自定义 API 地址
    model_name=settings.model_name,
    temperature=0.7  # 控制创造性,0-1 之间
)

配置参数说明

参数说明建议值
temperature控制输出的随机性问答场景建议 0.5-0.7
model_name使用的模型名称根据实际 API 配置
openai_api_baseAPI 基础地址支持本地部署或第三方服务

7.3.2 两种调用方式

方式一:使用 RetrievalQA 链
# app/services/rag_service.py

def _create_qa_chain(self) -> RetrievalQA:
    """创建检索问答链"""
    # 创建检索器
    retriever = self.vector_store.vector_store.as_retriever(
        search_type="similarity",
        search_kwargs={"k": 3}  # 检索 top-3 相关文档
    )

    # 创建检索问答链
    chain = RetrievalQA.from_chain_type(
        llm=self.llm,
        chain_type="stuff",  # 将所有文档塞入一个 Prompt
        retriever=retriever,
        return_source_documents=True  # 返回来源文档
    )
    return chain

Chain Type 选择

类型说明适用场景
stuff所有文档放入一个 Prompt文档较少,上下文窗口足够
map_reduce先分别处理,再合并大量文档,需要并行处理
refine迭代优化答案需要渐进式完善回答
方式二:自定义 Prompt 调用
# app/services/rag_service.py

def query_with_context(self, question: str, top_k: int = 3) -> Dict[str, Any]:
    """执行问答查询并返回详细的上下文信息"""
    # 先检索相关文档
    relevant_docs = self.vector_store.similarity_search_with_score(question, top_k=top_k)

    # 构建上下文和来源
    context_parts = []
    sources = []
    for doc, score in relevant_docs:
        context_parts.append(doc.page_content)
        sources.append({
            "content": doc.page_content,
            "metadata": doc.metadata,
            "relevance_score": float(score)
        })

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

    # 构建带上下文的提示词
    prompt = f"""你是一位专业的小说内容分析专家..."""

    # 调用 LLM 生成回答
    response = self.llm.invoke(prompt)
    answer = response.content

    return {
        "answer": answer,
        "sources": sources,
        "context": context
    }

7.3.3 调用流程图

image.png

7.4 返回来源引用:让答案可追溯

7.4.1 来源引用的重要性

在 RAG 系统中,返回来源引用至关重要:

  1. 可信度:用户可以验证答案的真实性
  2. 可追溯:方便用户深入阅读原文
  3. 透明度:让用户了解 AI 的"思考依据"
  4. 调试帮助:开发者可以定位检索问题

7.4.2 来源信息结构

# app/models/schemas.py

class SourceDocument(BaseModel):
    """来源文档模型"""
    content: str = Field(..., description="文档内容")
    metadata: Dict[str, Any] = Field(default_factory=dict, description="文档元数据")
    relevance_score: Optional[float] = Field(default=None, description="相关性分数")

字段说明

字段类型说明
contentstring文档块的原始内容
metadatadict包含来源文件、页码等元信息
relevance_scorefloat与问题的相似度分数(越小越相似)

7.4.3 API 响应示例

{
  "answer": "好,我现在要回答用户的问题:“段誉是谁”。首先...",
  "sources": [
    {
      "content": "段誉奔出几步,只因走得急了,足下一个踉跄,险些跌倒...",
      "metadata": {
        "source": "天龙八部.txt",
        "chunk_id": 42
      },
      "relevance_score": 0.15
    }
  ]
}

总结

以上内容就是我搭建小说RAG智能问题系统的所有经验了,从开始做这个系统我就一直在和AI沟通“我想做一个RAG系统需要那些知识点..”所有的内容都是和AI一点点沟通做出来的,包括本文大部分内容,都是通过AI生成(如有错误,多多包涵)。 在AI时代,我们学习的时候只需要把大致的思路梳理清楚就够了,有了思路那具体实现方案变得异常简单了。

image.png

需要完整代码可以评论区留言