在前面的文章中,我们学会了如何通过 langchain
实现本地文档库的 QA,又或者通过 langchain
来实现对话式的问答系统。
在这篇文章中,我们将会学习如何通过 langchain
来实现一个多模态的 chatbot。
本文会构建一个有如下功能的 chatbot:
- 可以生成图片
- 可以回答用户的问题
- 可以检索本地文档库中的信息
- 可以从互联网进行搜索信息
什么是多模态
在前面的大部分例子中,我们跟 LLM 对话的时候都是使用了文本作为输入和输出。
但是除了文本,我们也可以让 LLM 来为我们生成图片。
多模态是指同时使用两种或两种以上的信息模式或表现形式。在人工智能和机器学习的背景下,
多模态通常指的是能够处理和融合不同类型数据的系统,这些数据可能包括文本、图像、音频、视频或其他传感器数据。
准备操作
- 配置
OPENAI_API_KEY
和OPENAI_BASE_URL
环墋变量。 - 配置
SERPER_API_KEY
环境变量,可以从https://serper.dev
获取。
如和实现对本地文档的 QA
在 langchain
中,RetrievalQA
是一个结合了检索(Retrieval
)和问答(QA
)的组件。
它允许你构建一个系统,该系统能够根据用户的提问,从提供的文档或知识库中检索相关信息,并回答用户的问题。
RetrievalQA
的工作流程如下:
- 检索(
Retrieval
):当用户提出一个问题时,RetrievalQA
会使用一个检索机制(本文会使用向量数据库做语义检索) - 阅读理解:一旦检索到相关的信息,
RetrievalQA
会使用一个阅读理解模型来理解这些信息,并回答用户的问题。 - 问答:最后,
RetrievalQA
会使用一个问答模型(ChatModel
)来生成最终的回答。
RetrievalQA
的优势在于它能够处理大量复杂的信息,并提供精确的答案。它特别适合那些需要从大量文档中检索信息的场景,例如法律文件、医学文献、技术手册等。
直接跟 LLM 对话的时候,一般都会有一个上下文大小限制的问题,太大的文档无法全部放入到上下文中。
但是可以先分片存入向量数据库中,在跟 LLM 对话之前,再从向量数据库中检索出相关的文档。最终发给 LLM 的数据只有相关的文档,这样就能够更好地回答用户的问题。
将 pdf 存入向量数据库
我们可以使用自己的 pdf 文档。
在这个例子中,我们将会使用 langchain
来将一个 pdf 文档存入向量数据库中:
from langchain_community.document_loaders import PyPDFLoader # 加载 pdf 文档 loader = PyPDFLoader("Spotmax_intro_cn_2020.pdf") docs = loader.load() # 文档分片 from langchain.text_splitter import RecursiveCharacterTextSplitter text_spliter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=10) splits = text_spliter.split_documents(docs) persist_directory = 'data/' from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings embedding = OpenAIEmbeddings() # 创建向量数据库 vectordb = Chroma.from_documents( documents=splits, embedding=embedding, collection_name="spotmax", persist_directory=persist_directory, ) # 持久化向量数据库 vectordb.persist()
说明:
PyPDFLoader
是一个用于加载 pdf 文档的类。RecursiveCharacterTextSplitter
是一个用于将文档分片的类。Chroma
是一个向量数据库类,用于存储和检索向量化的文档。vectordb
是Chroma
的一个实例,用于存储和检索文档。vectordb.persist()
用于将向量数据库持久化到磁盘。
通过上面的代码,我们将会把 Spotmax_intro_cn_2020.pdf
文档存入到向量数据库中。
使用 RetrievalQA
进行问答
在上一步将 pdf 文档存入向量数据库之后,我们就可以通过 Chroma
的实例来对其做语义检索了。
def qa(question): from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings embedding = OpenAIEmbeddings() vectordb = Chroma(persist_directory='data/', embedding_function=embedding, collection_name='spotmax') from langchain.chains.retrieval_qa.base import RetrievalQA from langchain_openai import ChatOpenAI llm = ChatOpenAI( model_name="gpt-3.5-turbo", temperature=0, max_tokens=200, ) retriever = vectordb.as_retriever( search_type="mmr", search_kwargs={"k": 3} ) qa0 = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=retriever, return_source_documents=False, verbose=True) result = qa0({"query": question}) return result['result'] print(qa("Spotmax 是什么?"))
说明:
vectordb
是从现有的Chroma
向量数据库中加载的。llm
是最终回答用户问题的大模型。retriever
是用于检索文档的检索器,用户的问题会先通过检索器检索到相关的文档。RetrievalQA.from_chain_type
创建一个RetrievalQA
实例,用于回答用户的问题。qa0({"query": question})
用户的问题会先通过retriever
检索到相关的文档,然后再交给LLM
,通过llm
来回答用户的问题。
让 LLM 生成图片
这个比较简单,使用 OpenAI
的 dall-e-2
模型即可:
def create_image(prompt): from openai import OpenAI client = OpenAI() response = client.images.generate( model='dall-e-2', prompt=prompt, size='256x256', quality='standard', n=1 ) u = response.data[0].url markdown_url = f"![image]({u})" return markdown_url
这个例子中,我们会根据用户的 prompt
生成一张 256x256
像素的图片,并且返回一个 markdown 链接形式的图片地址。
从互联网搜索信息
我们可以使用 GoogleSerperAPIWrapper
来从互联网搜索信息:
def query_web(question): """查询谷歌搜索结果""" from langchain_community.utilities import GoogleSerperAPIWrapper search = GoogleSerperAPIWrapper() return search.run(question)
如何让 chatbot 理解不同的操作?
我们可以使用 Agent
来让 chatbot 理解不同的操作:
- 将上面提供的几种操作封装成不同的
Tool
。 - 创建一个
AgentExecutor
,根据用户的输入,选择合适的Tool
来执行。
from langchain_openai import ChatOpenAI llm = ChatOpenAI( model_name="gpt-4", temperature=0.7, max_tokens=1000, ) from langchain.agents import Tool tools = [ Tool( name="Get current info", func=query_web, description="""only invoke it when you need to answer question about realtime info. And the input should be a search query.""" ), Tool( name="query spotmax info", func=qa, description="""only invoke it when you need to get the info about spotmax/maxgroup/maxarch/maxchaos. And the input should be the question.""" ), Tool( name="create an image", func=create_image, description="""invoke it when you need to create an image. And the input should be the description of the image.""" ) ] from langchain.memory import ConversationBufferWindowMemory from langchain.agents import ZeroShotAgent, AgentExecutor from langchain.chains.llm import LLMChain prefix = """Have a conversation with a human, answering the following questions as best you can. You have access to the following tools:""" suffix = """Begin!" {chat_history} Question: {input} {agent_scratchpad}""" prompt = ZeroShotAgent.create_prompt( tools, prefix=prefix, suffix=suffix, input_variables=["input", "chat_history", "agent_scratchpad"], ) memory = ConversationBufferWindowMemory(k=10, memory_key="chat_history") llm_chain = LLMChain(llm=llm, prompt=prompt) agent = ZeroShotAgent(llm_chain=llm_chain, tools=tools) agent_chain = AgentExecutor.from_agent_and_tools( agent=agent, tools=tools, verbose=True, memory=memory, handle_parsing_errors=True
说明:
- 将前文提到的几种能力,封装为
AgentExecutor
可以使用的Tool
- 使用
llm
以及tools
作为参数创建一个AgentExecutor
AgentExecutor
在 LangChain 中,AgentExecutor
是一个组件,它负责执行一个代理(Agent
)的推理循环。Agent
是一个更高级的组件,它可以根据输入动态选择和执行工具(Tools
)。
Agent
通常用于构建更复杂的应用,其中 AI 模型需要根据上下文做出决策,选择合适的行动方案,并执行这些方案以达到某个目标。例如,一个 Agent
可能需要决定何时查询数据库,何时生成文本,或者何时调用外部 API。
AgentExecutor
的作用是作为一个执行环境,它接收用户的输入,然后根据 Agent
的策略或算法来指导 Agent
如何使用可用的工具来处理这个输入。代理会生成一个或多个动作(Actions
),每个动作都对应一个工具的调用。
AgentExecutor
会执行这些动作,并可能根据动作的结果更新 Agent
的状态,然后返回最终的输出给用户。
如何跟 AgentExecutor 交互
直接使用 AgentExecutor
的 invoke
方法即可:
agent_chain.invoke(question)
调用 invoke
之后,AgentExecutor
会根据用户的输入,选择合适的 Tool
来执行,然根据 Tool
的输出进行下一步操作(调用其他 Tool
或者生成最终答案等)。
界面展示
我们最后可以使用 gradio
来构建一个简单的 web
界面:
import gradio as gr with gr.Blocks() as demo: chatbot = gr.Chatbot(height=500) # 对话框 msg = gr.Textbox(label="Prompt") # 输入框 btn = gr.Button("Submit") # 按钮 clear = gr.ClearButton(components=[msg, chatbot], value="Clear console") # 清除按钮 btn.click(respond, inputs=[msg, chatbot], outputs=[msg, chatbot]) msg.submit(respond, inputs=[msg, chatbot], outputs=[msg, chatbot]) gr.close_all() demo.launch()
这个例子中,我们添加了一个 chatbot
组件,以及为用户提供了一个输入框和一个提交按钮。
inputs
和outputs
参数用于指定输入和输出的组件。inputs
会作为参数传递给respond
函数,respond
的返回值会被传递给outputs
组件。
最终效果如下:
AgentExecutor
的处理过程如下(Thought -> Action -> Observation -> Thought -> Final Answer
):
> Entering new AgentExecutor chain... Thought: The question is asking for the current weather in Guangzhou and a male outfit recommendation. I can use the 'Get current info' tool to find the weather, and the 'create an image' tool to generate the outfit image. Action: Get current info Action Input: Guangzhou weather today Observation: 94°F Thought:The weather in Guangzhou is quite hot today. Now I need to think of an outfit that would be suitable for such warm weather. Action: create an image Action Input: A light summer outfit for men suitable for 94°F weather Observation: ![image](https://oaidalleapiprodscus.blob.core.windows.net/private/org-GFz12lkhEotcvDvFYzePwrtK/user-1Ci7Ci1YNFjtlIO7AIY9aNux/img-zRsrd0cFFfxYAwW1oKZV9643.png?st=2024-07-24T05%3A29%3A33Z&se=2024-07-24T07%3A29%3A33Z&sp=r&sv=2023-11-03&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-07-23T23%3A15%3A19Z&ske=2024-07-24T23%3A15%3A19Z&sks=b&skv=2023-11-03&sig=g9L0m2GHy%2BHtC48NPVDBjZWVGfrXGQzRam6XayUZvJ0%3D) Thought:I now have the final answer. Final Answer: 广州今天的天气很热,达到了94°F。我为你创建了一张适合这种天气的男士夏季轻便穿搭图。请参考图片中的服装搭配。![image](https://oaidalleapiprodscus.blob.core.windows.net/private/org-GFz12lkhEotcvDvFYzePwrtK/user-1Ci7Ci1YNFjtlIO7AIY9aNux/img-zRsrd0cFFfxYAwW1oKZV9643.png?st=2024-07-24T05%3A29%3A33Z&se=2024-07-24T07%3A29%3A33Z&sp=r&sv=2023-11-03&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-07-23T23%3A15%3A19Z&ske=2024-07-24T23%3A15%3A19Z&sks=b&skv=2023-11-03&sig=g9L0m2GHy%2BHtC48NPVDBjZWVGfrXGQzRam6XayUZvJ0%3D) > Finished chain.
我们可以看到在我提这个问题的时候,它做了如下操作:
- 思考,然后发现需要获取今天广州的天气,这是 LLM 不懂的,所以使用了
Get current info
工具。 - 获取到了天气信息之后,思考,然后发现需要生成一张图片,而我们有一个
create an image
工具,因此使用了这个工具来生成图片 - 最终返回了今天广州的天气状况以及一张图片。
当然,我们也可以问它关于本地知识库的问题,比如 “什么是 spotmax?”(根据你自己的 pdf 提问,这里只是一个示例)
完整代码
最终完整的代码如下:
qa
函数用于回答用户关于本地知识库的问题create_image
函数用于生成图片query_web
函数用于从互联网搜索信息respond
函数用于处理chatbot
的对话响应agent_chain
是一个AgentExecutor
实例,用于执行Agent
的推理循环
import gradio as gr def qa(question): from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings embedding = OpenAIEmbeddings() vectordb = Chroma(persist_directory='data1/', embedding_function=embedding, collection_name='spotmax') from langchain.chains.retrieval_qa.base import RetrievalQA from langchain_openai import ChatOpenAI llm = ChatOpenAI( model_name="gpt-3.5-turbo", temperature=0, max_tokens=200, ) retriever = vectordb.as_retriever( search_type="mmr", search_kwargs={"k": 3} ) qa0 = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=retriever, return_source_documents=False, verbose=True) result = qa0({"query": question}) return result['result'] def create_image(prompt): from openai import OpenAI client = OpenAI() response = client.images.generate( model='dall-e-2', prompt=prompt, size='256x256', quality='standard', n=1 ) u = response.data[0].url markdown_url = f"![image]({u})" return markdown_url def query_web(question): """查询谷歌搜索结果""" from langchain_community.utilities import GoogleSerperAPIWrapper search = GoogleSerperAPIWrapper() return search.run(question) def respond(message, chat_history): """对话函数""" bot_message = get_response(message) chat_history.append((message, bot_message)) return "", chat_history from langchain_openai import ChatOpenAI llm = ChatOpenAI( model_name="gpt-4", temperature=0.7, max_tokens=1000, ) from langchain.agents import Tool tools = [ Tool( name="Get current info", func=query_web, description="""only invoke it when you need to answer question about realtime info. And the input should be a search query.""" ), Tool( name="query spotmax info", func=qa, description="""only invoke it when you need to get the info about spotmax/maxgroup/maxarch/maxchaos. And the input should be the question.""" ), Tool( name="create an image", func=create_image, description="""invoke it when you need to create an image. And the input should be the description of the image.""" ) ] from langchain.memory import ConversationBufferWindowMemory from langchain.agents import ZeroShotAgent, AgentExecutor from langchain.chains.llm import LLMChain prefix = """Have a conversation with a human, answering the following questions as best you can. You have access to the following tools:""" suffix = """Begin!" {chat_history} Question: {input} {agent_scratchpad}""" prompt = ZeroShotAgent.create_prompt( tools, prefix=prefix, suffix=suffix, input_variables=["input", "chat_history", "agent_scratchpad"], ) memory = ConversationBufferWindowMemory(k=10, memory_key="chat_history") llm_chain = LLMChain(llm=llm, prompt=prompt) agent = ZeroShotAgent(llm_chain=llm_chain, tools=tools, verbose=True, handle_parsing_errors=True) agent_chain = AgentExecutor.from_agent_and_tools( agent=agent, tools=tools, verbose=True, memory=memory, handle_parsing_errors=True ) def get_response(message): res = agent_chain.invoke(message) return res['output'] with gr.Blocks() as demo: chatbot = gr.Chatbot(height=500) # 对话框 msg = gr.Textbox(label="Prompt") # 输入框 btn = gr.Button("Submit") # 按钮 clear = gr.ClearButton(components=[msg, chatbot], value="Clear console") # 清除按钮 btn.click(respond, inputs=[msg, chatbot], outputs=[msg, chatbot]) msg.submit(respond, inputs=[msg, chatbot], outputs=[msg, chatbot]) gr.close_all() demo.launch()
总结
虽然 OpenAI 提供了 function calling
的特性,但是直接使用起来还是比较麻烦,通过 AgentExecutor
结合 tools
的方式,可以更好地组织和管理 chatbot 的能力。
在这篇文章中,我们学习了如何通过 langchain
来实现一个多模态的 chatbot,它可以生成图片、回答用户的问题、检索本地文档库中的信息、从互联网搜索信息等。