【浅学】RAG检索增强生成(Retrieval Augmented Generation)案例实践

本笔记所记录学习内容主要为一文搞懂大模型RAG应用(附实践案例),原文采用的是文心一言的大模型实现,本文使用的是星火大模型3.5,有任何疑问可评论留言,本人小白入门,如果有写的不对的地方感谢批评指正

实践案例

本次选用百度百科——藜麦数据模拟个人或企业私域数据,并基于langchain开发框架,实现一种简单的RAG问答应用示例。

前置准备

用讯飞大模型3.5搭建好应用,具体操作可以看我的这篇:讯飞星火大模型API,实名认证免费领一年有效期的200万Token,在控制台的左侧有星火知识库,实名认证过就可以开通免费的部分。用这个纯粹是因为免费,关于这个大模型的使用体验啥的不做评价,大家可以也选择自己喜欢的其他模型,如使用其他模型则下文代码中的API接口调用部分需要自行根据所选模型的文档说明进行调整

环境准备

注意如果你不想配本地环境可以使用在线的notebook,我图方便直接用的Kaggle的,但是Kaggle要科学上网(微软的浏览器安装扩展插件就能解决,我安的是Hoxx VPN,你们也可以自己选其他的方式)
kaggle环境下需要先执行完以下几个语句安装这些环境,确保安装完成然后重启kernel,注意重启不是刷新网页,具体操作参考【踩坑随笔】Kaggle安装langchain相关依赖报错,最后一张图

# 环境准备,安装langchain相关依赖可能会报错,需要先执行以下安装
%pip install keras-cv
%pip install keras-core
%pip install wurlitzer
%pip install sentence_transformers
%pip install google-cloud-storage
%pip install kubernetes

重启完kernel后执行以下再两句继续安装,如果是自配的环境只需要执行以下两句安装依赖即可,上面的不用管

%pip install oneapi2langchain -i https://pypi.org/simple/
%pip install chromadb

在Kaggle直接执行这两句可能会报错ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.,如果报错了参考我的这篇【踩坑随笔】Kaggle安装langchain相关依赖报错,(注意这篇踩坑安装的是文心一言的支持拓展,本篇笔记使用的是星火大模3.5,但是解决方式是一样的)

知识库文档准备

爬虫demo爬取的百度百科——藜麦数据,这个的demo有bug,百度百科网页的class_隔一段时间会变,如果执行报错了就打开百度百科——藜麦数据的网页源码找到这一段内容的类名替换即可,不会操作的话可以留言我出教程。(因为我还没学过这块所以只会替换类名这样的傻瓜式操作,如果有更好的方法感谢各位大佬的指点)

import requests
from bs4 import BeautifulSoup

url = "https://baike.baidu.com/item/%E8%97%9C%E9%BA%A6/5843874"
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")

# 获取词条名称
title = soup.find("div", class_="lemmaTitleBox_tfjaT").find("h1").text

summary_list = soup.find('div', class_='lemmaSummary_pGhoA J-summary').find_all('div',class_='para_guwWB summary_SdukW MARK_MODULE')

print('词条:'+ title)
print('简介:')

with open("./藜.txt","w") as file:    ##打开读写文件,逐行将列表读入文件内
    for summary in summary_list:
        file.write(summary.text+"\n")
        print(summary.text)  

载入本地数据

#载入本地数据
from langchain.document_loaders import TextLoader
loader = TextLoader("./藜.txt")
documents = loader.load()
documents

文档拆分

# 文档分割
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(        
    separator = "\n\n",
    chunk_size = 128,
    chunk_overlap  = 0,
    length_function = len,
)

texts = text_splitter.create_documents([documents[0].page_content],metadatas=[documents[0].metadata])
texts 

输出结果

