运行效果
什么是RAG(用PocketFlow作者通俗的话来理解)
想象RAG就像在AI回答问题之前给了它一个个人的研究图书馆员。以下是这个魔法如何发生的:
文档收集:你把你的文档(公司手册、文章、书籍)提供给系统,就像书被添加到图书馆一样。
切片:系统将这些文档分解成易于消化的小块——就像图书馆员将书籍按章节和部分划分,而不是处理整本书。
嵌入:每个小块被转换成一种特殊的数字格式(向量),能够捕捉其含义——类似于创建能够理解概念而不仅仅是关键词的详细索引卡。
索引:这些向量被组织在一个可搜索的数据库中——就像一个理解不同主题间关系的神奇卡片目录。
检索:当你提问时,系统会查阅其索引,找到与你的查询最相关的片段。
生成:AI利用你的问题和这些有用的参考,生成一个比仅依赖其预先训练的知识更优秀的回答。
结果?AI不会编造信息或提供过时的信息,而是基于你的特定文档来构建其回答,提供准确、相关且符合你信息的回应。
在这个例子中作者使用了两个工作流,一个是offline工作流一个是online工作流:
offline工作流包含切片、嵌入与创建索引节点。
online工作流包含询问嵌入、检索文档与生成回答节点。
认识一下切片(用PocketFlow作者通俗的话来理解)
切块:将文档分成易于管理的小部分
在我们的RAG系统能够有效工作之前,我们需要将文档分解成更小、更易于消化的部分。
想象一下切块就像为客人准备饭食——你不会未经切片就直接上整只火鸡!
为什么分块很重要?
分块的大小直接影响你的RAG系统的质量:
分块过大:系统检索到过多的无关信息(就像提供整只火鸡)
分块过小:你失去了重要的上下文(就像只提供单一的豌豆)
恰到好处的分块:系统能够准确找到所需信息(完美的分量!)
有一些实用的分块方法
1、固定大小分块:简单但不完美
def fixed_size_chunk(text, chunk_size=50):
chunks = []
for i in range(0, len(text), chunk_size):
chunks.append(text[i:i+chunk_size])
return chunks
这段代码会逐次处理文本中的50个字符。
让我们看看它在示例段落上是如何工作的:
Input Text:
The quick brown fox jumps over the lazy dog. Artificial intelligence has revolutionized many industries. Today's weather is sunny with a chance of rain. Many researchers work on RAG systems to improve information retrieval.
Output Chunks:
Chunk 1: "The quick brown fox jumps over the lazy dog. Arti"
Chunk 2: "ficial intelligence has revolutionized many indus"
Chunk 3: "tries. Today's weather is sunny with a chance of "
Chunk 4: "rain. Many researchers work on RAG systems to imp"
Chunk 5: "rove information retrieval."
注意问题了吗?单词 "Artificial" 被拆分在第1块和第2块之间。"Industries" 被拆分在第2块和第3块之间。这使得我们的系统难以正确理解内容。
2、句子级别的切分:尊重自然边界
一种更智能的方法是按完整的句子进行切分:
import nltk # Natural Language Toolkit library
def sentence_based_chunk(text, max_sentences=1)
sentences = nltk.sent_tokenize(text)
chunks = []
# Group sentences, 1 at a time
for i in range(0, len(sentences), max_sentences):
chunks.append(" ".join(sentences[i:i+max_sentences]))
return chunks
此方法首先识别完整的句子,然后逐个将它们分组:
Output Chunks:
Chunk 1: "The quick brown fox jumps over the lazy dog."
Chunk 2: "Artificial intelligence has revolutionized many industries."
Chunk 3: "Today's weather is sunny with a chance of rain."
Chunk 4: "Many researchers work on RAG systems to improve information retrieval."
好多了!每个片段现在都包含了一个完整的意思完整的句子。
其他切片策略
根据您的文档,以下方法也可能适用:
基于段落:在段落分隔处切分文本(通常由换行符标记)
语义切分:按主题或意义对文本进行分组(通常需要AI辅助)
混合方法:结合多种策略以获得最佳效果
尽管存在许多复杂的切分方法,但对于大多数实际应用而言,“简单愚蠢”(KISS)原则适用。从每块大约1,000字符的固定大小切分开始通常已经足够,并且可以避免系统过于复杂。
最佳的切分方法最终取决于您的具体文档和用例——就像厨师根据菜肴和客人调整份量一样!
认识一下嵌入(用PocketFlow作者通俗的话来理解)
嵌入:使检索成为可能
现在我们已经将文档切分,系统是如何找到与我们的问题最相关的片段的?这就是嵌入的作用!嵌入是驱动我们检索系统的魔法。没有它们,我们就无法为问题找到正确的信息!
什么是嵌入?
嵌入将文本转换为一个数字列表(向量),以捕捉其含义。可以将其视为在意义空间中创建一个特殊的“位置”,相似的想法会靠近放置。
例如,如果我们取这三个句子:
“猫坐在垫子上。” 🐱
“一只猫科动物休息在地板覆盖物上。” 🐈
“Python 是一种流行的编程语言。” 💻
一个好的嵌入系统会将句子 1 和 2 放置得很近(因为它们描述了相同的概念),而句子 3 会远离它们(完全不同的主题)。
类似这样:
创建嵌入:从简单到高级
1、简单方法:字符频率
这是一个初学者友好的创建嵌入的方法,通过计算字符频率:
def get_simple_embedding(text):
# Create a vector to store character frequencies
embedding = np.zeros(26, dtype=np.float32)
# Count character frequencies in the text
for char in text:
embedding[ord(char) % 26] += 1.0
# Normalize the vector
norm = np.linalg.norm(embedding)
if norm > 0:
embedding /= norm
return embedding
让我们看看这三个例句是什么样子:
🐱 "The cat sat on the mat." → [0.2, 0, 0, ...]
(high values at positions for 't', 'a', ' ', etc.)
🐈 "A feline rested on the floor covering." → [0.1, 0.2, 0, ...]
(different characters but similar number of spaces)
💻 "Python is a popular programming language." → [0.1, 0.2, 0.1, ...]
(completely different character pattern)
注意句子1和2因为使用了不同的词而有不同的值,而句子3则有一个完全不同的模式。这个简单的向量只捕捉到使用的字符,而不是它们的含义。
局限性:这种简单的方法无法识别“cat”和“feline”是相关概念,因为它们没有共享字符!
2、专业方法:AI 驱动的嵌入
在真实的 RAG 系统中,您将使用像 OpenAI 的嵌入 API 这样的高级模型:
def get_openai_embedding(text):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "YOUR_API_KEY"))
response = client.embeddings.create(
model="text-embedding-ada-002",
input=text
)
# Extract the embedding vector from the response
embedding = response.data[0].embedding
return np.array(embedding, dtype=np.float32)
我这里改为了使用硅基流动的嵌入API:
def get_embedding(text):
client = OpenAI(api_key="sk-xxx",
base_url="https://api.siliconflow.cn/v1")
response = client.embeddings.create(
model="BAAI/bge-m3",
input=text
)
# Extract the embedding vector from the response
embedding = response.data[0].embedding
# Convert to numpy array for consistency with other embedding functions
return np.array(embedding, dtype=np.float32)
这些高级嵌入在高维空间中(通常为1,536维的OpenAI嵌入)捕获了深层的语义关系。它们理解“猫”🐱和”猫科动物“🐈可能意味着同样的事情,即使字符完全不同!
🐱 "The cat sat on the mat." → [0.213, -0.017, 0.122, ...]
(pattern encoding "animal resting on object")
🐈 "A feline rested on the floor covering." → [0.207, -0.024, 0.118, ...]
(very similar pattern, also encoding "animal resting on object")
💻 "Python is a popular programming language." → [-0.412, 0.158, 0.367, ...]
(completely different pattern encoding "programming language")
尽管句子1和句子2使用了不同的词语,但它们的嵌入具有相似的模式,因为它们表达了相似的概念。与此同时,句子3的嵌入模式则完全不同,因为它涉及的是一个完全不同的主题。
嵌入式向量如何驱动我们的RAG系统
在我们的RAG管道中:
我们将所有文档片段嵌入并存储这些向量
当用户提出问题时,我们也会嵌入问题
我们找到嵌入向量与问题嵌入最接近的文档片段
向量数据库:实现快速检索
想象一下,要在一百万页书中寻找答案,这将耗时无尽!向量数据库通过以一种巧妙的方式组织我们的嵌入,使搜索变得闪电般快速。
为什么我们需要向量数据库
当你提问时,你的RAG系统需要将问题与可能成千上万甚至上百万的文档片段进行比较。这可以通过两种方式完成:
1、简单方法:穷举搜索
在这种方法中,我们逐一检查每个文档片段——就像在图书馆中查看每一页:
def retrieve_naive(question_embedding, chunk_embeddings):
best_similarity, best_chunk_index = -1, -1
for idx, chunk_embedding in enumerate(chunk_embeddings):
similarity = get_similarity(question_embedding, chunk_embedding)
if similarity > best_similarity:
best_similarity, best_chunk_index = similarity, idx
return best_chunk_index
这在处理几十个文档时完美工作,但当处理成千上万或数百万个片段时,会变得异常缓慢。
2、专业方法:使用向量数据库的智能索引
向量数据库就像拥有了一位神奇的图书管理员,他们组织书籍如此高效,以至于能够立即找到你需要的内容,而无需查看每个书架。
让我们通过两个简单的步骤来看看这是如何工作的:
步骤1:构建神奇的索引
首先,我们将所有的文档嵌入组织成一个特殊结构:
def create_index(chunk_embeddings):
dimension = chunk_embeddings.shape[1] # e.g. 128 or 1536
index = faiss.IndexFlatL2(dimension) # flat = exact search
index.add(chunk_embeddings) # add all document vectors
return index
这就像在我们的意义空间中为每份文档创建一个详细的地址图。
步骤2:使用索引快速检索
当问题进来时,我们使用我们神奇的索引来瞬间找到最相关的片段:
def retrieve_index(index, question_embedding, top_k=5):
_, chunk_indices = index.search(question_embedding, top_k)
return chunk_indices
不必检查每份文档,索引确切地知道在哪里查找——在几毫秒内就能给我们提供最相关的前5个片段!
是什么让向量数据库如此高效?
向量数据库的速度源自于三种巧妙的技术:
智能索引算法:诸如HNSW(分层可导航小世界)之类的方法在嵌入空间中创建快捷方式,因此系统只需检查文档的一小部分。
向量压缩:这些数据库可以缩小嵌入以节省内存,同时保留它们的关系——就像拥有一张压缩的地图,仍然显示所有重要的地标。
并行处理:现代向量数据库同时使用多个CPU/GPU核心,同时检查许多可能性——就像有一组图书管理员同时搜索图书馆的不同区域。
认识一下RAG的过程(用PocketFlow作者通俗的话来理解)
现在您了解了分块、嵌入和向量数据库,这里就是许多框架过度复杂化的RAG的美妙简洁之处:
RAG就是两个简单的工作流协同工作!
想象一下RAG就像您的个人研究助理,分为两个高效的过程:
离线流程:准备知识库
这只需要做一次,在任何问题提出之前:
切分:将文档分解成易于管理的部分(例如将一本书分成易于记忆的段落)
嵌入:将每个片段转换为数值向量(例如给每个段落赋予一个独特的“位置”在意义空间中)
索引:组织这些向量以便于高效检索(例如创建一个你所掌握知识的神奇地图)
在线流程:实时回答问题
这发生在每次有人提问时:
查询嵌入:将问题转换为向量(找到它在相同意义空间中的“位置”)
检索:找到与问题最接近的文档片段(定位最相关的知识)
增强生成:使用问题和检索到的上下文生成答案(基于特定文档编制回答)
整个过程
将文档切片:
效果:
将切片向量化:
效果:
创建索引:
faiss.IndexFlatL2
创建一个特定类型的 Faiss 索引。
IndexFlat
:这表示一个“平面”索引,意味着它将所有原始向量直接存储在内存中。当你执行搜索时,它会将查询向量与索引中的每个向量进行比较。这使得搜索结果非常准确(因为它进行了穷举搜索),但对于非常大的数据集来说可能会很慢。L2
:这指定了用于相似度搜索的距离度量是 L2(欧几里得)距离。两个向量之间的 L2 距离是它们对应分量差的平方和的平方根。在许多机器学习上下文中,较小的 L2 距离意味着更高的相似度。
dimension
(从上一行获取的值)作为参数传递给它,告诉 Faiss 它将存储和比较的向量的维度。
将提问向量化:
效果:
检索最相关的片段:
效果:
生成最终回答:
效果:
最后
感谢PocketFlow作者贡献的精彩教程
想看英文原版的朋友可以看:https://pocketflow.substack.com/p/retrieval-augmented-generation-rag
原创作者: mingupupu 转载于: https://www.cnblogs.com/mingupupu/p/18952426