RAG(Retrieval-Augmented Generation,检索增强生成)是一种通过先从外部知识库检索相关信息,再结合检索内容辅助生成模型输出更准确、可靠内容的技术框架,有效解决了传统生成模型知识局限与信息滞后的问题。

一、什么是RAG?

RAG(Retrieval-Augmented Generation,检索增强生成),顾名思义,就是通过检索和整合外部知识来增强模型生成文本的能力。

通俗的讲,RAG就是把问题相关的知识都先查出来,再加上问题,将这些信息一起作为上下文输入给到模型,让模型输出更丰富、更准确、更可靠的内容。就好像我们的开卷考试,当看到一个题目后,先去查找与题目有关的知识,最后再进行回答问题。

RAG技术由Facebook AI Research(FAIR)团队于2020年首次提出,并迅速成为大模型应用中的热门方案。


二、为什么要用RAG?

与普通大模型(LLM)相比,RAG的优势在于可以解决模型的以下问题:

  • 突破知识瓶颈

    • 减少幻觉:传统生成模型受限于训练数据的时间范围与覆盖范围,易产生“知识幻觉”(编造不实信息)或信息滞后;RAG通过动态检索外部知识库(如最新文献、实时新闻、专业数据库),确保生成内容基于真实、最新的信息源,显著提升输出可信度。
    • 可追溯来源:回答可以关联到具体文档、段落或数据库记录,便于审计、验证及模型调优。
  • 动态更新能力强

    • 无需重新训练模型:新增或修改知识只需更新检索库(如向量数据库)。
    • 支持实时或近实时数据:可接入最新文档、日志、新闻、业务数据等。
    • 避免模型“过时”问题:尤其适合快速变化的业务环境。
  • 支持私有与领域知识

    • 可使用内部数据:如公司文档、技术手册、合同、代码库。
    • 保证数据隐私:数据不进入模型参数,更符合数据安全和隐私合规要求。
    • 垂直领域适配性强:在垂直领域(如制造、政务、科研)表现优于通用 LLM。
  • 成本与效率优势

    • 减少长上下文成本:只把“相关内容”喂给模型,而不是整库数据。
    • 可使用更小的模型:在有高质量检索支持时,小模型也能达到较好效果。
    • 计算资源更可控:相比不断微调大模型更经济,知识库的扩展成本远低于模型迭代成本。

三、如何理解RAG?

RAG主要由外部知识库(Corpus)、信息检索器(Retriever)、生成器(Generator,即大语言模型)等多个功能模块组成。

具体而言,就是给定一个自然语言问题(Query),检索器将问题进行编码,并从知识库(如百度)中高效检索出与问题相关的文档。然后,将检索到的知识和原始问题一并传递给大语言模型,大语言模型根据检索到的知识和原始问题生成最终的输出。

接下来我们再来看一个例子,比如我们现在问大模型这样一个问题: “2023 年的考拉数量有多少?”

  • 如果不使用RAG技术时:假设模型训练参数内没有相关的知识或者相关知识过旧未更新,那模型就只能读取到当前大模型所知的统计数量,得到的可能是5000~8000只的结果,这显然是不对的。

  • 但是,如果使用了RAG技术时

    • 首先,该问题会传递给 RAG 框架的检索器模块,检索器从最新知识库中检索相关的知识文档,其中包含了与 2023 年的考拉数量相关的信息;
    • 接下来,这些信息通过 Prompt 的形式传递给大语言模型(大语言模型利用外部知识的形式是多样的,通过 Prompt 进行上下文学习是其中最常用的形式)。
    • 模型最终得出了正确的答案:“2023 年的考拉数量在 86,000 至 176,000 只之间。”

在这里插入图片描述
从上面可以看出,我们RAG技术在于通过检索外部知识库,增强模型生成能力,以此来提升生成回答的准确性。

问题来了,如果想要提高RAG系统的性能,我们该如何设计?
答案是:增强检索 + 增强生成。我们可以根据不同的场景需求,从这两个方面出发,来设计不同类型的RAG架构。


四、RAG架构分类

RAG系统是一个集成了外部知识库、检索器、生成器等多个功能模块的软件系统。在不同的协作方式下,检索器检索到的信息质量会有所不同,生成器生成的内容质量也会随之变化。

针对不同的业务场景,可以从是否对大语言模型进行微调的角度出发,将RAG系统架构分为两大类:黑盒增强架构、白盒增强架构

在这里插入图片描述

1. 黑盒增强架构

在某些情况下,由于无法获取大模型的结构和参数、或者没有足够的算力对模型进行微调,只能通过API与大模型进行交互时,大模型就被视为一个黑盒,此时RAG建立在黑盒大模型之上,我们只能对检索器进行优化。

因此,黑盒增强架构可以根据是否对检索器进行微调分为两类:无微调、检索器微调

1.1. 无微调

无微调架构是所有RAG架构中形式最简单的,检索器和语言模型经过分别独立的预训练后参数不再更新,直接组合使用。这种架构对计算资源需求较低,方便实现且易于部署,适合于对部署速度和灵活性有较高要求的场景。

In-Context RALM架构是无微调架构的典型代表,它包括检索和生成两个阶段:

  • 在检索阶段:输入的问题或部分句子作为查询从知识库中检索出相关文档。
  • 在生成阶段:这些检索到的文档被直接拼接到 Prompt 中的上下文部分,然后将 Prompt 输入给大语言模型。

在这里插入图片描述

一个 RAG 任务可能涉及多次执行检索和生成,在执行检索操作时,需要注意几个关键参数:

  • 检索步长:是指模型在生成文本时,每隔多少个词进行一次检索。这个参数影响到模型的响应速度和信息的即时性,较短的检索步长能够提供更为及时的信息更新,但同时也可能增加计算的复杂性和资源消耗。
  • 检索查询长度:是指用于检索的文本片段的长度,通常被设置为语言模型输入中的最后几个词,以确保检索到的信息与当前的文本生成任务高度相关。

1.2. 检索器微调

相比无微调架构,检索器微调架构可以进一步提升检索器与大模型之间的协同效应。在这种架构下,大模型的参数保持不变,需要根据模型输出来指导检索器的微调,使检索器能更好的适配大模型,提高RAG系统的表现。

REPLUG LSR架构是检索器微调架构典型代表,它使用大模型的困惑度分数(文后有介绍)作为监督信号来微调检索器,使其能更有效地检索出能够显著降低大模型困惑度的文档。微调检索器的过程中采用 KL 散度损失函数来训练检索器,目的是对齐检索到的文档的相关性分布与这些文档对大模型性能提升的贡献分布。
在这里插入图片描述
在微调检索器的过程中涉及两个关键的概率分布:

  • 检索器输出的文档分布:检索器在接收到当前上下文后检索与之相关的文档,并形成一个文档概率分布。这一分布是基于检索器计算的上下文与文档之间的相似度,通过余弦相似度来衡量,并将这些相似度分数转化为概率值。
  • 文档对语言模型的贡献分布:语言模型为每个被检索到的文档和原始上下文来生成预测,最终所有输出结果形成一个概率分布。在这个分布中,如果某个文档对语言模型生成准确预测特别关键,它会被赋予更高的概率权重。

在微调过程中,REPLUG LSR 将语言模型视为黑盒处理,仅通过模型的输出来指导检索器的训练,避免了对语言模型内部结构的访问和修改。

同时,REPLUG LSR 还采用了一种异步索引更新策略,即不会在每次训练步骤后立即更新知识库的向量编码,而是在一定的训练步骤之后才进行更新。这种策略降低了索引更新的频率,减少了计算成本,使模型能够在连续训练过程中更好地适应新数据。

此外,检索器微调框架中给还可以引入代理模型来指引检索器微调。例如,AAR方法通过引入额外的小型语言模型,使用它的交叉注意力得分标注偏好文档,以此来微调检索器,使REPLUG LSR 模型架构图其能够在不微调目标语言模型的情况下增强其在不同任务上的表现。

2. 白盒增强架构

通常大语言模型和检索器是独立预训练的,二者可能存在匹配欠佳的情况。白盒增强架构通过微调大语言模型来配合检索器,它也可以根据是否对检索器进行微调分为两类:仅微调语言模型、检索器与语言模型协同微调

2.1. 仅微调语言模型

仅微调语言模型指的是检索器作为一个预先训练好的组件其参数保持不变,大语言模型根据检索器提供的上下文信息,对自身参数进行微调。

RETRO架构模型是仅微调语言模型的典型代表,它通过修改语言模型的结构,使其在微调过程中能够将从知识库中检索到的文本直接融入到语言模型中间状态中,从而实现外部知识对大语言模型的增强。
在这里插入图片描述
RETRO 模型架构实现过程:

  • 首先将知识库中的文本进行切块,然后用 BERT 对每个文本块生成嵌入向量。
  • 其次在微调模型时的自回归过程中,每当模型生成一段文本块后,就去知识库中检索出与之最相似的嵌入向量。
  • 然后这些嵌入向量和模型注意力层的输出一起被送入一个外部的 Transformer 编码器进行编码。
  • 接着将得到的编码向量直接输入给模型的块交叉编码器的键(key)和值(value),以捕捉外部知识的关键信息。
  • 最后通过交叉编码,模型能够结合检索到的相关信息来生成新的文本块。

