想象一下:你首先将每个 PDF 页面转换成图像,然后发送出去进行 OCR(光学字符识别),接着再把原始文本费力地转换成可用的 HTML 或 Markdown。接下来,你仔细地检测和重建每个表格,将内容切分成块以便进行语义检索,最后将它们全部插入到向量数据库中。这已经是一个庞大的处理流程——通常需要花费大量成本来整合多个 ML/OCR 模型。
但是,如果只用一个大型语言模型——谷歌的 Gemini 2.0 Flash——就能简化整个流程呢?想象一下,在极低的成本下,只需一步就能完成 OCR 和内容切分。本文正是探讨了这种可能性。我们将展示 Gemini 2.0 Flash 如何一步到位地将 PDF 转换成已分块的、准备好用于 Markdown 的文本,让你摆脱以往繁琐的多步操作。然后,我们将把这些文本块存储在 KDB.AI 中,以便进行快速向量搜索,将这一切编织成一个比你以往见过的更优雅、更经济实惠的检索增强生成(RAG)工作流。
在此过程中,我们将突出来自 Hacker News 讨论的实际反馈,以及引用Sergey Filimonov的文章,他首次以接近完美的准确度测量了每美元约处理 6,000 页的速度。
关键要点:如果你不需要原始 PDF 中的边框框选(bounding boxes),这种方法比传统的 OCR 处理流程要简单得多且成本更低。
传统的 PDF 摄取问题
为什么 PDF 摄取这么难?
许多团队最终构建了一个庞大且脆弱且昂贵的处理流程。新的方法是:
“只需将 PDF 页面作为图像展示给多模态语言模型,给它一个用于切分的提示,然后见证奇迹的发生。”
为什么选择 Gemini 2.0 Flash?
根据Sergey Filimonov和多位 Hacker News 评论者的说法:
缺失的一大块是边框框选数据。如果你需要将内容精确地叠加回 PDF 上,Gemini 的边框框选生成还远远不够准确。但如果你的主要关注点是基于文本的检索或摘要,那么它更便宜、更快、更容易。
端到端架构
我们将在代码中执行以下操作:
下面,我们将分节逐步讲解代码,每一步都进行解释。
分步代码
安装依赖项并创建基本表
首先,我们安装所有所需的 Python 包:
要获取你的 KDB.AI 凭据,请前往 KDB.AI 并登录。免费的云服务提供 4GB 内存和 30GB 磁盘空间,如果正确量化,足以存储数百万个向量。
# SNIPPET 1: Installing packages & setting up
!apt-get update
!apt-get install -y poppler-utils
!pip install -q google-generativeai kdbai-client sentence-transformers pdf2image
import os
import kdbai_client as kdbai
from sentence_transformers import SentenceTransformer
# We'll connect to KDB.AI to store our chunk embeddings
KDBAI_ENDPOINT = "YOUR_KDBAI_ENDPOINT"
KDBAI_API_KEY = "YOUR_KDBAI_API_KEY"
session = kdbai.Session(endpoint=KDBAI_ENDPOINT, api_key=KDBAI_API_KEY)
db = session.database('default')
print("Connected to KDB.AI:", db)
安装完成后,我们创建一个会话对象以与我们的 KDB.AI 实例进行通信。
创建向量表
我们将为块文本和嵌入定义一个简单的模式。KDB.AI 支持对“vectors”列进行索引以进行相似度搜索。
# SNIPPET 2: Define KDB.AI table schema
VECTOR_DIM = 384 # we'll use all-MiniLM-L6-v2 for embeddings
schema = [
{"name": "id", "type": "str"},
{"name": "text", "type": "str"},
{"name": "vectors", "type": "float32s"}
]
# Build a simple L2 distance index
index = [
{
"name": "flat_index",
"type": "flat",
"column": "vectors",
"params": {"dims": VECTOR_DIM, "metric": "L2"}
}
]
table_name = "pdf_chunks"
try:
db.table(table_name).drop()
except kdbai.KDBAIException:
pass
table = db.create_table(table_name, schema=schema, indexes=index)
print(f"Table '{table_name}' created.")
解释:
将 PDF 页面转换为图像
Gemini 是一个多模态模型,因此我们可以直接输入图像。这意味着我们首先使用 pdf2image 将每个 PDF 页面转换为 PNG 格式。
# SNIPPET 3: Convert PDF to images
import requests
from pdf2image import convert_from_bytes
import base64
import io
pdf_url = "https://arxiv.org/pdf/2404.08865" # example PDF
resp = requests.get(pdf_url)
pdf_data = resp.content
pages = convert_from_bytes(pdf_data)
print(f"Converted {len(pages)} PDF pages to images.")
# We'll encode the images as base64 for easy sending to Gemini
images_b64 = {}
for i, page in enumerate(pages, start=1):
buffer = io.BytesIO()
page.save(buffer, format="PNG")
image_data = buffer.getvalue()
b64_str = base64.b64encode(image_data).decode("utf-8")
images_b64[i] = b64_str
解释:
调用 Gemini 2.0 Flash 进行 OCR+Chunking
让我们初始化 Gemini 客户端,并定义一个提示,指示模型执行以下操作:
# SNIPPET 4: Configure Gemini & define chunking prompt
import google.generativeai as genai
GOOGLE_API_KEY = "YOUR_GOOGLE_API_KEY"
genai.configure(api_key=GOOGLE_API_KEY)
model = genai.GenerativeModel(model_name="gemini-2.0-flash")
print("Gemini model loaded:", model)
CHUNKING_PROMPT = """\
OCR the following page into Markdown. Tables should be formatted as HTML.
Do not surround your output with triple backticks.
Chunk the document into sections of roughly 250 - 1000 words.
Surround each chunk with <chunk> and </chunk> tags.
Preserve as much content as possible, including headings, tables, etc.
"""
解释:
用一个提示处理每一页
我们将定义一个辅助函数 process_page(page_num, b64),该函数将 base64 编码的 PNG 图像和提示发送到 Gemini。然后,我们将从响应中解析出 <chunk> 块。
# SNIPPET 5: OCR + chunking function
import re
def process_page(page_num, image_b64):
# We'll create the message payload:
payload = [
{
"inline_data": {"data": image_b64, "mime_type": "image/png"}
},
{
"text": CHUNKING_PROMPT
}
]
try:
resp = model.generate_content(payload)
text_out = resp.text
except Exception as e:
print(f"Error processing page {page_num}: {e}")
return []
# parse <chunk> blocks
chunks = re.findall(r"<chunk>(.*?)</chunk>", text_out, re.DOTALL)
if not chunks:
# fallback if model doesn't produce chunk tags
chunks = text_out.split("\n\n")
results = []
for idx, chunk_txt in enumerate(chunks):
# store ID, chunk text
results.append({
"id": f"page_{page_num}_chunk_{idx}",
"text": chunk_txt.strip()
})
return results
all_chunks = []
for i, b64_str in images_b64.items():
page_chunks = process_page(i, b64_str)
all_chunks.extend(page_chunks)
print(f"Total extracted chunks: {len(all_chunks)}")
解释:
嵌入块并存储在 KDB.AI 中
现在我们已经有了块文本,让我们使用 all-MiniLM-L6-v2 进行嵌入,并上传到 KDB.AI。虽然这不是这个任务中最好的嵌入模型,但在这个示例中已经足够了。
# SNIPPET 6: Embedding & Insertion
embed_model = SentenceTransformer("all-MiniLM-L6-v2")
chunk_texts = [ch["text"] for ch in all_chunks]
embeddings = embed_model.encode(chunk_texts)
embeddings = embeddings.astype("float32")
import pandas as pd
row_list = []
for idx, ch_data in enumerate(all_chunks):
row_list.append({
"id": ch_data["id"],
"text": ch_data["text"],
"vectors": embeddings[idx].tolist()
})
df = pd.DataFrame(row_list)
table.insert(df)
print(f"Inserted {len(df)} chunks into '{table_name}'.")
解释:
此时,每个块都可以在向量空间中进行搜索。如果你快速执行 table.query(),你会看到它们都存储在 KDB 中。
查询与构建 RAG(检索增强生成)流程
我们现在可以嵌入用户查询,获取顶部块,并将它们传递给任何大型语言模型(LLM)以进行最终的问答。
相似度搜索
# SNIPPET 7: Vector query for RAG
user_query = "How does this paper handle multi-column text?"
qvec = embed_model.encode(user_query).astype("float32")
search_results = table.search(vectors={"flat_index": [qvec]}, n=3)
retrieved_chunks = search_results[0]["text"].tolist()
context_for_llm = "\n\n".join(retrieved_chunks)
print("Retrieved chunks:\n", context_for_llm)
解释:
最终生成
我们将检索到的块作为“上下文”提供给同一个 Gemini 模型(或任何其他大型语言模型),让它生成最终答案:
# SNIPPET 8: RAG generation
final_prompt = f"""Use the following context to answer the question:
Context:
{context_for_llm}
Question: {user_query}
Answer:
"""
resp = model.generate_content(final_prompt)
print("\n=== Gemini's final answer ===")
print(resp.text)
解释:
总结