为了账号安全,请及时绑定邮箱和手机立即绑定

通过利用更小规模的LLM进行改进的检索增强生成(RAG,一种通过检索信息来增强生成过程的技术)

Llama-3.2–1 B Instruct 和 LanceDB:

(注:B Instruct 是一个特定的概念,在技术圈中可能需要额外解释)

摘要 :检索增强生成(RAG)结合了大型语言模型和外部知识源,以生成更准确和上下文相关的响应。本文探讨了如何有效利用较小的语言模型(LLM),如Meta的10亿参数模型,对大型文档内容进行总结和索引,从而提高RAG系统的效率和扩展性。我们提供了一个逐步指南,包括代码片段,介绍如何从产品文档PDF中提取和总结文本片段,并将其存储在LanceDB数据库中,以便高效检索。

来,我们介绍一下。

检索增强生成是一种范式,通过将语言模型与外部知识库集成,增强了语言模型的能力。尽管像GPT-4这样的大型语言模型已经展示了非凡的能力,但它们也伴随着显著的计算成本。小型语言模型则提供了更为资源高效的替代方案,特别是在文本摘要和关键词提取等任务上。这些任务对于检索增强生成系统中的索引和检索至关重要。

在本文中,我们将演示如何使用一个小LLM:

  • 如何使用一个小LLM:
  1. 从PDF文档中提取并总结内容
  2. 生成摘要和关键词的嵌入表示
  3. 将数据高效存储在LanceDB数据库中
  4. 用于有效的RAG方法
  5. 还使用代理工作流来自检LLM中的错误

使用较小的LLM模型可以显著降低在大规模数据集上进行此类转换的成本,同时对于更简单的任务,它也能提供类似的效益,就像较大的LLM模型一样。并且可以轻松地在企业内部或从云中低成本部署,无需高昂费用。

我们将使用LLAMA 3.2 10亿参数模型,这目前是最先进的小型LLM。

增强版 LLM RAG(图片由作者提供)

关于直接嵌入文本的问题

在开始实现之前,理解为什么在基于检索的生成(RAG)系统中直接嵌入文档中的原始文本可能会遇到一些问题是很重要的。

不有效的上下文捕捉——

无效的上下文捕捉

插入或直接使用页面上的原始文本而不进行摘要往往会导致生成的嵌入向量这。

  • 高维度噪声:原始文本可能包含无关信息、格式化痕迹或不增加核心内容理解的套话。
  • 被稀释的关键概念:重要概念可能被冗余文本淹没,导致嵌入表示无法充分反映关键信息。
检索效率不高

当嵌入不能准确反映文本的核心概念时,检索系统可能无法正常工作。

  • 有效匹配用户的问题:嵌入可能与查询嵌入不太匹配,导致找到的相关文档不够好。
  • 提供正确的上下文:即便找到了文档,因为嵌入数据中的干扰因素,可能无法提供用户真正需要的信息。
解决方案:在嵌入前先做摘要

在生成嵌入前总结文本,这能解决这些问题。

  • 提炼核心信息:摘要提取核心要点和关键词,去除不必要的细节。
  • 提升嵌入向量质量:从摘要生成的嵌入向量更加集中且能更好地反映主要内容,从而提高检索的准确性。
前提条件

(注:此处添加了空白行以符合中文文档的典型格式。)

在我们开始之前,请确保已经安装了以下软件或工具。

  • Python 3.7 或更高版本的 Python

  • PyTorch

  • Transformers 库

  • SentenceTransformers

  • PyMuPDF(用于 PDF 处理)

  • LanceDB

  • 拥有至少 6GB 显存的笔记本电脑或类似 Colab 那样配备 T4 GPU 的环境或类似的工具
步骤1:搭建环境

首先,导入所有必要的库文件,并初始化日志,用于调试和记录。

import pandas as pd  # 引入pandas库,用于数据处理
import fitz  # PyMuPDF,用于处理PDF文件
from transformers import AutoModelForCausalLM, AutoTokenizer  # 从transformers库中引入自动模型和分词器
import torch  # 引入torch库,用于深度学习
import lancedb  # 引入lancedb库,用于数据管理
from sentence_transformers import SentenceTransformer  # 从sentence_transformers库中引入句子转换模型
import json  # 引入json库,用于处理JSON数据
import pyarrow as pa  # 引入pyarrow库,用于数据编码
import numpy as np  # 引入numpy库,用于数值计算
import re  # 引入re库,用于正则表达式
步骤2:定义一些辅助函数
编写提示语

