RAG & Embeddings & Vector Store

今天更一节大模型知识

这一篇教你理解掌握:

  1. 如何用你的垂域数据补充 LLM 的能力
  2. 如何构建你的垂域(向量)知识库
  3. 搭建一套完整 RAG 系统需要哪些模块
  4. 搭建 RAG 系统时更多的有用技巧

一、什么是检索增强的生成模型(RAG)

1.1 大模型目前固有的局限性

  1. LLM的知识不是实时的
  2. LLM 可能不知道你私有的领域/业务知识

1.2 检索增强生成RAG(Retrieval Augmented Generation)顾名思义,通过**检索**的方法来增强**生成模型**的能力。 

 

(超爽中英!) 2025公认最好的【构建RAG应用】教程!附课件代码

可以参考学习一下大佬们的讲解。

 

二、RAG系统的基本搭建流程

搭建过程:

  1. 文档加载,并按一定条件切割成片段
  2. 将切割的文本片段灌入检索引擎
  3. 封装检索接口
  4. 构建调用流程:Query -> 检索 -> Prompt -> LLM -> 回复

2.1 文档的加载与切割

# !pip install --upgrade openai
# 安装 pdf 解析库
# !pip install pdfminer.six
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer
def extract_text_from_pdf(filename, page_numbers=None, min_line_length=1):
    '''从 PDF 文件中(按指定页码)提取文字'''
    paragraphs = []
    buffer = ''
    full_text = ''
    # 提取全部文本
    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'
    # 按空行分隔,将文本重新组织成段落
    lines = full_text.split('\n')
    for text in lines:
        if len(text) >= min_line_length:
            buffer += (' '+text) if not text.endswith('-') else text.strip('-')
        elif buffer:
            paragraphs.append(buffer)
            buffer = ''
    if buffer:
        paragraphs.append(buffer)
    return paragraphs
paragraphs = extract_text_from_pdf("llama2.pdf", min_line_length=10)

for para in paragraphs[:4]:
    print(para+"\n")

2.2 LLM接口封装

# !pip install -U python-dotenv
from openai import OpenAI
import os

# 加载环境变量
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv(), verbose=True)  # 读取本地 .env 文件,里面定义了 OPENAI_API_KEY

client = OpenAI()
def get_completion(prompt, model="gpt-4o"):
    '''封装 openai 接口'''
    messages = [{"role": "user", "content": prompt}]
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,  # 模型输出的随机性,0 表示随机性最小
    )
    return response.choices[0].message.content

2.3 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)
prompt_template = """
你是一个问答机器人。
你的任务是根据下述给定的已知信息回答用户问题。

已知信息:
{context} # 检索出来的原始文档

用户问:
{query} # 用户的提问

如果已知信息不包含用户问题的答案,或者已知信息不足以回答用户的问题,请直接回复"我无法回答您的问题"。
请不要输出已知信息中不包含的信息或答案。
请用中文回答用户问题。
"""

三、向量检索

3.1 什么是向量

向量是一种有大小和方向的数学对象。它可以表示为从一个点到另一个点的有向线段。例如,二维空间中的向量可以表示为 (x,y),表示从原点 (0,0) 到点 (x,y) 的有向线段。

以此类推,我可以用一组坐标 (x0,x1,…,xN−1) 表示一个 N 维空间中的向量,N 叫向量的维度。 

3.1.1 文本向量(Text Embeddings)
  1. 将文本转成一组 N 维浮点数,即文本向量又叫 Embeddings
  2. 向量之间可以计算距离,距离远近对应语义相似度大小

 

3.1.2 文本向量是怎么得到的(选)

  1. 构建相关(正例)与不相关(负例)的句子对样本
  2. 训练双塔式模型,让正例间的距离小,负例间的距离大

例如:

 扩展阅读:https://www.sbert.net

3.2 向量间的相似度计算

import numpy as np
from numpy import dot
from numpy.linalg import norm
def cos_sim(a, b):
    '''余弦距离 -- 越大越相似'''
    return dot(a, b)/(norm(a)*norm(b))


def l2(a, b):
    '''欧氏距离 -- 越小越相似'''
    x = np.asarray(a)-np.asarray(b)
    return norm(x)
