前言

提示:承上启下,系列文章,通过前言会议一下上篇章内容,引入本文内容:

05.AI应用搭建–langchain输出解析器后,基本介绍完一个简单AI应用的基本流程。但是,大家很容易发现前面的内容,更多是一问一答形式,无法实现追问,那么对于这种场景应该如何解决呢?

立刻有人想到:既然可以拼接强化AI和user的提示词内容,那我直接将AI的输出和用户的历史提问记录下来,然后拼接进每次的提问不就可以了,那么恭喜你,已经掌握了多轮对话的实现基础


一、如何实现多轮对话存储

通过一段代码简单理解下基础原理:

# 构造提示词。其中MessagesPlaceholder是消息占位符,它的作用是可以动态插入历史对话记录
'''


MessagesPlaceholder的消息结构如下:
[
    ("human", "你好,我叫小明"),	
    ("system", "你好小明!有什么我能帮助你的吗?。"),
    ("human", "我最喜欢红色,帮我选一种适合圣诞节的礼物"), 
    ("system", "圣诞帽"),
]
'''
from langchain_community .chat_models import MessagesPlaceholder
#1、构造提示词模板,可以看出和之前讲的没什么不同,只是加了个MessagesPlaceholder,按照其数据结构,很容易理解,就是将历史对话拼接进了提示词中。
# variable_name="conversation",这就是声明历史对话通过哪个参数传入提示词

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个友好的助手,根据对话历史回答问题。"),
    MessagesPlaceholder(variable_name="conversation",optional = True ),  # 核心:动态插入对话历史,其中optional 默认 False,设为 True 则占位符无内容时不会报错
    ("human", "{input}"),  # 当前用户输入
])

# 2、 模拟对话历史(可来自用户与AI的交互记录)
conversation_history = [
    HumanMessage(content="你好,我叫小明"),
    AIMessage(content="你好小明!有什么我能帮助你的吗?"),
    HumanMessage(content="我忘记我叫什么了"),
]

# 3. 构造链,调用模型。可以看到传入历史记录和原来的填入用户输入没什么不同
chain = prompt | llm
response = chain.invoke({
    "conversation": conversation_history,  # 填充占位符
    "input": "提醒我一下我的名字"  # 当前输入
})

#从输出结果可以看出来,大模型从历史对话中找到了自己不知道的内容,并回答了用户的问题
print(response.content)
# 输出示例:你的名字是小明呀~

从这个例子可以看出:

1、想要传入历史对话,只需要在通过MessagesPlaceholder在提示词内拼接历史对话记录即可
2、历史对话记录存储时,需要记录对话的角色信息如system、human等
3、大模型会从历史对话记录中获取信息回答用户提问

但是,上面的例子有个大问题,对话记录存在内存里的,服务重启对话记录就清空了,若要实现大型、长期的对话应用,这肯定不行。有无办法想数据库一样,将数据存在本地/服务器上,要用的时候去获取出来? 有的,可以用FileChatMessageHistory、RedisChatMessageHistory将记录存储到文件或Redis内(本文只讲FileChatMessageHistory,RedisChatMessageHistory的原理差不多,只是调用Redis的方法存放在不同位置而已,可自行补充)

二、FileChatMessageHistory的使用方法

若相同代码import时报错,可能是langchain的版本不一致导致的,我使用的版本如下

pip install -i https://mirrors.aliyun.com/pypi/simple/ langchain==0.2.10 langchain-openai langchain-community python-dotenv

1.代码(为了演示多轮对话,使用了函数)

import os
from dotenv import load_dotenv  # 补充:加载环境变量
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_community.chat_message_histories import FileChatMessageHistory
from langchain.memory import ConversationBufferMemory

# 加载.env文件(国内用户必加,否则API Key获取不到)
load_dotenv()

# 大模型配置
MODULE_API_KEY = os.getenv("DASHSCOPE_API_KEY")
MODULE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
MODULE_NAME = "qwen-plus"

# ====================== 1. 初始化Qwen-plus模型 ======================
def init_qwen_plus_cn():

    return ChatOpenAI(
        api_key=MODULE_API_KEY,
        model=MODULE_NAME,
        base_url=MODULE_BASE_URL,
        temperature=0.6,
        max_tokens=2048,
        request_timeout=30,  # 补充:国内网络超时配置(避免卡死)
        max_retries=2  # 补充:失败重试(适配国内网络波动)
    )


