LLM - 使用 RAG (检索增强生成) 多路召回 实现 精准知识问答 教程

news2024/9/29 13:34:46

欢迎关注我的CSDN:https://spike.blog.csdn.net/
本文地址:https://spike.blog.csdn.net/article/details/142629289

免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。


RAG

RAG (Retrieval-Augmented Generation,检索增强生成) 的多路召回,包括向量召回和本文召回,可用于精准知识问答,减轻大模型的幻觉问题,即:

  1. 并行:同时使用文本召回和向量召回,合计获得 TopN 个样本,再使用重排序的方式,获得 TopK 个样本,作为最终的召回文本。
  2. 串行:优先使用文本召回,召回 TopN 个样本,再使用向量排序,获得 TopK 个样本,作为最终的召回样本。

启动 Ollama 服务:

# 配置 HOST
export OLLAMA_HOST="0.0.0.0:11434"
# 配置 模型路径
export OLLAMA_MODELS="ollama_models"

nohup ollama serve > nohup.ollama.out &

RAG 使用 LangChain 框架,参考:LangChain - Quickstart

LangChain 的相关依赖包,即:

pip install langchain
pip install beautifulsoup4
pip install faiss-cpu
pip install jieba

pip install langchain-community
pip install langchain-huggingface
pip install rank_bm25
pip install langchain_openai

准备编码模型 BGE,即:

# https://huggingface.co/BAAI/bge-large-zh-v1.5
modelscope download --model BAAI/bge-large-zh-v1.5 --local_dir BAAI/bge-large-zh-v1.5

导入 LangChain 的相关 Python 包:

from typing import List
import jieba

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import FAISS
from langchain.document_loaders import TextLoader
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.retrievers import BM25Retriever

使用 LangChain 读取外部文档 medical_data.txt,即:

loader = TextLoader('medical_data.txt')
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 500,
    chunk_overlap  = 0,
    length_function = len,
    separators=['\n']
)
docs = text_splitter.split_documents(documents)

其中 medical_data.txt (4999 条) 格式如下,已经组织成 questionanswer 的内容:

# ...
{'question': '曲匹地尔片的用法用量', 'answer': '注意:同种药品可由于不同的包装规格有不同的用法或用量。本文只供参考。如果不确定,请参看药品随带的说明书或向医生询问。口服。一次50~100mg(1-2片),3次/日,或遵医嘱。'}
# ...

Docs 是 list 格式,单项如下:

  • metadata 信息源
  • page_content 信息内容

即:

Document(metadata={'source': 'medical_data.txt'}, page_content="{'question': '曲匹地尔片的用法用量', 'answer': '注意:同种药品可由于不同的包装规格有不同的用法或用量。本文只供参考。如果不确定,请参看药品随带的说明书或向医生询问。口服。一次50~100mg(1-2片),3次/日,或遵医嘱。'}")

Query 是文档中已经问题,即:

query = "请问锁骨骨折多久能干活?"

使用 BM25Retriever 构建检索器,选择 TopK=10 个文档,因为是中文,预处理使用 Jieba 分词,即:

def preprocessing_func(text: str) -> List[str]:
    return list(jieba.cut(text))
retriever = BM25Retriever.from_documents(docs, preprocess_func=preprocessing_func, k=10)
bm25_res = retriever.invoke(query)

BM25 算法的核心,在于利用 词频(Term Frequency, TF) 和 逆文档频率(Inverse Document Frequency, IDF) 衡量文档与查询之间的相关性,同时引入文档长度信息,来调整相关性的计算。

构建向量 Embeddings 库:

embeddings = HuggingFaceEmbeddings(model_name='llm/BAAI/bge-large-zh-v1.5', model_kwargs = {'device': 'cuda:1'})
db = FAISS.from_documents(docs, embeddings)

其中 5000 条向量,构建 embeddings 需要 1min 15s,CPU 执行。

获取向量召回:

vector_res = db.similarity_search(query, k=10)

使用 RRF 算法,进行多路召回合并,10+10=20 选取最优的 10 个召回,即:

def rrf(vector_results: List[str], text_results: List[str], k: int=10, m: int=60):
        """
        使用 RRF 算法对两组检索结果进行重排序
        
        params:
        vector_results (list): 向量召回的结果列表, 每个元素是专利ID
        text_results (list): 文本召回的结果列表, 每个元素是专利ID
        k(int): 排序后返回前k个
        m (int): 超参数
        
        return:
        重排序后的结果列表,每个元素是(文档ID, 融合分数)
        """
        doc_scores = {}
        # 遍历两组结果,计算每个文档的融合分数
        for rank, doc_id in enumerate(vector_results):
            doc_scores[doc_id] = doc_scores.get(doc_id, 0) + 1 / (rank+m)
        for rank, doc_id in enumerate(text_results):
            doc_scores[doc_id] = doc_scores.get(doc_id, 0) + 1 / (rank+m)
        # 将结果按融合分数排序
        sorted_results = [d for d, _ in sorted(doc_scores.items(), key=lambda x: x[1], reverse=True)[:k]]
        return sorted_results

vector_results = [i.page_content for i in vector_res]
text_results = [i.page_content for i in bm25_res]
rrf_res = rrf(vector_results, text_results)

RRF (Reciprocal Rank Fusion, 倒数排名融合) 算法将多个检索结果合并一个聚合列表,通过每个列表中每个项目的排名取倒数,即 1 除以排名,将倒数排名在所有列表中相加,得到每个项目的最终得分。

提示词工程:

prompt = '''
任务目标:根据检索出的文档回答用户问题
任务要求:
    1、不得脱离检索出的文档回答问题
    2、若检索出的文档不包含用户问题的答案,请回答我不知道

用户问题:
{}

检索出的文档:
{}
'''

使用 Ollama 服务进行大模型推理,注意需要使用长 Token 模型,即:

from langchain_community.llms import Ollama
model = Ollama(model="qwen-2_5-32b-max-context:latest")
print(f"[Info] rrf_res: {len(rrf_res)}")
full_prompt = prompt.format(query, ''.join(rrf_res))
# print(f"[Info] prompt: {full_prompt}")
res = model.invoke(full_prompt)  # RAG
print(f"[Info] response: {res}")

res = model.invoke(query)  # 非 RAG
print(f"[Info] response: {res}")

RAG 的输出,与文档高度一致,即:

锁骨骨折的恢复时间一般在3个月左右。虽然骨折刚刚愈合时可以进行轻微的工作,但若涉及重体力劳动,则通常需要大约半年的时间才能重新开始,最少也需要4-5个月。过早地从事重体力工作有可能导致骨折处再次受伤。因此,在这期间避免过度负重活动是十分重要的,以确保锁骨能完全恢复并维持愈合效果。

非 RAG 的输出:

锁骨骨折的恢复时间取决于骨折的严重程度以及治疗方法。一般来说,轻微到中度的锁骨骨折可能需要大约6-8周的时间来初步愈合,在这段时间内,患者可能会被建议限制肩部和上肢的活动以促进骨折部位的稳定与修复。
但是,能否重新开始工作还依赖于具体工作的性质。如果工作不需要使用受伤的手臂或肩膀进行高强度劳动,则在几周后可能就可以慢慢恢复工作。然而,如果是需要手臂大力操作的工作,则可能需要等待3个月甚至更长时间才能安全地返回工作岗位,并且最好等到医生确认骨折完全愈合为止。
因此,在考虑重返岗位之前,应该咨询主治医师的意见,确保不会对康复过程造成负面影响或导致二次伤害。

参考:https://github.com/wyf3/llm_related

全部源码:

from typing import List

import jieba
from langchain.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.llms import Ollama
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings


class RagRetriever(object):
    """
    RAG retriever
    """
    def __init__(self):
        loader = TextLoader(db_path)
        documents = loader.load()
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=500,
            chunk_overlap=0,
            length_function=len,
            separators=['\n']
        )
        docs = text_splitter.split_documents(documents)
        def preprocessing_func(text: str) -> List[str]:
            return list(jieba.cut(text))
        self.doc_retriever = BM25Retriever.from_documents(docs, preprocess_func=preprocessing_func, k=10)
        print("[Info] init doc done!")

        embeddings = HuggingFaceEmbeddings(
            model_name=bge_path,
            model_kwargs={'device': 'cuda:1'}
        )
        self.db = FAISS.from_documents(docs, embeddings)
        print("[Info] init db done!")

        self.prompt = '''
            任务目标:根据检索出的文档回答用户问题
            任务要求:
                1、不得脱离检索出的文档回答问题
                2、若检索出的文档不包含用户问题的答案,请回答我不知道
            
            用户问题:
            {}
            
            检索出的文档:
            {}
        '''
        print("[Info] init all done!")

    @staticmethod
    def rrf(vector_results: List[str], text_results: List[str], k: int = 10, m: int = 60):
        """
        使用 RRF 算法对两组检索结果进行重排序

        params:
        vector_results (list): 向量召回的结果列表, 每个元素是专利ID
        text_results (list): 文本召回的结果列表, 每个元素是专利ID
        k(int): 排序后返回前k个
        m (int): 超参数

        return:
        重排序后的结果列表,每个元素是(文档ID, 融合分数)
        """
        doc_scores = {}
        # 遍历两组结果,计算每个文档的融合分数
        for rank, doc_id in enumerate(vector_results):
            doc_scores[doc_id] = doc_scores.get(doc_id, 0) + 1 / (rank + m)
        for rank, doc_id in enumerate(text_results):
            doc_scores[doc_id] = doc_scores.get(doc_id, 0) + 1 / (rank + m)
        # 将结果按融合分数排序
        sorted_results = [d for d, _ in sorted(doc_scores.items(), key=lambda x: x[1], reverse=True)[:k]]
        return sorted_results

    def retrieve(self, query):
        bm25_res = self.doc_retriever.invoke(query)
        vector_res = self.db.similarity_search(query, k=10)
        vector_results = [i.page_content for i in vector_res]
        text_results = [i.page_content for i in bm25_res]
        rrf_res = self.rrf(vector_results, text_results)
        model = Ollama(model="qwen-2_5-32b-max-context:latest")
        print(f"[Info] rrf_res: {len(rrf_res)}")
        full_prompt = self.prompt.format(query, ''.join(rrf_res))
        # print(f"[Info] prompt: {full_prompt}")
        res1 = model.invoke(full_prompt)
        print(f"[Info] rag response: {res1}")
        res2 = model.invoke(query)
        print(f"[Info] n-rag response: {res2}")
        return res1, res2


def main():
    query = "请问锁骨骨折多久能干活?"
    rr = RagRetriever()
    rr.retrieve(query)


if __name__ == '__main__':
    main()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2176865.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

短视频矩阵系统源码部署开发分享

在当今的软件开发领域,框架技术扮演了极为关键的角色,特别是Java开发中的Spring、Struts和Hibernate三大框架。这些框架各具特色,为开发者提供了强大的工具来构建高效、稳定的应用程序。 Spring框架,以其全面的依赖注入&#xff0…

C语言自定义类型:枚举

目录 前言枚举类型1.枚举类型的声明2.枚举类型的优点3.枚举类型的使⽤ 总结 前言 这期我们来学习C语言的最后一个自定义类型——枚举,话不多说,正文开始: 枚举类型 1.枚举类型的声明 枚举顾名思义就是⼀⼀列举。 把可能的取值⼀⼀列举。 …

Arthas stack (输出当前方法被调用的调用路径)

文章目录 二、命令列表2.3 monitor/watch/trace/stack/tt 相关2.3.2 stack (输出当前方法被调用的调用路径)举例1:输出当前方法被调用的调用路径,入口很多从哪调用的? 二、命令列表 2.3 monitor/watch/trace/stack/tt…

Pytorch 学习手册

零 相关资料 官方网址 官方网址下的API搜索网站 一 定义 深度学习框架是用于设计、训练和部署深度学习模型的软件工具包。这些框架提供了一系列预定义的组件,如神经网络层(卷积层、全连接层等)、损失函数、优化器以及数据处理工具&#xf…

高等数学(预备知识)

一、三角函数与反三角函数 注:arctanx arctan(1/x) π/2(x > 0)。 10. 辅助角公式:asin(α) bcos(α) (√(a2 b2))sin(α φ) ,其中 tan(φ) b/a 。 二、代数与方程 注:1 2 …… n n(n1)/2 。…

国庆头像免费制作赚钱项目,蹭热点自带流量日入1000+【保姆级教程】

时光真TM过得快,转眼间已到 10 月。对于上班族来说,10 月最期待的莫过于国庆节这个超长假期了。 今天要分享的就是国庆头像制作赚钱项目的实操教程和变现平台(文末获取) 第一:国庆头像制作赚钱项目是什么&#xff1f…

深N阱工艺剖面图及端口接法

