langchain 入门指南 - 实现一个多模态 chatbot

在前面的文章中,我们学会了如何通过 langchain 实现本地文档库的 QA,又或者通过 langchain 来实现对话式的问答系统。 在这篇文章中,我们将会学习如何通过 langchain 来实现一个多模态的 chatbot。

本文会构建一个有如下功能的 chatbot:

  • 可以生成图片
  • 可以回答用户的问题
  • 可以检索本地文档库中的信息
  • 可以从互联网进行搜索信息

什么是多模态

在前面的大部分例子中,我们跟 LLM 对话的时候都是使用了文本作为输入和输出。 但是除了文本,我们也可以让 LLM 来为我们生成图片。

多模态是指同时使用两种或两种以上的信息模式或表现形式。在人工智能和机器学习的背景下, 多模态通常指的是能够处理和融合不同类型数据的系统,这些数据可能包括文本、图像、音频、视频或其他传感器数据。

准备操作

  • 配置 OPENAI_API_KEYOPENAI_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 文档存入向量数据库中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 是一个向量数据库类,用于存储和检索向量化的文档。
  • vectordbChroma 的一个实例,用于存储和检索文档。
  • vectordb.persist() 用于将向量数据库持久化到磁盘。

通过上面的代码,我们将会把 Spotmax_intro_cn_2020.pdf 文档存入到向量数据库中。

使用 RetrievalQA 进行问答

在上一步将 pdf 文档存入向量数据库之后,我们就可以通过 Chroma 的实例来对其做语义检索了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 生成图片

这个比较简单,使用 OpenAIdall-e-2 模型即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 来从互联网搜索信息:

1
2
3
4
5
def query_web(question):
"""查询谷歌搜索结果"""
from langchain_community.utilities import GoogleSerperAPIWrapper
search = GoogleSerperAPIWrapper()
return search.run(question)

如何让 chatbot 理解不同的操作?

我们可以使用 Agent 来让 chatbot 理解不同的操作:

  1. 将上面提供的几种操作封装成不同的 Tool
  2. 创建一个 AgentExecutor,根据用户的输入,选择合适的 Tool 来执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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 交互

直接使用 AgentExecutorinvoke 方法即可:

1
agent_chain.invoke(question)

调用 invoke 之后,AgentExecutor 会根据用户的输入,选择合适的 Tool 来执行,然根据 Tool 的输出进行下一步操作(调用其他 Tool 或者生成最终答案等)。

界面展示

我们最后可以使用 gradio 来构建一个简单的 web 界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 组件,以及为用户提供了一个输入框和一个提交按钮。

inputsoutputs 参数用于指定输入和输出的组件。inputs 会作为参数传递给 respond 函数,respond 的返回值会被传递给 outputs 组件。

最终效果如下:

AgentExecutor 的处理过程如下(Thought -> Action -> Observation -> Thought -> Final Answer):

1
2
3
4
5
6
7
8
9
10
11
12
13
> 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 的推理循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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,它可以生成图片、回答用户的问题、检索本地文档库中的信息、从互联网搜索信息等。