机器问道:大模型RAG 解读凡人修仙传

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-smalltext-embedding-3-large,需注意其为闭源API服务 。
    • 特定需求 :处理超长文本可考虑 Nomic-Embed;专门优化中文可关注 M3E 系列 。
  • 重排模型选型参考

    • 开源优选BGE-Reranker 系列(如 BAAI/bge-reranker-base)是开源方案中的佼佼者,支持中英文,平衡了效果与成本 。
    • 商用API服务Cohere RerankVoyage Rerank 提供高精度的托管服务,适合追求效果且不愿自运维的团队 。
    • 效率与精度平衡ColBERT 采用"延迟交互"机制,在大规模文档库的检索中,能在精度和速度间取得较好平衡 。
  • 高效协作策略

    最佳实践是采用 "分层处理"管道

    1. 第一层:快速召回。使用 Embedding 模型从全部文档中快速检索出成百上千的候选文档(如Top 200)。
    2. 第二层:精准重排。将第一步得到的大量候选文档交给重排模型,由其精排出最相关的少量结果(如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生产级服务

💡 如何选择?

你可以根据项目的核心需求,参考以下路径进行选择:

  1. 追求快速验证和简单集成 :如果你的目标是快速构建一个原型或中小型应用,希望尽可能简化部署和集成工作,那么 Chroma 是一个很好的起点 。
  2. 专注高性能和本地部署 :如果你的数据量在百万级别以内,且非常看重检索速度,同时希望避免维护外部服务的开销,FAISS 是经过大量实践检验的可靠选择 。
  3. 应对海量数据和企业级需求 :当你的应用需要处理千万级甚至亿级的向量数据,并要求高可用性、可扩展性时,MilvusQdrant 这类专业的分布式向量数据库是更合适的选择 。

为了帮助您快速了解和选型,下面这个表格详细对比了四款主流的分布式向量数据库在核心架构、性能、功能特性和适用场景等方面的关键差异。

特性维度 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安装即可使用。服务器模式的部署也相对容易。
理想应用场景 企业级超大规模应用,如十亿级向量的推荐系统、图像/视频检索、生物基因分析等。 过滤条件复杂查询延迟敏感 的语义搜索、广告推荐、实时风控等中大规模场景。 需要强大多模态检索混合搜索能力的中大规模应用,如智能知识图谱、内容推荐平台。 快速原型开发本地实验、桌面应用以及数据量在百万级别以内的中小型项目。

💡 如何选择分布式向量数据库

选择哪款数据库,最终取决于您的具体需求、技术栈和资源。您可以参考以下路径进行决策:

  1. 应对企业级超大规模场景 :如果您的项目需要处理千万级乃至百亿级 的向量数据,对性能、可用性和扩展性有极致要求,并且拥有专业的运维团队,那么 Milvus 是最可靠的选择。
  2. 平衡性能、功能与复杂度 :如果您的数据量在千万到亿级 ,希望获得接近Milvus的高性能,同时看重高效过滤和相对简洁的架构,Qdrant 是一个非常好的折中方案。如果您的应用强烈依赖结合关键词和向量的混合搜索 ,或者需要开箱即用的多模态检索 能力,Weaviate 的独特功能会非常有吸引力。
  3. 追求快速验证与轻量部署 :如果您的目标是快速搭建原型 、进行概念验证,或者您的数据量在百万级以下 ,希望极简的部署和集成体验,Chroma 是理想的选择。

💎 总结与建议

总的来说,这四款数据库各有侧重。Milvus 和 Qdrant 更像是为大规模生产环境准备的"重器",而 Chroma 和 Weaviate 则在易用性和特定功能上表现突出。

一个实用的建议是:在项目早期,可以先用 Chroma 快速验证想法和流程。当业务数据和并发增长后,再根据对性能、扩展性和功能(如混合搜索、多模态)的具体要求,评估是否迁移到 QdrantWeaviateMilvus

embedding向量查找算法

理解您想系统了解嵌入向量的查找算法。这些算法对于构建高效的语义搜索、推荐系统或RAG应用至关重要。下面这个表格汇总了主要的向量查找算法及其核心特征,方便您快速对比。

算法类别 代表算法 核心原理 优点 缺点 典型适用场景
精确查找 线性搜索 计算查询向量与数据库中每一个向量的距离 结果100%准确 计算成本极高,无法扩展 数据量极小(如数百个)的验证性场景
基于树结构 KD树 递归地将k维空间分割成二叉树结构 在中低维空间效率较高 维数灾难(维度升高后性能急剧下降),构建耗时 维度较低(通常<20)且数据量不大的场景
基于哈希 局部敏感哈希 设计特殊哈希函数,使相似向量 以高概率落入同一个哈希桶 查询速度极快 精度通常较低,对参数敏感 对速度要求极高,可接受一定精度损失的场景
基于量化 乘积量化 将高维向量分割成子空间并分别量化,用编码近似表示原向量 压缩率极高,大幅节省存储和内存 量化会引入误差,精度有损失 海量数据(亿级以上)存储和检索,内存资源紧张
基于图结构 HNSW 构建分层导航小世界图,高层实现快速粗筛,底层实现精细搜索 速度和精度平衡极佳,是目前主流选择 索引构建较慢,内存占用较高 对查询速度和精度要求高的通用场景,如推荐系统、语义搜索

💡 核心概念:近似最近邻搜索

面对海量高维数据,精确查找的计算成本变得无法承受 。因此,在实际应用中,我们几乎总是采用近似最近邻搜索

ANN的核心思想是牺牲少量精度以换取查询速度的数量级提升。它不再保证找到绝对最近的邻居,而是通过高效的索引结构,快速找到距离查询向量"足够近"的候选向量。对于绝大多数应用来说,95%以上甚至99%的召回率已经足够,而速度却能提升成百上千倍。

🔍 主流算法详解

在ANN算法中,基于图的方法(尤其是HNSW)和基于量化的方法是目前应用最广泛的两大方向。

  • HNSW:高性能的通用选择

    HNSW可以说是当前最流行的向量检索算法。其灵感来自于可导航的小世界网络(如社交网络),通过构建一个多层次的图结构来工作。高层 的图连接较稀疏,便于快速进行远距离"跳跃",迅速定位到目标区域。底层 的图连接密集,用于在目标区域内进行精细搜索,找到最邻近的节点。这种结构使得HNSW在搜索时能够实现对数级别的时间复杂度,在亿级数据上也能达到毫秒级的响应速度,同时保持很高的召回率。

  • 乘积量化:为海量数据而生

    乘积量化是一种通过压缩技术来应对海量向量的方案。它的核心步骤是分块、聚类、编码 :首先将高维向量切分成多个低维子向量;然后在每个子空间内进行聚类,得到一组聚类中心(码本);最后,每个原始向量可以用其各个子向量所属聚类中心的索引编码来表示。这样,一个浮点数向量就被压缩成了一个短短的整数编码,存储空间可以降低数十倍。在搜索时,通过查询向量与码本中聚类中心预先计算的距离表,可以高效地估算出与库中压缩向量的近似距离。这种方法特别适合当数据量远超内存容量时的磁盘检索场景。

🛠️ 如何选择算法?

选择哪种算法取决于您的具体需求和约束条件:

  1. 数据规模与维度 :这是首要考虑因素。千万级以下 的数据,HNSW 通常是性能最优的选择。如果数据量达到亿级甚至更大 ,需要优先考虑内存占用,可以评估乘积量化HNSW与量化结合的混合方案
  2. 精度与速度要求 :如果对精度要求极高 且数据量不大,可以调大HNSW的参数或考虑精确搜索 。如果对速度要求极端苛刻,可以尝试LSH或高度优化的量化方法。
  3. 系统资源内存充足 时,HNSW的性能表现非常出色。如果内存紧张 或需要处理十亿级数据,量化方法是更可行的选择。
  4. 数据更新频率 :对于需要频繁更新的数据库,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. 请系统梳理韩立从凡人到飞升仙界的完整修仙历程与关键节点。

  1. 请归纳韩立在《凡人修仙传》系列中的主要身份转变、重要成就及对应阶段。

  2. 请概述韩立一生中的重要际遇、核心功法和对其道途产生决定性影响的人物事件。

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:

  1. 请系统梳理韩立从凡人到飞升仙界的完整修仙历程与关键节点。
  2. 请归纳韩立在《凡人修仙传》系列中的主要身份转变、重要成就及对应阶段。
  3. 请概述韩立一生中的重要际遇、核心功法和对其道途产生决定性影响的人物事件。
    answer:

1. 韩立从凡人到飞升仙界的完整修仙历程与关键节点

韩立的修仙历程可划分为以下关键阶段:

  • 凡人起步:出身越国镜州青牛镇五里沟的普通农家,因身具灵根被七玄门墨大夫引入修仙之路,修炼《长春功》,后凭借"升仙令"加入黄枫谷,正式踏入修仙界(见升仙令相关片段)。
  • 炼气至元婴期
    • 在黄枫谷期间修炼青元剑诀、大衍诀,参与血色试炼,结丹后游历乱星海,炼制本命法宝青竹蜂云剑(金雷竹法宝)。
    • 结婴后返回天南,参与对抗慕兰族、坠魔谷探险,后期进阶元婴后期(大修士),成为天南乃至大晋顶尖强者。
  • 化神与飞升灵界
    • 在人界昆吾山事件后,通过空间节点飞升灵界,飞升过程充满风险(见银月提及的"星盘之力"及"北冥岛"经历)。
    • 在灵界初期隐匿修为,加入天渊城青冥卫(卓冲提及韩立曾为队长),后游历各族大陆,修为逐步提升至炼虚、合体期。
  • 大乘与渡劫飞升
    • 进阶大乘后,参与魔界之战、冥河之地等事件,积累机缘(如始魔之地之行),最终渡飞升之劫。据紫灵所述,韩立飞升仙界的把握超过五成(见"一半几率飞入仙界"片段)。
  • 关键节点
    • 升仙令:奠定宗门根基;
    • 金雷竹法宝:强化战力与机缘;
    • 飞升灵界:突破人界资源限制;
    • 大乘渡劫:完成凡俗至仙道的终极跨越。

2. 韩立在《凡人修仙传》系列中的主要身份转变、重要成就及对应阶段

  • 身份转变
    1. 凡人→低阶修士:七玄门弟子→黄枫谷链气弟子(凭借升仙令)。
    2. 宗门精英→独行强者:黄枫谷结丹修士→乱星海散修(炼制金雷竹法宝,对抗逆星盟)。
    3. 人界巅峰→灵界飞升者:天南大修士→灵界青冥卫、炼虚修士(卓冲称其"神通深不可测")。
    4. 灵界大能→仙界飞升者:大乘期至尊(黑枭王称其"进阶大乘"),最终渡劫成仙。
  • 重要成就
    • 人界阶段:创青元剑诀体系、炼制元婴级傀儡、掌握破灭法目等神通。
    • 灵界阶段:修炼炼神术、融合梵圣真魔功、参与广寒界试炼,进阶大乘。
    • 终极成就:渡飞升之劫,成为人族数十万年来首位成功飞升仙界的修士。

3. 韩立一生中的重要际遇、核心功法及决定性人物事件

  • 重要际遇
    • 升仙令:获得黄枫谷入门资格(叶姓老者接待);
    • 金雷竹:炼制本命法宝,奠定越阶战斗基础;
    • 逆星盘传送:意外脱离险境(片段提及"被逆星盘传送");
    • 始魔之地机缘:提升飞升成功率(对紫灵提及"多出两成把握")。
  • 核心功法
    • 青元剑诀:主修功法,从链气期延续至大乘期;
    • 大衍诀:强化神识,修炼至顶层(片段提及其"神识强大倍许");
    • 明王诀/梵圣真魔功:炼体功法,增强肉身防御;
    • 疾风九变:身法神通,提升机动性。
  • 决定性人物与事件
    • 墨大夫:引路人,亦为初期危机来源;
    • 厉飞雨:世俗挚友,其子孙后代与韩立有渊源(虬须大汉片段);
    • 银月/紫灵:红颜知己,多次助其脱困并共参大道;
    • 飞升之劫:最终考验,融合毕生积累与机缘方得成功。

说明:以上梳理基于提供的文档片段,并依据《凡人修仙传》系列作品的一般情节进行了逻辑推导,以保持历程的连贯性。关键节点如升仙令、飞升灵界、大乘渡劫等均直接源自片段内容,其他细节根据修仙阶段常见设定做了合理补充。

question:

韩立是怎么一步步进阶到大乘期的?主要有哪些机缘

rewritten_question:

  1. 韩立从炼气期到大乘期的完整修炼历程与关键突破节点是什么?
  2. 韩立在进阶大乘期的过程中,获得了哪些核心机缘与关键资源?
  3. 韩立的修炼路径中,有哪些重要事件与抉择推动了他最终突破至大乘期?
    answer:
    韩立的完整修炼历程与关键突破节点
    韩立的修炼始于炼气期 (在黄枫谷修炼长春功,依靠黄龙丹、金髓丸等低阶丹药突破至十一层),随后通过筑基丹成功筑基 。筑基后,他修炼《青元剑诀》与《三转重元功》以巩固根基,并借助大衍决增强神识。在结丹阶段,他依靠丹药和功法积累,最终结丹成功
    进入元婴期 后,韩立在坠魔谷获得机缘,快速突破至元婴中期,后期则通过寒焰五魔、鬼罗幡等秘术与资源修炼至元婴后期顶峰。化神期 的突破得益于绛云丹的无供应(灵药移植秘术使其能大量炼制),使他在人界元气稀薄环境下仍修炼至化神初期顶峰。
    飞升灵界后,韩立经历炼虚期(在广寒界获得机缘,连跨两级至炼虚后期顶峰)、合体期(通过广寒界收获、法体双修及雷霆之道感悟,突破至合体中期),最终
    进阶大乘期
    *。关键节点包括:炼虚期广寒界机缘、合体期法体双修与心境突破、大乘期对天地法则的领悟与资源积累。

进阶大乘期的核心机缘与关键资源

  1. 广寒界机缘:韩立在广寒界中获得突破炼虚后期的关键资源,并积累了对后续境界有益的感悟。
  2. 丹药与秘术
    • 化神期依赖绛云丹(通过风希的灵药移植秘术实现无***炼制),快速修炼至化神初期顶峰。
    • 炼体之术与法体双修功法:在灵根不稳时仍可修炼,保障修炼连续性。
    • 大衍决增强神识,为高阶突破奠定基础。
  3. 天地感悟与心境突破:合体期时,韩立通过与天元圣皇等高阶修士论道、经历蛮荒冒险,提升对天地法则的理解。
  4. 关键资源获取
    • 芥子空间中的灵药移植,解决炼丹原料问题。
    • 在蛮荒、冥河之地等地获得异族功法、雷霆之道传承等。
  5. 渡劫准备:为应对天劫,韩立培养隐雷灵根弟子海大少,并修炼雷霆神通以减轻渡劫负担。

重要事件与抉择推动大乘期突破

  1. 功法抉择:早期选择《青元剑诀》《三转重元功》与大衍决,奠定扎实根基;后期兼修法体双修与雷霆之道,适应灵根变化并增强渡劫能力。
  2. 机缘探索:主动进入坠魔谷、广寒界、蛮荒等险地,获取突破瓶颈的关键资源(如灵药、秘术、跨界感悟)。
  3. 资源管理:巧妙利用芥子空间移植灵药,确保丹药无***供应;收徒传承以换取渡劫助力(如海大少)。
  4. 心境锤炼:经历多次生死危机(如煞气反噬、强敌追杀),并在合体期后与高阶修士论道,巩固道心。
  5. 长期规划:韩立始终围绕"突破瓶颈"布局,例如化神期专注绛云丹修炼,炼虚期后注重法体双修,合体期提前培养弟子辅助渡劫。
  6. 应变能力:面对灵根隐患、煞气反噬等意外,及时调整修炼策略(如炼体术替代法术修炼),保障修炼进程不中断。

:以上整合自上下文片段中韩立在不同阶段的修炼描述、资源获取及事件选择,并基于修仙小说的常见逻辑(如境界突破依赖资源、机缘与心境)进行了合理推导,以形成连贯的修炼历程。

相关推荐
未来之窗软件服务2 小时前
幽冥大陆(七十九)Python 水果识别训练视频识别 —东方仙盟练气期
开发语言·人工智能·python·水果识别·仙盟创梦ide·东方仙盟
weixin_462446232 小时前
用 python -m ensurepip --upgrade 修复 uv / venv 中缺失 pip 的问题
python·pip·uv
光影少年2 小时前
AI前端开发需要会哪些及未来发展?
前端·人工智能·前端框架
hqyjzsb2 小时前
2026年AI证书选择攻略:当“平台绑定”与“能力通用”冲突,如何破局?
大数据·c语言·人工智能·信息可视化·职场和发展·excel·学习方法
独自归家的兔2 小时前
基于 cosyvoice-v3-plus 的简单语音合成
人工智能·后端·语音复刻
民乐团扒谱机2 小时前
【微实验】Python——量子增强时频传递的精度量化
人工智能·python·aigc·量子力学·时空·参数敏感性·光量子
G***技2 小时前
杰和IB3-771:以RK3588赋能机场巡检机器人
人工智能·物联网
Feibo20112 小时前
管理agent
python
牛奔3 小时前
Linux 的日志分析命令
linux·运维·服务器·python·excel