def get_embeddings(texts, model="text-embedding-ada-002", dimensions=None):
    '''封装 OpenAI 的 Embedding 模型接口'''
    if model == "text-embedding-ada-002":
        dimensions = None
    if dimensions:
        data = client.embeddings.create(
            input=texts, model=model, dimensions=dimensions).data
    else:
        data = client.embeddings.create(input=texts, model=model).data
    return [x.embedding for x in data]
test_query = ["测试文本"]
vec = get_embeddings(test_query)[0]
print(f"Total dimension: {len(vec)}")
print(f"First 10 elements: {vec[:10]}")
query = "国际争端"

# 且能支持跨语言
# query = "global conflicts"

documents = [
    "联合国就苏丹达尔富尔地区大规模暴力事件发出警告",
    "土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判",
    "日本岐阜市陆上自卫队射击场内发生枪击事件 3人受伤",
    "国家游泳中心(水立方):恢复游泳、嬉水乐园等水上项目运营",
    "我国首次在空间站开展舱外辐射生物学暴露实验",
]

query_vec = get_embeddings([query])[0]
doc_vecs = get_embeddings(documents)

print("Query与自己的余弦距离: {:.2f}".format(cos_sim(query_vec, query_vec)))
print("Query与Documents的余弦距离:")
for vec in doc_vecs:
    print(cos_sim(query_vec, vec))

print()

print("Query与自己的欧氏距离: {:.2f}".format(l2(query_vec, query_vec)))
print("Query与Documents的欧氏距离:")
for vec in doc_vecs:
    print(l2(query_vec, vec))

3.3 向量数据库

向量数据库,是专门为向量检索设计的中间件!

向量数据库其实最早在传统的人工智能和机器学习场景中就有所应用。在大模型兴起后,由于目前大模型的token数限制,很多开发者倾向于将数据量庞大的知识、新闻、文献、语料等先通过嵌入(embedding)算法转变为向量数据,然后存储在Chroma等向量数据库中。当用户在大模型输入问题后,将问题本身也embedding,转化为向量,在向量数据库中查找与之最匹配的相关知识,组成大模型的上下文,将其输入给大模型,最终返回大模型处理后的文本给用户,这种方式不仅降低大模型的计算量,提高响应速度,也降低成本,并避免了大模型的tokens限制,是一种简单高效的处理手段。此外,向量数据库还在大模型记忆存储等领域发挥其不可替代的作用。

3.3.1 Chroma 向量数据库

官方文档:Introduction - Chroma Docs

# !pip install chromadb
# 为了演示方便,我们只取两页(第一章)
paragraphs = extract_text_from_pdf(
    "llama2.pdf",
    page_numbers=[2, 3],
    min_line_length=10
)
import chromadb
from chromadb.config import Settings

class MyVectorDBConnector:
    def __init__(self, collection_name, embedding_fn):
        # 内存模式
        chroma_client = chromadb.Client(Settings(allow_reset=True))
        # 数据持久化
        # chroma_client = chromadb.PersistentClient(path="./chroma")

        # 注意:为了演示,实际不需要每次 reset(),并且是不可逆的!
        chroma_client.reset()

        # 创建一个 collection
        self.collection = chroma_client.get_or_create_collection(name=collection_name)
        self.embedding_fn = embedding_fn

    def add_documents(self, documents):
        '''向 collection 中添加文档与向量'''
        self.collection.add(
            embeddings=self.embedding_fn(documents),  # 每个文档的向量
            documents=documents,  # 文档的原文
            ids=[f"id{i}" for i in range(len(documents))]  # 每个文档的 id
        )

    def search(self, query, top_n):
        '''检索向量数据库'''
        results = self.collection.query(
            query_embeddings=self.embedding_fn([query]),
            n_results=top_n
        )
        return results
# 创建一个向量数据库对象
vector_db = MyVectorDBConnector("demo", get_embeddings)
# 向向量数据库中添加文档
vector_db.add_documents(paragraphs)

user_query = "Llama 2有多少参数"
# user_query = "Does Llama 2 have a conversational variant"
results = vector_db.search(user_query, 2)

for para in results['documents'][0]:
    print(para+"\n")

 澄清几个关键概念:

  • 向量数据库的意义是快速的检索;
  • 向量数据库本身不生成向量,向量是由 Embedding 模型产生的;
  • 向量数据库与传统的关系型数据库是互补的,不是替代关系,在实际应用中根据实际需求经常同时使用。
3.3.2 Chroma 向量数据库服务

Server 端

chroma run --path /db_path

