检索增强生成(RAG)近年来在自然语言处理领域备受关注,声量持续走高。简单来说,它巧妙地将信息检索与文本生成两项核心能力融合在一起,根本目标十分明确:当用户提出问题时,系统首先检索相关的内容片段,然后基于这些素材生成更精准、更贴合需求的回答。

这项技术潜力巨大,尤其在开放域问答任务中表现突出。开放域问答系统旨在理解并回答各类开放性问题,其关键环节就是从海量非结构化数据中高效提取有用信息。
早期的信息检索技术很大程度上依赖于基于单词统计的稀疏表示方法。那时,文档通常采用词袋模型来表达,一篇文档与查询的相关性主要依据关键词是否出现来判定。
评分函数,例如广为人知的BM25和TF-IDF,通过关键词在文档中的出现频率以及该词在整个语料库中的普遍程度来为文档打分。实践中最常用的工具之一是Elasticsearch,它基于Lucene搜索引擎,结合了单词统计与编辑距离(一种语法层面的方法)执行文本搜索。
以下代码展示了如何使用Python的Elasticsearch库执行基于TF-IDF和BM25的查询:
from elasticsearch import Elasticsearch
es = Elasticsearch()
index_name = "example_index"
query = "sample query"
bm25_query = {
"query": {
"match": {
"content": {
"query": query,
"operator": "and"
}
}
}
}
bm25_response = es.search(index=index_name, body=bm25_query)
print("BM25 Query Results:")
for hit in bm25_response['hits']['hits']:
print(hit['_source'])
tfidf_query = {
"query": {
"match": {
"content": {
"query": query,
"operator": "and"
}
}
},
"similarity": {
"default": {
"type": "classic"
}
}
}
tfidf_response = es.search(index=index_name, body=tfidf_query)
print("TF-IDF Query Results:")
for hit in tfidf_response['hits']['hits']:
print(hit['_source'])
然而,随着机器学习技术的不断进步,信息检索领域也发生了深刻变革,研究者们纷纷转向功能更强大的密集嵌入表示。这些嵌入技术能够精准捕捉单词与短语的深层语义,实现对内容更深层次的理解。
与传统统计方法相比,密集表示在匹配查询与文档时准确度更高,因为它能够揭示微妙的语义相似性——而这些相似性往往是传统方法难以触及的。
可以说,从稀疏表示到密集表示的跃迁,不仅大幅提升了检索系统的整体性能,也显著提高了检索结果的精确度。
如果你对神经信息检索(Neural IR)感兴趣,可以查阅Mitra和Craswell在2018年的相关工作;想深入了解信息检索与问答系统的结合,Abbasiantaeb和Momtazi在2021年的综述文章也是很好的起点。
本文重点聚焦密集检索,这里介绍两种最具代表性的密集文档检索模型:密集段落检索(DPR,Karpukhin等人,2020)和基于BERT的情境化后期交互(ColBERT,Khattab和Zaharia,2020)。两者都依赖语义搜索来定位相关文档。
所谓语义搜索,就是利用文本的密集表示来度量查询与文档之间的相似性。不过,这两种模型在文档向量的存储方式和相似度计算方法上存在明显差异。
我们将在一个公认的基准数据集——自然问题(NQ,Kwiatkowski等人,2019)上,从准确性和延迟两个维度对它们进行比较。NQ数据集由用户提交的问题构成,这些问题的答案都可以在维基百科文章中查到。
我们使用的工具是FastRAG——一个由英特尔实验室开发并最近开源的框架。其目标是加速RAG应用的研发,将检索与生成相结合,用于问答、摘要、对话系统、内容创作等任务,同时利用外部知识库来锚定大语言模型的输出。
在FastRAG中,应用程序被抽象为一个管道,通常由知识库、检索器、排序器和阅读器(通常是一个LLM)构成。阅读器负责“阅读”查询与检索到的文档,并生成输出。开发者可以自由尝试不同的架构和模型,并对性能和延迟进行基准测试。值得一提的是,FastRAG中提供的几种模型针对英特尔硬件做了优化,能在保持相当精度的前提下,实现更低的延迟。
近期的研究推动了基于Transformer的编码器模型在检索中的应用。这些模型的优势很明显:知识库中的文档被转换成向量并存储在索引中。
查询时,查询内容被编码成同一个向量空间中的向量,然后通过向量相似性搜索来召回最相关的文档。这个过程不仅可以高效地筛选文档,还能用来对召回结果进行重排,进一步优化顺序。这样一来,在运行时就能实现更精准、更实时的检索。
在密集检索领域,主要有几种不同的方法。一种是用单个token的嵌入来代表整个文档。DPR就是这种思路的典型代表。
在DPR中,编码器被训练成把整个文档的内容“总结”到第一个token(通常是[CLS])的嵌入里。这属于双编码器架构,因为它用了两个独立的编码器:一个处理查询,一个处理文档。查询和文档各自被编码成向量,然后进行匹配。
另一种方法是后期交互(late interaction),由ColBERT首次提出(Khattab和Zaharia,2020)。它的理念是保存(并索引)文档中所有单词的编码向量。在运行时,查询向量会与所有文档单词的向量逐一比较——因此得名“后期”交互。这种方法通常能检索到比DPR更相关的文档。不过,为每个token建立索引(而不是每个文档只用一个token),索引大小自然就会增大。
后续的改进版本,ColBERT v2和PLAID(Santhanam等人,2022),通过两种策略大幅压缩了索引大小、降低了延迟:一是对索引中的向量进行量化和压缩,二是引入了一套启发式方法,利用K-means对向量聚类,然后基于簇的质心,分层地为查询选择相关文档的token。使用PLAID索引的ColBERT v2,在保持低延迟的同时实现了顶尖的检索性能,延迟水平已接近BM25、Lucene等稀疏检索方法,但精度却高出一大截。
在原代码块的位置,我们来看如何在FastRAG中使用ColBERT。首先,当然需要安装FastRAG:
pip install fastrag
接下来,初始化文档存储:
from fastrag.stores import PLAIDDocumentStore
document_store = PLAIDDocumentStore(
index_path="data-index/",
checkpoint_path="ColBERT-model/",
collection_path="my-documents.tsv")
然后,基于这个文档存储定义一个检索器:
from fastrag.retrievers.colbert import ColBERTRetriever
retriever = ColBERTRetriever(document_store=document_store)
我们可以用Haystack的管道API来连接各个组件。下面这个例子只包含检索器:
from haystack import Pipeline
p = Pipeline()
p.add_node(component=retriever, name="Retriever", inputs=["Query"])
最后,执行查询:
p.run(query, params={"Retriever": {"top_k": k}})
结果是一个哈希映射,包含文档键和对应的结果列表,每个文档都带有相关性分数。