我们定义了一个函数来生成与LLAMA 3.2模型兼容的提示文本。

    def create_prompt(question):  
        """  
        生成符合LLAMA 3.2格式的提示语。  
        """  
        system_message = "你是一个帮助总结文本并将结果以JSON格式呈现的助手"  
        prompt_template = f'''  
    system  
    {system_message}user  
    {question}assistant  
    '''  
        return prompt_template
处理提示信息

此功能使用模型和分词器处理提示内容。我们将温度值调整为0.1,使模型不太有创意,减少胡编乱造。

    def process_prompt(prompt, model, tokenizer, device, max_length=500):  
        """  
        处理提示,生成响应,并提取助手的回复。  
        """  
        prompt_encoded = tokenizer(prompt, truncation=True, padding=False, return_tensors="pt")  
        model.eval()  
        output = model.generate(  
            input_ids=prompt_encoded.input_ids.to(device),  
            max_new_tokens=max_length,  
            attention_mask=prompt_encoded.attention_mask.to(device),  
            temperature=0.1  # 更确定  
        )  
        answer = tokenizer.decode(output[0], skip_special_tokens=True)  
        parts = answer.split("assistant1231231222", 1)  
        if len(parts) > 1:  
            words_after_assistant = parts[1].strip()  
            return words_after_assistant  
        else:  
            print("未找到助手的回复。")  
            return "NONE"
步骤三:加载模型文件

我们使用的是LLAMA 3.2 1B Instruct模型来进行摘要。我们用bfloat16格式加载模型来减少内存使用,并在配备了NVIDIA GeForce RTX 3060 6GB的GPU(CUDA工具包版本12.5(V12.5.40))的装有Linux系统的笔记本电脑上运行。

更好的方式是通过vLLM或更好的exLLamaV2托管。

    model_name_long = "meta-llama/Llama-3.2-1B-Instruct"  
    tokenizer = AutoTokenizer.from_pretrained(model_name_long)  
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  
    log.info(f"加载模型 {model_name_long}")  
    bf16 = False  
    fp16 = True  
    if torch.cuda.is_available():  
        major, _ = torch.cuda.get_device_capability()  
        if major >= 8:  
            log.info("您的 GPU 支持 bfloat16,可以设置 bf16=True 来加速训练")  
            bf16 = True  
            fp16 = False  
    # 加载到 GPU 0 上  
    device_map = {"": 0}  
    torch_dtype = torch.bfloat16 if bf16 else torch.float16  
    model = AutoModelForCausalLM.from_pretrained(  
        model_name_long,  
        torch_dtype=torch_dtype,  
        device_map=device_map,  
    )  
    log.info(f"模型加载完成,torch 数据类型为 {torch_dtype}")
第四步:阅读并处理PDF文档

我们从每个PDF文件的页面中提取文本。

    file_path = './data/troubleshooting.pdf'  
    dict_pages = {}  
    # 打开PDF文件  
    with fitz.open(file_path) as pdf_document:  
        for page_number in range(pdf_document.page_count):  
            page = pdf_document.load_page(page_number)  
            page_text = page.get_text()  
            dict_pages[page_number] = page_text  
            print(f"已处理PDF的第 {page_number + 1} 页")
步骤五:配置 LanceDB 和 SentenceTransformer

我们初始化SentenceTransformer模型以生成嵌入,并设置LanceDB来存储数据。我们使用基于PyArrow的模式来定义LanceDB表的结构。

请注意,目前不使用关键词,但可以用来进行混合搜索,也就是向量相似性搜索和文本搜索。

    # 初始化SentenceTransformer模型如下  
    sentence_model = SentenceTransformer('all-MiniLM-L6-v2')  
    # 连接到LanceDB  
    db = lancedb.connect('./data/my_lancedb')  
    # 使用PyArrow定义模式  
    schema = pa.schema([  
        pa.field("page_number", pa.int64()),  
        pa.field("original_content", pa.string()),  
        pa.field("summary", pa.string()),  
        pa.field("keywords", pa.string()),  
        pa.field("vectorS", pa.list_(pa.float32(), 384)),  # 嵌入维度为384  
        pa.field("vectorK", pa.list_(pa.float32(), 384)),  
    ])  
    # 创建或连接到名为'summaries'的表  
    table = db.create_table('summaries', schema=schema, mode='overwrite')
