概述
此文最终将展示一个简单的用于调试报错的对话机器人。用户仅需提供文档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需要接收图片作为输出