三、(基础)使用 LangChain 构建一个有上下文记忆简单聊天-2

0 阅读9分钟

前言

  1. 上一章讲解了如何使用 LangChain 构建一个有上下文记忆简单聊天程序,并且增加了提示词模版,但是我们还是没有很好的体现和管理对话历史,控制上下文长度等。
  2. 本章将继续讲解如何使用LangChain 构建一个有上下文记忆简单聊天程序并管理对话历史,控制上下文长度。

准备工作

  • 需要了解构建 LangChain 上下文记忆的基础用法,可以阅读上一篇文章
  • pip安装LangChain:pip install langchain
  • 调用大模型key,我们主要是学习为主,能白嫖自然白嫖,不需要多么快速的响应,下面是对应的申请方式,都是免费的,其他模型都是需要对应token花费钱的。注意:我们只要申请openai的key,openai更加通用
    • 腾讯元宝:hunyuan-lite,申请地址
    • 智谱AI:GLM-4-Flash-250414,申请地址,更推荐,响应速度快,更精准,对 openai 接口兼容性更好
  • 以上两个模型都是免费的,可以放心使用,注意申请 openai 访问方式的key
  • 建议使用 Jupyter Notebook,更加方便,安装教程

1. 管理对话历史

  1. 调用大模型聊天时,一个重要的概念是如何管理对话历史。如果不加以管理,消息列表将无限增长,并可能溢出大型语言模型的上下文窗口。因此,添加一个限制传入消息大小的步骤是很重要的。
  2. 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. 在链中使用

  1. 要在我们的链中使用它,我们只需在将 messages 输入传递给提示之前运行修剪器。
  2. 现在如果我们尝试询问模型我们的名字,它将不知道,因为我们修剪了聊天历史的那部分:
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 构建一个有上下文记忆简单聊天程序第二部分,管理对话历史,控制上下文长度等。下篇我们将讲解如何调用向量模型、使用向量数据库等,为建立我们自己的知识库做准备