0%

在前面的文章中,我们学会了如何通过 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,它可以生成图片、回答用户的问题、检索本地文档库中的信息、从互联网搜索信息等。

In-context Learning 是一种上下文学习,在 LLM 出现之后,这种学习方式变得更加实用。 因为 LLM 是训练好的模型,并不了解我们当前需要解决的问题背景是什么,但是 LLM 本身具有很强的分析推理能力, 在这种情况下,我们只需要将我们的问题和上下文传递给 LLM,它就可以帮我们进行分析推理,从而得到答案。

对于大语言模型而言,我们可以通过在输入的 Prompt 中给出关于解决问题方式的描述或者示例,而不需要利用大量数据对模型进行训练。

In-context Learning 包括了 Zero-shot LearningFew-shot Learning 两种方式。

Zero-shot Learning

一种机器学习方法,它允许模型在没有见过任何训练样本的情况下,对新类别的数据进行分类或识别。 这种方法通常依赖于模型在训练过程中学到的知识,以及对新类别的一些描述性信息,如属性或元数据。

意图识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0.3,
api_key='your key',
base_url="https://api.openai-hk.com/v1"
)

def recognize_intent(text: str):
response = llm.invoke([
SystemMessage("""Recognize the intent from the user's input and format output as JSON string.
The output JSON string includes: "intention", "parameters" """),
HumanMessage(text)
])
return response.content

print(recognize_intent("订8月19日北京到上海的飞机"))

输出:

1
2
3
4
5
6
7
8
{
"intention": "flight_booking",
"parameters": {
"departure_date": "8月19日",
"departure_city": "北京",
"destination_city": "上海"
}
}

情感分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0.3,
api_key='your key',
base_url="https://api.openai-hk.com/v1"
)

def classify_sentiment(text: str):
response = llm.invoke([
SystemMessage("""根据给定文字所表述的情感,将文字分为三类:正面,中立,负面。请将用户给定的文字进行分类,并输出分类。"""),
HumanMessage(text)
])
return response.content

print(classify_sentiment("通过前面的课程我对大模型的认识提高了很多。"))
print(classify_sentiment("今天是2024年7月17日"))
print(classify_sentiment("最近感觉很累"))

输出:

1
2
3
正面
中立
负面

Few-shot Learning

教导模型使用非常有限的训练数据来识别新的对象、类或任务。 在这里是通过在 Prompt 里加入少量示例,来实现模型学习。

示例的作用有时可以超过千言万语,Few-shot Learning 通常可以帮助我们描述更复杂的模式。

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
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0.3,
api_key='your key',
base_url="https://api.openai-hk.com/v1"
)

def book_flight(text: str):
messages = [
{"role": "system", "content": """
通过用户描述,提取航班信息并以 JSON 格式输出。
以下是一些示例:
示例1
输入:“订8月9日上海到北京的航班,上午出发
输出:"{"date":"0809","dispatch_time":"6-12","from":"上海","to":"北京"}"

示例2
输入:“订8月9日上海到北京的航班,下午出发
输出:"{"date":"0809","dispatch_time":"12-18","from":"上海","to":"北京"}"

示例3
输入:“订8月9日上海到北京的航班,晚上出发
输出:"{"date":"0809","dispatch_time":"21-18","from":"上海","to":"北京"}"

示例4
输入:“订8月9日上海到北京的航班
输出:"{"date":"0809","dispatch_time":"unknown","from":"上海","to":"北京"}"
”"""},
{"role": "user", "content": text}
]
response = llm.invoke(messages)
return response.content

print(book_flight("预定9月1日广州到北京的航班,下午出发。"))
print(book_flight("预定9月1日广州到北京的航班"))
print(book_flight("预定9月1日夜里广州到北京的航班"))
print(book_flight("预定广州到北京的航班"))

输出:

1
2
3
4
{"date":"0901","dispatch_time":"12-18","from":"广州","to":"北京"}
{"date":"0901","dispatch_time":"unknown","from":"广州","to":"北京"}
{"date":"0901","dispatch_time":"21-6","from":"广州","to":"北京"}
{"date":"unknown","dispatch_time":"unknown","from":"广州","to":"北京"}

总结

  1. 应用大语言模型要从传统机器学习思维切换为上下文学习的思路。
  2. 上下文学习包括 Zero-shot LearningFew-shot Learning,两者并无明显界限,可以根据实际需要灵活运用。

