没有人比我更懂 读文档 —— 快速上手大模型RAG与多模态

概述

此文最终将展示一个简单的用于调试报错的对话机器人。用户仅需提供文档URL和报错截图,对话大模型即可向用户告知下一步应该怎么做。(所有模型均采用在线调用的形式,参见 https://build.nvidia.com/explore/discover

在文中,我们将采用一些极为偏门的例子用于展示,以表现RAG对新知识的获取和对图像数据的认知。

实例:MCDReforged 是一个基于 Python 的 Minecraft 服务端控制工具,其提供丰富的插件系统,我们将扮演初学者向LLM求助 Prime Backup 插件的使用方法

技术方案与实施步骤

概览我们不难看出,最终的程序需要以下这些能力:

  • 访问指定URL
  • 获取URL中的内容并格式化为LLM易读的格式
  • 让LLM能够结合用户上下文和给出的资料返回内容
  • 读取并理解图片中的翻译
  • 给出图文反馈

模型选择

在实际项目中,考虑到成本和速度因素,我们会选择多个模型参与整个流程,包括但不限于:文本向量化模型:NV-Embed-QA,用于基本对话的LLM:microsoft/phi-3-small-128k-instruct,用于理解图像的大模型:microsoft/phi-3-vision-128k-instruct

数据的构建

传统训练LLM需要构建大量的优质模型,但是我们只需要让模型学习一点点🤏新知识,离微调都还有十万八千里。所以我们只需要一个纯文本文档就可以啦!

具体的形式就是直接提供一个整理好的txt文件或者一段URL,然后我们用Python抓取并解析出一段txt文件。

对于RAG部分,我们采用NV-Embed-QA模型,完成对数据的向量化,然后再发送具体请求时,首先对向量数据库检索,携带上相关详细,一起发送给LLM,这样LLM就可以理解相关资料并给出回复。

向量化
纯文本知识库
向量数据库
回答
用户提问

Agent

现有的LLM不能直接输出图像,但对于图表等数据,我们可以给出数据,让模型返回用于绘制相应图表的的代码。然后我们直接运行这段代码就可以直接从数据得到图表了。这种让模型调用外部能力的功能可以称为AI-Agent(的最最简单形式)。

开工!

环境搭建

代码中的所有模型调用均采用线上调用的模式,所以我们无需在本地配置高性能的具体服务器。

代码中所展示的所有模型均为nvidiaa NIM平台,调用该平台的模型需要先申请token。其发送报文和响应报文均与OpenAI格式相同。

此为调用模型返回结果的最基本展示。确保环境配置正确

from openai import OpenAI

client = OpenAI(
  base_url = "https://integrate.api.nvidia.com/v1",
  api_key = "nvapi-***********************************"
)

completion = client.chat.completions.create(
  model="ai-mixtral-8x7b-instruct",
  messages=[
      {"role": "user", "content": "How to export a backup with MCDReforged's Prime Backup plugin, in a sentence"}],
  temperature=0.2,
  top_p=0.7,
  max_tokens=1024,
  stream=True
)

for chunk in completion:
  if chunk.choices[0].delta.content is not None:
    print(chunk.choices[0].delta.content, end="")
# Use the "/pb export" command in-game with MCDReforged's Prime Backup plugin to export a backup of your Minecraft world to a specified directory.

# 编者:这是错的,它在瞎编

代码实现

此章节将展示满足主要功能的核心函数,其中包含RAG与多模态相关,还有调用本地代码

从URL获取数据
def html_document_loader(url: Union[str, bytes]) -> str:
    try:
        response = requests.get(url)
        html_content = response.text
    except Exception as e:
        print(f"Failed to load {url} due to exception {e}")
        return ""

    try:
        # 创建Beautiful Soup对象用来解析html
        soup = BeautifulSoup(html_content, "html.parser")

        # 删除脚本和样式标签
        for script in soup(["script", "style"]):
            script.extract()

        # 从 HTML 文档中获取纯文本
        text = soup.get_text()

        # 去除空格换行符
        text = re.sub("\s+", " ", text).strip()
        print(text)
        return text
    except Exception as e:
        print(f"Exception {e} while loading document")
        return ""
将获取到的数据向量化
def index_docs(url: Union[str, bytes], splitter, documents: List[str], dest_embed_dir) -> None:
    """
    Split the document into chunks and create embeddings for the document

    Args:
        url: Source url for the document.
        splitter: Splitter used to split the document
        documents: list of documents whose embeddings needs to be created
        dest_embed_dir: destination directory for embeddings

    Returns:
        None
    """
    # 通过NVIDIAEmbeddings工具类调用NIM中的"ai-embed-qa-4"向量化模型
    embeddings = NVIDIAEmbeddings(model="nvidia/nv-embed-v1")
    
    for document in documents:
        texts = splitter.split_text(document.page_content)

        # 根据url清洗好的文档内容构建元数据
        metadatas = [document.metadata]

        # 创建embeddings嵌入并通过FAISS进行向量存储
        if os.path.exists(dest_embed_dir):
            update = FAISS.load_local(folder_path=dest_embed_dir, embeddings=embeddings, allow_dangerous_deserialization=True)
            update.add_texts(texts, metadatas=metadatas)
            update.save_local(folder_path=dest_embed_dir)
        else:
            docsearch = FAISS.from_texts(texts, embedding=embeddings, metadatas=metadatas)
            docsearch.save_local(folder_path=dest_embed_dir)
定义数据向量化工具

我们之后只需要调用这个

def create_embeddings(embedding_path: str = "./embed"):
    embedding_path = "./embed"
    print(f"Storing embeddings to {embedding_path}")
	# 在这里传入需要解析的URL
    urls = [
        "https://tisunion.github.io/PrimeBackup/zh/cli/"
    ]

    # 使用html_document_loader对PrimeBackup的cli文档数据进行加载
    documents = []
    for url in urls:
        document = html_document_loader(url)
        documents.append(document)

    #进行chunk分词分块处理
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=0,
        length_function=len,
    )
    texts = text_splitter.create_documents(documents)
    index_docs(url, text_splitter, texts, embedding_path)
    print("Generated embedding successfully")