Client 端

import chromadb
chroma_client = chromadb.HttpClient(host='localhost', port=8000)

 3.3.3 主流向量数据库功能对比

扩展阅读:https://guangzhengli.com/blog/zh/vector-database

3.4 基于向量检索的RAG 

class RAG_Bot:
    def __init__(self, vector_db, llm_api, n_results=2):
        self.vector_db = vector_db
        self.llm_api = llm_api
        self.n_results = n_results

    def chat(self, user_query):
        # 1. 检索
        search_results = self.vector_db.search(user_query, self.n_results)

        # 2. 构建 Prompt
        prompt = build_prompt(
            prompt_template, context=search_results['documents'][0], query=user_query)

        # 3. 调用 LLM
        response = self.llm_api(prompt)
        return response
# 创建一个RAG机器人
bot = RAG_Bot(
    vector_db,
    llm_api=get_completion
)

user_query = "llama 2有多少参数?"

response = bot.chat(user_query)

print(response)

3.5 OpenAI 新发布的两个 Embedding 模型

2024 年 1 月 25 日,OpenAI 新发布了两个 Embedding 模型

  • text-embedding-3-large
  • text-embedding-3-small

其最大特点是,支持自定义的缩短向量维度,从而在几乎不影响最终效果的情况下降低向量检索与相似度计算的复杂度。

通俗的说:越大越准、越小越快。 官方公布的评测结果:

注:MTEB 是一个大规模多任务的 Embedding 模型公开评测集

model = "text-embedding-3-large"
dimensions = 256

query = "国际争端"

# 且能支持跨语言
# query = "global conflicts"

documents = [
    "联合国就苏丹达尔富尔地区大规模暴力事件发出警告",
    "土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判",
    "日本岐阜市陆上自卫队射击场内发生枪击事件 3人受伤",
    "国家游泳中心(水立方):恢复游泳、嬉水乐园等水上项目运营",
    "我国首次在空间站开展舱外辐射生物学暴露实验",
]

query_vec = get_embeddings([query], model=model, dimensions=dimensions)[0]
doc_vecs = get_embeddings(documents, model=model, dimensions=dimensions)

print("向量维度: {}".format(len(query_vec)))

print()

print("Query与Documents的余弦距离:")
for vec in doc_vecs:
    print(cos_sim(query_vec, vec))

print()

print("Query与Documents的欧氏距离:")
for vec in doc_vecs:
    print(l2(query_vec, vec))

 扩展阅读:这种可变长度的 Embedding 技术背后的原理叫做 Matryoshka Representation Learning

四、实战 RAG 系统的进阶知识

4.1 文本分割的粒度

缺陷

  1. 粒度太大可能导致检索不精准,粒度太小可能导致信息不全面
  2. 问题的答案可能跨越两个片段
# 创建一个向量数据库对象
vector_db = MyVectorDBConnector("demo_text_split", get_embeddings)
# 向向量数据库中添加文档
vector_db.add_documents(paragraphs)

# 创建一个RAG机器人
bot = RAG_Bot(
    vector_db,
    llm_api=get_completion
)
# user_query = "llama 2有商用许可协议吗"
user_query="llama 2 chat有多少参数"
search_results = vector_db.search(user_query, 2)

for doc in search_results['documents'][0]:
    print(doc+"\n")

print("====回复====")
bot.chat(user_query)
for p in paragraphs:
    print(p+"\n")

改进: 按一定粒度,部分重叠式的切割文本,使上下文更完整

from nltk.tokenize import sent_tokenize
import json


# chunk_size 一般根据文档内容或大小来设置
# overlap_size 一般设置 chunk_size 大小的10%-20%之间
def split_text(paragraphs, chunk_size=300, overlap_size=100):
    '''按指定 chunk_size 和 overlap_size 交叠割文本'''
    sentences = [s.strip() for p in paragraphs for s in sent_tokenize(p)]
    chunks = []
    i = 0
    while i < len(sentences):
        chunk = sentences[i]
        overlap = ''
        prev_len = 0
        prev = i - 1
        # 向前计算重叠部分
        while prev >= 0 and len(sentences[prev])+len(overlap) <= overlap_size:
            overlap = sentences[prev] + ' ' + overlap
            prev -= 1
        chunk = overlap+chunk
        next = i + 1
        # 向后计算当前chunk
        while next < len(sentences) and len(sentences[next])+len(chunk) <= chunk_size:
            chunk = chunk + ' ' + sentences[next]
            next += 1
        chunks.append(chunk)
        i = next
    return chunks

 此处 sent_tokenize 为针对英文的实现,针对中文的实现请参考 chinese_utils.py