[Document(page_content='藜麦(18张)藜蒸企胶麦是印第安人的传统主食,几乎和水稻同时被驯服有着6000多年的种植和食用历史。藜麦具润棵有相当全面营养成分,并且藜麦的口感口味都容易被人接受。在藜麦这种营养丰富的粮食滋养下南美洲的印第龙坑迁安人创造了伟大的印加文明,印加人将藜精探麦尊为粮食之母。美国遥雅嘱人早在80年代就将藜麦引入NASA,作为宇航员的日常口粮,FAO认定藜麦是唯一一种单作物即可满足人类所需的全部热乐习营养的粮食,并进乃匪敬叠行藜麦的推广和宣传。2013年是联合国钦定的国际藜麦年。以此呼吁人们注意粮食安墓担全和营养均衡。 [1]', metadata={'source': './藜.txt'}),
 Document(page_content='藜麦穗部可呈红、紫、黄,植株形状类似灰灰菜,成熟后穗部类似高粱穗。植株大小受环境及遗传因素影响较大,从0.3-3米不等,茎部质地较硬,可分枝可不分。单叶互生,叶片呈鸭掌状,叶缘分为全缘型与锯齿缘型。根系庞大但分布较浅,根上的须根多,吸水能力强。藜麦花两性,花序呈伞状、穗状、圆锥状,藜麦种子较小,呈小圆药片状,直径1.5-2毫米,千粒重1.4-3克。 [1]', metadata={'source': './藜.txt'}),
 Document(page_content='原产于南美洲安第斯山脉的哥伦比亚、厄瓜多尔、秘鲁等中高海拔山区。具有一定的耐旱、耐寒、耐盐性,生长范围约为海平面到海拔4500米左右的高原上,最适的高度为海拔3000-4000米的高原或山地地区。 [1] [4]', metadata={'source': './藜.txt'}),
 Document(page_content='繁殖地块选择:应选择地势较高、阳光充足、通风条件好及肥力较好的地块种植。藜麦不宜重茬,忌连作,应合理轮作倒茬。前茬以大豆、薯类最好,其次是玉米、高粱等。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='施肥整地:早春土壤刚解冻,趁气温尚低、土壤水分蒸发慢的时候,施足底肥,达到土肥融合,壮伐蓄水。播种前每降1次雨及时耙耱1次,做到上虚下实,干旱时只耙不耕,并进行压实处理。一般每亩(667平方米/亩,下同)施腐熟农家肥1000-2000千克、硫酸钾型复合肥20-30千克。如果土壤比较贫瘠,可适当增加复合肥的施用量。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='播种期一般选在5月中旬、气温在15-20℃时为宜。播种量为每亩0.4千克。播种深度1-2厘米。一般使用耧播,也可采用谷子精量播种机播种。行距50厘米左右,株距15-25厘米。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='苗期查苗补苗:藜麦出苗后,要及时查苗,发现漏种和缺苗断垄时,应采取补种。对少数缺苗断垄处,可在幼苗4-5叶时雨后移苗补栽。移栽后,适度浇水,确保成活率。对缺苗较多的地块,采用催芽补种,先将种子浸入水中3-4小时,捞出后用湿布盖上,放在20-25℃条件下闷种10小时以上,然后开沟补种。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='间苗定苗:藜麦出苗后应及早间苗,并注意拔除杂草。当幼苗长到10厘米、长出5-6叶时间苗,按照留大去小的原则,株距保持在15-25厘米。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='中耕除草:中耕结合间苗进行,应掌握浅锄、细锄、破碎土块,围正幼苗,做到深浅一致,草净地平,防止伤苗压苗。中耕后如遇大雨,应在雨后表土稍干时破除板结。当藜麦长到50厘米以上时,还需除草1-2次。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='在藜麦8叶龄时,将行中杂草、病株及残株拔掉,提高整齐度,增加通风透光,同时,进行根部培土,防止后期倒伏。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='要求一次性施足底肥,如果生长中后期发现有缺肥症状,可适当追肥。一般在植株长到40-50厘米时,每亩撒施三元复合肥10千克。在藜麦生育后期,叶面喷洒磷肥和微量元素肥料,可促进开花结实和子粒灌浆。藜麦主要以旱作为主,如发生严重干旱,应及时浇水。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='在成熟期,要严防麻雀为害,及时收获,防止大风导致脱粒,造成损失。 [4]\n\n叶斑病病害:主要防治叶斑病,使用12.5%的烯唑醇可湿性粉剂3000-4000倍液喷雾防治,一般防治1-2次即可收到效果。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='虫害:藜麦常见虫害有象甲虫、金针虫、蝼蛄、黄条跳甲、横纹菜蝽、萹蓄齿胫叶甲、潜叶蝇、蚜虫、夜蛾等。防治方法:可每亩用3%的辛硫磷颗粒剂2-2.5千克于耕地前均匀撒施,随耕地翻入土中。也可以每亩用40%的辛硫磷乳油250毫升,加水1-2千克,拌细土20-25千克配成毒土,撒施地面翻入土中,防治地下害虫。 [4] [7]', metadata={'source': './藜.txt'}),
 Document(page_content='藜麦藜麦的营养价值超过任何一种传统的粮食作物,藜麦是一种全谷全营养完全蛋白碱性食物,藜麦作为一种藜科植物其蛋白质含量与牛肉相当,其品质也不亚于肉源蛋白与奶源蛋白。藜麦所含氨基酸种类丰富,除了人类必须的9种必须氨基酸,还含有许多非必须氨基酸,特别是富集多数作物没有的赖氨酸,并且含有种类丰富且含量较高的矿物元素,以及多种人体正常代谢所需要的维生素,不含胆固醇与麸质,糖含量、脂肪含量与热量都属于较低水平。 [1]', metadata={'source': './藜.txt'}),
 Document(page_content='藜麦富含的维生素、多酚、类黄酮类、皂苷和植物甾醇类物质具有多种健康功效。藜麦具有高蛋白,其所含脂肪中不饱和脂肪酸占83%。藜麦还是一种低果糖低葡萄糖的食物,能在糖脂代谢过程中发挥有益功效。 [5]', metadata={'source': './藜.txt'}),
 Document(page_content='藜麦的全营养性和高膳食纤维等特性决定了它对健康的益处。研究表明,藜麦富含的维生素、多酚、类黄酮类、皂苷和植物甾醇类物质具有多种健康功效。藜麦具有高蛋白,其所含脂肪中不饱和脂肪酸占83%,还是一种低果糖低葡萄糖的食物能在糖脂代谢过程中发挥有益功效。 [1]', metadata={'source': './藜.txt'})]

向量化&数据入库

接下来对分割后的数据进行embedding,并写入数据库。这里选用m3e-base作为embedding模型,向量数据库选用Chroma

from langchain.embeddings import HuggingFaceBgeEmbeddings
from langchain.vectorstores import Chroma

# embedding model: m3e-base
model_name = "moka-ai/m3e-base"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}
embedding = HuggingFaceBgeEmbeddings(
                model_name=model_name,
                model_kwargs=model_kwargs,
                encode_kwargs=encode_kwargs,
                query_instruction="为文本生成向量表示用于文本检索"
            )