通过上述方式微调后的 RETRO 模型能够充分整合检索到的信息,生成连贯且富含信息的文本。面对用户查询时,模型能展现出优秀的理解能力和知识整合能力,大幅提升了生成的质量和准确性,尤其在处理复杂任务时,其表现更为突出。

2.2. 检索器与语言模型协同微调

在仅微调语言模型的架构下,检索器作为固定组件,微调过程中其参数保持不变。这导致检索器无法根据语言模型的需求进行适应性调整,从而限制了检索器与语言模型之间的相互协同。

在检索器和语言模型协同微调的架构中,检索器和语言模型的参数更新同步进行。这种微调的方式使得检索器能够在检索的同时学习如何更有效地支持语言模型的需求,而语言模型则可以更好地适应并利用检索到的信息,以进一步提升 RAG 的性能。

Atlas架构是检索器与语言模型协同微调的典型代表,与REPLUGLSR类似,它在预训练和微调阶段使用KL散度损失函数来联合训练检索器和语言模型,以确保检索器输出的文档相关性分布与文档对语言模型的贡献分布相一致。

不同之处在于,Atlas在预训练和微调过程中,检索器和语言模型参数同步被更新,检索器学习向语言模型提供最相关的文档,而语言模型则学习如何利用这些文档来改善其对查询的响应。为了确保检索结果与模型最新状态保持同步,Atlas同样需要定期更新语料库文档的向量编码,从而维持检索的准确性。
在这里插入图片描述

3. 不同架构总结

3.1. 黑盒增强架构(闭源模型背景)
黑盒增强架构是在闭源模型的背景下提出的,包含了无微调、检索器微调两种策略。它限制了对模型内部参数的直接调整,只允许对检索器进行微调优化。

优势:

  • 部署快速简便:无微调策略可直接使用预训练模型和检索器,无需任何调整,适合紧急上线场景。
  • 资源消耗低:仅需检索器操作,无需额外训练,计算成本极低(仅需基础GPU资源)。
  • 维护简单:保持模型原始状态,避免因微调导致的模型不稳定或性能波动。

劣势:

  • 优化能力受限:无法调整语言模型内部参数,难以适应特定任务需求(如专业领域问答)。
  • 性能天花板明显:系统性能受限于预训练模型的通用能力,无法突破基础性能瓶颈。
  • 检索器依赖性强:检索器微调的效果高度依赖检索器的准确性(如检索质量差则整体效果差),且无法优化语言模型与检索器的协同性。

3.2. 白盒增强架构(开源模型背景)
白盒增强架构则利用开源模型的优势,它包含了仅微调语言模型、检索器和语言模型协同微调两种策略。它不仅可以对检索器进行微调优化,还允许调整语言模型结构和参数,使两者能动态同步更新,互相适应,从而提升系统的整体性能。

优势:

  • 深度优化能力:允许调整语言模型结构和参数,实现精准定制(如仅微调语言模型或协同微调)。
  • 协同性能提升:协同微调策略通过同步更新检索器和语言模型,使两者动态适配,显著提升整体性能(如准确率提升15-30%)。
  • 高适应性:可针对不同任务(如医疗诊断、金融分析)定制优化,实现“一模型多场景”应用。

劣势:

  • 资源需求高昂:协同微调需大量计算资源(如多GPU集群)和训练时间(通常需数天至数周),成本显著增加。
  • 实施复杂度高:需专业团队设计微调策略,调试过程复杂(如超参数调优、训练稳定性问题)。
  • 维护成本高:模型迭代需持续投入资源,且不当微调可能导致性能下降或过拟合。

五、RAG的实现原理

高性能的RAG系统的实现主要包含两大模块:增强检索、增强生成

1. 增强检索

在RAG中,检索的效果(召回率、精度、多样性等)会直接影响大模型的生成质量,如果检索器返回不正确的外部知识,就可能导致大模型生成错误的答案。同时,检索的耗时也是RAG总耗时的关键部分,因此检索的效率也会直接影响用户的体验。

为改善RAG系统的性能,针对优化检索过程,提升检索的效果和效率,我们可以从知识库构建、查询增强、检索器、检索结果重排序等几个关键技术入手。
在这里插入图片描述

1.1. 知识库构建

知识库是RAG系统的根基,只有构建了全面、优质、高效的知识库,检索才能高效有保证。在RAG框架中,知识库的构建主要涉及两个步骤:数据采集及预处理、知识库增强

1.1.1. 数据采集及预处理

数据采集及预处理为构建知识库提供“原材料”,整体上又可以分两步:数据采集、数据预处理。

数据采集
在构建文本型知识库的数据采集过程中,来自不同渠道的数据被整合、转换为统一的文档对象。这些文档对象不仅包含原始的文本信息,还携带有关文档的元信息(Metadata),这些元信息可以用于后续的检索和过滤。

比如,现在我们要构建维基百科语料库,数据采集主要通过提取维基百科网站页面内容来实现。这些内容不仅包括正文描述的内容,还包括一系列知识检索流程图的元信息,例如文章标题,分类信息,时间信息,关键词等。

数据预处理
在采集到相应的数据后,还需通过数据预处理来提升数据质量和可用性。在构建文本型知识库时,数据预处理主要包括以下两个过程:

  • 数据清洗:目的是清除文本中的干扰元素,如特殊字符、异常编码和无用的 HTML 标签,以及删除重复或高度相似的冗余文档,从而提高数据的清晰度和可用性。
  • 文本分块:将长文本分割成较小文本块的过程,例如把一篇长文章分为多个短段落。对长文本进行分块有两个好处:
    • 一是为了适应检索模型的上下文窗口长度限制,避免超出其处理能力;
    • 二是通过分块可以减少长文本中的不相关内容,降低噪音,从而提高检索的效率和准确性。

文本分块的效果直接影响后续检索结果的质量。如果分块处理不当,可能会破坏内容的连贯性。因此,制定合适的分块策略至关重要,包括确定切分方法(如按句子或段落切分)、设定块大小,以及是否允许块之间有重叠。

1.1.2. 知识库增强

知识库增强是通过改进和丰富知识库的内容和结构,以提升其质量和实用性。这一过程通常涉及查询生成、标题生成等多个步骤,以此为文档建立语义“锚点”,方便检索时准确定位到相应文本。

查询生成
查询生成指的是利用大语言模型生成与文档内容紧密相关的伪查询。这些伪查询从查询的角度来表达文档的语义,可以作为相关文档的“键”,供检索时与用户查询进行匹配。通过这种方式,可以增强文档与用户查询的匹配度。

比如,对于一篇介绍考拉和树袋熊关系的文档,生成的查询“考拉和树袋熊之间的关系是什么?”不仅准确反映了文档的主题,还能有效引导检索器更精确的检索到与用户提问相关的信息。

标题生成
标题生成指的是利用大语言模型为没有标题的文档生成合适的标题。这些生成的标题提供了文档的关键词和上下文信息,能用来帮助快速理解文档内容,并在检索时更准确地定位到与用户提问相关的信息。对于那些原始文档中缺乏标题的情况,通过语言模型生成标题显得尤为重要。

1.2. 查询增强

知识库对知识的表达形式是有限的,但用户的提问确是千人千面的。每一个用户的提问对问题的描述可能与知识库保存的文本存在差异,无法达到完全匹配效果,使得检索效果不佳。

为了解决这个问题,我们可以对用户查询的语义和内容进行扩展,即查询增强,以更好的匹配知识库中的文本。因此,查询增强技术主要从两个方面出发:查询语义增强、查询内容增强

1.2.1. 查询语义增强

查询语义增强又可以通过 同义改写、多视角分解 等方法来扩展、丰富用户查询的语义,以提高检索的准确性和全面性。

同义改写
同义改写就是将原始查询改写成相同语义下不同的表达方式,来解决用户查询单一的表达形式可能无法全面覆盖到知识库中多样化表达的知识。改写工作可以调用大语言模型完成。

比如,对于这样一个原始查询:“考拉的饮食习惯是什么?”,可以改写成下面几种同义表达:1、“考拉主要吃什么?”;2、“考拉的食物有哪些?”;3、“考拉的饮食结构是怎样的?”。每个改写后的查询都可独立用于检索相关文档,随后从这些不同查询中检索到的文档集合进行合并和去重处理,从而形成一个更大的相关文档集合。

多视角分解
多视角分解采用分而治之的方法来处理复杂查询,将复杂查询分解为来自不同视角的子查询,以检索到查询相关的不同角度的信息。

比如,对于这样一个问题:“考拉面临哪些威胁?”,可以从多个视角分解为:1、“考拉的栖息地丧失对其有何影响?”;2、“气候变化如何影响考拉的生存?”;3、“人类活动对考拉种群有哪些威胁?”;4、“自然灾害对考拉的影响有哪些?”等子问题。每个子问题能检索到不同的相关文档,这些文档分别提供来自不同视角的信息。通过综合这些信息,语言模型能够生成一个更加全面和深入的最终答案。