chunks = split_text(paragraphs, 300, 100)
# 创建一个向量数据库对象
vector_db = MyVectorDBConnector("demo_text_split", get_embeddings)
# 向向量数据库中添加文档
vector_db.add_documents(chunks)
# 创建一个RAG机器人
bot = RAG_Bot(
    vector_db,
    llm_api=get_completion
)
# user_query = "llama 2有商用许可协议吗"
user_query="llama 2 chat有多少参数"

search_results = vector_db.search(user_query, 2)
for doc in search_results['documents'][0]:
    print(doc+"\n")

response = bot.chat(user_query)
print("====回复====")
print(response)

4.2 检索后排序

问题: 有时,最合适的答案不一定排在检索的最前面

user_query = "how safe is llama 2"
search_results = vector_db.search(user_query, 5)

for doc in search_results['documents'][0]:
    print(doc+"\n")

response = bot.chat(user_query)
print("====回复====")
print(response)

方案:

  1. 检索时过招回一部分文本
  2. 通过一个排序模型对 query 和 document 重新打分排序

以下代码运行前要确保能访问 Hugging Face! 

# !pip install sentence_transformers
from sentence_transformers import CrossEncoder

model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2', max_length=512) # 英文,模型较小
# model = CrossEncoder('BAAI/bge-reranker-large', max_length=512) # 多语言,国产,模型较大
user_query = "how safe is llama 2"
# user_query = "llama 2安全性如何"
scores = model.predict([(user_query, doc) for doc in search_results['documents'][0]])
# 按得分排序
sorted_list = sorted(zip(scores, search_results['documents'][0]), key=lambda x: x[0], reverse=True)
for score, doc in sorted_list:
    print(f"{score}\t{doc}\n")
一些 Rerank 的 API 服务

4.3 混合检索(Hybrid Search)(选)

实际生产中,传统的关键字检索(稀疏表示)与向量检索(稠密表示)各有优劣。

举个具体例子,比如文档中包含很长的专有名词,关键字检索往往更精准而向量检索容易引入概念混淆。

# 背景说明:在医学中“小细胞肺癌”和“非小细胞肺癌”是两种不同的癌症

query = "非小细胞肺癌的患者"

documents = [
    "玛丽患有肺癌,癌细胞已转移",
    "刘某肺癌I期",
    "张某经诊断为非小细胞肺癌III期",
    "小细胞肺癌是肺癌的一种"
]

query_vec = get_embeddings([query])[0]
doc_vecs = get_embeddings(documents)

print("Cosine distance:")
for vec in doc_vecs:
    print(cos_sim(query_vec, vec))

所以,有时候我们需要结合不同的检索算法,来达到比单一检索算法更优的效果。这就是混合检索

混合检索的核心是,综合文档 d 在不同检索算法下的排序名次(rank),为其生成最终排序。

一个最常用的算法叫 Reciprocal Rank Fusion(RRF)

rrf(d)=∑a∈A1k+ranka(d)

其中 A 表示所有使用的检索算法的集合,ranka(d) 表示使用算法 a 检索时,文档 d 的排序,k 是个常数。

很多向量数据库都支持混合检索,比如 WeaviatePinecone 等。也可以根据上述原理自己实现。

4.3.1 手写个简单的例子

注意:需要安装好 Elastic Search Server,并启动! 

# !pip install elasticsearch7
  1. 基于关键字检索的排序
import time


class MyEsConnector:
    def __init__(self, es_client, index_name, keyword_fn):
        self.es_client = es_client
        self.index_name = index_name
        self.keyword_fn = keyword_fn

    def add_documents(self, documents):
        '''文档灌库'''
        if self.es_client.indices.exists(index=self.index_name):
            self.es_client.indices.delete(index=self.index_name)
        self.es_client.indices.create(index=self.index_name)
        actions = [
            {
                "_index": self.index_name,
                "_source": {
                    "keywords": self.keyword_fn(doc),
                    "text": doc,
                    "id": f"doc_{i}"
                }
            }
            for i, doc in enumerate(documents)
        ]
        helpers.bulk(self.es_client, actions)
        time.sleep(1)

    def search(self, query_string, top_n=3):
        '''检索'''
        search_query = {
            "match": {
                "keywords": self.keyword_fn(query_string)
            }
        }
        res = self.es_client.search(
            index=self.index_name, query=search_query, size=top_n)
        return {
            hit["_source"]["id"]: {
                "text": hit["_source"]["text"],
                "rank": i,
            }
            for i, hit in enumerate(res["hits"]["hits"])
        }