Agent 的核心思想是使用 LLM 来选择要采取的一系列动作。在前面学习过的 Chain 中,一系列操作是硬编码的, 而 Agent 使用 LLM 作为推理引擎来确定采取哪些操作以及采取的顺序。

实例

下面的例子中,我们询问了 LLM 一个问题:“今天广州适合穿什么?”。

LLM 因为无法知道当前广州的天气情况,所以它会调用一个函数 query_web 来获取广州的天气情况。

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
from langchain.agents import initialize_agent, AgentType
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_core.tools import Tool
from langchain_openai import ChatOpenAI

import os
# https://serper.dev
os.environ['SERPER_API_KEY'] = 'your serper api key'

llm = ChatOpenAI(
model_name="yi-large",
temperature=0.3,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

def query_web(question: str):
search = GoogleSerperAPIWrapper()
return search.run(question)

tools = [
Tool(
name="query_web",
description="""当你需要回答关于当前信息的问题时调用。返回的是搜索引擎的搜索结果。参数为问题""",
func=query_web
)
]
agent = initialize_agent(tools, llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

print(agent.run('今天广州适合穿什么?给我返回中文的输出'))

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> Entering new AgentExecutor chain...
Thought: To determine what to wear in Guangzhou today, I need to check the current weather conditions.
I'll use the query_web tool to find the latest weather information.

Action:
```
{
"action": "query_web",
"action_input": "广州今天天气"
}
```


Observation: 83°F
Thought:Thought: The temperature in Guangzhou is 83°F, which indicates a warm day.
I should recommend light clothing suitable for such weather.

Final Answer: 今天广州的天气适合穿轻薄的衣服,比如短袖衬衫、短裤或者连衣裙。记得涂抹防晒霜,戴上太阳帽和太阳镜来保护自己免受阳光直射。

> Finished chain.
今天广州的天气适合穿轻薄的衣服,比如短袖衬衫、短裤或者连衣裙。记得涂抹防晒霜,戴上太阳帽和太阳镜来保护自己免受阳光直射。

从上面的输出中,我们可以完整地看到 LLM 推理以及调用 tool 的过程:

  • LLM 首先思考了一下,需要查询广州的天气情况。
  • 然后 LLM 选择了调用 query_web 这个工具,传递了参数 广州今天天气
  • query_web 返回了广州今天的天气情况,LLM 根据这个情况给出了回答。
  • 最终我们得到了广州今天适合穿什么的回答。

定义多个工具函数

在上面的例子中,我们只给 agent 指定了一个工具函数,在实际使用中,我们可以定义多个工具函数,让 LLM 在推理的过程中选择合适的工具函数。

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
from langchain.agents import initialize_agent, AgentType
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_core.tools import Tool
from langchain_openai import ChatOpenAI

import os
# https://serper.dev
os.environ['SERPER_API_KEY'] = 'your serper api key'

llm = ChatOpenAI(
model_name="yi-large",
temperature=0.3,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

def query_web(question: str):
search = GoogleSerperAPIWrapper()
return search.run(question)

def translate(text: str):
yi_large_llm = ChatOpenAI(
model_name="yi-large",
temperature=0.3,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

return yi_large_llm.invoke(f'please translate "{text}" to Chinese')

tools = [
Tool(
name="query_web",
description="""当你需要回答关于当前信息的问题时调用。返回的是搜索引擎的搜索结果。参数为问题""",
func=query_web
),
Tool(
name="translate",
description="""当你需要将英文翻译成中文时调用。返回的是翻译结果。参数为英文文本""",
func=translate
)
]
agent = initialize_agent(tools, llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

print(agent.run('translate "hello" to Chinese'))

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> Entering new AgentExecutor chain...
Thought: To translate "hello" to Chinese, I should use the 'translate' tool.

Action:
```
{
"action": "translate",
"action_input": "hello"
}
```


Observation: content='"hello" 翻译成中文是 "你好"。' response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 19, 'total_tokens': 33}, 'model_name': 'yi-large', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-9ef86681-7d64-44b6-876c-951c54686ef1-0' usage_metadata={'input_tokens': 19, 'output_tokens': 14, 'total_tokens': 33}
Thought:I now know the final answer
Final Answer: "hello" 翻译成中文是 "你好"。

> Finished chain.
"hello" 翻译成中文是 "你好"。

在这个例子中,我们可以看到 agent 正确地选择了 translate 这个工具函数,并且返回了正确的翻译结果。

总结

Agent 是一个强大的工具,可以让我们使用 LLM 来去使用一系列工具函数,从而完成一系列复杂的任务。 比如 LLM 无法获取我们服务器地集群信息,但是我们可以定义一个工具函数来获取这些信息,然后让 LLM 来调用这个函数。

在我们通过 web 端的聊天界面与 AI 对话时,AI 会记住你说过的话。这样,你可以在对话中引用之前的话语,或者在之后的对话中提到之前的话语。

但是如果我们像下面这样调用 API 的时候,就会发现 AI 不会记住我们之前说过的话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from langchain_openai import ChatOpenAI

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

response = chat.invoke('今天广州天气晴朗,26~35摄氏度。')
print(response.content)

response = chat.invoke('今天广州适合穿什么?')
print(response.content)

输出:

1
2
3
4
5
这句话的意思是今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个适合户外活动的好天气,但也要注意防晒和补水,因为气温较高。

很抱歉,我无法提供实时天气信息或建议。要了解今天的广州适合穿什么,您可以查看当地的天气预报,了解当前的气温、湿度和天气状况,然后根据这些信息选择合适的衣物。
通常,广州属于亚热带季风气候,夏季炎热潮湿,冬季温和,春秋季节宜人。根据季节和天气预报,您可以选择穿短袖、长袖、薄外套或厚外套等。
别忘了查看是否需要携带雨具,因为广州的降雨量也比较丰富。

虽然我们告诉了 LLM 今天广州的天气,但是在第二次调用的时候,AI 并没有记住我们之前说过的话,所以不能依据当前的天气状况给我提供穿衣建议。

为什么 AI 不会记住我说过的话

这是因为大模型本身并不具备记忆功能。在我们每次使用大模型的 API 的时候,它都是基于训练模型时候的数据以及我们传递的信息来进行推理的。

如果让大模型记住我们说过的话,那么它需要存储的信息量会非常庞大,这样的成本是非常高昂的。 同时,如果每一次调用的时候,都在一个庞大的上下文中进行推理,那么推理的时间也会非常长,消耗的资源会非常多。

所以,大模型通常是不会记住我们说过的话的。

解决办法:我们自己记住

既然大模型记不住我们说过的话,那唯一的办法就是我们自己记住,然后下次调用的时候,将之前的话语传递给 AI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage

chat = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

messages = [
HumanMessage('今天广州天气晴朗,26~35摄氏度。'),
]

response = chat.invoke(messages)

messages.append(AIMessage(response.content))
messages.append(HumanMessage('今天广州适合穿什么?'))

print(messages)

response = chat.invoke(messages)
print(response.content)

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
HumanMessage(content='今天广州天气晴朗,26~35摄氏度。'),
AIMessage(content='这句话的意思是,今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个非常适合户外活动的天气,既不太热也不太冷。'),
HumanMessage(content='今天广州适合穿什么?')
]

根据您提供的信息,今天广州的天气晴朗,气温在26到35摄氏度之间。这个温度范围适合穿着轻薄、透气的衣物。以下是一些建议:

1. 上衣:可以选择短袖T恤、薄衬衫或棉质衣物,以保持凉爽。
2. 下装:可以穿短裤、七分裤或轻薄的牛仔裤。
3. 鞋子:舒适的凉鞋、帆布鞋或运动鞋都是不错的选择。
4. 配件:如果需要外出,可以戴上一顶遮阳帽和太阳镜,以保护皮肤和眼睛不受紫外线伤害。
5. 防晒:由于天气晴朗,紫外线可能较强,建议涂抹防晒霜以保护皮肤。

请根据您的个人舒适度和活动需求来调整着装。如果您的活动包括室内外结合,可以准备一件轻薄的外套或披肩,以

输出的第一部分是我们问第二个问题的上下文,第二部分是 AI 的回答。

在这个例子中,我们将之前的对话保存在了 messages 中,然后在下一次调用的时候,将之前的对话传递给 AI。

当然,这里只是举个例子。真实使用中,我们可能会将一大段信息交给 LLM,然后让它来帮我们进行分析推理,然后可以问它问题从而得到答案。

对话内容记忆的抽象

上一个例子中,我们是每一次请求和响应的内容都保存在了 messages 中,然后传递给 AI。 这样操作可能会比较麻烦,因为消息历史会逐渐增长,直到达到 LLM 的最大上下文长度。 这个时候,我们就需要删除一部分历史消息,从而保证 LLM 可以正常处理我们的请求。

除了最大上下文限制的原因,太长的上下文也会带来大量的 token 消耗,这样会增加我们的成本。

因此,我们非常需要定期对历史消息进行处理,删除一部分意义不大的历史消息,或者删除那些最久远的消息,只保留最近的消息。

这跟人类的记忆一样,我们对近期发生的事情记忆深刻,而对很久远的事情记忆模糊。

ConversationBufferWindowMemory

为了解决这个问题,langchain 提供了一些处理历史消息的工具。

比如适合我上面说的这个场景的 RunnableWithMessageHistory,它可以记住指定次数的消息,然后在超过指定次数的时候,删除最早的消息。

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
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import AIMessage
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferWindowMemory

llm = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

store = {} # memory is maintained outside the chain
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]

memory = ConversationBufferWindowMemory(
chat_memory=store[session_id],
k=10,
return_messages=True,
)
assert len(memory.memory_variables) == 1
key = memory.memory_variables[0]
messages = memory.load_memory_variables({})[key]
store[session_id] = InMemoryChatMessageHistory(messages=messages)
return store[session_id]

chain = RunnableWithMessageHistory(llm, get_session_history)

conf = {"configurable": {"session_id": "1"}}
response = chain.invoke('今天广州天气晴朗,26~35摄氏度。', config=conf) # type: AIMessage
print(f"response 1: {response.content}")

response = chain.invoke('今天广州适合穿什么?', config=conf) # type: AIMessage
print(f"response 2: {response.content}")

输出:

1
2
3
4
5
6
7
8
response 1: 这句话的意思是今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这个温度范围对于夏天来说是比较舒适的,适合户外活动。
response 2: 根据您提供的信息,今天广州的天气晴朗,气温在26到35摄氏度之间。这个温度范围适合穿着轻薄、透气的衣物。以下是一些建议:

1. 上衣:可以选择短袖T恤、薄衬衫或棉质衣物,避免穿得过多导致出汗后衣服湿透。
2. 下装:可以穿短裤、七分裤或轻薄的牛仔裤。如果是在室内或者空调环境中,可以考虑带一件长裤以防着凉。
3. 鞋子:舒适的凉鞋、帆布鞋或运动鞋都是不错的选择。
4. 配件:可以戴一顶遮阳帽和太阳镜来保护皮肤和眼睛免受紫外线伤害。如果需要长时间在户外,可以考虑涂抹防晒霜。
5. 携带物品:由于气温较高,建议随身携带水瓶以保持水分,同时可以携带

相比之下,现在我们并不需要每次都手动保存历史消息,而是交给 ConversationBufferWindowMemory 来处理。 这样,我们就可以更加专注于对话的内容,而不用担心历史消息的处理。

在上面这个例子中,我们指定了 k=10,也就是说,只保存最近的 20 条消息, 超过 20 条的消息之后会删除最早的消息(这是因为在底层实现中,会使用 k * 2,而不是 k)。

我们可以指定 k=1 来验证一下():

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
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import AIMessage
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferWindowMemory

llm = ChatOpenAI(
model="yi-large",
temperature=0.3,
max_tokens=200,
api_key='your key',
base_url="https://api.lingyiwanwu.com/v1",
)

store = {} # memory is maintained outside the chain
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]

memory = ConversationBufferWindowMemory(
chat_memory=store[session_id],
k=1,
return_messages=True,
)
assert len(memory.memory_variables) == 1
key = memory.memory_variables[0]
# 返回最新的 k * 2 条消息
messages = memory.load_memory_variables({})[key]
store[session_id] = InMemoryChatMessageHistory(messages=messages)
return store[session_id]

chain = RunnableWithMessageHistory(llm, get_session_history)

conf = {"configurable": {"session_id": "1"}}
response = chain.invoke('今天广州天气晴朗,26~35摄氏度。', config=conf) # type: AIMessage
print(f"response 1: {response.content}")

response = chain.invoke('这是一条无用的消息,请你忽略。', config=conf) # type: AIMessage
print(f"response 2: {response.content}")

response = chain.invoke('今天广州适合穿什么?', config=conf) # type: AIMessage
print(f"response 3: {response.content}")

输出:

1
2
3
response 1: 这句话的意思是,今天广州的天气非常好,晴朗无云,气温在26摄氏度到35摄氏度之间。这是一个非常适合户外活动的天气,既不太热也不太冷。
response 2: 好的,我会忽略这条消息。如果您有其他问题或需要帮助,请随时告诉我!
response 3: 很抱歉,我无法提供实时数据或当前的天气情况。........<一大堆>

因为我们使用了 k=1,所以当我们交谈了三次之后,第一次发送的内容就会被删除了。 所以当我们问第三个问题的时候,AI 并没有记住我们之前说过的话。

本文例子用到的一些类的介绍

InMemoryChatMessageHistory

没有特殊功能,只有一个 messages 属性,用于保存消息,是 list 类型。

ConversationBufferWindowMemory

  • 它有一个 chat_memory 属性,用于保存历史消息。
  • 当我们从它的实例中获取消息的时候(调用它的 load_memory_variables 方法的时候),它只会返回最近的 k * 2 条历史消息。

ConversationSummaryBufferMemory

除了 ConversationBufferWindowMemorylangchain 还提供了 ConversationSummaryBufferMemory,它会将历史消息进行摘要(当超过了指定长度的时候),然后保存摘要:

1
2
3
4
5
6
7
8
9
10
11
12
def prune(self) -> None:
"""Prune buffer if it exceeds max token limit"""
buffer = self.chat_memory.messages
curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
if curr_buffer_length > self.max_token_limit:
pruned_memory = []
while curr_buffer_length > self.max_token_limit:
pruned_memory.append(buffer.pop(0))
curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
self.moving_summary_buffer = self.predict_new_summary(
pruned_memory, self.moving_summary_buffer
)

prune 方法会在超过指定长度的时候,将历史消息进行摘要,然后保存摘要。

优缺点

优点:

  • 控制了缓存内容大小
  • 尽量记忆了对话的内容

缺点:

  • 在缓存内容超出限制后,为控制缓存的大小,会持续通过大模型来总结较早的内容。
  • 相应延迟增加很多
  • 成本增加

总结

在使用 LLM 的时候,我们需要注意到 LLM 并不会记住我们之前说过的话。

但是我们可以自行保存历史消息,然后在下一次调用的时候,将之前的消息传递给 AI。

为了方便处理历史消息,langchain 提供了 ConversationBufferWindowMemory 这个工具,可以帮助我们保存历史消息,并在超过指定数量的时候删除最早的消息。

有时候,我们只是想利用 LLM 来帮助我们完成一些特定类型的任务, 但每次可能我们跟 LLM 对话的内容只是有一些细微的差别,这时候我们可以使用提示词模板来帮助我们更方便地构建对话。

PromptTemplate

langchain 中,我们可以使用 PromptTemplate 来定义一个提示词模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI

prompt_template = "What is a good name for a company that makes {product}?"
prompt = PromptTemplate(template=prompt_template, input_variables=["product"])

llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
max_tokens=200,
api_key="your key",
base_url="https://api.openai-hk.com/v1",
)