1.2.2. 查询内容增强

查询内容增强是通过生成与原始查询相关的背景信息和上下文,从而丰富查询内容,提高检索的准确性和全面性。与传统的仅依赖于检索的方式相比,查询内容增强方法通过引入大语言模型生成的辅助文档,为原始查询提供更多维度的信息支持。

1.3. 检索器

检索器的作用就是找到知识库中与用户查询相关的知识文本。目前检索器主要有两类:判别式检索器、生成式检索器

1.3.1. 判别式检索器

判别式检索器通过判别模型对查询和文档是否相关进行打分。判别式检索器通常分为两大类:稀疏检索器、稠密检索器

稀疏检索器
稀疏检索器(Sparse Retriever)是指使用稀疏表示方法来匹配文本的模型。简单的说就是关键词匹配,只能匹配出现了关键词的文档。

典型的稀疏检索技术包括 TF-IDF 和 BM25 等,它们通过分析词项的分布和频率来评估文档与查询的相关性:

  • TF-IDF算法:基于词频(TF)和逆文档频率(IDF)来衡量词语在文档或语料库中的重要性,然后用此重要性对文本进行编码。TF-IDF 通过高词频和低文档频率产生高权重,倾向于过滤常见词语,保留重要词语。
  • BM25算法:是一种改进的文本检索算法,它在 TF-IDF 基础上通过文档长度归一化和词项饱和度调整,更精确地评估词项重要性,优化了词频和逆文档频率的计算,并考虑了文档长度对评分的影响。虽然不涉及词项上下文,但是 BM25 在处理大规模数据时表现优异,广泛应用于搜索引擎和信息检索系统。

稠密检索器
稠密检索器一般利用预训练语言模型对文本生成低维、密集的向量表示,通过计算向量间的相似度进行检索。简单的说就是向量检索,能匹配出语义相近的文档。

按照所使用的模型结构的不同,稠密检索器大致可以分为两类:交叉编码类(Cross-Encoder)、双编码器类(Bi-Encoder)
在这里插入图片描述
交叉编码类
交叉编码类“端到端”的给出查询和文档的相似度。这类模型将查询和文档拼接在一起,随后利用预训练语言模型作为编码器(例如 BERT)生成一个向量表示。接着,通过一个分类器处理这个向量,最终输出一个介于 0 和 1 之间的数值,表示输入的查询和文档之间的相似程度。

交叉编码类模型的优点在于模型结构简单,能够实现查询和文档之间的深度交互。但由于其需要进行高复杂度的交叉注意力操作,计算量大,因此不适合在大规模检索阶段使用。这种模型更适用于对少量候选文档进行更精确排序的阶段,可以显著提升检索结果的相关性。

双编码器类
与交叉编码类模型不同,双编码类模型采用了一种“两步走”的策略。第一步,查询和文档首先各自通过独立的编码器生成各自的向量表示;第二步,对这两个向量之间的相似度进行计算,以评估它们的相关性。

双编码器类模型的优势在于,它允许预先离线计算并存储所有文档的向量表示,在线检索时则可直接进行向量匹配。因此,双编码器非常适合在工业环境中部署,具有极高的匹配效率。然而,在这种分离的处理方式中,查询与文档在提取特征向量时缺乏交互,会对向量匹配的精度产生影响,容易出现幻觉。

DPR(Dense Passage Retriever)是稠密检索器的一个典型代表。它使用两个独立的 BERT 编码器,分别将查询和文档映射到低维特征向量,然后通过向量点积衡量相似度。为了缓解查询与文档缺乏交互的问题,DPR 通过对比学习优化编码器,最大化查询与相关段落相似度的同时最小化与负面段落相似度。

为了缓解查询与文档在提取特征向量时缺乏交互的问题,还可以在双编码器的基础上引入查询与文档的交互,以进一步提升双编码器的效果。ColBERT 就是以查询和文档间的 Token 级的相似度为度量,然后通过对比学习对双编码器进行微调,使双编码器编码的特征向量可以兼顾查询和文档。

此外,在 RAG 中,我们有时会对查询加入大段上下文进行增强,此时查询的长度急剧增长,传统的检索方法可能难以有效的处理这些长查询。为解决此问题,可以采用 Poly-encoder 模型架构,它沿用双编码器的形式,但是它使用 m 个向量来捕获长查询的多个特征,而不是像普通双编码器那样只用一个向量来表示整个查询。并且,这 m 个向量随后与文档的向量通过注意力机制进行交互,其中的注意力模块采用查询和文档间的对比学习进行训练。

1.3.1. 生成式检索器

生成式检索器通过生成模型对输入查询直接生成相关文档的标识符。与判别式检索器不断地从知识库中去匹配相关文档不同,生成式检索器直接将知识库中的文档信息记忆在模型参数中。然后,在接收到查询请求时,能够直接生成相关文档的标识符(即 DocID),以完成检索。

生成式检索器通常采用基于Encoder-Decoder 架构的生成模型,如 T5、BERT等。它的训练过程通常分为两个阶段:

  • 第一阶段:模型通过序列到序列的学习方法,学习如何将查询映射到相关的文档标识符。这一阶段主要通过最大似然估计(MLE)来优化模型,确保生成的文档标识符尽可能准确。
  • 第二阶段:模型通过数据增强和排名优化进一步提高检索效率和准确性。
    • 数据增强:主要通过生成伪查询或使用文档片段作为查询输入,以增加训练数据的多样性和覆盖面。
    • 排名优化:通过使用特定的损失函数,如对比损失或排名损失,来调整模型生成文档标识符的顺序和相关性,从而更好地匹配查询的需求。

在生成式检索器中,文档的标识符(即 DocID)的设计至关重要。它需要在语义信息的丰富性与标识符的简洁性之间取得平衡。常用的 DocID 形式分为两类:

  • 基于数字的 DocID:使用唯一的数字值或整数字符串来表示文档,虽然构建简单,但在处理大量文档时可能导致标识符数量激增,增加计算和存储负担。
  • 基于词的 DocID:直接从文档的标题、URL 或 N-gram 中提取表示,能更自然地传达文档的语义信息。通常,标题是最佳选择,因为它提供了文档的宏观概述。但在缺乏高质量标题时,URL 或 N-gram 也可作为有效的替代方案。

尽管生成式检索器在性能上取得了一定的进步,但与稠密检索器相比,其效果仍稍逊一筹。此外,生成式检索器还面临着一系列挑战,包括如何突破模型输入长度的限制、如何有效处理大规模文档以及动态新增文档的表示学习等,这些都是亟待解决的问题。

1.4. 检索效率增强

知识库中通常包含海量的文本,对知识库中文本进行逐一检索缓慢而低效。为提升检索效率,可以引入向量数据库来实现检索中的高效向量存储和查询。向量数据库的核心就是设计高效的相似度索引算法。

1.4.1. 什么是向量?

向量是一种有大小和方向的数学对象,它可以表示为从一个点到另一个点的有向线段。
比如:在二维空间中的向量可以表示为[a,b],表示的是从原点[0,0]到点[a,b]的有向线段。
在这里插入图片描述
而在向量数据库的中,它不是传统数学中的"有大小有方向的量",而是一组有序的数字,可以表示数据的特征。比如一个人的特征有:姓名、性别、身高、体重、年龄,这些特征就可以组成一个 5 维向量 [姓名,性别,身高,体重,年龄]。

简单来说,向量就是一个一维数组或列表,如[0.12, 0.35, …, 0.78],它代表了数据在高维空间中的坐标。

在AI时代,向量是AI理解世界的通用数据形式,是AI的灵魂。

1.4.2. 什么是文本向量?

文本向量是将非结构化文本数据转换为高维数值向量的过程,是AI理解文本的"数学语言"。

简单来说,文本向量就是一组有序的数值,每个文本段落都被转化为一个独特的数字坐标,这个数字坐标包含了文本段落的深层语义信息。如[0.12, 0.35, …, 0.78],它代表了文本在多维空间中的坐标位置,而向量数据库通过计算文本向量之间的距离(如余弦相似度)来判断文本的相似性。

为什么需要文本向量?
因为计算机无法直接理解人类语言,而文本向量将文本转化为计算机可以处理的数值形式保存在向量数据库中,向量数据库再通过计算向量之间的距离(如余弦相似度)来判断文本的相似性。

1.4.3. 常用相似度算法

向量相似度用于衡量两个向量在空间中的“相似程度”,本质上是比较方向、长度或分量差异。
假设现在有两个向量:a=(a1​,a2​,…,an​),b=(b1​,b2​,…,bn​),我们来看一下常用的相似度算法。

点积相似度(Dot Product)

定义:点积也叫内积,是向量空间中最基础的相似度度量之一,定义为两个向量对应元素的乘积之和。
计算公式:
在这里插入图片描述
特点:

  • 计算简单,速度快,时间复杂度 O(n)。
  • 依赖向量长度,如果向量未归一化,结果可能失真。