from chinese_utils import to_keywords  # 使用中文的关键字提取函数

# 引入配置文件
ELASTICSEARCH_BASE_URL = os.getenv('ELASTICSEARCH_BASE_URL')
ELASTICSEARCH_PASSWORD = os.getenv('ELASTICSEARCH_PASSWORD')
ELASTICSEARCH_NAME= os.getenv('ELASTICSEARCH_NAME')

es = Elasticsearch(
    hosts=[ELASTICSEARCH_BASE_URL],
    http_auth=(ELASTICSEARCH_NAME, ELASTICSEARCH_PASSWORD),  # 用户名,密码
)


# 创建 ES 连接器
es_connector = MyEsConnector(es, "demo_es_rrf", to_keywords)

# 文档灌库
es_connector.add_documents(documents)

# 关键字检索
keyword_search_results = es_connector.search(query, 3)

print(json.dumps(keyword_search_results, indent=4, ensure_ascii=False))

2.基于向量检索的排序

# 创建向量数据库连接器
vecdb_connector = MyVectorDBConnector("demo_vec_rrf", get_embeddings)

# 文档灌库
vecdb_connector.add_documents(documents)

# 向量检索
vector_search_results = {
    "doc_"+str(documents.index(doc)): {
        "text": doc,
        "rank": i
    }
    for i, doc in enumerate(
        vecdb_connector.search(query, 3)["documents"][0]
    )
}  # 把结果转成跟上面关键字检索结果一样的格式

print(json.dumps(vector_search_results, indent=4, ensure_ascii=False))

 3.基于 RRF 的融合排序

参考资料:混合搜索评分 (RRF) - Azure AI Search | Microsoft Learn

def rrf(ranks, k=1):
    ret = {}
    # 遍历每次的排序结果
    for rank in ranks:
        # 遍历排序中每个元素
        for id, val in rank.items():
            if id not in ret:
                ret[id] = {"score": 0, "text": val["text"]}
            # 计算 RRF 得分
            ret[id]["score"] += 1.0/(k+val["rank"])
    # 按 RRF 得分排序,并返回
    return dict(sorted(ret.items(), key=lambda item: item[1]["score"], reverse=True))
import json

# 融合两次检索的排序结果
reranked = rrf([keyword_search_results, vector_search_results])