步骤6:汇总和保存数据

我们逐页处理,生成摘要 和 关键词,并将它们及其嵌入一起存储到数据库中。

    # 遍历 PDF 中的每一页  
    for page_number, text in dict_pages.items():  
        question = f"""对于给定的段落,提供一个长摘要,包含段落中的所有主要关键词。  
    格式应为如下 JSON 格式:  
    {{  
        "summary": <文本摘要>,  
        "keywords": <段落中出现的主要关键词和缩写,以逗号分隔的列表>,  
    }}  
    确保 JSON 字段使用双引号,并使用正确的闭合符号。  
    段落: {text}"""  

        prompt = create_prompt(question)  
        response = process_prompt(prompt, model, tokenizer, device)  

        # JSON 解码错误处理  
        try:  
            summary_json = json.loads(response)  
            log.warning(f"验证生成的 JSON 是否符合预期格式: {response}")  
        except json.decoder.JSONDecodeError as e:  
            exception_msg = str(e)  
            question = f"""纠正以下 JSON {response},该 JSON 包含错误 {exception_msg},并输出正确的 JSON 格式。"""  
            log.warning(f"{exception_msg} for {response}")  
            prompt = create_prompt(question)  
            response = process_prompt(prompt, model, tokenizer, device)  
            log.warning(f"已纠正为 '{response}'")  
            try:  
                summary_json = json.loads(response)  
                log.warning(f"验证生成的 JSON 是否符合预期格式: {response}")  
            except Exception as e:  
                log.error(f"解析 JSON 失败: '{e}',错误 JSON 内容为 '{response}'")  
                continue  

        keywords = ', '.join(summary_json['keywords'])  

        # 生成摘要向量和关键词向量  
        vectorS = sentence_model.encode(summary_json['summary'])  
        vectorK = sentence_model.encode(keywords)  

        # 将数据存储在 LanceDB 中  
        table.add([{  
            "page_number": int(page_number),  
            "original_content": text,  
            "summary": summary_json['summary'],  
            "keywords": keywords,  
            "vectorS": vectorS,  
            "vectorK": vectorK  
        }])  

        print(f"页面 {page_number} 的数据已成功保存。")
使用大语言模型来修正它们的输出

在生成摘要和提取关键词时,大语言模型有时会产生不符合预期的输出,比如格式错误的JSON。

我们就可以利用LLM来修正这些输出,让它自己纠正错误。上面的代码就展示了这一点。(LLM, 大型语言模型)

    # 使用Small LLAMA 3.2 1B模型创建摘要
    for page_number, text in dict_pages.items():
        question = f"""为给定段落提供一个包含所有主要关键词的详细摘要。格式应如下所示的JSON格式:
        {{
            "summary": <文本摘要> 示例 "一些摘要文本",
            "keywords": <段落中出现的关键字和缩写的逗号分隔列表> 示例 ["关键字1","关键字2"],
        }}
        确保JSON字段使用双引号,例如使用"summary"而不是'summary',并正确使用开始和结束分隔符。
        段落:{text}"""
        prompt = create_prompt(question)
        response = process_prompt(prompt, model, tokenizer, device)
        try:
            summary_json = json.loads(response)
        except json.decoder.JSONDecodeError as e:
            exception_msg = str(e)
            # 使用LLM来纠正其自身的输出
            question = f"""纠正以下JSON {response},该JSON格式存在{exception_msg}问题,使其符合正确的JSON格式。仅输出纠正后的JSON。
            格式应如下所示的JSON格式:
            {{
                "summary": <文本摘要> 示例 "一些摘要文本",
                "keywords": <段落中出现的关键字和缩写的逗号分隔列表> 示例 ["关键字1","关键字2"],
            }}"""
            log.warning(f"{exception_msg} for {response}")
            prompt = create_prompt(question)
            response = process_prompt(prompt, model, tokenizer, device)
            log.warning(f"修正了 '{response}'")
            # 尝试解析修正后的JSON
            try:
                summary_json = json.loads(response)
            except json.decoder.JSONDecodeError as e:
                log.error(f"无法解析修正后的JSON: '{e}' for '{response}'")
                continue

