目标

我们将建立一个带有对话历史的聊天机器人。

以下是我们将要使用的一些组件:

  1. 聊天模型(Chat Models):聊天机器人的界面是基于消息传递而不是原始文本,因此它更适合使用聊天模型而不是文本型的 LLM。
  2. 提示模板(Prompt Templates):这些模板简化了组装提示的过程,可以结合默认消息、用户输入、聊天历史以及(可选的)额外检索到的上下文。
  3. 聊天历史(Chat History):聊天历史允许聊天机器人“记住”过去的交互,并在回应后续问题时考虑这些历史。
  4. 使用 LangSmith 调试和跟踪你的应用程序(非必须)。

聊天历史

在[Vol][2]中,我们使用了一个 messages[]来保存和传递消息,它是这个样子

1
2
3
4
5
[
SystemMessage(content="Translate the following from English into Italian"),
HumanMessage(content="hi!"),
...
]

其实,随着对话的增长,我们可以把AI消息和我们自己的消息一起添加到这个列表里

1
2
3
4
5
6
7
8
9
10
11
[
SystemMessage(content="Translate the following from English into Italian"),
HumanMessage(content="hi!"),
AIMessage(content="...")
HumanMessage(content="..."),
AIMessage(content="...")
HumanMessage(content="..."),
AIMessage(content="...")
HumanMessage(content="..."),
...
]

上面[]的内容,就是对话历史…

我们先不看官方的例子,简单的事非得搞复杂…

我们先自己维护一下对话历史,感受一下最底层的对话历史逻辑~

这里我们用通义千问做演示

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
# 使用通义千问
# DASHSCOPE_API_KEY 需要在阿里云里面申请相关Key
api_key = DASHSCOPE_API_KEY
qwen_chat = ChatOpenAI(
model_name="qwen-max",
openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
openai_api_key=api_key,
)

# 输出解析
parser = StrOutputParser()

# 提示词
system_template = "你是一个AI助手。"

# 生成系统提示词 obj
sys_msg = SystemMessage(content=system_template)

# 对话历史,手工添加
messages = []

# 先添加系统消息
messages.append(sys_msg)

res = qwen_chat.invoke("hi,你好,我是粥~")
print(res)

# 添加我们的输入
messages.append(HumanMessage(content="hi,你好,我是粥~"))

# 添加Ai的回答
# 注意: 大模型输出是一个 AIMessage, 所以我们可以这样添加
messages.append(res)

# 我们验证一下,对话历史是否 有效果, 用模型直接根据历史回答
messages.append(HumanMessage(content="我是谁?"))
res = qwen_chat.invoke(messages)
print(res.content)
pass

在res = qwen_chat.invoke(messages) 调用前,对话历史是这样的

1
2
3
4
5
6
[
SystemMessage(content='你是一个AI助手。'),
HumanMessage(content='hi,你好,我是粥~'),
AIMessage(content='你好,粥~!很高兴能与你交流。有什么可以帮助你的吗?', response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 15, 'total_tokens': 31}, 'model_name': 'qwen-max', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-ae2d37f0-287f-4c37-a0cd-bc0a8cc2114f-0'),
HumanMessage(content='我是谁?')
]

AI最后的回答

可以看到,AI还记得我是粥,这个效果是因为我们在messages里有对话历史

1
content='你是卷儿,刚刚你自己介绍过了哦。如果有什么想聊的或者需要帮助的,尽管告诉我!' response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 54, 'total_tokens': 77}, 'model_name': 'qwen-max', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-979eb2bb-fa93-4d18-bda0-288d6d0a11b5-0'

Message History

这是langchain 实现的一个对话历史的方法,不是那么重要,但是其中提到的 通过config结构获取对话历史,这种

模式比较重要~

1
config = {"configurable": {"session_id": "abc2"}}

在代码中讲解

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
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 通过ID 返回一个ChatMessageHistory
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]

# 使用通义千问
model = qwen_chat

# langchain 的RunnableWithMessageHistory
# 需要传递一个runnable,一个 获得历史的方法 :get_session_history
with_message_history = RunnableWithMessageHistory(model, get_session_history)