最近用深N阱工艺做了一个项目,记录一下深N阱工艺的剖面图,以及各个端口的接法接法(NMOS深N阱)。 首先是CMOS工艺的深N阱技术的剖面图,图源自拉扎维课本;NMOS也有了自己的“阱”,所以它相当于是…

一文说清楚:如何学习好K8s、OpenStack、Docker、Linux?

大家好,我是你们熟悉的-CloudJourney。在这个信息爆炸的时代,我一直致力于通过博客、公众号等平台,与大家分享关于Linux、K8S、Docker、网络、服务器以及OpenStack等前沿技术的见解与心得。然而,随着交流的深入,我逐渐…

中国国画-孙溟㠭浅析碑帖《龙藏寺碑》

中国国画——孙溟㠭浅析碑帖《龙藏寺碑》 《龙藏寺碑》 《龙藏寺碑》 全称是《恒州刺史鄂国公为国劝造龙藏寺碑》,属楷书体。碑通高3.15米,宽0.90米,厚0.29米。碑文楷书30行,行50字,1500余字,碑为龟趺。…

基于开源WQ装备知识图谱的智能问答优化2

基于笔者之前写的博客基础上:https://blog.csdn.net/zhanghan11366/article/details/142139488【基于开源WQ装备知识图谱的智能问答全流程构建】进行优化。新增处理基于特定格式下的WQ文档,抽取文档的WQ属性和关系,并抽取对应WQt图片存储至mi…

支付宝开放平台-开发者社区——AI 日报「9 月 29 日」

1 支付宝进军大模型医疗应用,技术一号位:我们有4个切入点 量子位|阅读原文 面对来势汹汹的大模型应用浪潮,支付宝医疗技术一号位魏鹏这样说道。今年,蚂蚁大举进军医疗,已是再明显不过。作为蚂蚁大模型应用…

零基础快速上手JAVA代码审计

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 《Java代码审…

吐血整理:国内一站式儿童有声绘本创作平台

儿童绘本儿童故事这个领域在Stable Diffusion、Midjourney、ChatGPT产品推出后开始有大量自媒体达人纷纷发布教程,热度一直都在。但由于创作门槛较高、需要对AI类和制作类软件都需要掌握、流程制作复杂,且由于创作者提示词的影响出图效果不稳定&#xff…

LeetCode 面试经典150题 69.x的平方根

题目:给你一个非负整数 x ,计算并返回 x 的 算术平方根 。 由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。 思…

「实战应用」如何用DHTMLX Gantt集成工具栏部件更好完成项目管理?

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的所有需求,是最完善的甘特图图表库。 今天给大家分享一种方法,通过将DHTMLX Gantt集成工具栏来简化交互,为此选用了DHTMLX Suite的Toolbar&#…

互联网大厂不喜欢提拔老实人,因为老实人除了老实和干活踏实之外,在职场没其他优点...

上一篇:一线体面男的收入 最近,在互联网上有一个热门的话题,戳中了很多人的内心。 一位来自互联网的朋友发帖吐槽职场,说领导都不喜欢提拔老实人。因为老实人一般除里老实和干活踏实外,在职场基本没有其他的优点&#…

L8打卡学习笔记

🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊 SVM与集成学习 SVMSVM线性模型SVM非线性模型SVM常用参数 集成学习随机森林导入数据查看数据信息数据分析随机森林模型预测结果结果分析 个人总结 SVM 超平面&…

Windows如何远程Kylin系统

Windows如何远程Kylin系统 一. 配置 yum源 二. 清理yum缓存 三. 安装VNC并配置 nkvers yum install tigervnc tigervnc-server -ycp /lib/systemd/system/vncserver.service /etc/systemd/system/vncserver:1.service 说明:vncserver:1.service中的:1表…

HCIP和HCIE有什么区别呢?

HCIP和HCIE有什么区别呢?今天给大家介绍下两者的不同 ‌认证层次‌:HCIE屹立于华为认证体系的顶端,定位为专家级认证;而HCIP则位于中坚位置,属于中级认证。 难度与专业要求‌:通往HCIE之路布满挑战&…

refline.js, 一款开箱即用的参考线吸附插件

嗨, 大家好, 我是徐小夕. 之前一直在社区分享零代码&低代码的技术实践,也陆陆续续设计并开发了多款可视化搭建产品,比如: H5-Dooring(页面可视化搭建平台)橙子试卷(表单搭建引擎)flowmix/fl…