在这个代码示例中,如果 LLM 的初始输出无法解析成 JSON,我们会再次提示 LLM 来修正这个 JSON。这种自我修正的方式使我们的流程更加稳固。

比如说,LLM生成了如下错误格式的JSON:

    {  
        'summary': '本页面解释了该产品的安装步骤。',
        'keywords': ['安装', '配置', '产品']  
    }

尝试解析这个 JSON 时会因为用了单引号而不是双引号而出现错误。我们捕获这个错误,并提示 LLM 进行修正:

    exception_msg = "期望的属性名应被双引号包围"  
    question = f"""请修正以下JSON字符串{response},它包含错误:{exception_msg},使其成为标准的JSON格式。只需输出修正后的JSON。"""

LLM然后提供修正的JSON:

    {  
        "summary": "本页解释了产品的安装步骤。",
        "keywords": ["安装", "安装步骤", "产品"]  
    }

通过让LLM修正自身的输出,我们确保数据符合用于后续处理的正确格式。

基于LLM代理通过自我修正扩展自校正

注:LLM指“大型语言模型”。

通过使用LLM来纠正其输出的方式可以通过使用LLM代理来扩展和自动化这一过程。LLM代理可以做到以下几点:

  • 自动处理错误:自动发现错误并决定如何修正,无需具体指导。
  • 提升效率:减少手动干预和额外修复代码的需求。
  • 增强健壮性:从错误中学习以改善未来的输出。

大模型代理(LLM代理)充当管理信息流并智能处理异常的角色。它们可以被设计为来执行特定任务。

  • 解析输出并检查格式。
  • 遇到错误时,使用更具体的指令重新提示LLM。
  • 记录错误及修正,供未来参考和模型微调。

近似实现方法

而不是手动处理异常并重新请求输入,LLM Agent 可以将这逻辑封装。

    def 生成摘要_by_agent(text):  
        # 生成摘要_by_agent 函数通过代理生成给定文本的摘要和关键词
        agent = LLMAgent(model, tokenizer, device)  
        question = f"""请根据给定的段落,生成一个摘要和关键词,并确保格式符合JSON。"""  
        prompt = 创建提示(question)  # 创建提示函数用于生成指导文本
        response = agent.处理并校正(prompt)  # 处理并校正提示以获取正确格式的响应
        return response

LLMAgent 类将处理初始处理、错误检测和纠正、重新提示用户。

让我们来看看如何利用词嵌入有效地应用RAG模式,并再次利用LLM来帮助进行排名。

搜索和生成:处理用户的查询

这是一般的流程。我们拿到用户的问题,然后查找最相关的摘要内容。

    # 用法示例  
    user_question = "搞不定新玩意儿"  
    results = search_summary(user_question, sentence_model)
整理检索到的摘要

我们将检索到的摘要整理成一个列表,并将每个摘要与其对应的页码关联起来,以便参考。

    summary_list = []  
    # 对于每个结果,迭代结果列表
    for idx, result in enumerate(results):  
        # 将页码和摘要添加到summary_list中
        summary_list.append(f"{result['页码']}# {result['摘要']}")
给摘要排名

我们让语言模型根据与用户问题的相关性对检索到的摘要进行排序,并选择最相关的那个。这再次使用了大语言模型对摘要进行排序,而不是仅依靠K-最近邻、余弦相似度或其他匹配算法进行上下文嵌入(向量)对比。

    question = f"""从给定的摘要列表 {summary_list} 中,找出可能包含回答 '{user_question}' 的摘要。仅返回该摘要。"""
    log.info(question)  
提取选定摘要并得出答案

我们检索与选定摘要相关联的原始内容,并提示要求语言模型根据此上下文为用户的问题生成一个详细的解答。

    for idx, result in enumerate(results):  
        if int(page_number) == result['page_number']:  
            page = result['original_content']  
            question = f"""你能回答查询:'{user_question}'利用以下上下文吗?  
    上下文:'{page}'  
    """  
            log.info(question)  
            prompt = create_prompt(  
                question,  
                "你是一个乐于助人的助手,将通过给定的查询和上下文进行思考,分步骤回答查询,尽量根据上下文信息来回答问题。"  
            )  
            response = process_prompt(prompt, model, tokenizer, device, temperature=0.01)  # 减少自由发挥的空间  
            log.info(response)  
            print("最终答案是:")  
            print(response)  
            break