# load data to Chroma db
db = Chroma.from_documents(texts, embedding)
# similarity search
test_texts=db.similarity_search("藜在几月播种?")#这里的输出是测试用的,检索问题要换成"藜怎么防治虫害?"才能正常输出后面问答的结果
test_texts

with open("./test.txt","w") as file:    ##打开读写文件,逐行将列表读入文件内
    for text in test_texts:
        file.write(text.page_content+"\n\n")

输出结果

Batches: 100%
1/1 [00:07<00:00, 7.50s/it]
Batches: 100%
1/1 [00:00<00:00, 8.37it/s]
[Document(page_content='播种期一般选在5月中旬、气温在15-20℃时为宜。播种量为每亩0.4千克。播种深度1-2厘米。一般使用耧播,也可采用谷子精量播种机播种。行距50厘米左右,株距15-25厘米。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='繁殖地块选择:应选择地势较高、阳光充足、通风条件好及肥力较好的地块种植。藜麦不宜重茬,忌连作,应合理轮作倒茬。前茬以大豆、薯类最好,其次是玉米、高粱等。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='间苗定苗:藜麦出苗后应及早间苗,并注意拔除杂草。当幼苗长到10厘米、长出5-6叶时间苗,按照留大去小的原则,株距保持在15-25厘米。 [4]', metadata={'source': './藜.txt'}),
 Document(page_content='在藜麦8叶龄时,将行中杂草、病株及残株拔掉,提高整齐度,增加通风透光,同时,进行根部培土,防止后期倒伏。 [4]', metadata={'source': './藜.txt'})]