调用定义好的函数执行文档嵌入Embeddings的生成
create_embeddings()
embedding_model = NVIDIAEmbeddings(model="ai-embed-qa-4")
# Embed documents
embedding_path = "embed/"
docsearch = FAISS.load_local(folder_path=embedding_path, embeddings=embedding_model, allow_dangerous_deserialization=True)

llm = ChatNVIDIA(model="ai-mixtral-8x7b-instruct")
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
question_generator = LLMChain(llm=llm, prompt=CONDENSE_QUESTION_PROMPT)
chat = ChatNVIDIA(model="ai-mixtral-8x7b-instruct", temperature=0.1, max_tokens=1000, top_p=1.0)
doc_chain = load_qa_chain(chat , chain_type="stuff", prompt=QA_PROMPT)
qa = ConversationalRetrievalChain(
    retriever=docsearch.as_retriever(),
    combine_docs_chain=doc_chain,
    memory=memory,
    question_generator=question_generator,
)
query = "How to export a backup with MCDReforged's Prime Backup plugin, in a sentence"
result = qa({"question": query})
print(result.get("answer"))

# "You can export a backup using MCDReforged's Prime Backup plugin by running the command `python3 PrimeBackup.pyz export [backup_id] output` in your terminal or command prompt."
# 注:这是对的

小结

最终,模型给出的正确的答案。

相同的问题,相同的模型,从胡言乱语变成了正确回答,这就是RAG的一大优势。在面对未经过训练的数据和乱语时,RAG的优势是不可替代的。

大模型不止于此

应用场景

现在我们有了新的需求:
这是一个线上环境,服务器已经累计了一些存档的数据,现在我们想用这些数据绘制一个折线图,用来直观展示存档的体积变化。但是我不会写代码,只会打字和截图。怎么办!

没关系,拥有视觉能力的LLM可以直接接收图像并理解其中的含意。图片样例

chart_reading = ChatNVIDIA(model="ai-phi-3-vision-128k-instruct")
result = chart_reading.invoke(
    f'向我介绍一下这个图片的内容 , : <img src="data:image/png;base64,{image_b64}" />')
print(result.content)
# 这个图片显示了一个在 Minecraft 中使用的具体命令,它是 `!pb` 命令。在这个图片中,我们可以看到 `!pb` 命令的各种启动和操作方法,例如 `!pb help` 可以查看各种启动和操作的具体信息, `!pb make` 可以创建一个新的 Minecraft 项目,`!pb back` 可以返回到 Minecraft 的主屏,`!pb list` 可以查看所有的 Minecraft 项目,`!pb show` 可以查看 Minecraft 项目的详细信息,`!pb rename` 可以重命名 Minecraft 项目,`!pb delete` 可以删除 Minecraft 项目,`!pb confirm` 可以确认重命名 Minecraft 项目的操作,`!pb abort` 可以撤销重命名 Minecraft 项目的操作。此外,这个图片还显示了一些 Minecraft 项目的信息,例如 `1.13GiB` 的大小,以及它们的创建日期。

可以看到模型正确理解了图片的内容还聪明地发现这是Minecraft的聊天框

流程概述

简单梳理,我们需要的是:

接收图片
理解图片
得到规范数据
视情况让模型修改数据
生成绘制图片的代码
执行代码
展示结果
定义提示词模板和数据分析Agent
def chart_agent(image_b64, user_input, table):
    # Chart reading Runnable
    chart_reading = ChatNVIDIA(model="ai-phi-3-vision-128k-instruct")
    chart_reading_prompt = ChatPromptTemplate.from_template(
        'Generate underlying data table of the figure below, : <img src="data:image/png;base64,{image_b64}" />'
    )
    chart_chain = chart_reading_prompt | chart_reading

    # Instruct LLM Runnable
    # instruct_chat = ChatNVIDIA(model="nv-mistralai/mistral-nemo-12b-instruct")
    # instruct_chat = ChatNVIDIA(model="meta/llama-3.1-8b-instruct")
    #instruct_chat = ChatNVIDIA(model="ai-llama3-70b")
    instruct_chat = ChatNVIDIA(model="meta/llama-3.1-405b-instruct")

    instruct_prompt = ChatPromptTemplate.from_template(
        "Do NOT repeat my requirements already stated. Based on this table {table}, {input}" \
        "If has table string, start with 'TABLE', end with 'END_TABLE'.Do not edit the format of table" \
        "If has code, start with '```python' and end with '```'." \
        "Do NOT include table inside code, and vice versa."
    )
    instruct_chain = instruct_prompt | instruct_chat

    # 根据“表格”决定是否读取图表
    chart_reading_branch = RunnableBranch(
        (lambda x: x.get('table') is None, RunnableAssign({'table': chart_chain })),
        (lambda x: x.get('table') is not None, lambda x: x),
        lambda x: x
    )
    # 根据需求更新table
    update_table = RunnableBranch(
        (lambda x: 'TABLE' in x.content, save_table_to_global),
        lambda x: x
    )
    # 执行绘制图表的代码
    execute_code = RunnableBranch(
        (lambda x: '```python' in x.content, execute_and_return),
        lambda x: x
    )

    chain = (
        chart_reading_branch
        #| RunnableLambda(print_and_return)
        | instruct_chain
        #| RunnableLambda(print_and_return)
        | update_table
        | execute_code
    )

    return chain.invoke({"image_b64": image_b64, "input": user_input, "table": table}).content
理解图片

接下来我们尝试编辑提示词,让模型知道我们需要让它干什么,并定义需要的工具函数。

# 使用全局变量 table 来存储数据
table = None
URL = "C:/Users/Yang/Desktop/screen.png"
# 将要处理的图像转换成base64格式
image_b64 = image2b64(URL)
user_input = "The picture shows the data from the latest ten backups, could you please show them in a table form, showing only the storage capacity and the time, where the time should contain the time and date. Use the string,the table should be in markdown format."
chart_agent(image_b64, user_input, table)
print(table) 

这是我们给模型的图片
请添加图片描述
输出

| #  | Storage Capacity | Time                |
|----|------------------|---------------------|
| 14 | 1.13GiB          | 2024-08-18 05:36:35 |
| 13 | 1.13GiB          | 2024-08-18 03:36:35 |
| 12 | 1.13GiB          | 2024-08-18 01:36:32 |
| 11 | 1.13GiB          | 2024-08-17 23:36:29 |
| 10 | 1.13GiB          | 2024-08-17 21:36:22 |
| 9  | 1.13GiB          | 2024-08-17 19:36:21 |
| 8  | 1.13GiB          | 2024-08-17 17:36:19 |
| 7  | 1.13GiB          | 2024-08-17 15:36:10 |
| 6  | 1.13GiB          | 2024-08-17 13:36:06 |
| 5  | 1.08GiB          | 2024-08-17 11:35:59 |

可以看到模型对图片中数据的的理解完全正确。

视情况让模型修改数据

由于服务器实际上没什么活动,所以备份的大小没有显著变化。
我们可以让模型编辑这个表格,这样后面我们的图片可以直观一些。

user_input = "Modify the storage space for id 11 to 1.5GiB, id 13 to 2GiB, and id 14 to 3.1GiB"
chart_agent(image_b64, user_input, table)
print(table)

输出:

| #  | Storage Capacity | Time                |
|----|------------------|---------------------|
| 14 | 3.1GiB           | 2024-08-18 05:36:35 |
| 13 | 2GiB             | 2024-08-18 03:36:35 |
| 12 | 1.13GiB          | 2024-08-18 01:36:32 |
| 11 | 1.5GiB           | 2024-08-17 23:36:29 |
| 10 | 1.13GiB          | 2024-08-17 21:36:22 |
| 9  | 1.13GiB          | 2024-08-17 19:36:21 |
| 8  | 1.13GiB          | 2024-08-17 17:36:19 |
| 7  | 1.13GiB          | 2024-08-17 15:36:10 |
| 6  | 1.13GiB          | 2024-08-17 13:36:06 |
| 5  | 1.08GiB          | 2024-08-17 11:35:59 |
用Pyhton绘制图表
# 具体的Python程序执行过程在chart_agent中实现
user_input = "draw this table as line graph in python"
result = chart_agent(image_b64, user_input, table)
print("result: "+result)

结果:折线图输出

尾声:封装到Gradio

相关代码没有什么区别,只是注意Gradio需要接收图片作为输出
Gradio样例

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值