工作流程的解释
  1. 用户查询的向量化:用户的问题通过与索引时相同的SentenceTransformer模型转换成嵌入向量。
  2. 相似度查找:使用查询嵌入搜索向量数据库(LanceDB),查找最相似的摘要,并返回前三名结果
    >> 从VectorDB余弦搜索和前三个最近邻搜索结果,   
    在结果前面加上页面编号  

    07:04:00 信息:从给定的摘要列表 [[ 中,  
    '112# 无法将新发现的设备置于管理状态',   
    '113# 该段落讨论了在NSF平台上管理新发现设备的故障排除步骤,具体解决了设备放置、配置和部署方面的问题。',  
    '116# 设备配置备份故障排除']] 中,哪个摘要可能包含答案。无法管理新设备。仅返回该摘要。

3. 摘要排序:检索到的摘要会被送到语言模型中,该模型会根据与用户问题的相关性对这些摘要进行排序。

    >> 请求LLM根据上下文从最佳N项中选择

    07:04:01 信息提示:选择了简述 ''113# 该段落讨论了在NSF(网络系统和功能要求平台)上管理新识别的设备的故障排除指南,特别是处理设备放置、配置和部署过程中遇到的问题。''

4. 相关上下文检索:通过解析出页码并从LanceDB获取相关页面的内容,检索与最相关的摘要中的原始内容关联的原始内容。

07:04:01 信息时间:页码: 113  
07:04:01 信息时间:能否回答问题或提供更多详细信息的查询: '无法管理新设备' 请参考以下上下文  
context:3  
确认服务器和客户端平台是否适当匹配。等  
服务器与管理设备之间的SNMP通信失败。  
来自管理设备的SNMP陷阱到达某一个服务器,或者根本没有SNMP陷阱等  

**5\. 答案生成**:语言模型根据找到的信息,给出用户问题的详尽回答。

这里是从我用过的某个示例PDF中得到的一个输出示例。
07:04:08 信息:我将逐步提供详细信息来回答问题。

问题为:无法管理新设备。

以下是我的逐步分析:

**第一步:检查服务器和客户端平台是否适配**

上下文提到,NSP规划指南可供使用,这意味着NSP(网络服务提供商)有一个规划流程来确保服务器和客户端平台被适当配置。这表明NSP有一个过程来评估服务器和客户端平台的性能和容量,以确定它们是否适合管理新设备。

**第二步:检查管理网络和NFM-P之间的重新同步问题**

上下文还提到,管理网络和NFM-P之间的重新同步问题会导致管理新设备出现问题。这表明服务器和客户端平台间的通信可能存在问题,这可能会阻止新设备被成功管理。

**第三步:检查服务器和管理设备之间的SNMP通信失败**

上下文特别提到,服务器和管理设备之间的SNMP通信失败会导致管理新设备出现问题。这表明服务器和管理设备间的通信可能存在问题,这可能会阻止新设备被成功管理。

**第四步:检查配置请求的部署失败**

上下文还提到,配置请求的部署失败会导致管理新设备出现问题。这表明部署过程可能存在问题,这可能会阻止新设备被成功管理。

**第五步:执行以下步骤**

上下文指示用户执行以下步骤:
  1. 从XXX主菜单选择“管理”→“NE维护”→“部署”。

  2. 部署表单打开,列出了未完成的部署、部署者、标签、状态等。

    根据上下文,用户需要审查部署历史以识别可能阻止新设备部署的问题。

    答案

    根据分析,用户需要:

  3. 检查服务器和客户端平台是否适配。

  4. 检查管理网络和NFM-P之间的重新同步问题。

  5. 检查服务器和管理设备之间的SNMP通信失败。

  6. 检查配置请求的部署失败。

    通过执行这些步骤,用户应该能够识别并解决阻止管理新设备的问题。

结论部分

我们可以使用像LLAMA 3.2 1B Instruct这样的小型LLM高效地从大型文档中总结并提取关键词。这些摘要和关键词可以嵌入并存储在像LanceDB这样的数据库中,以便RAG系统不仅在生成时,还能高效地检索这些信息。

参考资料
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消