前言
- 上一章讲解了如何使用 LangChain 构建一个有上下文记忆简单聊天程序,并且增加了提示词模版,但是我们还是没有很好的体现和管理对话历史,控制上下文长度等。
- 本章将继续讲解如何使用LangChain 构建一个有上下文记忆简单聊天程序并管理对话历史,控制上下文长度。
准备工作
- 需要了解构建 LangChain 上下文记忆的基础用法,可以阅读上一篇文章
- pip安装LangChain:
pip install langchain
- 调用大模型key,我们主要是学习为主,能白嫖自然白嫖,不需要多么快速的响应,下面是对应的申请方式,都是免费的,其他模型都是需要对应token花费钱的。注意:我们只要申请openai的key,openai更加通用
- 以上两个模型都是免费的,可以放心使用,注意申请 openai 访问方式的key
- 建议使用 Jupyter Notebook,更加方便,安装教程
1. 管理对话历史
- 调用大模型聊天时,一个重要的概念是如何管理对话历史。如果不加以管理,消息列表将无限增长,并可能溢出大型语言模型的上下文窗口。因此,添加一个限制传入消息大小的步骤是很重要的。
- LangChain 提供了一些内置的助手来 管理消息列表。在这种情况下,我们将使用 trim_messages 助手来减少我们发送给模型的消息数量。修剪器允许我们指定希望保留的令牌数量,以及其他参数,例如是否希望始终保留系统消息以及是否允许部分消息:
from langchain_core.messages import SystemMessage, trim_messages, HumanMessage, AIMessage
from langchain_core.messages.utils import count_tokens_approximately
from langchain_openai import ChatOpenAI
# model = ChatOpenAI(model="GLM-4-Flash-250414", api_key="申请的key", base_url="https://open.bigmodel.cn/api/paas/v4/")
trimmer = trim_messages(
max_tokens=65,
strategy="last",
# 国产大模型一般不兼容 langchain 的 get_num_tokens_from_messages() token获取接口,也就是token_counter=model,
# 可以使用 count_tokens_approximately(预估token数)、len(每一条一个token)等方法
# token_counter=model,
# token_counter=len,
token_counter=count_tokens_approximately,
include_system=True,
allow_partial=False,
start_on="human",
)
# 模拟从数据库加载的对话数据
messages = [
SystemMessage(content="你是个好助手"),
HumanMessage(content="你好!我是小明"),
AIMessage(content="你好,小明!很高兴见到你。请问有什么我可以帮助你的吗?"),
HumanMessage(content="我喜欢香草冰淇淋"),
AIMessage(content="很好"),
HumanMessage(content="2 + 2等于多少"),
AIMessage(content="4"),
HumanMessage(content="谢谢"),
AIMessage(content="不客气!"),
HumanMessage(content="你开心吗?"),
AIMessage(content="是的!"),
]
print(trimmer.invoke(messages))
可以看到 count_tokens_approximately 截断后的对话数据,最开始的2条信息已经被截断
[ SystemMessage(content='你是个好助手', additional_kwargs={}, response_metadata={}), HumanMessage(content='我喜欢香草冰淇淋', additional_kwargs={}, response_metadata={}), AIMessage(content='很好', additional_kwargs={}, response_metadata={}), HumanMessage(content='2 + 2等于多少', additional_kwargs={}, response_metadata={}), AIMessage(content='4', additional_kwargs={}, response_metadata={}), HumanMessage(content='谢谢', additional_kwargs={}, response_metadata={}), AIMessage(content='不客气!', additional_kwargs={}, response_metadata={}), HumanMessage(content='玩得开心吗?', additional_kwargs={}, response_metadata={}), AIMessage(content='是的!', additional_kwargs={}, response_metadata={}) ]
2. 在链中使用
- 要在我们的链中使用它,我们只需在将 messages 输入传递给提示之前运行修剪器。
- 现在如果我们尝试询问模型我们的名字,它将不知道,因为我们修剪了聊天历史的那部分:
from langchain_core.messages import SystemMessage, trim_messages, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langchain_core.messages.utils import count_tokens_approximately
from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
model = ChatOpenAI(model="GLM-4-Flash-250414", api_key="申请的key", base_url="https://open.bigmodel.cn/api/paas/v4/")
trimmer = trim_messages(
max_tokens=65,
strategy="last",
# 国产大模型一般不兼容 langchain 的 get_num_tokens_from_messages() token获取接口,也就是token_counter=model,
# 可以使用 count_tokens_approximately(预估token数)、len(每一条一个token)等方法
# token_counter=model,
# token_counter=len,
token_counter=count_tokens_approximately,
include_system=True,
allow_partial=False,
start_on="human",
)
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你是个乐于助人的助手。尽你所能用{language}回答所有问题",
),
MessagesPlaceholder(variable_name="messages"),
]
)
# 模拟从数据库加载的对话数据
messages = [
SystemMessage(content="你是个好助手"),
HumanMessage(content="你好!我是小明"),
AIMessage(content="你好,小明!很高兴见到你。请问有什么我可以帮助你的吗?"),
HumanMessage(content="我喜欢香草冰淇淋"),
AIMessage(content="很好"),
HumanMessage(content="2 + 2等于多少"),
AIMessage(content="4"),
HumanMessage(content="谢谢"),
AIMessage(content="不客气!"),
HumanMessage(content="你开心吗?"),
AIMessage(content="是的!"),
]
chain = (
RunnablePassthrough.assign(messages=itemgetter("messages") | trimmer)
| prompt
| model
)
response = chain.invoke(
{
"messages": messages + [HumanMessage(content="我叫什么名字?")],
"language": "中文",
}
)
print(response.content)
模型的回答,可以看到它不知道我的名字
您没有告诉我您的名字,如果您愿意,可以告诉我您的名字。
我们换个在聊天历史的问题
response = chain.invoke(
{
"messages": messages + [HumanMessage(content="我问了什么数学题?")],
"language": "中文",
}
)
print(response.content)
模型的回答,可以看到它是知道我们问的什么的
你问的是“2 + 2 等于多少”。
3. 包装入上下文会话中
from langchain_core.messages import SystemMessage, trim_messages, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langchain_core.messages.utils import count_tokens_approximately
from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
model = ChatOpenAI(model="GLM-4-Flash-250414", api_key="申请的key", base_url="https://open.bigmodel.cn/api/paas/v4/")
trimmer = trim_messages(
max_tokens=65,
strategy="last",
# 国产大模型一般不兼容 langchain 的 get_num_tokens_from_messages() token获取接口,也就是token_counter=model,
# 可以使用 count_tokens_approximately(预估token数)、len(每一条一个token)等方法
# token_counter=model,
# token_counter=len,
token_counter=count_tokens_approximately,
include_system=True,
allow_partial=False,
start_on="human",
)
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你是个乐于助人的助手。尽你所能用{language}回答所有问题",
),
MessagesPlaceholder(variable_name="messages"),
]
)
messages = [
SystemMessage(content="你是个好助手"),
HumanMessage(content="你好!我是小明"),
AIMessage(content="你好,小明!很高兴见到你。请问有什么我可以帮助你的吗?"),
HumanMessage(content="我喜欢香草冰淇淋"),
AIMessage(content="很好"),
HumanMessage(content="2 + 2等于多少"),
AIMessage(content="4"),
HumanMessage(content="谢谢"),
AIMessage(content="不客气!"),
HumanMessage(content="你开心吗?"),
AIMessage(content="是的!"),
]
def add_messages(model_message):
"""
模拟自动添加消息到记录操作,加入链中
:param model_message:
:return:
"""
messages.append(model_message)
return model_message
chain = (
RunnablePassthrough.assign(messages=itemgetter("messages") | trimmer)
| prompt
| model
| add_messages
)
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
with_message_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="messages",
)
config = {"configurable": {"session_id": "aaaaaaa"}}
# 将加载的历史并入到 config 的 session_id 中
messages.append(HumanMessage(content="我叫什么名字?"))
response = with_message_history.invoke(
{
"messages": messages,
"language": "中文",
},
config=config,
)
print(response.content)
# 当问了新问题,历史消息会多出两条
messages.append(HumanMessage(content="我刚问了什么数学题?"))
response = with_message_history.invoke(
{
"messages": messages,
"language": "中文",
},
config= config
)
print(response.content)
当问了新问题,历史消息会多出两条,我们问了第一个问题 “我叫什么名字?” 会进行新的截断,当我们再次问 “我刚问了什么数学题?”时,模型将不知道我们的问题,这就体现出模型上下文是有一定长度的,超出后将不再有记忆。
对不起,我不能确定你的名字。你希望我记住你的名字吗?
很抱歉,由于我是一个人工智能助手,我无法记住你之前的提问或对话。每次与我交互时,我都会尽力根据当前的信息来回答你的问题。如果你需要帮助,请再次告诉我你的数学问题。
当我们再次查看截断的对话数据时
print(trimmer.invoke(messages))
可以看到历史对话被我们新问的问题截断
[ SystemMessage(content='你是个好助手', additional_kwargs={}, response_metadata={}), HumanMessage(content='我叫什么名字?', additional_kwargs={}, response_metadata={}), AIMessage(content='对不起,我不能确定你的名字。你希望我记住你的名字吗?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 68, 'total_tokens': 85, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'GLM-4-Flash-250414', 'system_fingerprint': None, 'id': '20250630163449f5eea44259c541d5', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--af679e07-301f-4d10-bf1b-893695fd0965-0', usage_metadata={'input_tokens': 68, 'output_tokens': 17, 'total_tokens': 85, 'input_token_details': {}, 'output_token_details': {}}), HumanMessage(content='我刚问了什么数学题?', additional_kwargs={}, response_metadata={}), AIMessage(content='很抱歉,由于我是一个人工智能助手,我无法记住你之前的提问或对话。每次与我交互时,我都会尽力根据当前的信息来回答你的问题。如果你需要帮助,请再次告诉我你的数学问题。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 45, 'prompt_tokens': 73, 'total_tokens': 118, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'GLM-4-Flash-250414', 'system_fingerprint': None, 'id': '20250630163449887e83e423c54d8f', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--a7900a73-bf27-4716-ab9b-505eb1d57188-0', usage_metadata={'input_tokens': 73, 'output_tokens': 45, 'total_tokens': 118, 'input_token_details': {}, 'output_token_details': {}}) ]
当我们将 max_tokens 设置变大后所有的记忆都会有效,我们刚才问的问题也会有正确答案
max_tokens=100,
你叫小明。
你问的是“2 + 2 等于多少”。
4. 流式处理
大型语言模型有时可能需要一段时间才能响应,因此为了改善用户体验,大多数应用程序所做的一件事是随着每个令牌的生成流回。这样用户就可以看到进度。
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
model = ChatOpenAI(model="GLM-4-Flash-250414", api_key="申请的key", base_url="https://open.bigmodel.cn/api/paas/v4/")
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你是个乐于助人的助手。尽你所能用{language}回答所有问题",
),
MessagesPlaceholder(variable_name="messages"),
]
)
chain = (
RunnablePassthrough.assign(messages=itemgetter("messages"))
| prompt
| model
)
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
with_message_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="messages",
)
config = {"configurable": {"session_id": "aaaaaaa"}}
# 流式处理
for r in with_message_history.stream(
{
"messages": [HumanMessage(content="你好!我是小明。给我讲个笑话")],
"language": "中语",
},
config=config,
):
print(r.content, end="|")
模型的回答
你好|,|小明|!|很高兴|为你|讲|笑话|。|这里|有一个|: 有一天|,|小明|在|数学|课上|睡觉|,|老师|发现了|,|就|问他|:“|小明|,|你知道|你现在|在|做什么|吗|?”| 小明|迷|迷糊|糊|地|回答|:“|我在|做梦|。”| 老师|笑着说|:“|那|你的|数学|梦|做得|怎么样|?|” 希望|这个|笑话|能|让你|开心|!|还有|其他|想|听的|笑话|吗|?||
本章讲解了如何使用 LangChain 构建一个有上下文记忆简单聊天程序第二部分,管理对话历史,控制上下文长度等。下篇我们将讲解如何调用向量模型、使用向量数据库等,为建立我们自己的知识库做准备