将检索结果上传

# -*- coding:utf-8 -*-
import hashlib
import base64
import hmac
import time
import random
from urllib.parse import urlencode
import json
import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder

class Document_Upload:
    def __init__(self, APPId, APISecret, timestamp):
        self.APPId = APPId
        self.APISecret = APISecret
        self.Timestamp = timestamp

    def get_origin_signature(self):
        m2 = hashlib.md5()
        data = bytes(self.APPId + self.Timestamp, encoding="utf-8")
        m2.update(data)
        checkSum = m2.hexdigest()
        return checkSum


    def get_signature(self):
        # 获取原始签名
        signature_origin = self.get_origin_signature()
        # 使用加密键加密文本
        signature = hmac.new(self.APISecret.encode('utf-8'), signature_origin.encode('utf-8'),
                                 digestmod=hashlib.sha1).digest()
        # base64密文编码
        signature = base64.b64encode(signature).decode(encoding='utf-8')
        return signature

    def get_header(self):
        signature = self.get_signature()
        header = {
            "appId": self.APPId,
            "timestamp": self.Timestamp,
            "signature": signature,
        }
        return header

    # 提交网络文件
    def get_body(self):
        body = {
            "file": "",
            "url": "文件网络地址 例如: https://xxx.xx.com/xxx.pdf",
            "fileName": "文件名, 例如:xxx.pdf",
            "fileType": "wiki",
            "callbackUrl": "your_callbackUrl"
        }
        form = MultipartEncoder(
            fields=body,
            boundary='------------------' + str(random.randint(1e28, 1e29 - 1))
        )
        return form

    # 提交本地文件
    def get_files_and_body(self,fileName):
        body = {
            "url": "",
            "fileName": fileName,
            "fileType": "wiki",
            "needSummary": False,
            "stepByStep": False,
            "callbackUrl": "your_callbackUrl",
        }
        files = {'file': open(fileName, 'rb')}
        return files, body



#if __name__ == '__main__':
def Xinghuo_Upload(fileName):
    # 先去 开放平台控制台(https://console.xfyun.cn)创建应用,获取下列应用信息进行替换
    APPId = "*****"
    APISecret = "*****"

    curTime = str(int(time.time()))
    request_url = "https://chatdoc.xfyun.cn/openapi/fileUpload"

    document_upload = Document_Upload(APPId, APISecret, curTime)
    headers = document_upload.get_header()

    # ******************提交网络文件
    # body = document_upload.get_body()
    # headers['Content-Type'] = body.content_type
    # response = requests.post(request_url, data=body, headers=headers)

    # ******************提交本地文件
    files, body = document_upload.get_files_and_body(fileName)
    response = requests.post(request_url, files=files, data=body, headers=headers)
    
    file_id = response.json()['data']['fileId']

    
    return file_id

    # 文档上传成功

提问

# -*- coding:utf-8 -*-
import hashlib
import base64
import hmac
import time
from urllib.parse import urlencode
import json
import websocket
import _thread as thread
import ssl