chain = prompt | llm | StrOutputParser()

response = chain.invoke({"product": "shoes"})

print(response)

输出:

1
SoleStride Footwear Co.

在上面这个例子中,我们的 PromptTemplateWhat is a good name for a company that makes {product}?, 在这里,product 是一个变量,我们可以在调用 chain.invoke 的时候传入一个字典,来替换这个变量。

这样,我们就实现了一个模板化的提示词,可以方便地构建对话。

RunnableSerializable

在上面的例子中,chain = prompt | llm | StrOutputParser() 这一行代码我们可能会看得有点懵逼。

这里的 | 并不是常见的 or 运算,而是 prompt 里面的 __or__ 方法调用,等于是运算符重载了。

这一行代码的最终结果是,返回一个 RunnableSequence 的实例,我们可以调用它的 invoke 方法。 接着就执行类似管道的操作,前一个操作的输出作为后一个操作的输入。

管道抽象

如果熟悉 linux 命令行的话,我们会知道,其实 linux 中的管道操作符也是 |。与之类似的,langchain 重载 | 操作符也是为了抽象管道这种操作。 在这行代码中,prompt 的输出会作为 llm 的输入,同时,llm 的输出也会作为 StrOutputParser() 的输入。然后最终得到多个管道处理后的结果。

invoke 实现管道操作的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# invoke all steps in sequence
try:
for i, step in enumerate(self.steps):
# mark each step as a child run
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
)
if i == 0:
input = step.invoke(input, config, **kwargs)
else:
input = step.invoke(input, config)
# finish the root run
except BaseException as e:
run_manager.on_chain_error(e)
raise

批量调用

我们也可以使用 chainbatch 方法来传入多个模板参数:

1
2
3
4
response = chain.batch([
{"product": "shoes"},
{"product": "cup"},
])

这样我们就可以同时得到多个问题的答案了。