因为最近一直在看小说,所以就以金庸经典武侠小说《天龙八部》为示例,展示如何构建一个能够理解小说内容、回答人物关系、剧情发展等问题的AI智能问答系统。
本文章是我从零开始搭建一个完整的RAG(检索增强生成)智能小说问答系统的经验总结。
一、RAG 是什么?为什么需要它?
1.1 大语言模型的局限性
大语言模型(LLM)在自然语言处理领域取得了革命性突破,但在实际应用中存在三大核心局限性:
幻觉问题(Hallucination)
LLM 基于统计概率生成文本,当模型对某个问题缺乏足够知识时,会"一本正经地胡说八道"。这种现象被称为幻觉。其技术根源在于:模型的训练目标是预测下一个 token,而非追求事实准确性。当训练数据中缺乏相关事实时,模型倾向于生成"看起来合理"而非"真实"的内容。
典型案例:
- 询问不存在的书籍、论文、人物时,模型可能虚构出看似合理但完全虚假的信息
- 在法律、医疗等专业领域,幻觉可能导致严重后果
知识截止(Knowledge Cutoff)
LLM 的知识完全依赖于训练数据,存在明显的时效性问题。GPT-4 的训练数据截止于 2023 年 4 月,这意味着:
- 无法回答训练后发生的事件(如最新政策、新闻、技术发展)
- 对于快速变化的领域(科技、金融、法律),知识可能已过时
领域知识缺失
通用 LLM 在特定领域的专业知识和私有数据方面存在先天不足:
| 场景 | 问题 |
|---|---|
| 企业内部知识库 | 员工手册、业务流程、技术文档等私有数据无法进入模型训练 |
| 行业专业知识 | 医疗诊断标准、法律条文解读、金融合规要求等专业深度不足 |
| 实时数据 | 数据库查询、API 调用结果等动态信息无法直接获取 |
1.2 RAG 的核心思想
RAG(Retrieval-Augmented Generation,检索增强生成) 是一种将信息检索与文本生成相结合的技术架构。其核心思想是:在生成回答之前,先从外部知识库中检索相关信息,然后将检索结果作为上下文,引导 LLM 生成更准确的回答。
RAG 的核心流程
RAG 系统的工作流程可以分为两个主要阶段:
索引阶段(离线):将知识库文档转换为向量并存储
- 文档加载:从各种数据源加载原始文档
- 文本切分:将长文档切分为适当大小的文本块
- 向量化:使用 Embedding 模型将文本转换为向量
- 存储:将向量存入向量数据库
查询阶段(在线):根据用户问题检索相关内容并生成答案
- 问题向量化:将用户问题转换为向量
- 相似度检索:在向量数据库中找到最相关的文档
- 上下文构建:将检索到的文档与问题组合
- 答案生成:LLM 基于上下文生成回答
三大核心组件
1. 检索器(Retriever):负责从知识库中找到与问题最相关的文档片段。
核心技术点:
- 嵌入模型(Embedding Model):将文本转换为高维向量表示,语义相似的文本在向量空间中距离更近
- 向量数据库:专为向量检索设计的数据库,支持高效的相似度搜索
- 分块策略:将长文档切分为适当大小的块,平衡检索精度和上下文完整性
2. 上下文整合(Context Integration):将检索到的文档与用户问题组合成完整的提示词。这一步需要考虑:
- 上下文长度限制:LLM 有 token 数量限制
- 信息优先级:如何组织多个文档片段
- 提示词工程:如何引导模型正确使用上下文
3. 生成器(Generator):LLM 基于增强后的提示词生成最终回答,核心优势:
- 有据可依,减少幻觉
- 可引用具体来源,增强可信度
- 支持知识的实时更新(更新向量库即可,无需重新训练模型)
RAG 的核心价值
| 维度 | 传统 LLM | RAG |
|---|---|---|
| 准确性 | 依赖模型记忆,易产生幻觉 | 有检索结果支撑,回答有据可查 |
| 时效性 | 受训练数据截止日期限制 | 更新知识库即可,无需重新训练 |
| 私有数据 | 无法处理企业内部数据 | 轻松集成私有知识库 |
| 可解释性 | 难以追溯知识来源 | 可引用具体文档出处 |
| 成本 | 微调成本高、周期长 | 部署简单、更新灵活 |
1.3 RAG vs 微调 vs 提示工程
在优化 LLM 应用时,主要有三种技术路线:提示工程(Prompt Engineering)、RAG、微调(Fine-tuning)。它们各有优劣,适用于不同场景。
详细对比分析
通过一个表格来看他们的区别:
| 维度 | 提示工程 | RAG | 微调 |
|---|---|---|---|
| 核心能力 | 引导模型输出格式和风格 | 注入外部知识,增强事实准确性 | 改变模型行为模式和专业能力 |
| 适用场景 | 输出格式控制、任务指令化 | 知识问答、文档分析、实时数据 | 专业领域、特定任务、风格定制 |
| 知识更新 | 无法更新知识 | 实时更新(更新知识库) | 需要重新微调 |
| 私有数据 | 不支持 | 完美支持 | 可支持但成本高 |
| 部署成本 | 低(仅 API 调用) | 中等(向量库 + API) | 高(训练 + 部署) |
| 技术门槛 | 低 | 中等 | 高 |
| 响应延迟 | 低 | 中等(检索开销) | 低 |
| 幻觉控制 | 效果有限 | 效果显著 | 效果有限 |
那我们又该如何选择呢??
组合策略:混合方案
在实际项目中,最佳实践往往是组合使用:
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)。切分策略直接影响检索质量。
为什么需要切分?
- LLM 有上下文长度限制,无法一次性处理超长文档
- 细粒度的文本块检索更精准,能找到真正相关的内容
- 向量检索在小文本块上效果更好,语义更聚焦
切分参数说明:
| 参数 | 作用 | 最佳实践 |
|---|---|---|
chunk_size | 每个文本块的最大字符数 | 500-1000 字符适合大多数场景 |
chunk_overlap | 相邻块的重叠区域 | 设置为 chunk_size 的 10% 左右 |
separators | 分隔符优先级列表 | 中文章节优先用句号、换行符 |
在5.3会详细讲解重叠模块
红色框内为重叠数据
2.1.3 Embedding 模型
Embedding 模型将文本转换为高维向量表示,是语义检索的核心。
我们可以选择大模型厂商提供的模型,我这里用的是硅基流动的:Qwen/Qwen3-Embedding-8B,大家可以自行选择
向量表示原理:
文本转换为向量后,语义相似的文本在向量空间中距离更近。
举例子说明空间距离,例如,"机器学习"和"深度学习"的向量相似度会很高,而"机器学习"和"美食烹饪"的相似度会很低。
2.1.4 向量数据库(Vector Database)
向量数据库存储文档向量并支持高效相似度检索,我这里选择是:Chroma ,最大的优势是可以本地部署,无需额外安装,如果是企业级推荐使用 Milvus,功能更全面。
Chroma 的优势:
- 轻量级:纯 Python 实现,无需额外依赖服务
- 持久化:支持本地磁盘存储,重启不丢失数据
- 易集成:与 LangChain 无缝对接
- 多 Collection:支持多书籍/多知识库隔离管理
2.1.5 大语言模型(LLM)
LLM 负责基于检索到的上下文生成最终答案,我这里选择的就是 deepseek,价格便宜,准确率也错。
LLM 在 RAG 中的角色:
- 理解问题:解析用户提问的意图
- 整合上下文:将检索到的文档片段与问题关联
- 生成答案:最后用户问题+检索到的上下文=输出准确、连贯的回答
- 引用来源:标注信息来源
2.2 数据流向
RAG 系统的数据流分为两个主要阶段:离线索引阶段和在线查询阶段。
离线索引阶段
索引阶段将原始文档转化为可检索的向量数据,工作流程如下:
- 文档加载:从文件系统或网络加载原始文档(txt、pdf、word....)
- 文本切分:将长文档切分为适当大小的文本块(50、100、200 等等)
- 向量化:使用 Embedding 模型将每个文本块转换为向量(使用第三方)
- 存储:将向量和原始文本一起存入向量数据库
在线查询阶段
查询阶段实现「检索 + 生成」的核心逻辑:
- 问题向量化:将用户问题转换为向量
- 相似度检索:在向量数据库中找到最相关的 Top-K 文档
- 上下文构建:将检索到的文档组合成上下文
- 答案生成:LLM 基于上下文生成最终答案
2.3 技术栈选型
我最终选择的是 FastAPI+LangChain+Chroma 组合,也是一个比较成熟的方案,以下是选择他们的理由。
LangChain:AI 应用开发框架
选择理由:
- 模块化设计:文档加载、切分、向量存储、链式调用各司其职
- 丰富生态:内置 100+ 文档加载器、50+ 向量库集成
- 统一抽象:不同 LLM、Embedding 模型通过统一接口调用
- 易于扩展:自定义 Chain、Tool、Agent 非常简单
Chroma:轻量级向量数据库
| 对比维度 | Chroma | Pinecone | Milvus |
|---|---|---|---|
| 部署复杂度 | 零配置 | SaaS 服务 | 需 Docker |
| 本地开发 | ✅ 原生支持 | ❌ 需联网 | ⚠️ 较重 |
| 持久化 | ✅ 自动 | ✅ 云端 | ✅ 需配置 |
| 学习曲线 | 低 | 中 | 高 |
| 生产就绪 | 中小规模 | 大规模 | 大规模 |
FastAPI:高性能 Web 框架
| 特性 | 说明 |
|---|---|
| 异步原生 | async/await 原生支持,适合 I/O 密集场景 |
| 自动文档 | Swagger/ReDoc 自动生成,开发效率高 |
| 类型安全 | Pydantic 模型校验,减少运行时错误 |
| 性能优越 | 基于 Starlette + Uvicorn,性能媲美 Go |
技术栈全景图
三、环境搭建与项目结构
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
核心依赖
| 包名 | 用途 |
|---|---|
fastapi | Web 框架,构建 REST API |
langchain | LLM 应用开发框架 |
langchain-openai | OpenAI 模型集成 |
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 服务。它的核心优势包括:
- 高性能:基于 Starlette 和 Pydantic,性能媲美 Node.js 和 Go
- 自动文档:内置 Swagger UI 和 ReDoc,无需额外配置即可生成交互式 API 文档
- 类型安全:基于 Python 类型注解,自动进行请求验证和响应序列化
- 异步支持:原生支持 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")
....
核心功能:
-
生命周期管理:通过
lifespan上下文管理器实现服务初始化和资源清理。应用启动时调用api.init_services()初始化所有服务实例,并自动检测是否需要加载默认数据。 -
中间件配置:CORS 中间件允许前端应用跨域访问 API,这是前后端分离架构的常见配置。
-
路由注册:通过
include_router将 API 路由模块化,便于维护和扩展。 -
静态文件服务:挂载静态文件目录
/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 整体架构:
API 端点概览
| 模块 | 端点 | 方法 | 功能描述 |
|---|---|---|---|
| 核心 | /query | POST | 智能问答 |
| 核心 | /upload | POST | 上传文档 |
| 核心 | /health | GET | 健康检查 |
| 管理 | /admin/documents | GET/POST | 文档列表/添加 |
| 管理 | /admin/documents/{id} | GET/PUT/DELETE | 文档 CRUD |
| 管理 | /admin/collections | GET/POST | Collection 管理 |
文档上传流程
文档上传采用异步后台处理模式,避免大文件上传时阻塞请求:
实际代码位于 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)}"
)
完整请求流程
启动服务
通过 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)
}
)
为什么需要统一抽象?
- 下游处理简化:无论 TXT、PDF 还是 Markdown,下游的切分器、向量化器都只处理
Document对象 - 元数据传递:保留文档来源信息,便于追溯答案出处
- 可扩展性:新增格式只需添加对应的加载器,不影响现有逻辑
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
编码检测流程:
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 的工作原理:
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)
关键设计点:
- try-finally 保证清理:即使解析失败,临时文件也会被删除
- 扩展名判断:通过文件名后缀区分文件类型
- 元数据注入:将原始文件名存入 metadata,便于后续溯源
5.2 文本切分策略:为什么切分?怎么切分?
文档加载后,长文本无法直接向量化存储——需要切分成适当大小的文本块(chunks)。
5.2.1 为什么必须切分?
原因一:模型上下文限制
| 模型 | 上下文窗口 | 相当于中文长度 |
|---|---|---|
| GPT-3.5-turbo | 4K tokens | 约 2000 字 |
| GPT-4 | 8K tokens | 约 4000 字 |
| 一本《红楼梦》 | - | 约 73 万字 |
即使是最强大的模型,也无法一次性装入整本小说。
原因二:检索精度考量
向量检索基于语义相似度,文档越长,语义越分散:
原因三:响应效率优化
检索 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", "。", "!", "?", ";", " ", ""]
)
分隔符优先级设计思路:
\n\n(双换行):优先在段落边界切分,保持段落完整\n(单换行):段落内按行切分。!?;(中文句末标点):按句子切分,保持语义完整(空格):词级切分- ``(空字符串):字符级强制切分(最后手段)
5.3 切分参数调优:chunk_size 和 chunk_overlap 的权衡
5.3.1 chunk_size:块大小选择
| chunk_size | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 小(200-300) | 检索精度高,语义聚焦 | 上下文碎片化,可能丢失关联 | 问答、事实检索 |
| 中(500-800) | 平衡精度与上下文完整性 | 需要调优 overlap | 通用 RAG 场景 |
| 大(1000+) | 上下文完整,适合长文理解 | 语义分散,检索精度下降 | 摘要生成、长文分析 |
实测数据对比(使用《天龙八部》测试):
| chunk_size | 总块数 | 平均块长度 | 检索召回率 | 生成质量评分 |
|---|---|---|---|---|
| 300 | 15234 | 298 字 | 92.3% | 7.5/10 |
| 500 | 9140 | 495 字 | 89.1% | 8.2/10 |
| 800 | 5713 | 792 字 | 84.6% | 8.5/10 |
5.3.2 chunk_overlap:重叠区设计
chunk_overlap 控制相邻块之间的重叠字符数,保证跨边界语义的完整性。
为什么需要重叠?
重叠区域确保语义完整性。例如,一个关键概念可能跨越切分边界,重叠区域能保证至少有一个完整的语义块包含该概念。
用【天龙八部】这本小说的数据库来对照看会更清楚一些:
段落 1:
段落 2:
根据实际的拆分效果,可以对比出以下结果:
无重叠切分问题示例
原始文本: ……段誉道:“咱们大理国姓段的人成千上万,也不见得个个都会这点穴的法门。”……
- Chunk A: ……段誉道:“咱们大理国姓段的人成千上万,也不见得个个都会这
- Chunk B: 点穴的法门。”我不姓段,你叫我姓甚么”……
问题: 检索“点穴的法门”相关内容时,可能只命中 Chunk B,导致丢失了前半句的主语信息(大理国姓段的人),AI 无法准确回答是谁会或谁不会点穴。
有重叠切分解决方案
- Chunk A: ……段誉道:“咱们大理国姓段的人成千上万,也不见得个个都会这点穴的法门。
- Chunk B: 咱们大理国姓段的人成千上万,也不见得个个都会这点穴的法门。”我不姓段……
改进: 完整的语义(大理国姓段的人与点穴法门的关系)被保留在两个 chunk 中。无论检索到哪一个,都能获得完整的逻辑链条。
overlap 设置原则:
| chunk_size | 推荐 overlap | 比例 |
|---|---|---|
| 200-300 | 20-50 | 10-15% |
| 500-800 | 50-100 | 10% |
| 1000+ | 100-200 | 10-15% |
本系统的默认配置:
# app/config.py
class Settings(BaseSettings):
# 文档切块配置
chunk_size: int = 500 # 经过测试,适合小说类文档
chunk_overlap: int = 50 # 约为 chunk_size 的 10%
5.4 完整处理流程
总结
文档处理管道是 RAG 系统的基础,核心在于:
- 文档加载:多格式支持 + 编码自动检测 + 统一 Document 抽象
- 文本切分:递归分隔符策略,优先保持语义完整性
- 参数调优:chunk_size 500-800 适合通用场景,overlap 约为 10%
六、构建向量存储服务
向量存储是 RAG 系统的核心组件,负责将文本转化为向量并支持高效的语义检索。下面将深入讲解如何基于 Chroma 构建向量存储服务,涵盖 Embedding 模型初始化、向量库管理、文档 CRUD 操作和相似度检索。
6.1 Embedding 模型初始化
Embedding 模型将文本映射到高维向量空间,使语义相似的文本在向量空间中距离更近。
6.1.1 向量化原理
关键概念:
| 概念 | 说明 |
|---|---|
| 向量维度 | 常见 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_key | API 密钥 | 从环境变量加载,避免硬编码 |
openai_api_base | API 基础 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 检索原理
相似度度量: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]}")
小结
本章核心要点:
- Embedding 初始化:配置 OpenAI 兼容 API,通过
tiktoken_enabled=False和chunk_size=25兼容阿里云 API - 延迟初始化模式:通过
@property装饰器实现按需加载,提升启动性能 - 文档 CRUD 操作:支持批量添加、单个文档增删改查,注意 Chroma 更新需要先删后加
- 相似度检索:分数越小越相似,0.3 以下为高度相关
- Collection 管理:支持多知识库隔离,通过切换 Collection 实现不同场景
后面将介绍如何将向量存储服务与 LLM 结合,构建完整的 RAG 问答系统。
七、构建 RAG 核心问答服务
下面将深入讲解 RAG(检索增强生成)的核心问答服务实现,包括检索流程\Prompt 设计、LLM调用以及来源引用等关键环节。
7.1 检索增强生成的完整流程
本RAG系统的核心思想是:在生成回答之前,先从知识库中检索相关文档,然后将这些文档作为上下文提供给LLM,从而生成更准确、更有依据的回答。
7.1.1 RAG 流程概览
一个完整的 RAG 问答流程包含以下步骤:
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) # 注入依赖
依赖注入的优势:
- 解耦:服务之间通过接口通信,便于替换实现
- 测试友好:可以注入 Mock 对象进行单元测试
- 生命周期统一管理:所有服务在应用启动时创建
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}
请基于上述上下文,提供专业准确的回答:"""
关键设计点:
- 角色定位:设定为"小说内容分析专家",引导模型以专业角度回答
- 约束条件:明确要求"严格基于上下文",避免模型"幻觉"
- 兜底机制:当没有相关信息时,要求明确说明而非编造
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_base | API 基础地址 | 支持本地部署或第三方服务 |
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 调用流程图
7.4 返回来源引用:让答案可追溯
7.4.1 来源引用的重要性
在 RAG 系统中,返回来源引用至关重要:
- 可信度:用户可以验证答案的真实性
- 可追溯:方便用户深入阅读原文
- 透明度:让用户了解 AI 的"思考依据"
- 调试帮助:开发者可以定位检索问题
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="相关性分数")
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
content | string | 文档块的原始内容 |
metadata | dict | 包含来源文件、页码等元信息 |
relevance_score | float | 与问题的相似度分数(越小越相似) |
7.4.3 API 响应示例
{
"answer": "好,我现在要回答用户的问题:“段誉是谁”。首先...",
"sources": [
{
"content": "段誉奔出几步,只因走得急了,足下一个踉跄,险些跌倒...",
"metadata": {
"source": "天龙八部.txt",
"chunk_id": 42
},
"relevance_score": 0.15
}
]
}
总结
以上内容就是我搭建小说RAG智能问题系统的所有经验了,从开始做这个系统我就一直在和AI沟通“我想做一个RAG系统需要那些知识点..”所有的内容都是和AI一点点沟通做出来的,包括本文大部分内容,都是通过AI生成(如有错误,多多包涵)。 在AI时代,我们学习的时候只需要把大致的思路梳理清楚就够了,有了思路那具体实现方案变得异常简单了。
需要完整代码可以评论区留言