典型应用:

  • 深度学习模型内部,NLP / Embedding 检索。
  • 大规模推荐系统,分析用户对物品的兴趣分数。

余弦相似度(Cosine Similarity)

定义:余弦相似度通过计算两个向量夹角的余弦值来衡量相似程度,只关注方向,不关注长度(模长)。
计算公式:
在这里插入图片描述
特点:

  • 越大越相似。
  • 与向量长度无关。
  • 对文本、语义向量极其友好。
  • 高维空间表现稳定。

典型应用:

  • 文本语义搜索(BERT、Sentence Transformer)
  • 向量数据库(Milvus、FAISS、Pinecone)

欧氏距离(Euclidean Distance,L2)

定义:欧氏距离是最常用的空间距离度量,反映了两个点在 n 维空间中的直线距离。
计算公式:
在这里插入图片描述
特点:

  • 越小越相似。
  • 同时考虑方向和长度。
  • 对尺度非常敏感。
  • 高维空间中“距离集中效应”明显。

典型应用:

  • 图像特征匹配,SIFT、SURF、CNN 特征匹配。
  • 聚类,K-Means / KNN / DBSCAN 等算法。
  • 机器学习距离度量,KNN 分类 / 回归。

曼哈顿距离(Manhattan Distance,L1)

定义:顾名思义,在曼哈顿街区要从一个十字路口开车到另一个十字路口,驾驶距离显然不是两点间的直线距离。这个实际驾驶距离就是“曼哈顿距离”。曼哈顿距离也称为“城市街区距离”(City Block distance)。
计算公式:
在这里插入图片描述
特点:

  • 对高维稀疏向量比 L2 稳定,特别是 L1 正则化场景。
  • 对尺度敏感,不同维度单位不同,需归一化或标准化。

典型应用:

  • 稀疏向量度量,文本词频向量(Bag of Words)。
  • 异常检测,每维度误差累加,L1比L2更敏感小幅偏差。
  • KNN分类、回归或聚类(如K-Medians),特别适合稀疏特征数据。

汉明距离(Hamming Distance)

定义:用于衡量两个等长字符串或向量在对应位置上不同的个数。
计算公式:
在这里插入图片描述
特点:

  • 二进制数据格式。
  • 只适用于维度相同的向量。
  • 汉明距离小 ,文本相似。

典型应用:

  • SimHash,文本去重。
  • 图像感知哈希(pHash / aHash / dHash),图片去重。
  • 指纹匹配、生物特质识别。

皮尔逊相关系数(Pearson Correlation Coefficient, PCC)
定义:皮尔逊相关系数用于衡量两个变量的线性相关程度。它反映了变量之间的线性关系强度和方向。
计算公式:
在这里插入图片描述
特点:

  • 具有对称性,ρ(X,Y)=ρ(Y,X) 。
  • 不受原始单位影响,适合不同尺度数据。
  • 对非线性关系不敏感(可能是 0,但仍有强非线性)。

典型应用:

  • 数据分析,统计两组连续变量的相关性。
  • 适用推荐系统,协同过滤(用户-用户或物品-物品)相似度,PCC > 0 表示趋势一致,推荐相似用户/物品。

杰卡德相似度(Jaccard Similarity)
定义:衡量两个集合相似程度的一种指标,关注共享元素占总元素的比例。
计算公式:
在这里插入图片描述
特点:

  • 只关注存在与否,不关心元素出现次数(适合集合 / 标签 / 指纹)。
  • 对稀疏向量友好,非零元素少,计算仍高效。
  • 适合离散特征,如文本关键词、用户行为集合、标签集合。
  • 越小越相似。

典型应用:

  • MinHash 近似计算,如网页去重、文档重复检测。
  • NLP / 文本去重,判断抄袭或重复。

常用算法总结

算法数据类型取值范围长度/频率敏感稀疏/稠密优点缺点典型应用
点积相似度 (Dot Product)连续向量(-∞, ∞)稠密计算快,可直接用于评分/排序未归一化受向量长度影响推荐系统评分、注意力机制、嵌入检索
余弦相似度 (Cosine Similarity)连续向量[-1,1]否(归一化后)稠密/稀疏均可关注方向,长度无影响不保留幅度信息文本 embedding 检索、向量相似度
欧氏距离 (Euclidean Distance, L2)连续向量[0, ∞)稠密直观、解释性强高维稀疏数据易出现距离集中现象图像特征匹配、KNN、聚类
曼哈顿距离 (Manhattan Distance, L1)连续向量[0, ∞)稀疏/稠密对异常值鲁棒、计算简单高维稀疏数据幅度累加KNN、稀疏向量特征、异常检测
汉明距离 (Hamming Distance)二进制向量[0,n]稀疏/二进制简单,适合指纹、二进制特征仅限等长二进制向量指纹识别、去重、布尔特征
皮尔逊相关系数 (Pearson Correlation, PCC)连续向量[-1,1]否(去均值后)稠密衡量线性相关,去均值消除偏置只捕捉线性关系,对异常值敏感协同过滤、统计分析、时间序列
杰卡德相似度 (Jaccard Similarity)集合 / 二进制[0,1]稀疏简单,适合集合/标签/二进制数据忽略频率信息文本去重、用户行为集合、标签相似度
1.4.4. 常用向量数据库
名称类型开源/商业典型应用场景AI 应用领域备注
Pinecone专用向量数据库商业 / 托管高性能向量搜索、RAG、推荐大语言模型检索增强生成 (RAG)、推荐系统、知识库云原生、易用性强
Milvus分布式向量数据库开源 & 商业(Zilliz Cloud)大规模向量存储与查询图像/视频检索、NLP embedding 搜索、推荐系统支持集群、高可用
Weaviate向量数据库 + 多模搜索开源混合搜索、结构化 + 向量文本、图像、知识图谱、RAG支持 GraphQL、Filter
Qdrant向量搜索引擎开源高并发向量检索NLP embedding 检索、推荐系统、实时向量更新Rust 实现,实时更新能力强
Chroma轻量级向量数据库开源快速实验、开发本地/小规模 LLM RAG、嵌入存储与 LangChain 等集成良好
Elasticsearch Vector Search通用搜索引擎扩展商业/开源文本 + 向量混合搜索文本检索、企业知识库、推荐强全文搜索能力,可混合结构化数据
OpenSearch k‑NN通用搜索引擎扩展开源文本 + 向量检索NLP embedding 搜索、日志/文档检索Amazon 支持的开源搜索
pgvector (PostgreSQL)RDBMS 扩展开源小型向量搜索、嵌入 & 关系数据融合小规模 RAG、推荐系统、SQL 环境嵌入与 SQL 融合,可快速集成现有数据库
FAISS向量相似度搜索库开源高效 ANN 库图像检索、文本 embedding 检索、近似最近邻搜索更多是搜索引擎组件而非完整数据库
Vespa大规模搜索 & 向量检索平台开源向量 + 结构化搜索企业级推荐、广告检索、知识库检索企业级大规模部署,支持复杂检索逻辑

1.5. 检索结果重排序

检索器可能检索出与查询相关性不高的文档,这些文档如果直接交给大模型就可能导致生成的质量下降。因此,在交给大模型之前,我们还可以对这些文档进一步精选。精选的主要方法就是对检索到的文档进行重新排序,简称重排,然后从中选出排序靠前的文档。

重排方法主要分为两类:基于交叉编码的重排方法、基于上下文学习的重排方法。

1.5.1. 基于交叉编码的重排方法

基于交叉编码的重排方法利用交叉编码器(Cross-Encoders)来评估文档与查询之间的语义相关性。

MiniLM-L 模型是基于交叉编码的重排的典型代表,该模型通过减少层数和隐层单元数来降低参数数量,同时采用知识蒸馏技术从大型、高性能的语言模型中继承学习,以此来提高模型性能。

1.5.2. 基于上下文学习的重排方法

基于上下文学习的方法是指通过设计精巧的 Prompt,利用大语言模型优良的深层语义理解能力,使用大语言模型来执行重排任务。

RankGPT 是基于上下文学习的重排方法中的代表性方法。在重排任务中,输入文档长度有时会超过上下文窗口长度的限制。为了解决该问题,RankGPT 采用了滑动窗口技术来优化排序过程。该技术将所有待排序的文档分割成多个连续的小部分,每个部分作为一个窗口。
在这里插入图片描述
整个重排序过程:

  • 首先,从文档集的末尾开始,对最后一个窗口内的文档进行排序,并将排序后的结果替换原始顺序。
  • 然后,窗口按照预设的步长向前移动,重复排序和替换的过程。
  • 这个过程将持续进行,直到所有文档都被处理和排序完毕。

通过这种分步处理的方法,RankGPT 能够有效地对整个文档集合进行排序,而不受限于单一窗口所能处理的文档数量。

2. 生成增强

当检索器得到检索结果信息后,将其传给大模型以增强大模型的生成能力,但利用这些信息进行生成增强是一个复杂的过程,我们该何时增强?何处增强?如何多次增强?如何降本增效?每个环节的处理不同都会影响RAG的性能。

2.1. 何时增强

