
RAG核心
文档分割
在RAG(检索增强生成)系统中,文档分割(Chunking)是至关重要的一步,它直接影响检索质量和最终生成答案的效果。以下是几种主流的文档分割方式及其核心特点,供您参考。
| 分割方法 | 核心原理 | 优点 | 缺点 | 典型适用场景 |
|---|---|---|---|---|
| 固定大小分块 | 按预设字符数/Token数均等切割 | 实现简单,计算效率高 | 易割裂语义,破坏句子完整性 | 日志分析、预处理等对语义要求不高的任务 |
| 递归字符分块 | 按分隔符优先级列表(如["\n\n", "\n", "。", " "])递归切割 |
通用性强,在语义完整性和长度控制间取得平衡 | 对无标点、结构松散文本效果下降 | 中英文混合文本、通用文本 |
| 语义分块 | 计算句子嵌入的相似度,在语义转折点切割 | 块内语义一致性高,边界自然 | 计算成本高,实现复杂 | 法律、学术论文等对准确性要求极高的长文 |
| 基于文档结构的分块 | 利用标题、章节等标记(如Markdown的#)切割 |
保留宏观逻辑结构,元数据丰富 | 依赖文档本身的结构化质量 | 技术手册、论文、API文档等高度结构化内容 |
| 延迟分块 | 查询时动态分割大片段 | 保留完整上下文,避免预处理信息丢失 | 查询延迟高,计算成本大 | 文档频繁变更或对精度极其敏感的场景(如法律、医疗) |
💡 选择分割策略的实用建议
选择哪种策略并没有放之四海而皆准的答案,关键在于匹配你的具体需求。你可以从以下几个方面进行考量:
- 评估文档特性:分析你的文档类型(是技术手册、新闻还是对话记录)、结构强度(是否有清晰的标题、段落)和信息密度。
- 明确任务目标 :权衡对检索精度 和处理效率的要求。例如,对答案准确性要求极高的场景(如法律条款分析)值得投入更多计算资源使用语义分块。
- 考虑系统约束:确保分割后的块长度加上你的查询和指令后,不会超出所选LLM的上下文窗口限制。
- 迭代与测试 :最重要的步骤是实验。你可以先用递归分块作为稳健的基线,然后根据实际效果(如检索召回率、生成答案质量)进行调整和优化。
Query改写
在 RAG 系统中,用户的原始查询可能存在信息缺失、表述模糊或包含噪声等问题,直接检索往往效果不佳。查询改写 技术正是在检索之前,对原始查询进行优化,以提升检索效果的关键步骤。
下表详细介绍了 RAG 中主要的查询改写方法及其核心思路。
| 方法名称 | 核心思路 | 典型应用场景 |
|---|---|---|
| 多查询重写 (Multi-Query) | 针对原始查询,利用大模型生成多个在语义上相关但表述不同的查询变体,并行检索后综合结果,以扩大检索范围并提升召回率。 | 用户查询表述简短、模糊或存在多种理解角度时。 |
| 假设文档嵌入 (HyDE) | 要求大模型根据原始查询生成一个假设的理想答案文档,然后使用这个假设文档的嵌入向量(而非原始查询的向量)进行检索。 | 通用场景,尤其适用于零样本情况,旨在缩小查询与答案文档之间的语义鸿沟。 |
| 上下文依赖改写 | 在多轮对话中,自动将对话历史中提及的关键信息补充到当前查询中,使其成为独立的、完整的查询。 | 当前查询包含"这个"、"它"、"上面说的"等指代词的对话场景。 |
| 对比型查询改写 | 当查询涉及比较多个实体时,在查询中显式添加"与...的区别"、"优缺点对比"等比较性词汇,使查询意图更明确。 | 用户提问涉及两个或多个实体比较的场景。 |
| 查询分解 (子查询改写) | 将一个复杂的、包含多个子问题的查询拆解成一系列简单的子查询,分别检索后再综合答案。 | 处理复杂或多主题的查询,避免不同主题信息相互干扰。 |
| 退一步提示 (Step-Back Prompting) | 让大模型从包含具体细节的原始查询中,抽象出更高层次的概念或通用原则,先基于这个抽象问题进行检索,为回答具体问题提供背景框架。 | 查询非常具体或专业,直接检索相关文档困难时。 |
| 去噪与关键词提取 | 去除查询中冗余的语气词、修饰语和背景信息,直接提取核心关键词,使查询更简洁,更适合关键词检索。 | 查询冗长、包含大量无关细节(噪声)时。 |
💡 如何选择适合的改写方法
选择合适的查询改写方法取决于你的具体应用场景和查询特点:
- 通用场景的稳健选择 :多查询重写 和假设文档嵌入 是两种应用广泛且效果稳定的方法,适合作为大多数系统的起点。
- 处理对话上下文 :如果你的应用包含多轮对话,上下文依赖改写 是必不可少的,它能有效解决指代消解问题。
- 应对复杂查询 :对于需要进行对比或包含多个意图的复杂查询,可以尝试对比型查询改写 或查询分解。
- 追求深度理解 :当需要模型进行更深层推理时,退一步提示 能引导模型从基本原理出发,提升答案的深度和准确性。
🚀 进阶技巧
- 组合使用 :这些方法并不互斥。例如,可以先将一个复杂的对比查询分解 成若干子查询,再对每个子查询应用HyDE 或多查询重写,最后综合所有结果。
- 评估与迭代:引入查询改写后,务必通过检索召回率、答案准确性等指标评估其效果,并根据反馈进行迭代优化。改写后的查询质量很大程度上依赖于所用大模型的能力。
希望以上梳理能帮助你理解和选择适合的查询改写方法。如果你有特定的应用场景或遇到具体问题,欢迎提出,我们可以继续深入探讨。
Embedding & Rerank
在 RAG 系统中,Embedding 模型和重排模型分别承担着"海量筛选"和"精准排序"的关键角色。下面这个表格清晰地展示了它们的核心区别,并列举了当前主流的选择。
| 特性维度 | Embedding模型 (负责"初步召回") | 重排模型 (负责"精细排序") |
|---|---|---|
| 核心功能 | 将文本转换为向量,通过计算向量间相似度,从海量文档中快速找出大量可能相关的候选文档。目标是"广撒网",提高召回率。 | 对初步召回的少量候选文档进行深度语义分析,精确评估其与查询的相关性并重新打分排序。目标是"精捕捞",提高精确率。 |
| 工作阶段 | 检索流程的第一阶段 | 检索流程的第二阶段(在Embedding模型之后工作) |
| 典型输出 | 文本的稠密向量表示 | 查询-文档对的相关性分数 |
| 计算复杂度/速度 | 相对较低/较快(可预先计算文档向量) | 相对较高/较慢(需实时计算查询与每个候选文档的相关性) |
| 核心技术/架构 | 双编码器,如BGE、GTE、OpenAI text-embedding系列 | 交叉编码器(如BGE-Reranker, Cohere Reranker)或LLM |
💡 主流模型选择与协作策略
了解区别后,如何为你的项目选择合适的模型并让它们协同工作呢?
-
Embedding模型选型参考
- 通用多语言/中文场景 :BGE系列 (如
BAAI/bge-large-zh-v1.5)和GTE系列是不错的选择,它们在多项基准测试中表现优异,且开源免费 。 - 依赖OpenAI生态 :可选用
text-embedding-3-small或text-embedding-3-large,需注意其为闭源API服务 。 - 特定需求 :处理超长文本可考虑
Nomic-Embed;专门优化中文可关注M3E系列 。
- 通用多语言/中文场景 :BGE系列 (如
-
重排模型选型参考
- 开源优选 :BGE-Reranker 系列(如
BAAI/bge-reranker-base)是开源方案中的佼佼者,支持中英文,平衡了效果与成本 。 - 商用API服务 :Cohere Rerank 和 Voyage Rerank 提供高精度的托管服务,适合追求效果且不愿自运维的团队 。
- 效率与精度平衡 :ColBERT 采用"延迟交互"机制,在大规模文档库的检索中,能在精度和速度间取得较好平衡 。
- 开源优选 :BGE-Reranker 系列(如
-
高效协作策略
最佳实践是采用 "分层处理"管道 :
- 第一层:快速召回。使用 Embedding 模型从全部文档中快速检索出成百上千的候选文档(如Top 200)。
- 第二层:精准重排。将第一步得到的大量候选文档交给重排模型,由其精排出最相关的少量结果(如Top 5)再提供给LLM生成答案。
💎 总结与核心建议
简单来说,Embedding模型负责"找得多",重排模型负责"选得准"。它们不是替代关系,而是互补关系 。
在进行技术选型时,最关键的是结合你的具体应用场景、数据特性(如语言、长度)、对响应速度的要求以及计算资源预算。通常建议先从一款成熟的开源Embedding模型(如BGE)和一款开源重排模型(如BGE-Reranker)开始搭建基线系统,再根据实际效果进行迭代优化。
在构建RAG系统时,如何在BGE和Qwen3这两个优秀的模型系列中选择合适的Embedding和Rerank模型,确实是一个关键问题。下面的对比将帮助你清晰地把握它们的特点。
| 特性维度 | BGE 系列 | Qwen3 Embedding & Reranker 系列 |
|---|---|---|
| 核心架构 | 基于Transformer的Encoder 架构。使用特殊的[CLS]标记,其对应的向量代表整句语义。 |
基于Qwen3的Decoder 架构(移除了因果掩码)。使用句子末尾的[EOS]标记对应的向量代表整句语义。 |
| 训练数据与方法 | 依赖从开源社区收集的公开数据集,采用Mask猜词 与相似度计算进行训练。 | 利用Qwen3基座模型主动合成高质量、多维度数据 ,并采用多阶段训练 与模型融合技术。 |
| 指令感知 | 支持基础的指令微调,旨在提升模型在不同任务下的泛化能力。 | 支持强大的动态指令感知。查询时可附加Prompt(如"寻找写作风格相似的文档"),使同一文档库支持多维度检索。 |
| Embedding模型核心优势 | 成熟稳定 、生态完善 、部署简单。在中文场景下表现优异,是经过大量实践检验的可靠选择。 | 性能领先 、灵活智能 。尤其在多语言 、代码理解 和长文本处理上表现卓越,支持通过指令动态调整检索意图。 |
| Rerank模型工作方式 | 采用典型的交叉编码器架构,将查询和文档拼接后输入模型,直接输出一个相关性分数。 | 将重排任务转化为二元分类问题。模型会判断文档是否相关,并以概率形式输出得分(score = P("yes")),更具解释性。 |
| Rerank模型核心优势 | 技术路线经典,与主流框架兼容性好,理解和调试相对直观。 | 在多项基准测试中性能显著领先同参数规模模型,对代码、多语言等复杂语义理解更深。 |
💡 如何选择:场景与建议
了解区别后,你可以根据具体需求做出选择:
-
追求稳定与快速落地 :如果你的项目需求相对稳定,对成本敏感 ,或者希望使用一个社区成熟、文档丰富 的模型来快速搭建和验证系统,BGE系列是一个非常稳妥和经典的选择。它在中文任务上的表现已经相当出色。
-
追求极致性能与灵活性 :如果你的应用场景涉及多语言内容 、代码检索 ,或者需要处理超长文档 ,并且你希望系统能够根据用户意图进行智能、动态的调整 (例如实现个性化推荐),那么Qwen3系列在性能上的优势会更加明显。它的指令感知能力为产品设计提供了更大空间。
-
构建高性能RAG流水线 :一个常见的优化策略是采用"BGE Embedding + Qwen3 Reranker"的混合方案。即用BGE Embedding进行初步的快速召回(因为它效率高),再用Qwen3 Reranker对召回结果进行精细重排(因为它精度高)。这在平衡效率和效果方面往往能取得很好的结果。
embedding数据库选择
| 特性维度 | FAISS | Chroma | Milvus | Qdrant |
|---|---|---|---|---|
| 核心定位 | 专注于向量相似性搜索的高性能库 | 开发者友好的嵌入式向量数据库,开箱即用 | 专为超大规模 数据设计的分布式向量数据库 | 提供丰富功能 和API优先的向量数据库 |
| 部署方式 | 作为一个Python库直接集成到应用中,无独立服务 | 可作为嵌入式数据库使用,也支持客户端-服务器模式 | 需要独立部署和运维的分布式服务 | 需要独立部署服务,通过HTTP/gRPC接口调用 |
| 扩展性 | 主要用于单机,扩展性有限 | 适合中小规模应用,扩展性一般 | 扩展性极佳 ,支持分布式集群,可处理亿级甚至更多的向量 | 支持分布式部署,扩展性好,适合中大型在线服务 |
| 性能特点 | 检索速度极快,尤其在使用GPU加速时 | 性能适中,对于中小规模数据表现良好 | 为海量数据 下的高性能、低延迟检索而优化 | 使用Rust开发,注重性能和高效率 |
| 易用性 | 需手动处理索引构建和持久化,有一定复杂度 | API简单直观 ,内置持久化,上手非常快 | 学习曲线较陡峭,部署和配置相对复杂 | 提供清晰的API,但需要额外部署服务 |
| 主要优势 | Facebook开源,在单一机器上速度优势明显,社区活跃 | 集成简便,适合快速原型开发和实验 | 专为企业级海量数据场景设计,功能全面 | 支持多种搜索方式(如元数据过滤),为生产环境打造 |
| 理想适用场景 | 数据量在百万级以内,追求极致检索速度的本地化应用或研究项目 | 中小型项目 、概念验证、需要快速上手的开发场景 | 需要处理千万级及以上 向量数量的大型企业级应用 | 需要强大功能 、分布式特性 和REST API 的生产级服务 |
💡 如何选择?
你可以根据项目的核心需求,参考以下路径进行选择:
- 追求快速验证和简单集成 :如果你的目标是快速构建一个原型或中小型应用,希望尽可能简化部署和集成工作,那么 Chroma 是一个很好的起点 。
- 专注高性能和本地部署 :如果你的数据量在百万级别以内,且非常看重检索速度,同时希望避免维护外部服务的开销,FAISS 是经过大量实践检验的可靠选择 。
- 应对海量数据和企业级需求 :当你的应用需要处理千万级甚至亿级的向量数据,并要求高可用性、可扩展性时,Milvus 或 Qdrant 这类专业的分布式向量数据库是更合适的选择 。
为了帮助您快速了解和选型,下面这个表格详细对比了四款主流的分布式向量数据库在核心架构、性能、功能特性和适用场景等方面的关键差异。
| 特性维度 | Milvus | Qdrant | Weaviate | Chroma |
|---|---|---|---|---|
| 核心架构与设计语言 | 专为海量数据设计的云原生分布式架构(Go/C++),采用计算存储分离的微服务设计。 | 基于Rust 语言构建,侧重高性能与内存安全,支持单机和集群模式,架构相对简洁。 | 基于 Go 语言,以 GraphQL 为核心接口,支持模块化扩展,设计上支持单节点和集群部署。 | 设计初衷是轻量级 和开发者友好(Python核心),支持嵌入式运行和客户端/服务器模式,易于快速集成。 |
| 分布式与扩展能力 | 扩展性极佳 ,原生为水平扩展设计,可处理千亿级向量,支持动态扩缩容。 | 支持分片与副本机制,扩展性良好,但在大规模数据集和高并发场景下的表现可能略逊于Milvus。 | 通过分片机制支持水平扩展,能处理亿级向量,混合搜索是其亮点。 | 扩展能力相对较弱,主要面向中小规模数据(百万级以下),大规模扩展并非其强项。 |
| 性能特点 | 高性能 ,针对十亿级向量数据能做到毫秒级延迟,支持多种高性能索引(如HNSW, IVF, DiskANN)。 | 低延迟、高吞吐,Rust内核带来优异的性能表现,特别擅长高效的元数据过滤。 | 并发性能好 ,支持HNSW索引,其混合搜索(向量+关键词)能力突出。 | 在中小型数据集上表现良好,适合快速原型开发。大规模数据下性能有显著下降。 |
| 关键功能 | 功能非常丰富,支持多索引、多租户、复杂过滤、TTL等,是企业级应用的强大选择。 | 功能丰富,在强元数据过滤(可前置/后置)、地理搜索、推荐API等方面表现强劲。 | 功能非常丰富,原生支持多模态检索 和混合搜索,内置模块可自动向量化数据。 | 核心的向量存储、搜索和过滤功能完善,API简洁。与AI生态(如LangChain)集成紧密,易于上手。 |
| 部署与运维 | 复杂,组件较多,推荐使用Kubernetes部署,运维专业性要求高,适合有专业运维团队的场景。 | 灵活度中等,支持Docker、Kubernetes等多种方式,相比Milvus部署相对简单。 | 灵活度中等,支持Docker、Kubernetes部署,集群配置需要理解其特定概念。 | 极其简单,特别是本地开发模式, pip安装即可使用。服务器模式的部署也相对容易。 |
| 理想应用场景 | 企业级超大规模应用,如十亿级向量的推荐系统、图像/视频检索、生物基因分析等。 | 对过滤条件复杂 和查询延迟敏感 的语义搜索、广告推荐、实时风控等中大规模场景。 | 需要强大多模态检索 或混合搜索能力的中大规模应用,如智能知识图谱、内容推荐平台。 | 快速原型开发 、本地实验、桌面应用以及数据量在百万级别以内的中小型项目。 |
💡 如何选择分布式向量数据库
选择哪款数据库,最终取决于您的具体需求、技术栈和资源。您可以参考以下路径进行决策:
- 应对企业级超大规模场景 :如果您的项目需要处理千万级乃至百亿级 的向量数据,对性能、可用性和扩展性有极致要求,并且拥有专业的运维团队,那么 Milvus 是最可靠的选择。
- 平衡性能、功能与复杂度 :如果您的数据量在千万到亿级 ,希望获得接近Milvus的高性能,同时看重高效过滤和相对简洁的架构,Qdrant 是一个非常好的折中方案。如果您的应用强烈依赖结合关键词和向量的混合搜索 ,或者需要开箱即用的多模态检索 能力,Weaviate 的独特功能会非常有吸引力。
- 追求快速验证与轻量部署 :如果您的目标是快速搭建原型 、进行概念验证,或者您的数据量在百万级以下 ,希望极简的部署和集成体验,Chroma 是理想的选择。
💎 总结与建议
总的来说,这四款数据库各有侧重。Milvus 和 Qdrant 更像是为大规模生产环境准备的"重器",而 Chroma 和 Weaviate 则在易用性和特定功能上表现突出。
一个实用的建议是:在项目早期,可以先用 Chroma 快速验证想法和流程。当业务数据和并发增长后,再根据对性能、扩展性和功能(如混合搜索、多模态)的具体要求,评估是否迁移到 Qdrant 、Weaviate 或 Milvus。
embedding向量查找算法
理解您想系统了解嵌入向量的查找算法。这些算法对于构建高效的语义搜索、推荐系统或RAG应用至关重要。下面这个表格汇总了主要的向量查找算法及其核心特征,方便您快速对比。
| 算法类别 | 代表算法 | 核心原理 | 优点 | 缺点 | 典型适用场景 |
|---|---|---|---|---|---|
| 精确查找 | 线性搜索 | 计算查询向量与数据库中每一个向量的距离 | 结果100%准确 | 计算成本极高,无法扩展 | 数据量极小(如数百个)的验证性场景 |
| 基于树结构 | KD树 | 递归地将k维空间分割成二叉树结构 | 在中低维空间效率较高 | 维数灾难(维度升高后性能急剧下降),构建耗时 | 维度较低(通常<20)且数据量不大的场景 |
| 基于哈希 | 局部敏感哈希 | 设计特殊哈希函数,使相似向量 以高概率落入同一个哈希桶 | 查询速度极快 | 精度通常较低,对参数敏感 | 对速度要求极高,可接受一定精度损失的场景 |
| 基于量化 | 乘积量化 | 将高维向量分割成子空间并分别量化,用编码近似表示原向量 | 压缩率极高,大幅节省存储和内存 | 量化会引入误差,精度有损失 | 海量数据(亿级以上)存储和检索,内存资源紧张 |
| 基于图结构 | HNSW | 构建分层导航小世界图,高层实现快速粗筛,底层实现精细搜索 | 速度和精度平衡极佳,是目前主流选择 | 索引构建较慢,内存占用较高 | 对查询速度和精度要求高的通用场景,如推荐系统、语义搜索 |
💡 核心概念:近似最近邻搜索
面对海量高维数据,精确查找的计算成本变得无法承受 。因此,在实际应用中,我们几乎总是采用近似最近邻搜索。
ANN的核心思想是牺牲少量精度以换取查询速度的数量级提升。它不再保证找到绝对最近的邻居,而是通过高效的索引结构,快速找到距离查询向量"足够近"的候选向量。对于绝大多数应用来说,95%以上甚至99%的召回率已经足够,而速度却能提升成百上千倍。
🔍 主流算法详解
在ANN算法中,基于图的方法(尤其是HNSW)和基于量化的方法是目前应用最广泛的两大方向。
-
HNSW:高性能的通用选择
HNSW可以说是当前最流行的向量检索算法。其灵感来自于可导航的小世界网络(如社交网络),通过构建一个多层次的图结构来工作。高层 的图连接较稀疏,便于快速进行远距离"跳跃",迅速定位到目标区域。底层 的图连接密集,用于在目标区域内进行精细搜索,找到最邻近的节点。这种结构使得HNSW在搜索时能够实现对数级别的时间复杂度,在亿级数据上也能达到毫秒级的响应速度,同时保持很高的召回率。
-
乘积量化:为海量数据而生
乘积量化是一种通过压缩技术来应对海量向量的方案。它的核心步骤是分块、聚类、编码 :首先将高维向量切分成多个低维子向量;然后在每个子空间内进行聚类,得到一组聚类中心(码本);最后,每个原始向量可以用其各个子向量所属聚类中心的索引编码来表示。这样,一个浮点数向量就被压缩成了一个短短的整数编码,存储空间可以降低数十倍。在搜索时,通过查询向量与码本中聚类中心预先计算的距离表,可以高效地估算出与库中压缩向量的近似距离。这种方法特别适合当数据量远超内存容量时的磁盘检索场景。
🛠️ 如何选择算法?
选择哪种算法取决于您的具体需求和约束条件:
- 数据规模与维度 :这是首要考虑因素。千万级以下 的数据,HNSW 通常是性能最优的选择。如果数据量达到亿级甚至更大 ,需要优先考虑内存占用,可以评估乘积量化 或HNSW与量化结合的混合方案。
- 精度与速度要求 :如果对精度要求极高 且数据量不大,可以调大HNSW的参数或考虑精确搜索 。如果对速度要求极端苛刻,可以尝试LSH或高度优化的量化方法。
- 系统资源 :内存充足 时,HNSW的性能表现非常出色。如果内存紧张 或需要处理十亿级数据,量化方法是更可行的选择。
- 数据更新频率 :对于需要频繁更新的数据库,HNSW的增量插入能力通常比需要全局重新构建的量化方法更有优势。
📚 学习与工具推荐
理论结合实践能更好地掌握这些算法。推荐您使用Faiss这个强大的开源库,它由Facebook开发,集成了上述几乎所有主流算法(包括HNSW、PQ、LSH等),并提供了Python接口,非常适合进行实验和原型开发。
数据:
bash
https://www.sudugu.org/txt/?id=128&p=1
https://www.sudugu.org/txt/?id=128&p=2
https://www.sudugu.org/txt/?id=128&p=3
https://www.sudugu.org/txt/?id=128&p=4
https://www.sudugu.org/txt/?id=128&p=5
https://www.sudugu.org/txt/?id=128&p=6
首先安装必要的库
bash
pip install langchain-experimental langchain-community transformers torch chromadb flask langchain_openai
文档分割与embedding
python
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
import glob
from langchain_community.document_loaders import TextLoader
from langchain_experimental.text_splitter import SemanticChunker
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
import hashlib
def generate_document_id(text, metadata=None):
"""为文档生成唯一ID"""
content_hash = hashlib.md5(text.encode()).hexdigest()[:10]
source = metadata.get('source', 'unknown') if metadata else 'unknown'
return f"{source}_{content_hash}"
def get_text_files(directory_path, file_pattern="*.txt"):
"""
获取目录下所有文本文件的路径
参数:
directory_path: 目录路径
file_pattern: 文件匹配模式,如 "*.txt" 或 "*.md"
"""
# 方法1: 使用glob获取当前目录下的匹配文件
file_paths = glob.glob(os.path.join(directory_path, file_pattern))
# 方法2: 如果需要递归获取子目录中的文件,可以使用下面这行代码
# file_paths = glob.glob(os.path.join(directory_path, "**", file_pattern), recursive=True)
print(f"找到 {len(file_paths)} 个匹配的文件")
return file_paths
def chunk_multiple_novels(directory_path, file_pattern="f0*.txt", persist_directory="./chroma_novel_db"):
"""
处理目录下的多个小说文件:分割 → 生成嵌入 → 存入ChromaDB
参数:
directory_path: 包含小说文件的目录路径
file_pattern: 文件匹配模式
persist_directory: ChromaDB持久化存储目录
"""
# 1. 获取所有文本文件路径
file_paths = get_text_files(directory_path, file_pattern)
if not file_paths:
print("未找到匹配的文本文件")
return None
# 2. 初始化嵌入模型
embeddings = HuggingFaceEmbeddings(
model_name="Qwen/Qwen3-Embedding-0.6B", # 使用适合中文的轻量级模型
model_kwargs={
'device': 'cuda',
'trust_remote_code': True
},
encode_kwargs={
'normalize_embeddings': True,
'batch_size': 8
}
)
print("✅ 嵌入模型加载完成")
# 3. 创建语义分割器(优化中文分割)
novel_splitter = SemanticChunker(
embeddings=embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=85,
sentence_split_regex=r'(?<=[。!?\n])', # 适配中文标点
buffer_size=1,
add_start_index=True
)
all_chunks = []
processed_files = 0
# 4. 逐个处理每个文件
for file_path in file_paths:
filename = os.path.basename(file_path)
print(f"\n📖 处理文件 ({processed_files + 1}/{len(file_paths)}): {filename}")
try:
# 加载文档
loader = TextLoader(file_path, encoding='utf-8')
documents = loader.load()
print(f" 文件加载成功,字符数:{len(documents[0].page_content)}")
# 对每个文档进行语义分割
for doc in documents:
chunks = novel_splitter.split_documents([doc])
print(f" 📊 生成 {len(chunks)} 个语义块")
# 为每个块添加文件来源元数据
for i, chunk in enumerate(chunks):
chunk.metadata.update({
'source_file': filename,
'file_path': file_path,
'chunk_id': i,
'total_chunks_in_file': len(chunks)
})
all_chunks.extend(chunks)
processed_files += 1
# 显示前2个块作为示例
for i, chunk in enumerate(chunks[:2]):
preview = chunk.page_content[:100].replace('\n', ' ')
print(f" 示例块{i+1}: {preview}...")
except Exception as e:
print(f" ❌ 处理文件 {filename} 时出错: {e}")
continue
# 5. 如果有成功处理的块,存入ChromaDB
if all_chunks:
print(f"\n🚀 开始处理 {len(all_chunks)} 个文本块,生成嵌入并存入ChromaDB...")
try:
# 构建向量数据库
vectorstore = Chroma.from_documents(
documents=all_chunks,
embedding=embeddings,
persist_directory=persist_directory,
collection_metadata={"hnsw:space": "cosine"}
)
# 持久化到磁盘
vectorstore.persist()
print(f"✅ 向量数据库构建完成!")
print(f"💾 数据库已保存至: {persist_directory}")
print(f"📊 总共处理了 {processed_files} 个文件,生成 {len(all_chunks)} 个语义块")
return vectorstore
except Exception as e:
print(f"❌ 向量数据库构建失败: {e}")
return None
return None
def query_vector_db(vectorstore, question, k=3):
"""
查询向量数据库
参数:
vectorstore: Chroma向量数据库实例
question: 查询问题
k: 返回最相似的结果数量
"""
if vectorstore is None:
print("❌ 向量数据库未初始化")
return None
print(f"\n🔍 执行查询: '{question}'")
# 相似度搜索
similar_docs = vectorstore.similarity_search(question, k=k)
print(f"找到 {len(similar_docs)} 个相关片段:")
for i, doc in enumerate(similar_docs):
print(f"\n--- 相关片段 {i+1} (来自: {doc.metadata.get('source_file', '未知')}) ---")
print(f"内容: {doc.page_content[:200]}...")
return similar_docs
def load_existing_vector_db(persist_directory="./chroma_novel_db"):
if not os.path.exists(persist_directory):
return None
"""
加载已存在的向量数据库
"""
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5"
)
try:
vectorstore = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings
)
# 检查集合中是否有文档
count = vectorstore._collection.count()
print(f"✅ 数据库加载成功!包含 {count} 个文档")
return vectorstore
except Exception as e:
print(f"❌ 数据库加载失败: {e}")
return None
# 使用示例
if __name__ == "__main__":
# 配置参数
novels_directory = "./data" # 包含多个小说文件的目录
db_directory = "./chroma_multiple_novels_db" # 向量数据库存储目录
print("=" * 60)
print("📚 多文件小说语义分割与向量数据库构建系统")
print("=" * 60)
# 检查是否已存在数据库
existing_db = load_existing_vector_db(db_directory)
if existing_db is None:
# 执行完整的处理流程
vector_db = chunk_multiple_novels(novels_directory, "*.txt", db_directory)
else:
vector_db = existing_db
print("使用已存在的数据库")
if vector_db:
# 测试查询
test_questions = [
"不同小说中的主人公特点",
"故事中的冒险场景描述",
"情感描写的段落"
]
for question in test_questions:
query_vector_db(vector_db, question, k=2)
print("\n" + "=" * 60)
print("🎉 多文件处理完成!")
print(" 现在您可以对多个小说文件进行语义搜索")
print("=" * 60)
# 显示数据库统计信息
collection_count = vector_db._collection.count()
print(f"\n📈 数据库统计:")
print(f" 总文档数: {collection_count}")
# 获取所有文件的来源统计
if hasattr(vector_db, '_collection'):
results = vector_db._collection.get()
if results and 'metadatas' in results:
sources = [meta.get('source_file', 'unknown') for meta in results['metadatas']]
from collections import Counter
source_count = Counter(sources)
print(f" 文件来源分布:")
for source, count in source_count.items():
print(f" {source}: {count} 个块")
query 重写+embedding查找+rerank+大模型
python
#encoding:utf-8
import os
import argparse
from flask import Flask
from flask import Flask, request, jsonify, render_template_string
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["DEEPSEEK_API_KEY"] = "sk-replace to yours"
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableParallel, RunnableLambda
from langchain_core.output_parsers import StrOutputParser
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
from sentence_transformers import SentenceTransformer
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from typing import List, Tuple
import traceback
class LocalChineseReranker:
def __init__(self, reranker_model_name='Qwen/Qwen3-Reranker-0.6B', device=None):
"""
修复版的重排模型 - 解决padding token未定义问题
"""
# 设备配置
if device is None:
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
else:
self.device = torch.device(device)
print(f"正在加载重排模型: {reranker_model_name}")
print(f"使用设备: {self.device}")
# 加载tokenizer和模型
self.tokenizer = AutoTokenizer.from_pretrained(reranker_model_name)
self.model = AutoModelForSequenceClassification.from_pretrained(reranker_model_name)
# 🚨 关键修复:检查并设置padding token
self._setup_padding_token()
# 将模型移动到设备
self.model = self.model.to(self.device)
self.model.eval()
print("✅ 重排模型加载完成!")
def _setup_padding_token(self):
"""
设置padding token - 解决批量处理错误的核心方法
"""
# 检查当前的pad_token
print(f"当前pad_token: {self.tokenizer.pad_token}")
print(f"当前pad_token_id: {self.tokenizer.pad_token_id}")
# 如果pad_token未设置,使用eos_token作为pad_token
if self.tokenizer.pad_token is None:
print("⚠️ 检测到pad_token未设置,正在配置...")
if self.tokenizer.eos_token is not None:
# 方法1:使用eos_token作为pad_token(最常见解决方案)
self.tokenizer.pad_token = self.tokenizer.eos_token
self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
print(f"✅ 设置pad_token为eos_token: {self.tokenizer.pad_token}")
else:
# 方法2:添加自定义pad_token
self.tokenizer.add_special_tokens({'pad_token': '[PAD]'})
print("✅ 添加自定义pad_token: [PAD]")
# 确保模型配置与tokenizer同步[5](@ref)
if hasattr(self.model.config, 'pad_token_id'):
self.model.config.pad_token_id = self.tokenizer.pad_token_id
print(f"✅ 同步模型pad_token_id: {self.model.config.pad_token_id}")
print("🎯 Padding token配置完成")
def rerank(self, query: str, documents: List[str], top_k: int = 3, batch_size: int = 20):
"""
对初步召回的文档进行重排(分批处理版本)
参数:
query: 用户查询字符串
documents: 初步召回的文档列表
top_k: 返回前K个最相关的文档
batch_size: 批处理大小,默认为20
返回:
ranked_results: 排序后的文档列表,格式为 [(文档内容, 得分), ...]
"""
if not documents:
return []
all_scores = [] # 存储所有文档的分数
processed_docs = [] # 存储处理过的文档,确保顺序一致
# 将文档分成批次处理
for i in range(0, len(documents), batch_size):
batch_docs = documents[i:i + batch_size]
processed_docs.extend(batch_docs)
# 构建当前批次的查询-文档对
batch_pairs = [[query, doc] for doc in batch_docs]
try:
with torch.no_grad():
# Tokenize并自动padding
inputs = self.tokenizer(
batch_pairs,
padding=True, # 启用自动padding
truncation=True,
max_length=512,
return_tensors='pt'
).to(self.device) # 确保输入数据在正确设备上
# 模型推理
outputs = self.model(**inputs)
# 正确的分数提取
if outputs.logits.dim() == 2 and outputs.logits.shape[1] == 1:
# 标准情况: [batch_size, 1] -> [batch_size]
batch_scores = outputs.logits.squeeze(1)
elif outputs.logits.dim() == 2 and outputs.logits.shape[1] > 1:
# 多分数情况: 取第一列(通常相关性分数)
batch_scores = outputs.logits[:, 0]
else:
# 降级处理
batch_scores = outputs.logits.flatten()
# 将分数移到CPU并转换为Python列表
batch_scores = batch_scores.float().cpu().tolist()
all_scores.extend(batch_scores)
# 打印批次处理进度
print(f"已处理批次 {i//batch_size + 1}/{(len(documents)-1)//batch_size + 1}, "
f"当前批次大小: {len(batch_docs)}")
except Exception as e:
print(f"处理批次 {i//batch_size + 1} 时出错: {e}")
# 为当前批次的文档设置默认分数0
all_scores.extend([0.0] * len(batch_docs))
# 确保分数和文档数量一致
if len(all_scores) != len(processed_docs):
print(f"警告: 分数数量({len(all_scores)})与文档数量({len(processed_docs)})不匹配")
# 截断或填充以保持一致性
min_len = min(len(all_scores), len(processed_docs))
all_scores = all_scores[:min_len]
processed_docs = processed_docs[:min_len]
# 按分数从高到低排序
scored_docs = list(zip(processed_docs, all_scores))
ranked_results = sorted(scored_docs, key=lambda x: x[1], reverse=True)
# 返回前top_k个结果
return ranked_results[:top_k]
reranker = LocalChineseReranker()
app = Flask(__name__)
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com/v1",
temperature=0.7,
max_tokens=10000
)
# 新增:Query 改写模块
rewrite_prompt = PromptTemplate.from_template("""
你是一个修仙小说智能问答助手,你的任务是将用户的口语化、不清晰的问题改写为更专业清晰、更准确的表达,以便更好地进行信息检索。将一个问题从三个方面改出三个问题
原始问题: {question}
改写后的问题:
""")
query_rewriter = rewrite_prompt | llm | StrOutputParser()
embeddings = HuggingFaceEmbeddings(model_name="Qwen/Qwen3-Embedding-0.6B")
vectorstore = Chroma(persist_directory="./chroma_multiple_novels_db", embedding_function=embeddings)
def retriever(vectorstore):
ret = vectorstore.as_retriever(search_kwargs={"k": 300})
template = """使用以下上下文来回答问题。可以加入合理的逻辑推导,但是要说明是推理而来,不用说是第几个文档片段。
{context}
问题: {question}
答案:"""
prompt = PromptTemplate.from_template(template)
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
def rerank_docs(inputs):
question = inputs["question"]
docs = inputs["context"].split("\n\n")
print("before rerank:", len(docs))
# 使用 reranker 对文档进行重排序
reranked_docs = reranker.rerank(question, docs, 30)
outputs = {"question":question, "context": []}
for i, (doc, score) in enumerate(reranked_docs):
print(f"{i+1}. 得分: {score:.4f} 文档: {doc}\n")
outputs["context"].append(doc)
print("after rerank:", len(reranked_docs))
return outputs
rag_chain = (
RunnableParallel({"context": ret | format_docs, "question": RunnablePassthrough()})
| RunnableLambda(rerank_docs)
| prompt
| llm
| StrOutputParser()
)
return ret, rag_chain
ret, chain = retriever(vectorstore)
def ask(question):
inputs = {"question": question}
rewritten_question = query_rewriter.invoke(inputs)
print("rewritten_question: ", rewritten_question)
answer = chain.invoke(rewritten_question)
docs = ret.invoke(rewritten_question)
sources = [{"page_content": doc.page_content} for doc in docs]
return {
"question": question,
"rewritten_question" : rewritten_question,
"answer": answer,
"sources": sources
}
print("load finish")
python
print("使用 `exit` 退出程序。")
while True:
query = input("\nUser: ")
if query.strip() == "exit":
break
rets = ask(query)
print("question:\n", rets['question'])
print("rewritten_question:\n", rets['rewritten_question'])
print("answer:\n", rets['answer'])
运行
User: 总结一下韩立的生平
rewritten_question: 1. 请系统梳理韩立从凡人到飞升仙界的完整修仙历程与关键节点。
-
请归纳韩立在《凡人修仙传》系列中的主要身份转变、重要成就及对应阶段。
-
请概述韩立一生中的重要际遇、核心功法和对其道途产生决定性影响的人物事件。
before rerank: 382
已处理批次 1/20, 当前批次大小: 20
已处理批次 2/20, 当前批次大小: 20
已处理批次 3/20, 当前批次大小: 20
已处理批次 4/20, 当前批次大小: 20
已处理批次 5/20, 当前批次大小: 20
已处理批次 6/20, 当前批次大小: 20
已处理批次 7/20, 当前批次大小: 20
已处理批次 8/20, 当前批次大小: 20
已处理批次 9/20, 当前批次大小: 20
已处理批次 10/20, 当前批次大小: 20
已处理批次 11/20, 当前批次大小: 20
已处理批次 12/20, 当前批次大小: 20
已处理批次 13/20, 当前批次大小: 20
已处理批次 14/20, 当前批次大小: 20
已处理批次 15/20, 当前批次大小: 20
已处理批次 16/20, 当前批次大小: 20
已处理批次 17/20, 当前批次大小: 20
已处理批次 18/20, 当前批次大小: 20
已处理批次 19/20, 当前批次大小: 20
已处理批次 20/20, 当前批次大小: 2
after rerank: 30
question:
总结一下韩立的生平
rewritten_question:
- 请系统梳理韩立从凡人到飞升仙界的完整修仙历程与关键节点。
- 请归纳韩立在《凡人修仙传》系列中的主要身份转变、重要成就及对应阶段。
- 请概述韩立一生中的重要际遇、核心功法和对其道途产生决定性影响的人物事件。
answer:
1. 韩立从凡人到飞升仙界的完整修仙历程与关键节点
韩立的修仙历程可划分为以下关键阶段:
- 凡人起步:出身越国镜州青牛镇五里沟的普通农家,因身具灵根被七玄门墨大夫引入修仙之路,修炼《长春功》,后凭借"升仙令"加入黄枫谷,正式踏入修仙界(见升仙令相关片段)。
- 炼气至元婴期 :
- 在黄枫谷期间修炼青元剑诀、大衍诀,参与血色试炼,结丹后游历乱星海,炼制本命法宝青竹蜂云剑(金雷竹法宝)。
- 结婴后返回天南,参与对抗慕兰族、坠魔谷探险,后期进阶元婴后期(大修士),成为天南乃至大晋顶尖强者。
- 化神与飞升灵界 :
- 在人界昆吾山事件后,通过空间节点飞升灵界,飞升过程充满风险(见银月提及的"星盘之力"及"北冥岛"经历)。
- 在灵界初期隐匿修为,加入天渊城青冥卫(卓冲提及韩立曾为队长),后游历各族大陆,修为逐步提升至炼虚、合体期。
- 大乘与渡劫飞升 :
- 进阶大乘后,参与魔界之战、冥河之地等事件,积累机缘(如始魔之地之行),最终渡飞升之劫。据紫灵所述,韩立飞升仙界的把握超过五成(见"一半几率飞入仙界"片段)。
- 关键节点 :
- 升仙令:奠定宗门根基;
- 金雷竹法宝:强化战力与机缘;
- 飞升灵界:突破人界资源限制;
- 大乘渡劫:完成凡俗至仙道的终极跨越。
2. 韩立在《凡人修仙传》系列中的主要身份转变、重要成就及对应阶段
- 身份转变 :
- 凡人→低阶修士:七玄门弟子→黄枫谷链气弟子(凭借升仙令)。
- 宗门精英→独行强者:黄枫谷结丹修士→乱星海散修(炼制金雷竹法宝,对抗逆星盟)。
- 人界巅峰→灵界飞升者:天南大修士→灵界青冥卫、炼虚修士(卓冲称其"神通深不可测")。
- 灵界大能→仙界飞升者:大乘期至尊(黑枭王称其"进阶大乘"),最终渡劫成仙。
- 重要成就 :
- 人界阶段:创青元剑诀体系、炼制元婴级傀儡、掌握破灭法目等神通。
- 灵界阶段:修炼炼神术、融合梵圣真魔功、参与广寒界试炼,进阶大乘。
- 终极成就:渡飞升之劫,成为人族数十万年来首位成功飞升仙界的修士。
3. 韩立一生中的重要际遇、核心功法及决定性人物事件
- 重要际遇 :
- 升仙令:获得黄枫谷入门资格(叶姓老者接待);
- 金雷竹:炼制本命法宝,奠定越阶战斗基础;
- 逆星盘传送:意外脱离险境(片段提及"被逆星盘传送");
- 始魔之地机缘:提升飞升成功率(对紫灵提及"多出两成把握")。
- 核心功法 :
- 青元剑诀:主修功法,从链气期延续至大乘期;
- 大衍诀:强化神识,修炼至顶层(片段提及其"神识强大倍许");
- 明王诀/梵圣真魔功:炼体功法,增强肉身防御;
- 疾风九变:身法神通,提升机动性。
- 决定性人物与事件 :
- 墨大夫:引路人,亦为初期危机来源;
- 厉飞雨:世俗挚友,其子孙后代与韩立有渊源(虬须大汉片段);
- 银月/紫灵:红颜知己,多次助其脱困并共参大道;
- 飞升之劫:最终考验,融合毕生积累与机缘方得成功。
说明:以上梳理基于提供的文档片段,并依据《凡人修仙传》系列作品的一般情节进行了逻辑推导,以保持历程的连贯性。关键节点如升仙令、飞升灵界、大乘渡劫等均直接源自片段内容,其他细节根据修仙阶段常见设定做了合理补充。
question:
韩立是怎么一步步进阶到大乘期的?主要有哪些机缘
rewritten_question:
- 韩立从炼气期到大乘期的完整修炼历程与关键突破节点是什么?
- 韩立在进阶大乘期的过程中,获得了哪些核心机缘与关键资源?
- 韩立的修炼路径中,有哪些重要事件与抉择推动了他最终突破至大乘期?
answer:
韩立的完整修炼历程与关键突破节点
韩立的修炼始于炼气期 (在黄枫谷修炼长春功,依靠黄龙丹、金髓丸等低阶丹药突破至十一层),随后通过筑基丹成功筑基 。筑基后,他修炼《青元剑诀》与《三转重元功》以巩固根基,并借助大衍决增强神识。在结丹阶段,他依靠丹药和功法积累,最终结丹成功 。
进入元婴期 后,韩立在坠魔谷获得机缘,快速突破至元婴中期,后期则通过寒焰五魔、鬼罗幡等秘术与资源修炼至元婴后期顶峰。化神期 的突破得益于绛云丹的无供应(灵药移植秘术使其能大量炼制),使他在人界元气稀薄环境下仍修炼至化神初期顶峰。
飞升灵界后,韩立经历炼虚期(在广寒界获得机缘,连跨两级至炼虚后期顶峰)、合体期(通过广寒界收获、法体双修及雷霆之道感悟,突破至合体中期),最终进阶大乘期*。关键节点包括:炼虚期广寒界机缘、合体期法体双修与心境突破、大乘期对天地法则的领悟与资源积累。
进阶大乘期的核心机缘与关键资源
- 广寒界机缘:韩立在广寒界中获得突破炼虚后期的关键资源,并积累了对后续境界有益的感悟。
- 丹药与秘术 :
- 化神期依赖绛云丹(通过风希的灵药移植秘术实现无***炼制),快速修炼至化神初期顶峰。
- 炼体之术与法体双修功法:在灵根不稳时仍可修炼,保障修炼连续性。
- 大衍决增强神识,为高阶突破奠定基础。
- 天地感悟与心境突破:合体期时,韩立通过与天元圣皇等高阶修士论道、经历蛮荒冒险,提升对天地法则的理解。
- 关键资源获取 :
- 芥子空间中的灵药移植,解决炼丹原料问题。
- 在蛮荒、冥河之地等地获得异族功法、雷霆之道传承等。
- 渡劫准备:为应对天劫,韩立培养隐雷灵根弟子海大少,并修炼雷霆神通以减轻渡劫负担。
重要事件与抉择推动大乘期突破
- 功法抉择:早期选择《青元剑诀》《三转重元功》与大衍决,奠定扎实根基;后期兼修法体双修与雷霆之道,适应灵根变化并增强渡劫能力。
- 机缘探索:主动进入坠魔谷、广寒界、蛮荒等险地,获取突破瓶颈的关键资源(如灵药、秘术、跨界感悟)。
- 资源管理:巧妙利用芥子空间移植灵药,确保丹药无***供应;收徒传承以换取渡劫助力(如海大少)。
- 心境锤炼:经历多次生死危机(如煞气反噬、强敌追杀),并在合体期后与高阶修士论道,巩固道心。
- 长期规划:韩立始终围绕"突破瓶颈"布局,例如化神期专注绛云丹修炼,炼虚期后注重法体双修,合体期提前培养弟子辅助渡劫。
- 应变能力:面对灵根隐患、煞气反噬等意外,及时调整修炼策略(如炼体术替代法术修炼),保障修炼进程不中断。
注:以上整合自上下文片段中韩立在不同阶段的修炼描述、资源获取及事件选择,并基于修仙小说的常见逻辑(如境界突破依赖资源、机缘与心境)进行了合理推导,以形成连贯的修炼历程。