# session_id 是一个 TAG, 一个记录,表示不同的配置,比如你有很多对话,用不同的tag 表示不同的对话历史
config = {"configurable": {"session_id": "abc2"}}

# 自动保存历史,不用我们手动保存了
response = with_message_history.invoke(
[HumanMessage(content="Hi! I'm Bob")],
config=config,
)

print(response.content)
pass

# 同样的config, 同样的对话历史,AI记得我们是谁
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)

print(response.content)

# 使用不同TAG,abc3, 即不同的历史,AI不记得我们是谁
config = {"configurable": {"session_id": "abc3"}}

response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)

print(response.content)

# 用之间的TAG,就可以识别了~
config = {"configurable": {"session_id": "abc2"}}

response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)

print(response.content)

Streaming

langchain提供了很多方法, 流式输出是其中的一个例子,还包括 批处理(batch),异步调用 等等。

一个chain 定义之后,我们想用哪种方法调用都可以,只需要换一个方法即可~

1
2
3
4
5
6
# 流式输出

for r in with_message_history.stream([HumanMessage(content="hi! tell me a joke")],
config=config,
):
print(r.content, end="|")

输出

1
2
3
4
5
|Sure|,| here|'s a joke for you|, Bob: <--AI 还记得我们

Why don't scientists trust| atoms?

Because they make up everything!||

参考代码

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
import os

from langchain_community.chat_models import QianfanChatEndpoint
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import AzureChatOpenAI, ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.output_parsers import StrOutputParser

from llm_cfg import AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, DEPLOYMENT_NAME_GPT3P5, MY_QIANFAN_AK, MY_QIANFAN_SK, \
DASHSCOPE_API_KEY

if __name__ == '__main__':

# 使用 通义千问
api_key = DASHSCOPE_API_KEY
qwen_chat = ChatOpenAI(
model_name="qwen-max",
openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
openai_api_key=api_key,
)

# 使用 Azure OpenAi
# os.environ["AZURE_OPENAI_API_KEY"] =AZURE_OPENAI_API_KEY
# os.environ["AZURE_OPENAI_ENDPOINT"] =AZURE_OPENAI_ENDPOINT
# gpt3p5_model = AzureChatOpenAI(
# openai_api_version="2024-02-15-preview",
# azure_deployment=DEPLOYMENT_NAME_GPT3P5,
# )

parser = StrOutputParser()

# 提示词
system_template = "你是一个AI助手。"

# 生成系统提示词 obj
sys_msg = SystemMessage(content=system_template)

# 对话历史,手工添加
messages = []

# 先添加系统消息
messages.append(sys_msg)

# res = qwen_chat.invoke("hi,你好,我是卷儿")
# print(res)

# 添加我们的输入
messages.append(HumanMessage(content="hi,你好,我是卷儿"))

# 添加Ai的回答
# 注意: 大模型输出是一个 AIMessage, 所以我们可以这样添加
# messages.append(res)

# 我们验证一下,对话历史是否 有效果, 用模型直接根据历史回答
messages.append(HumanMessage(content="我是谁?"))
# res = qwen_chat.invoke(messages)
# print(res.content)
pass

store = {}

#Message History
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]

model = qwen_chat
with_message_history = RunnableWithMessageHistory(model, get_session_history)

# session_id 是一个 TAG, 一个记录,表示不同的配置,比如你有很多对话,用不同的tag 表示不同的对话历史
config = {"configurable": {"session_id": "abc2"}}
response = with_message_history.invoke(
[HumanMessage(content="Hi! I'm Bob")],
config=config,
)

print(response.content)
pass

# 同样的config, 同样的对话历史
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)

print(response.content)

# 使用不同TAG,abc3, 即不同的历史
config = {"configurable": {"session_id": "abc3"}}

response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)

# 不认识之前提到的名字
print(response.content)

# 用之间的TAG,就可以识别了~
config = {"configurable": {"session_id": "abc2"}}

response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)

print(response.content)

for r in with_message_history.stream([HumanMessage(content="hi! tell me a joke")],
config=config,
):
print(r.content, end="|")