大大模型在训练过程中掌握了大量的知识,这些知识被称为内部知识(Self-Knowledge)。对于内部知识可以解决的问题,我们可以不对该问题进行增强,避免影响生成效率和生成质量。

判断是否需要增强的核心在于判断大模型是否具有内部知识,如果这个问题具备内部知识解答,那我们就可以避免检索增强。判断大模型是否具有内部知识的方法可以分为两类:外部观测法、内部观测法

外部观测法

通过 Prompt 直接询问模型是否具备内部知识,或应用统计方法对是否具备内部知识进行估计,这种方法无需感知模型参数。外部观测法主要涉及以下方式:

  • 直接询问:在编写Prompt直接询问大模型是否需要外部知识,但大模型可能经常认为自己不需要外部知识,这种方式准确率较低。
  • 多次询问:让大模型重复多次回答同一个问题,根据回答的一致性判断是否具备内部知识。回答的一致性越高,一般就表示大模型具备内部知识。但这个方式仍然无法避免大模型每次都给出相同的错误答案,可靠性也较低,而且多次询问会导致性能大大降低。
  • 观察训练数据:大模型的内部知识来源于训练,因此我们可以通过判断训练数据是否包含问题相关信息来判断是否具备内部知识。但大模型与人相似,对高频学习的知识掌握度高,对低频学习的知识可能掌握度较低,并且大模型的训练数据量相当庞大,查询起来会非常耗时,所以这种方式也存在一定的局限性。还有就是部分大模型都是闭源的,训练数据无法获取到,这种方式就无法执行。
  • 构造伪训练数据统计量:当训练数据不可获取时,通过设定一个流行度阈值来判断模型是否具备相应的内部知识。所谓流行度就是用户对某些数据的访问量的大小,因为模型对高频知识掌握更好,即认为访问量大的数据更容易被大模型记住,所以可以根据这个访问量大小来作为伪训练数据统计量,以此来识别是否内部知识。但这种流行度依赖于数据形成,不一定精准,存在一定的局限性。

内部观测法

当我们可以访问大模型的内部参数时,我们可以通过检测模型内部神经元的状态信息来判断模型是否存在内部知识。这种方式类似人类测谎的过程,科学家通过观察分析人的脑电波、脉搏、血压等人类内部状态来推断大脑的活动和认知状态,从而判断其是否说谎。

对大模型而言,大模型可以通过分析模型在生成内容时每一层的隐藏状态变化,比如注意力模块的输出、多层感知器层的输出与激活值变化等,来进行评估内部知识水平。大模型在处理包含或不包含内部知识的不同问题时,模型的中间层会展现出不同的动态变化,比如模型注意力分布较为分散、激活值变化较大时,就表示模型对当前上下文缺乏充分的理解,无法给出准确的推理结果。

大模型专家就对注意力层输出(Attention Output)、MLP 层输出(MLP Output)和隐层状态(Hidden States)这三种类型的内部隐藏状态设计了探针实验。研究结果表明:不同大语言模型在利用中间层的内部表示进行分类时,均能够实现较高的分类准确率。这表明中间层的内部隐藏状态能够有效地反映模型对问题的理解和相关知识储备。
在这里插入图片描述

然而,这种依赖于内部状态的检测方法也存在一定的局限性,它并不适用于模型内部状态探针黑盒语言模型,因为我们无法直接访问其内部隐藏状态。另外,模型的输入也需要仔细设计,因为模型有时所展现出的不确定性,可能并非源于对问题的知识缺失,而是问题本身固有的模糊性或歧义性所致。

总体而言,基于内部状态评估内部知识的工作目前尚处于初步探索阶段,具体的方案设计还有待发展。

2.2. 何处增强

在确定大模型需要访问外部知识时,我们就需要考虑在如何运用检索到的外部知识。由于大模型具有上下文学习能力、注意力机制的可扩展性、自回归生成能力等特性,因此我们在输入端、中间层、输出端都可以进行知识融合的操作。
在这里插入图片描述
在输入端增强

在输入端增强的方法直接将检索到的外部知识文本与用户查询拼接到Prompt中,然后输入给大语言模型,是当前主流的增强方法。这种方式的重点在于Prompt设计以及检索到的外部知识的排序,良好的Prompt设计和外部知识排序,可以使模型更好的理解、利用外部知识。

然而这种方式也存在一个问题:那就是当检索到的文本太长时,就会导致输入序列过长,甚至超出模型的最大序列长度,增加大模型对上下文解决和推理的计算成本。

在中间层增强

在中间层增强的方法是利用注意力机制的灵活性,先将检索到的外部知识转换为向量表示,然后将这些向量插入通过交叉注意力融合到模型的隐藏状态中。这种方法能够更深入地影响模型的内部表示,有助于模型更好地理解和利用外部知识。

同时,由于向量表示通常比原始文本更为紧凑,这种方法可以减少对模型输入长度的依赖。然而,这种方法需要对模型的结构进行复杂的设计和调整,无法应用于黑盒模型。

在输出层增强

在输出端增强的方法利用检索到的外部知识对大语言模型生成的结果进行校准,是一种后处理的方法。在这类方法中,模型首先在无外部知识的情况下生成一个初步回答,然后再利用检索到的外部知识来验证或校准这一答案。这种方法的优点是可以确保生成的文本与外部知识保持一致,提高答案的准确性和可靠性。

然而,其效果在很大程度上依赖于检索到的外部知识的质量和相关性。若检索到的文档不准确或不相关,则会导致错误的校准结果。

以上三种增强方式各有优势,他们之间也是相互独立的,可以根据业务需求场景,选择不同的方案,也可以进行组合方式使用,效果更佳。

2.3. 如何多次增强

在实际应用中,用户对大模型的提问可能是复杂或模糊的,大模型很难通过一次检索增强就确保生成正确,为了保证生成的准确和可靠,可能需要多次迭代检索增强。

通常大模型在处理复杂问题时,采用分解式增强的方案。而在处理模糊问题时,采用渐进式增强的方案。

分解式增强方案

复杂问题通常包含多个知识点,大模型对复杂问题进行回答时需要多跳(multi-hop)的理解。这种情况下,大模型会将多跳问题分解为多个子问题,然后在子问题间迭代进行检索增强,最后得出正确结论,即分解式增强。

DEMONSTRATE–SEARCH–PREDICT(DSP)是一种典型的分解式增强框架。该框架主要包含以下三个模块:

  • DEMONSTRATE 模块:通过上下文学习的方法,将复杂问题分解为子问题。
  • SEARCH 模块:对子问题进行迭代检索增强,为最终决策提供综合的外部知识。
  • PREDICT 模块:根据SEARCH 提供的综合外部知识生成最终回答。

在这里插入图片描述
分解式增强将复杂问题化整为零,降低了单次检索增强的难度。其性能很大程度上取决于子问题分解的质量。不同领域的复杂问题可能有着不同的分解范式,因此常常需要根据具体任务对问题分解方案进行设计。

渐进式增强

大模型通常采用渐进式增强来处理模糊问题。所谓模糊问题,指的是语义不明确,模棱两可,容易引发歧义的问题。对于这类问题,我们就可以对问题进行渐进式拆解、细化,然后对细化后的问题进行检索,最后在根据检索结果增强大模型的输出。

比如“国宝动物爱吃什么?” ,这个问题没有明确是什么国家的国宝动物,那么大模型对这种模糊问题一般采用渐进式检索方法。TREE OF CLARIFICATIONS(TOC)是渐进式增强的典型框架,主要流程如下:

  • 拆解问题:将问题拆解为“中国国宝动物爱吃什么”、“澳洲国宝动物爱吃什么”等多个子问题。
  • 细化子问题:得到多个子问题后再进行检索,进一步细化问题“中国国宝动物大熊猫爱吃什么”、“澳洲国宝动物考拉爱吃什么”等。
  • 生成结果:结合细化后的子问题的检索结果得到最终的答案“中国国宝动物大熊猫爱吃竹子,澳洲国宝动物考拉爱吃桉树叶…”。
    在这里插入图片描述

渐进式检索方法能显著提升大模型对模糊问题的生成效果,但是它需要进行多轮检索和生成才能生成最终答案,必然会大大提升系统的耗时与计算资源消耗。甚至有可能因为多轮检索和生成的影响,导致最终生成错误的结果。

2.4. 如何降本增效

检索出的外部知识通常包含大量的原始文本,将这些文本直接组成Prompt输入给大模型,会大大增加输入的token数量,从而增加推理计算成本。为了降本增效,我们通常有两个手段:去除冗余文本、复用计算结果

2.4.1. 去除冗余文本
在 RAG 中,检索出的原始文本通常包含大量的无益于增强生成的冗余信息。这些冗余信息不仅增加了输入 token 的长度,而且还有可能对大模型产生干扰,导致生成错误答案。