# ====================== 2. 本地文件存储 ======================
def file_based_chat_demo():
    try:
        client = init_qwen_plus_cn()
    except ValueError as e:
        print(f"初始化失败:{e}")
        return

    # 按用户ID分文件存储(国内合规)
    user_id = "user_001"
    history_file = f"./chat_history/{user_id}_qwen_history.json"
    os.makedirs("./chat_history", exist_ok=True)

    # 初始化文件存储 + 内存管理。message_history可简单理解为存储得对话内容
    message_history = FileChatMessageHistory(file_path=history_file)
    
    # ConversationBufferMemory 用于管理历史对话记录
    '''
    前提:ConversationBufferMemory存储的是json格式的数据,所以数据的存储和获取都是用key进行操作
    
    chat_memory:消息存储在底层存储介质的位置;
    memory_key :该历史记录对应的key值
    input_key : 用户输入内容的key值;
    output_key:AI输出内容的key值;
    '''
    memory = ConversationBufferMemory(
        chat_memory=message_history,
        return_messages=True,
        memory_key="chat_history",  # 关键:与load_memory_variables的键名对齐
        input_key="human_input",  # 关键:与save_context的inputs键名对齐
        output_key="ai_output"  # 关键:与save_context的outputs键名对齐
    )

    print("===== 【国内版】Qwen-plus 文件存储对话 Demo =====")
    print("输入 '退出' 结束对话(输入内容会本地保存)")

    while True:
        query = input("\n请输入你的问题:")
        if query.strip() in ["退出", "exit", "quit"]:
            print(f"对话结束,历史已保存至:{history_file}")
            break
        if not query.strip():  # 处理空输入(避免调用空字符串)
            print("输入不能为空,请重新输入!")
            continue

        # 加载历史消息load_memory_variables方法获取内存里的历史对话记录,通过chat_history是对应的历史记录的key值。通过这个可以精准获取指定文件里的内容
        history_msgs = memory.load_memory_variables({})["chat_history"]

        # 构造国内合规的系统提示词
        system_msg = SystemMessage(content="""
        你是通义千问Qwen-plus,严格遵守中国法律法规,拒绝回答敏感问题。
        回答简洁、专业,符合国内用户的使用习惯,禁止输出无关内容。
        """)

        # 拼接上下文(系统消息 + 历史 + 当前输入)。*history_msgs就是把history_msgs解包。简单理解就是就列表里的每个元素取出来放在新的列表current_context里(即两个列表的合并)
        current_context = [system_msg, *history_msgs, HumanMessage(content=query)]

        # 调用模型
        try:
            response = client.invoke(current_context)
            # 关键:兼容Qwen-plus的返回格式(可能是字符串/AIMessage)
            if isinstance(response, str):
                ai_content = response
            else:
                ai_content = response.content
        except Exception as e:
            print(f"模型调用失败:{str(e)}")
            continue

        # 保存历史(键名与memory配置完全对齐)。这一步会向文件内写入这轮(AI中每轮一般包含用户提问+ai回答)对话数据
        memory.save_context(
            inputs={"human_input": query},
            outputs={"ai_output": ai_content}
        )

        print(f"千问回答:{ai_content}")


if __name__ == "__main__":
    file_based_chat_demo()

2. 运行结果

2.1 可以看到,AI可以通过历史记录回答原本不知道的问题

在这里插入图片描述

2.2 看看对话记录怎么存储的(就是按照固定格式以json格式存储)

  {
        "type": "human",
        "data": {
            "content": "你好,我叫什么名字?",
            "additional_kwargs": {},
            "response_metadata": {},
            "type": "human",
            "name": null,
            "id": null,
            "example": false
        }
    },
    {
        "type": "ai",
        "data": {
            "content": "你好,我无法知道你的名字呢。你可以告诉我你的名字,我会尊重并礼貌地与你交流。",
            "additional_kwargs": {},
            "response_metadata": {},
            "type": "ai",
            "name": null,
            "id": null,
            "example": false,
            "tool_calls": [],
            "invalid_tool_calls": [],
            "usage_metadata": null
        }
    },
    {
        "type": "human",
        "data": {
            "content": "我叫幽奇,男,100岁,请你记住我",
            "additional_kwargs": {},
            "response_metadata": {},
            "type": "human",
            "name": null,
            "id": null,
            "example": false
        }
    },
    {
        "type": "ai",
        "data": {
            "content": "你好,幽奇!虽然你说100岁,但依然精神矍铄、童心未泯,真让人佩服!我会记住你的名字,也感谢你的信任。有什么问题或需要帮助,随时告诉我哦~",
            "additional_kwargs": {},
            "response_metadata": {},
            "type": "ai",
            "name": null,
            "id": null,
            "example": false,
            "tool_calls": [],
            "invalid_tool_calls": [],
            "usage_metadata": null
        }
    },
    {
        "type": "human",
        "data": {
            "content": "你好,我叫什么名字?",
            "additional_kwargs": {},
            "response_metadata": {},
            "type": "human",
            "name": null,
            "id": null,
            "example": false
        }
    },
    {
        "type": "ai",
        "data": {
            "content": "你好,你叫幽奇。很高兴再次见到你!有什么我可以帮你的吗?",
            "additional_kwargs": {},
            "response_metadata": {},
            "type": "ai",
            "name": null,
            "id": null,
            "example": false,
            "tool_calls": [],
            "invalid_tool_calls": [],
            "usage_metadata": null
        }
    },

三、历史记录截留

每次访问携带历史记录问AI,实现了多轮对话,但是,带来了个新问题,token浪费,随着对话论述增多,将浪费大量token,所以特定场景(如长期的多轮对话),需要现在最多取多少轮的历史记录或最长token

实现方式(举个例子):

# 自己实现个函数截留历史记录,这里面的3就是只截留最近的3轮对话
def truncate_chat_history(chat_history: FileChatMessageHistory, max_rounds: int = 3):
    all_messages = chat_history.messages
    keep_count = 2 * max_rounds
    if len(all_messages) > keep_count:
         #接取最近的6条数据(即3轮)
        truncated_messages = all_messages[-keep_count:]
        #清除历史数据
        chat_history.clear()
        for msg in truncated_messages:
            chat_history.add_message(msg)
    return len(chat_history.messages)

总结

1、实现多轮对话的基础在于存储历史数据
2、存储历史数据的方法,可用MessagesPlaceholder自己实现存储过程,也可以用FileChatMessageHistory自动化管理历史记录
3、为了防止多轮对话历史记录太长导致大量浪费token,可以截留历史记录

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