print(json.dumps(reranked, indent=4, ensure_ascii=False)

五、PDF 文档中的表格怎么处理

  1. 将每页 PDF 转成图片
# !pip install PyMuPDF
# !pip install matplotlib
import os
import fitz
from PIL import Image

def pdf2images(pdf_file):
    '''将 PDF 每页转成一个 PNG 图像'''
    # 保存路径为原 PDF 文件名(不含扩展名)
    output_directory_path, _ = os.path.splitext(pdf_file)
    
    if not os.path.exists(output_directory_path):
        os.makedirs(output_directory_path)
    
    # 加载 PDF 文件
    pdf_document = fitz.open(pdf_file)
    
    # 每页转一张图
    for page_number in range(pdf_document.page_count):
        # 取一页
        page = pdf_document[page_number]
    
        # 转图像
        pix = page.get_pixmap()
    
        # 从位图创建 PNG 对象
        image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
    
        # 保存 PNG 文件
        image.save(f"./{output_directory_path}/page_{page_number + 1}.png")
    
    # 关闭 PDF 文件
    pdf_document.close()
from PIL import Image
import os
import matplotlib.pyplot as plt

def show_images(dir_path):
    '''显示目录下的 PNG 图像'''
    for file in os.listdir(dir_path):
        if file.endswith('.png'):
            # 打开图像
            img = Image.open(os.path.join(dir_path, file)) 

            # 显示图像
            plt.imshow(img)
            plt.axis('off')  # 不显示坐标轴
            plt.show()
pdf2images("llama2_page8.pdf")
show_images("llama2_page8")

2.识别文档(图片)中的表格

class MaxResize(object):
    '''缩放图像'''
    def __init__(self, max_size=800):
        self.max_size = max_size

    def __call__(self, image):
        width, height = image.size
        current_max_size = max(width, height)
        scale = self.max_size / current_max_size
        resized_image = image.resize(
            (int(round(scale * width)), int(round(scale * height)))
        )

        return resized_image
import torchvision.transforms as transforms

# 图像预处理
detection_transform = transforms.Compose(
    [
        MaxResize(800),
        # 将原始的PILImage格式的数据格式化为可被pytorch快速处理的张量类型
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
    ]
)
from transformers import AutoModelForObjectDetection

# 加载 TableTransformer 模型
model = AutoModelForObjectDetection.from_pretrained(
    "microsoft/table-transformer-detection"
)
# 识别后的坐标换算与后处理

def box_cxcywh_to_xyxy(x):
    '''坐标转换'''
    x_c, y_c, w, h = x.unbind(-1)
    b = [(x_c - 0.5 * w), (y_c - 0.5 * h), (x_c + 0.5 * w), (y_c + 0.5 * h)]
    return torch.stack(b, dim=1)


def rescale_bboxes(out_bbox, size):
    '''区域缩放'''
    width, height = size
    boxes = box_cxcywh_to_xyxy(out_bbox)
    boxes = boxes * torch.tensor(
        [width, height, width, height], dtype=torch.float32
    )
    return boxes


def outputs_to_objects(outputs, img_size, id2label):
    '''从模型输出中取定位框坐标'''
    m = outputs.logits.softmax(-1).max(-1)
    pred_labels = list(m.indices.detach().cpu().numpy())[0]
    pred_scores = list(m.values.detach().cpu().numpy())[0]
    pred_bboxes = outputs["pred_boxes"].detach().cpu()[0]
    pred_bboxes = [
        elem.tolist() for elem in rescale_bboxes(pred_bboxes, img_size)
    ]

    objects = []
    for label, score, bbox in zip(pred_labels, pred_scores, pred_bboxes):
        class_label = id2label[int(label)]
        if not class_label == "no object":
            objects.append(
                {
                    "label": class_label,
                    "score": float(score),
                    "bbox": [float(elem) for elem in bbox],
                }
            )

    return objects
import torch

# 识别表格,并将表格部分单独存为图像文件

def detect_and_crop_save_table(file_path):
    # 加载图像(PDF页)    
    image = Image.open(file_path)

    filename, _ = os.path.splitext(os.path.basename(file_path))

    # 输出路径
    cropped_table_directory = os.path.join(os.path.dirname(file_path), "table_images")

    if not os.path.exists(cropped_table_directory):
        os.makedirs(cropped_table_directory)

    # 预处理
    pixel_values = detection_transform(image).unsqueeze(0)

    # 识别表格
    with torch.no_grad():
        outputs = model(pixel_values)

    # 后处理,得到表格子区域
    id2label = model.config.id2label
    id2label[len(model.config.id2label)] = "no object"
    detected_tables = outputs_to_objects(outputs, image.size, id2label)

    print(f"number of tables detected {len(detected_tables)}")

    for idx in range(len(detected_tables)):
        # 将识别从的表格区域单独存为图像
        cropped_table = image.crop(detected_tables[idx]["bbox"])
        cropped_table.save(os.path.join(cropped_table_directory,f"{filename}_{idx}.png"))
detect_and_crop_save_table("llama2_page8/page_1.png")
show_images("llama2_page8/table_images")

 

3.基于 GPT-4 Vision API 做表格问答

import base64
from openai import OpenAI

client = OpenAI()

def encode_image(image_path):
  with open(image_path, "rb") as image_file:
    return base64.b64encode(image_file.read()).decode('utf-8')

def image_qa(query, image_path):
    base64_image = encode_image(image_path)
    response = client.chat.completions.create(
        model="gpt-4o",
        temperature=0,
        seed=42,
        messages=[{
            "role": "user",
              "content": [
                  {"type": "text", "text": query},
                  {
                      "type": "image_url",
                      "image_url": {
                          "url": f"data:image/jpeg;base64,{base64_image}",
                      },
                  },
              ],
        }],
    )

    return response.choices[0].message.content
response = image_qa("哪个模型在AGI Eval数据集上表现最好。得分多少","llama2_page8/table_images/page_1_0.png")
print(response)

4.用 GPT-4 Vision 生成表格(图像)描述,并向量化用于检索

import chromadb
from chromadb.config import Settings


class NewVectorDBConnector:
    def __init__(self, collection_name, embedding_fn):
        chroma_client = chromadb.Client(Settings(allow_reset=True))

        # 为了演示,实际不需要每次 reset()
        chroma_client.reset()

        # 创建一个 collection
        self.collection = chroma_client.get_or_create_collection(
            name=collection_name)
        self.embedding_fn = embedding_fn

    def add_documents(self, documents):
        '''向 collection 中添加文档与向量'''
        self.collection.add(
            embeddings=self.embedding_fn(documents),  # 每个文档的向量
            documents=documents,  # 文档的原文
            ids=[f"id{i}" for i in range(len(documents))]  # 每个文档的 id
        )

    def add_images(self, image_paths):
        '''向 collection 中添加图像'''
        documents = [
            image_qa("请简要描述图片中的信息",image)
            for image in image_paths
        ]
        self.collection.add(
            embeddings=self.embedding_fn(documents),  # 每个文档的向量
            documents=documents,  # 文档的原文
            ids=[f"id{i}" for i in range(len(documents))],  # 每个文档的 id
            metadatas=[{"image": image} for image in image_paths] # 用 metadata 标记源图像路径
        )

    def search(self, query, top_n):
        '''检索向量数据库'''
        results = self.collection.query(
            query_embeddings=self.embedding_fn([query]),
            n_results=top_n
        )
        return results
images = []
dir_path = "llama2_page8/table_images"
for file in os.listdir(dir_path):
    if file.endswith('.png'):
        # 打开图像
        images.append(os.path.join(dir_path, file))

new_db_connector = NewVectorDBConnector("table_demo",get_embeddings)
new_db_connector.add_images(images)
query  = "哪个模型在AGI Eval数据集上表现最差。得分多少"

results = new_db_connector.search(query, 1)
metadata = results["metadatas"][0]
print("====检索结果====")
print(metadata)
print("====回复====")
response = image_qa(query,metadata[0]["image"])
print(response)

一些面向 RAG 的文档解析辅助工具

  • PyMuPDF: PDF 文件处理基础库,带有基于规则的表格与图像抽取(不准)
  • RAGFlow: 一款基于深度文档理解构建的开源 RAG 引擎,支持多种文档格式(火爆)
  • Unstructured.io: 一个开源+SaaS形式的文档解析库,支持多种文档格式
  • LlamaParse:付费 API 服务,由 LlamaIndex 官方提供,解析不保证100%准确,实测偶有文字丢失或错位发生
  • Mathpix:付费 API 服务,效果较好,可解析段落结构、表格、公式等,贵!

在工程上,PDF 解析本身是个复杂且琐碎的工作。以上工具都不完美,建议在自己实际场景测试后选择使用。

六、说说 GraphRAG

 

  1. 什么是 GraphRAG:核心思想是将知识预先处理成知识图谱
  2. 优点:适合复杂问题,尤其是以查询为中心的总结,例如:“XXX团队去年有哪些贡献”
  3. 缺点:知识图谱的构建、清洗、维护更新等都有可观的成本
  4. 建议
    • GraphRAG 不是万能良药
    • 领会其核心思想
    • 遇到传统 RAG 无论如何优化都不好解决的问题时,酌情使用

总结

RAG 的流程

  • 离线步骤:
    1. 文档加载
    2. 文档切分
    3. 向量化
    4. 灌入向量数据库
  • 在线步骤:
    1. 获得用户问题
    2. 用户问题向量化
    3. 检索向量数据库
    4. 将检索结果和用户问题填入 Prompt 模版
    5. 用最终获得的 Prompt 调用 LLM
    6. 由 LLM 生成回复
&lt;think&gt;好的,我需要帮助用户了解或实现RAG(检索增强生成)的相关技术。首先,我应该明确RAG的基本概念。根据引用[2],RAG结合了检索外部知识库和大语言模型的生成能力,类似于开卷考试,让模型在生成答案时可以参考外部信息。接下来,用户可能需要一个具体的实现步骤或教程,所以我得整理出一个清晰的流程。 第一步是确定知识库的来源。用户需要收集和预处理相关数据,比如PDF、网页文本或数据库内容。然后,将这些数据转化为嵌入向量,可能要用到像Sentence-BERT这样的模型。这里需要提到向量数据库的重要性,比如FAISS或Pinecone,用于高效检索。 接下来是检索阶段。当用户输入查询时,系统需要从知识库中找到最相关的文档片段。这部分需要说明如何计算相似度,比如余弦相似度,以及如何选择top-k个结果。 然后是生成部分。将检索到的上下文和用户问题一起输入大模型,比如GPT-3或Llama,生成最终答案。这里可以举例说明不同的提示模板,比如LangChain的RAG链。 此外,用户可能关心如何优化RAG系统。可以提到调整检索策略,比如混合搜索或重新排序,以及评估检索结果的质量指标。引用[3]提到的Langchain和llama_index作为工具,应该给出具体的代码示例,比如使用LangChain的VectorstoreIndexCreator。 还要注意常见问题,比如数据预处理不当导致检索效果差,或者大模型生成时的幻觉问题。需要强调评估和迭代的重要性,引用[1]提到的上下文过滤可能也是优化点。 最后,生成相关问题,覆盖RAG的原理、工具选择、评估方法、优化技巧和应用场景,帮助用户进一步探索。确保所有数学表达式使用正确的LaTeX格式,比如嵌入向量的距离计算$d = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$,并检查引用标记的正确添加。&lt;/think&gt;以下分步说明如何实现一个基础的RAG系统,并结合代码示例和关键概念解析: ### 一、RAG实现核心步骤 1. **知识库构建** - 数据收集:从PDF/网页/数据库获取原始数据,格式如: ```python documents = [ &quot;RAG通过检索外部知识增强生成...&quot;, &quot;LangChain提供RAG工具链...&quot; ] ``` - 文本分块:使用LangChain的`RecursiveCharacterTextSplitter` ```python from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter(chunk_size=500) chunks = text_splitter.split_documents(documents) ``` 2. **向量化处理** $$ \text{embed}(text) \rightarrow \mathbb{R}^d $$ 使用HuggingFace嵌入模型: ```python from sentence_transformers import SentenceTransformer encoder = SentenceTransformer(&#39;all-MiniLM-L6-v2&#39;) embeddings = encoder.encode(chunks) ``` 3. **向量数据库** 使用FAISS实现相似度检索: ```python import faiss index = faiss.IndexFlatL2(384) index.add(embeddings) ``` 4. **检索-生成流程** ```python query = &quot;如何实现RAG系统?&quot; query_embed = encoder.encode(query) D, I = index.search(query_embed, k=3) # 取top3 context = [chunks[i] for i in I[0]] ``` 5. **提示工程** LangChain的RAG提示模板: ```python from langchain.prompts import PromptTemplate template = &quot;&quot;&quot;基于以下上下文回答: {context} 问题:{question}&quot;&quot;&quot; prompt = PromptTemplate(template=template, input_variables=[&quot;context&quot;, &quot;question&quot;]) ``` ### 二、关键技术组件 1. **检索优化** - 混合搜索:结合$BM25$与向量相似度 - 重新排序:使用`bge-reranker`等模型 2. **生成控制** 通过温度参数调节创造性: $$ P(w_t|w_{&lt;t},c) = \text{softmax}(f(w_{&lt;t},c)/\tau) $$ 3. **评估指标** - 检索召回率:$Recall@k = \frac{\text{相关文档命中数}}{总相关文档数}$ - 生成质量:BLEU、ROUGE等 ### 三、完整代码示例(LangChain实现) ```python from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import FAISS from langchain.llms import LlamaCpp # 知识库构建 embeddings = HuggingFaceEmbeddings(model_name=&quot;all-MiniLM-L6-v2&quot;) vector_store = FAISS.from_texts(chunks, embeddings) # 检索增强生成 retriever = vector_store.as_retriever(search_kwargs={&quot;k&quot;: 3}) llm = LlamaCpp(model_path=&quot;llama-2-7b.Q4_K_M.gguf&quot;) from langchain.chains import RetrievalQA qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, chain_type=&quot;stuff&quot; ) print(qa_chain.run(&quot;RAG的实现步骤有哪些?&quot;)) ``` ### 四、常见问题与优化 1. **冷启动问题**:可使用Wikipedia预构建索引 2. **长上下文处理**:采用`sliding window`分块策略 3. **多模态扩展**:CLIP支持图像检索增强
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值