去除冗余文本的方法通过对检索出的原始文本的词句进行过滤,从中选择出部分有益于增强生成的部分。去除冗余文本的方法主要分为以下三类:

  • token级别的方法:通过对 Token 进行评估,对文本中不必要的 Token 进行剔除。
    • 困惑度是判断token重要性的重要指标,LongLLMLingua框架常用于评估token的困惑度。如果一个token的困惑度低,表示token携带的新信息较少,模型已经掌握了token的大部分内容,信息就可能是冗余的。反之,如果一个token的困惑度高则表示携带的新信息较多。
  • 子文本级别的方法:通过对子文本进行打分,对不必要的子文本删除。
    • FIT-RAG是子文本级别方法的典型代表,该方法采用了预先训练的双标签子文档打分器,对评分较低的文本,也就是冗余文本进行删除。打分器主要从两个维度评估文档的有用性:
      • 事实性:即文档是否包含的答案信息。
      • 模型的偏好度:即文档是否易于模型理解。
  • 全文本级别的方法:直接从整个文档中抽取重要信息,去掉冗余信息。
    • PRCA是典型代表,通过训练能够提炼重点内容的信息提取器对文本中的重要信息进行提取,主要分两个阶段:
      • 上下文提取阶段:以最小化压缩文本与原输入文本的差异,对信息提取器进行监督学习训练,学习如何将输入文档提炼为信息丰富的压缩文本。
      • 奖励驱动阶段:大模型作为奖励模型,根据压缩文本生成的答案与真实答案之间的相似度作为奖励信号,通过强化学习对信息提取器进行优化。最终得到的信息提取器可以直接将输入文档转化为压缩文本,端到端地去除冗余文本。

2.4.2. 复用计算结果
在大模型推理的自回归过程中,每个token都需要用到之前token在注意力模块涉及的key、value结果,为了避免对每个token的key、value结果重新计算,我们可以对之前已经计算过的token的key、value结果进行缓存,即(KV-cache),在需要使用时直接调用结果。

RAGCache设计了一种RAG系统专用的多级动态缓存机制,主要由以下三个核心系统部分组成:

  • KV张量缓存库:采用树结构来缓存所计算出的文档KV张量,其中每个树节点代表一个文档。
  • 缓存检索器:负责在缓存库中快速查找是否存在所需的缓存节点。
  • RAG控制器:负责制定核心的缓存策略。主要有以下几个策略:
    • 前缀感知的贪婪双重尺度频率(PGDSF)替换策略:该策略综合考虑了文档节点的访问频率、大小、访问成本以及最近访问时间,使得频繁使用的文档能够被快速检索到。
    • 重排策略:通过调整请求的处理顺序,优先处理那些能够更多利用缓存数据的请求,优化资源使用。
    • 动态推测流水策略:通过并行KV张量检索和模型推理的步骤,有效减少了端到端的延迟。
      在这里插入图片描述

然而,随着请求输入的文本长度变长,再加上RAG本身的检索结果文本,使整个输入文本很长,KV-cache的GPU显存随之剧增,甚至超过模型参数占用的显存大小,缓存成本也将是个问题。


六、RAG实战进阶

基于windows系统本地向量数据库chromadb搭建一个RAG系统使用demo实战案例。

1. 安装向量数据库

基于windows系统本地安装向量数据库chromadb,需注意python版本>3.7+。
因为我们已经搭建好了python环境,所以这里就不再细谈。不清楚怎么安装python的朋友可以看这篇:【AI大模型第4集】大语言模型(LLM)开发环境安装

pip install chromadb

如果没有翻墙速度可能有点慢,那我们可以尝试使用国内镜像下载:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple chromadb

在这里插入图片描述
安装完成后查看chromdb版本,如果正常显示版本,那就说明安装成功。

chroma -V

在这里插入图片描述
安装完成后,本地启动chromdb,可以在windows+R进入cmd里面执行下面命令:

注意:path可以替换成自己的路径,关闭cmd窗口后chromadb数据库也会被关掉。
为了方便启动,可以把命令加到bat脚本文件中,这里我创建了一个start_chromadb.bat文件

chroma run --host 0.0.0.0 --port 8000 --path "D:\Program Files\Python\chromadb-data"

恭喜你咯,到这里就表示启动成功了。
在这里插入图片描述

2. 简单的RAG项目完整代码

基于python实现,将文本向量化后保存到本地向量库,把向量查询结果作为知识补充,交给大语言模型参考,最后回答用户。

安装python依赖:

# 安装可能需要用到的python依赖插件包,进入cmd执行pip install 命令,有些本次用不到,可适度删减
pip install lxml python-docx tqdm httpx ipykernel numpy jieba openai pdfminer.six requests pydantic nltk debugpy python-dotenv

完整代码:

代码中需要使用到两种大模型,这里我用的是阿里的:

  • 文本向量模型:文本向量化,存储到向量数据库,提供向量查询。
  • 文本生成模型:根据提示词推理、生成文本,回答用户问题。
import os
import chromadb
import json
from openai import OpenAI
from tqdm import tqdm
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer

# 定义一个辅助函数,用来打印日志
def print_json(data):
    """
        如果参数是有结构的(如字典或列表),则以 JSON 形式打印,否则直接打印。
    """
    if hasattr(data, 'model_dump_json'):
        data = json.loads(data.model_dump_json())
    
    if (isinstance(data, (list))):
        for item in data:
            print_json(item)
    elif (isinstance(data, (list, dict))):
        print(json.dumps(data, indent=4, ensure_ascii=False))
    else:
        print(data)

# 从pdf文件中提取文字内容
def extract_text_from_pdf(filename, page_numbers=None, min_text_length=1):
    """提取 PDF 指定页码内的文本

        参数:
            filename: PDF 文件名
            page_numbers: 指定页码列表,传None或不传表示全部页。如 [1, 3, 5] 表示只处理第一页、第三页和第五页的页面
            min_text_length: 最小文本长度,小于该长度的文本需和其他组合成段落
    """
    extract_result=[]
    buffer=''
    full_text=''

    # 按指定页码顺序读取 PDF
    try:
        for i, page_layout in enumerate(extract_pages(filename)):
            # 如果指定了页码,则只处理指定页码的页面
            if page_numbers is not None and i not in page_numbers:
                continue
            for element in page_layout:
                if isinstance(element, LTTextContainer):
                    full_text += element.get_text() + '\n'
    except Exception as e:
        print(f"读取PDF文件出错: {e}")
        return []
    
    # 按空行分割,将文本重新组成段落
    lines=full_text.split('\n')       
    for text in lines:
        # 当这个文本块大于指定的最小段落长度block长度,则加入段落中
        if len(text.strip()) > min_text_length:
            buffer += (' ' + text.strip()) if not text.strip().endswith('-') else text.strip().rstrip('-')
        elif buffer.strip():
            extract_result.append(buffer.strip())
            buffer=''
    
    # 处理最后的缓冲区内容
    if buffer.strip():
        extract_result.append(buffer.strip())
    
    # 过滤掉空字符串
    extract_result = [item for item in extract_result if item.strip()]
    
    if not extract_result:
        print(f"错误:从 {filename} 中没有提取到任何文本")
    
    print_json("=======================文本内容提取完成:")
    print_json(extract_result)
    return extract_result


def split_texts(texts, chunk_size=100, overlap_size=20, separator=" ", sentence_endings=[".", "!", "?", "。", "!", "?"]):
    """
    使用改进的滑动窗口算法分割文本,更好地保持语义完整性
    
    参数:
        texts: 输入的文本列表
        chunk_size: 每个块的最大字符数
        overlap_size: 重叠部分的字符数
        separator: 词之间的分隔符
        sentence_endings: 句子结束符列表
    """
    # 将输入文本合并并分割为句子
    all_text = separator.join([text for text in texts if text and text.strip()])
    sentences = []
    
    # 更精确地按句子分割
    current_sentence = ""
    for char in all_text:
        current_sentence += char
        if char in sentence_endings:
            sentences.append(current_sentence.strip())
            current_sentence = ""
    
    # 添加剩余的文本(如果没有以句子结束符结尾)
    if current_sentence.strip():
        sentences.append(current_sentence.strip())
    
    # 过滤空句子
    sentences = [s for s in sentences if s.strip()]
    
    chunks = []
    start_idx = 0
    
    while start_idx < len(sentences):
        chunk = ""
        end_idx = start_idx
        
        # 添加句子直到达到最大块大小
        while end_idx < len(sentences):
            sentence = sentences[end_idx]
            test_chunk = chunk + separator + sentence if chunk else sentence
            
            if len(test_chunk) <= chunk_size:
                chunk = test_chunk
                end_idx += 1
            else:
                # 如果第一句就超出大小限制,强制添加但要标记
                if end_idx == start_idx:
                    chunk = sentence[:chunk_size]
                    end_idx += 1
                break
        
        if chunk.strip():
            chunks.append(chunk.strip())
        
        # 计算下一个起始位置(考虑重叠)
        if start_idx == end_idx:
            # 防止无限循环
            break
            
        # 寻找合适的重叠位置
        overlap_start = max(start_idx, end_idx - 1)  # 至少重叠一个句子
        overlap_sentences = []
        
        # 尝试从end_idx往前找,直到满足重叠大小要求
        temp_overlap = ""
        check_idx = end_idx - 1
        while check_idx > start_idx and len(temp_overlap) < overlap_size:
            sentence_to_add = sentences[check_idx]
            new_overlap = sentence_to_add + separator + temp_overlap if temp_overlap else sentence_to_add
            if len(new_overlap) <= overlap_size:
                temp_overlap = new_overlap
                check_idx -= 1
            else:
                break
        
        # 更新下一个块的起始位置
        if temp_overlap:
            # 找到重叠部分后,移动到适当位置开始下一循环
            start_idx = max(check_idx + 1, start_idx + 1)  # 确保向前移动
        else:
            start_idx = end_idx
    
    # 最终清理和验证
    chunks = [chunk for chunk in chunks if chunk.strip()]
    
    print_json("=======================改进版文本滑动窗口切割完成:")
    print_json(chunks)
    return chunks