class Document_Q_And_A:
    def __init__(self, APPId, APISecret, TimeStamp, OriginUrl):
        self.appId = APPId
        self.apiSecret = APISecret
        self.timeStamp = TimeStamp
        self.originUrl = OriginUrl

    def get_origin_signature(self):
        m2 = hashlib.md5()
        data = bytes(self.appId + self.timeStamp, encoding="utf-8")
        m2.update(data)
        checkSum = m2.hexdigest()
        return checkSum



    def get_signature(self):
        # 获取原始签名
        signature_origin = self.get_origin_signature()
        # print(signature_origin)
        # 使用加密键加密文本
        signature = hmac.new(self.apiSecret.encode('utf-8'), signature_origin.encode('utf-8'),
                             digestmod=hashlib.sha1).digest()
        # base64密文编码
        signature = base64.b64encode(signature).decode(encoding='utf-8')
        # print(signature)
        return signature



    def get_header(self):
        signature = self.get_signature()
        header = {
            "Content-Type": "application/json",
            "appId": self.appId,
            "timestamp": self.timeStamp,
            "signature": signature
        }
        return header

    def get_url(self):
        signature = self.get_signature()
        header = {
            "appId": self.appId,
            "timestamp": self.timeStamp,
            "signature": signature
        }
        return self.originUrl + "?" + f'appId={self.appId}&timestamp={self.timeStamp}&signature={signature}'
        # 使用urlencode会导致签名乱码
        # return self.originUrl + "?" + urlencode(header)



    def get_body(self,content,fileIds):
        data = {
            "chatExtends": {
                "wikiPromptTpl": "请将以下内容作为已知信息:\n<wikicontent>\n请根据以上内容回答用户的问题。\n问题:<wikiquestion>\n回答:",
                "wikiFilterScore": 0.83,
                "temperature": 0.5
            },
            "fileIds": [          
                fileIds
            ],
            "messages": [
                {
                    "role": "user",
                    "content": content
                }
            ]
        }
        return data

# 收到websocket错误的处理
def on_error(ws, error):
    print("### error:", error)


# 收到websocket关闭的处理
def on_close(ws, close_status_code, close_msg):
    print("### closed ###")
    print("关闭代码:", close_status_code)
    print("关闭原因:", close_msg)


# 收到websocket连接建立的处理
def on_open(ws):
    thread.start_new_thread(run, (ws,))


def run(ws, *args):
    data = json.dumps(ws.question)
    ws.send(data)

# 收到websocket消息的处理
def on_message(ws, message):
    #print(message)
    data = json.loads(message)
    code = data['code']
    if code != 0:
        print(f'请求错误: {code}, {data}')
        ws.close()
    else:
        content = data["content"]
        status = data["status"]
        #print(f'status = {status}')
        print(content, end='')
        if status == 2:
            ws.close()


#if __name__ == '__main__':
def XinghuoQ_A(content,fileIds):
    # 先去 开放平台控制台(https://console.xfyun.cn)创建应用,获取下列应用信息进行替换
    APPId = "*****"
    APISecret = "*****"

    curTime = str(int(time.time()))
    OriginUrl = "wss://chatdoc.xfyun.cn/openapi/chat"
    document_Q_And_A = Document_Q_And_A(APPId, APISecret, curTime, OriginUrl)

    wsUrl = document_Q_And_A.get_url()
    print(wsUrl)
    headers = document_Q_And_A.get_header()
    body = document_Q_And_A.get_body(content,fileIds)

    # 禁用WebSocket库的跟踪功能,使其不再输出详细的调试信息。
    websocket.enableTrace(False)
    ws = websocket.WebSocketApp(wsUrl, on_message=on_message, on_error=on_error, on_close=on_close, on_open=on_open)
    ws.appid = APPId
    ws.question = body
    ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})


if __name__ == '__main__':
	#上传
    fileIds = Xinghuo_Upload('test.txt')
    #提问
    XinghuoQ_A("藜怎么防治虫害?",fileIds)

输出结果

### error: 'content'
### error: 'content'
藜麦的常见虫害有象甲虫、金针虫、蝼蛄、黄条跳甲、横纹菜蝽、萹蓄齿胫叶甲、潜叶蝇、蚜虫、夜蛾等。对于这些虫害的防治,可以采取以下方法:

1. 每亩用3%的辛硫磷颗粒剂2-2.5千克于耕地前均匀撒施,随耕地翻入土中。

2. 每亩用40%的辛硫磷乳油250毫升,加水1-2千克,拌细土20-25千克配成毒土,撒施地面翻入土中,这样可以有效防治地下害虫。

此外,在藜麦8叶龄时,需要将行中的杂草、病株及残株拔掉,提高整齐度和增加通风透光,同时进行根部培土,以防止后期倒伏。### closed ###
关闭代码: None
关闭原因: None

但是其实跳过文档处理的步骤直接将文档传入星火知识库Demo然后进行问答也可以实现,参考这篇【浅学】星火知识库文档检索生成问答Demo实测

  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值