典型的检索增强生成(RAG)方法相当简单粗暴——将庞大的非结构化文档切成每个约1000令牌的块,再加上一些块重叠以确保完整性,然后将所有内容嵌入向量,就这样就算完成了。这种方法既不准确也不完整。
那么,我们怎么解决这个问题呢?这个问题一直萦绕在我的脑海里。经过一番阅读、在不同的代码库上摆弄以及尝试之后,我认为我终于找到了一个更好的解决方法,我可以为此感到自豪。
为了让大家站在同一立场上,让我们试着理解一下简单的RAG(检索增强生成,即Retrieval Augmented Generation)是如何工作的。假设你有一个PDF文档。你选择一个任意的块大小,比如512字节,然后将整个文档按这个大小切割,并添加一些重叠。然后你将这些块转换成嵌入向量,现在你就可以进行语义搜索了,这一步。当然,我简化了这个过程,因为有不同的切分策略,但你大概明白了这一点。但是,如果我们先通过主题或实体识别文档中的相似块,然后再创建这些块的嵌入,我这么想过(也看到过类似的观点)。
如果你有10万份文档,每份文档都很庞大,用大型语言模型来做这件事会非常昂贵。直到谷歌推出了Gemini Flash 2.0,它比之前的某些模型便宜得多,性能也更胜一筹。
所以我不得不试一下,同时也需要将其与我读到的一个策略结合使用——知识增强生成(KAG)。
但是,问题来了——我只想用一个数据库。不需要像向量加图或Postgres加图功能和多个扩展这样的复杂配置,只需要一个能同时处理SQL、JSON、向量和基本图形的数据库。
所以我选了SingleStore作为数据库产品,Python(Fast API)来搭建后端,以及NextJS来构建前端。总的来说,这就是我的计划。
- 摒弃随意的片段划分。 相反,通过LLM运行文档以识别语义连贯的部分。我选择了Gemini Flash的低延迟、经济实惠的API。
- 提取结构化知识。 在检索文本的同时,使用Gemini从文档中提取关键实体及其关系。将这些结构化数据存储在关系型数据库中(如SingleStore,方便快速查找)。
- 混合检索。 不仅依赖向量搜索,还要结合语义分块检索与知识图谱查找。逻辑排序结果后,将最佳上下文传递给LLM。
简而言之——这样做准确度如何?可以说我相当满意。
在这篇文章中,我将一步步地介绍整个方法的过程。你也可以在我的仓库里获取我的代码,并在你自己的数据集上试一试。
首先,我们来看一下数据库的简单架构——文档表用于记录文档,Documents_Embeddings表用来存储语义块及其对应向量,还有实体表和关系表。
以下SQL用于创建具有向量索引和关键字匹配索引的内容。
CREATE TABLE Document_Embeddings (
embedding_id BIGINT PRIMARY KEY AUTO_INCREMENT,
doc_id BIGINT NOT NULL,
content TEXT,
embedding VECTOR(1536),
FULLTEXT USING VERSION 2 content_ft_idx (content), -- 在 content 上的全文索引(v2)
VECTOR INDEX embedding_vec_idx (embedding), -- 在 embedding 列上的向量索引(v2)
INDEX_OPTIONS '{ "index_type": "HNSW_FLAT", "metric_type": "DOT_PRODUCT" }'
);
ALTER TABLE Entities
ADD FULLTEXT USING VERSION 2 ft_idx_name (name);
CREATE TABLE Documents (
doc_id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255),
author VARCHAR(100),
publish_date DATE,
source JSON -- 其他元数据字段(例如摘要、URL)可以根据需要添加
);
CREATE TABLE Relationships (
relationship_id BIGINT PRIMARY KEY AUTO_INCREMENT,
source_entity_id BIGINT NOT NULL,
target_entity_id BIGINT NOT NULL,
relation_type VARCHAR(100),
doc_id BIGINT, -- 对 Documents.doc_id 的引用(不是强制的外键约束)
KEY (source_entity_id) USING HASH, -- 通过来源快速查找关系的索引
KEY (target_entity_id) USING HASH, -- 通过目标快速查找关系的索引
KEY (doc_id) -- 通过文档查询关系的索引
);
CREATE TABLE Entities (
entity_id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
description TEXT,
aliases JSON,
category VARCHAR(100),
PRIMARY KEY (entity_id, name), -- 包含分片键的复合主键
SHARD KEY (entity_id, name), -- 在分片键上强制局部唯一性约束
FULLTEXT USING VERSION 2 name_ft_idx (name) -- 名称搜索用的全文索引(v2)
);
现在我们已经设置了表格之后,用PDF填充表格既不复杂也不有趣。我为每个步骤采取了不同的方法。
抓取语义片段并生成嵌入,插入到文档数据和文档嵌入中,提取实体和关系并填充实体和关系表。
现在,让我们看看检索策略,因为现在有几种不同的方法可以做这件事,无论你是更注重准确性还是速度。我选择了下面这个策略。
让我们来看一个简单的例子来探讨这个策略。假设我们要搜索一个简单的词汇或短语——“hello world”——
。
第一步:使用与我们用于创建文档嵌入的相同模型将查询转换为嵌入,并对Documents_Embeddings表进行这些嵌入的混合查询。
— 简化结合向量和文本搜索的查询
SELECT chunk_text,
dot_product(chunk_embedding, query_embedding) AS vector_score,
MATCH(chunk_text) AGAINST('你好 世界') AS text_score
FROM Document_Embeddings
ORDER BY (0.7 * vector_score + 0.3 * text_score) DESC
LIMIT 5;
步骤2 — 我们现在查看结果,并运行查询以找到之前步骤中重排结果对应的实体。
提取可能的相关实体:
“Hello World”(编程概念)
"Brian Kernighan"(人物)
"C 编程语言"(编程语言)
"Java"(编程语言)
"Python"(编程语言)
看来看看这些关系,例如
"Brian Kernighan" -> "创建了" -> "Hello World"
"C 编程语言" -> "介绍给" -> "Hello World"
"Brian Kernighan" -> "编写了" -> "C 编程语言"
步骤3 — 我们现在将结果合并并增强,作为LLM(大型语言模型)使用的最终上下文构建,通过几个子步骤。
- 根据相关性得分对片段进行排序
- 添加实体信息
- 增加关系信息
- 格式化为大语言模型输入提示
我们现在看的示例如下,然后将其传给模型以得到回复
context = f"""
相关信息:
{top_chunks_with_scores}
关键实体:
- Hello World(编程概念)
- Brian Kernighan(人,创造者)
重要关系:
- Brian Kernighan 在 1972 年创建了 "Hello World"
- "Hello World" 首次出现在 C 编程语言中
这是一个更详细的视角,更喜欢视觉呈现的朋友可能会觉得更清晰。
结论
RAG 自从两年前引入以来已经取得了很大的进步,但在我看来,随着更快、更便宜且更好的推理模型的出现,我们现在又迈出了巨大的一步。是的,上下文的大小也增加了,因此在某些情况下,直接将整个文档发送给模型可能更简单,但在我看来,这仍然无法取代在数十万结构化和非结构化数据中检索特定领域的知识增强。为此,我们仍然需要某种形式的检索增强。
如果你正在构建一个企业级AI应用,希望这可以给你一个起点,不只是简单的文档处理和信息整合,而是更适合企业需求的应用。
✌️ 加油