# 初始化 OpenAI 客户端
client=OpenAI(
    # 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)

# 定义prompt模板
prompt_template="""
你是一个问答机器人,你的任务是根据下述给定的已知信息回答用户的问题。

已知信息:
{context}

用户问题:
{query}

如果已知信息不包含用户问题的答案,或者已知信息不足以回答用户的问题,请直接回复:"很抱歉,这个问题我不会。"
请不要输出已知信息中不包含的信息或答案。
请用中文回答用户的问题。
"""

# 构建prompt模板
def build_prompt(prompt_template, **kwargs):
    """将 prompt 模板赋值"""
    inputs={}
    for k,v in kwargs.items():
        if isinstance(v,list) and all(isinstance(elem, str) for elem in v):
            Val='\n\n'.join(v)
        else:
            Val=v
        inputs[k]=Val
    return prompt_template.format(**inputs)

messages=[]

# 定义一个大模型交互接口,基于prompt 生成文本
def get_completion(prompt, response_format="text", model="qwen3-max-preview"):
    # 把用户输入加入历史消息
    messages.append({"role": "user", "content": prompt})

    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,  # 取值范围: [0, 2),temperature越高,生成的文本更多样,反之,生成的文本更确定。
        response_format={"type": response_format} # 返回消息的格式
    )

    #返回模型生成的文本
    msg = response.choices[0].message.content

    # 把模型生成的回复加入消息历史,让大模型下次调用时知道上下文信息
    messages.append({"role": "assistant", "content": msg})

    return msg  

# 文本向量模型,将文本向量化
def get_embeddings(texts, model="text-embedding-v4", dimensions=None, operation=None):
    if not texts or not any(text.strip() for text in texts):
        print("警告:输入文本为空,返回空向量列表")
        return []
    
    # 过滤掉空文本
    texts = [text for text in texts if text and text.strip()]
    
    if not texts:
        print("警告:过滤后没有有效文本,返回空向量列表")
        return []
    
    embeddings = []
    batch_size = 10
    
    try:
        # 分批处理文本
        for i in range(0, len(texts), batch_size):
            batch_texts = texts[i:i + batch_size]
            
            if dimensions:
                data = client.embeddings.create(
                    input=batch_texts, model=model, dimensions=dimensions, timeout=120
                ).data
            else:
                data = client.embeddings.create(input=batch_texts, model=model).data
            
            batch_embeddings = [x.embedding for x in data]
            embeddings.extend(batch_embeddings)
            
            # 可选:添加进度提示
            print(f"=======================当前操作类型:{operation},已处理批次:{i},进度: {i//batch_size + 1}/{(len(texts)-1)//batch_size + 1}")
        
        return embeddings
    except Exception as e:
        print(f"生成嵌入向量时出错: {e}")
        return []

# 定义chromadb访问对象及操作方法
class ChromaConnector:
    def __init__(self, collection_name, embedding_function):
        # 创建本地客户端
        chroma_client=chromadb.Client()
        # 本地测试,如果存在则先删除
        collections=chroma_client.list_collections()
        if collection_name in collections:
            chroma_client.delete_collection(collection_name)
        # 创建集合
        self.collection=chroma_client.create_collection(collection_name)
        # 指定embedding方法
        self.embedding_function=embedding_function

    def add_documents(self, documents):
        """往向量库指定的 collection_name 中添加文档向量"""
        # 过滤掉空文档
        valid_docs = [doc for doc in documents if doc and doc.strip()]
        
        if not valid_docs:
            print("警告:没有有效文档可添加到向量库")
            return
        
        self.collection.add(
            # 文档向量
            embeddings=self.embedding_function(valid_docs, operation="add"),
            # 文档原文
            documents=valid_docs,
            # 每个文档id
            ids=[f"id{i}" for i in range(len(valid_docs))]
        )

    def search(self, query, top_n):
        """向量数据库查询 top n 记录"""
        if not query or not query.strip():
            print("错误:查询不能为空")
            return {}
        
        results=self.collection.query(
            query_embeddings=self.embedding_function([query.strip()], operation="query"),
            n_results=top_n
        )
        return results

class Chatbot:
    def __init__(self, vector_db, llm_api, n_results=5):
        self.vector_db = vector_db
        self.llm_api = llm_api
        self.n_results = n_results

    def chat(self, query):
        """与 Chatbot 交互"""
        # 向量数据库查询
        results = self.vector_db.search(query, self.n_results)

        # 检查是否有结果
        if not results or 'documents' not in results or not results['documents'] or not results['documents'][0]:
            response = "很抱歉,这个问题我不会。"
            print_json("=======================没有找到相关文档,返回默认响应")
            return response
        
        # 构建 prompt
        # 注意:这里应该获取第一个结果而不是整个数组
        context = results['documents'][0] if results['documents'] else []
        prompt=build_prompt(prompt_template, context=context, query=query)
        print_json("=======================提问的prompt是:"+prompt)

        # 调用 LLM
        response = self.llm_api(prompt)
        return response
        

# 1.初始化向量数据库对象
vector_db=ChromaConnector("test_chroma", get_embeddings)

# 2.读取文本、切割片段
vector_texts=extract_text_from_pdf("./test_chroma.pdf", page_numbers=None, min_text_length=10)

# 3.重叠式切分段落,滑动窗口切割文本
chunk_texts=split_texts(vector_texts)

# 4.文本段落向量化保存到chromadb
vector_db.add_documents(chunk_texts)

# 5.创建问答机器人
mybot=Chatbot(vector_db, llm_api=get_completion, n_results=5)

# 6.提问测试
query="东方求败喜欢玩什么游戏?"

# 7.调用问答机器人大模型
response=mybot.chat(query)

print_json(f"=======================大模型返回最终结果:{response}")


执行结果日志:

pip install lxml tqdm ipykernel numpy openai pdfminer.six pydantic nltk python-dotenv
=======================文本内容提取完成:
我叫东方求败,1990 年出生在南方一个普通知识分子家庭。2015 年,我带
着优异成绩从清华计算机系博士毕业,现在是个扎在 AI 互联网开发领域的高级
工程师。在同事眼里,我既是能啃硬骨头的“代码侠”,也是游戏里能 Carry
全场的“战术大师”,妥妥在技术和热爱之间找到了舒服的平衡点。
博士那几年,是我和 AI 死磕的起点。我主攻深度学习和自然语言处理的交
叉方向,尤其在多模态数据融合这块搞出了突破性成果。我的博士论文《基于跨
模态对齐的语义理解模型》不仅登上了国际顶会 ACL,还拿了清华优秀博士论文
奖。那些在实验室泡过的日夜,我不是在调代码就是在啃文献,慢慢养成了对技
术细节“钻牛角尖”的执念。这段经历也让我摸清了:AI 从来不是冷冰冰的算
法堆料,而是能拉近人与人、人与世界距离的有温度的桥梁。
毕业后,我加入了国内一家头部互联网公司,牵头负责 AI 研发团队。我带
队搞的“智能语义理解引擎”,直接把公司客服系统的准确率拉满,用户满意度
暴涨 35%。印象最深的一次产品迭代,原计划半年的活,我们团队咬牙死磕三个
月就搞定了,帮公司省了几百万研发成本。对我来说,写代码只是基础,核心是
让 AI 技术落地开花,变成大家能用得上、用得爽的体验。“技术要服务于人”,
这是我一直揣在心里的工作信条。
抛开工作,我就是个妥妥的家庭型选手。2018 年认识了同为教育工作者的
妻子,2020 年迎来了我们的小棉袄,现在小家伙刚满两岁半,是家里的快乐源
泉。一到周末,我就彻底切换“家庭模式”:陪女儿读绘本、去公园疯跑打闹,
或者一起扎进厨房折腾简单的小点心。妻子总调侃我,连育儿都带着“代码思维”,
比如用游戏闯关的方式教她认数字,把枯燥的启蒙玩成了乐趣。
电子游戏则是我的专属解压神器。从《英雄联盟》到《DOTA1》,我不光是
图个乐子的玩家,更是爱拆解战术的分析师。去年和一群志同道合的队友组队打
线上赛,靠着默契配合和精准战术执行,从几千支队伍里杀出重围,最终拿下了
季军。我一直觉得,游戏里的团队协作、高压下的即时决策,和 AI 开发的项目
管理其实一脉相承。在游戏里练出的冷静心态、快速选最优解的能力,都悄悄反
展望未来,我想在 AI 和教育的交叉领域找新方向,做更智能、更懂人的学
习工具,让技术真正帮到大家成长。同时也希望在工作和家庭之间找到更舒服的
平衡点,多花点时间陪着女儿长大。在这个 AI 飞速迭代的数字时代,我始终相
信:技术的终极价值,从来都是让生活变得更美好、更有烟火气。
从清华实验室到互联网大厂,从代码的逻辑世界到游戏的热血战场,我一直
试着拿捏一种平衡:既能在技术的海洋里探索未知,也能在生活的琐碎里藏着烟
火气。这就是我,东方求败——在数字浪潮里,稳稳做一名有温度的科技践行者。
=======================改进版文本滑动窗口切割完成:
我叫东方求败,1990 年出生在南方一个普通知识分子家庭。 2015 年,我带 着优异成绩从清华计算机系博士毕业,现在是个扎在 AI 互联网开发领域的高级 工程师。
在同事眼里,我既是能啃硬骨头的“代码侠”,也是游戏里能 Carry 全场的“战术大师”,妥妥在技术和热爱之间找到了舒服的平衡点。 博士那几年,是我和 AI 死磕的起点。
博士那几年,是我和 AI 死磕的起点。 我主攻深度学习和自然语言处理的交 叉方向,尤其在多模态数据融合这块搞出了突破性成果。
我的博士论文《基于跨 模态对齐的语义理解模型》不仅登上了国际顶会 ACL,还拿了清华优秀博士论文 奖。 那些在实验室泡过的日夜,我不是在调代码就是在啃文献,慢慢养成了对技 术细节“钻牛角尖”的执念。
这段经历也让我摸清了:AI 从来不是冷冰冰的算 法堆料,而是能拉近人与人、人与世界距离的有温度的桥梁。 毕业后,我加入了国内一家头部互联网公司,牵头负责 AI 研发团队。
我带 队搞的“智能语义理解引擎”,直接把公司客服系统的准确率拉满,用户满意度 暴涨 35%。 印象最深的一次产品迭代,原计划半年的活,我们团队咬牙死磕三个 月就搞定了,帮公司省了几百万研发成本。
对我来说,写代码只是基础,核心是 让 AI 技术落地开花,变成大家能用得上、用得爽的体验。 “技术要服务于人”, 这是我一直揣在心里的工作信条。 抛开工作,我就是个妥妥的家庭型选手。
抛开工作,我就是个妥妥的家庭型选手。 2018 年认识了同为教育工作者的 妻子,2020 年迎来了我们的小棉袄,现在小家伙刚满两岁半,是家里的快乐源 泉。
一到周末,我就彻底切换“家庭模式”:陪女儿读绘本、去公园疯跑打闹, 或者一起扎进厨房折腾简单的小点心。
妻子总调侃我,连育儿都带着“代码思维”, 比如用游戏闯关的方式教她认数字,把枯燥的启蒙玩成了乐趣。 电子游戏则是我的专属解压神器。
电子游戏则是我的专属解压神器。 从《英雄联盟》到《DOTA1》,我不光是 图个乐子的玩家,更是爱拆解战术的分析师。
去年和一群志同道合的队友组队打 线上赛,靠着默契配合和精准战术执行,从几千支队伍里杀出重围,最终拿下了 季军。 我一直觉得,游戏里的团队协作、高压下的即时决策,和 AI 开发的项目 管理其实一脉相承。
在游戏里练出的冷静心态、快速选最优解的能力,都悄悄反 展望未来,我想在 AI 和教育的交叉领域找新方向,做更智能、更懂人的学 习工具,让技术真正帮到大家成长。
同时也希望在工作和家庭之间找到更舒服的 平衡点,多花点时间陪着女儿长大。 在这个 AI 飞速迭代的数字时代,我始终相 信:技术的终极价值,从来都是让生活变得更美好、更有烟火气。
从清华实验室到互联网大厂,从代码的逻辑世界到游戏的热血战场,我一直 试着拿捏一种平衡:既能在技术的海洋里探索未知,也能在生活的琐碎里藏着烟 火气。
这就是我,东方求败——在数字浪潮里,稳稳做一名有温度的科技践行者。
=======================当前操作类型:add,已处理批次:0,进度: 1/2
=======================当前操作类型:add,已处理批次:10,进度: 2/2
=======================当前操作类型:query,已处理批次:0,进度: 1/1
=======================提问的prompt是:
你是一个问答机器人,你的任务是根据下述给定的已知信息回答用户的问题。

已知信息:
这就是我,东方求败——在数字浪潮里,稳稳做一名有温度的科技践行者。

电子游戏则是我的专属解压神器。 从《英雄联盟》到《DOTA1》,我不光是 图个乐子的玩家,更是爱拆解战术的分析师。

我叫东方求败,1990 年出生在南方一个普通知识分子家庭。 2015 年,我带 着优异成绩从清华计算机系博士毕业,现在是个扎在 AI 互联网开发领域的高级 工程师。

在同事眼里,我既是能啃硬骨头的“代码侠”,也是游戏里能 Carry 全场的“战术大师”,妥妥在技术和热爱之间找到了舒服的平衡点。 博士那几年,是我和 AI 死磕的起点。

妻子总调侃我,连育儿都带着“代码思维”, 比如用游戏闯关的方式教她认数字,把枯燥的启蒙玩成了乐趣。 电子游戏则是我的专属解压神器。

用户问题:
东方求败喜欢玩什么游戏?

如果已知信息不包含用户问题的答案,或者已知信息不足以回答用户的问题,请直接回复:"很抱歉,这个问题我不会。"
请不要输出已知信息中不包含的信息或答案。
请用中文回答用户的问题。

=======================大模型返回最终结果:东方求败喜欢玩《英雄联盟》和《DOTA1》。

pdf原文内容(虚构内容,AI生成)

我叫东方求败,1990 年出生在南方一个普通知识分子家庭。2015 年,我带
着优异成绩从清华计算机系博士毕业,现在是个扎在 AI 互联网开发领域的高级
工程师。在同事眼里,我既是能啃硬骨头的“代码侠”,也是游戏里能 Carry
全场的“战术大师”,妥妥在技术和热爱之间找到了舒服的平衡点。
博士那几年,是我和 AI 死磕的起点。我主攻深度学习和自然语言处理的交
叉方向,尤其在多模态数据融合这块搞出了突破性成果。我的博士论文《基于跨
模态对齐的语义理解模型》不仅登上了国际顶会 ACL,还拿了清华优秀博士论文
奖。那些在实验室泡过的日夜,我不是在调代码就是在啃文献,慢慢养成了对技
术细节“钻牛角尖”的执念。这段经历也让我摸清了:AI 从来不是冷冰冰的算
法堆料,而是能拉近人与人、人与世界距离的有温度的桥梁。
毕业后,我加入了国内一家头部互联网公司,牵头负责 AI 研发团队。我带
队搞的“智能语义理解引擎”,直接把公司客服系统的准确率拉满,用户满意度
暴涨 35%。印象最深的一次产品迭代,原计划半年的活,我们团队咬牙死磕三个
月就搞定了,帮公司省了几百万研发成本。对我来说,写代码只是基础,核心是
让 AI 技术落地开花,变成大家能用得上、用得爽的体验。“技术要服务于人”,
这是我一直揣在心里的工作信条。
抛开工作,我就是个妥妥的家庭型选手。2018 年认识了同为教育工作者的
妻子,2020 年迎来了我们的小棉袄,现在小家伙刚满两岁半,是家里的快乐源
泉。一到周末,我就彻底切换“家庭模式”:陪女儿读绘本、去公园疯跑打闹,
或者一起扎进厨房折腾简单的小点心。妻子总调侃我,连育儿都带着“代码思维”,
比如用游戏闯关的方式教她认数字,把枯燥的启蒙玩成了乐趣。
电子游戏则是我的专属解压神器。从《英雄联盟》到《DOTA1》,我不光是
图个乐子的玩家,更是爱拆解战术的分析师。去年和一群志同道合的队友组队打
线上赛,靠着默契配合和精准战术执行,从几千支队伍里杀出重围,最终拿下了
季军。我一直觉得,游戏里的团队协作、高压下的即时决策,和 AI 开发的项目
管理其实一脉相承。在游戏里练出的冷静心态、快速选最优解的能力,都悄悄反
哺到了工作里。
展望未来,我想在 AI 和教育的交叉领域找新方向,做更智能、更懂人的学
习工具,让技术真正帮到大家成长。同时也希望在工作和家庭之间找到更舒服的
平衡点,多花点时间陪着女儿长大。在这个 AI 飞速迭代的数字时代,我始终相
信:技术的终极价值,从来都是让生活变得更美好、更有烟火气。
从清华实验室到互联网大厂,从代码的逻辑世界到游戏的热血战场,我一直
试着拿捏一种平衡:既能在技术的海洋里探索未知,也能在生活的琐碎里藏着烟
火气。这就是我,东方求败——在数字浪潮里,稳稳做一名有温度的科技践行者。

更多推荐