文章目录
- 前言
- 一、如何实现多轮对话存储
- 二、FileChatMessageHistory的使用方法
- 1.代码(为了演示多轮对话,使用了函数)
- 2. 运行结果
- 2.1 可以看到,AI可以通过历史记录回答原本不知道的问题
- 2.2 看看对话记录怎么存储的(就是按照固定格式以json格式存储)
- 三、历史记录截留
- 实现方式(举个例子):
- 总结
前言
提示:承上启下,系列文章,通过前言会议一下上篇章内容,引入本文内容:
从05.AI应用搭建–langchain输出解析器后,基本介绍完一个简单AI应用的基本流程。但是,大家很容易发现前面的内容,更多是一问一答形式,无法实现追问,那么对于这种场景应该如何解决呢?
立刻有人想到:既然可以拼接强化AI和user的提示词内容,那我直接将AI的输出和用户的历史提问记录下来,然后拼接进每次的提问不就可以了,那么恭喜你,已经掌握了多轮对话的实现基础。
一、如何实现多轮对话存储
通过一段代码简单理解下基础原理:
# 构造提示词。其中MessagesPlaceholder是消息占位符,它的作用是可以动态插入历史对话记录''' MessagesPlaceholder的消息结构如下: [ ("human", "你好,我叫小明"), ("system", "你好小明!有什么我能帮助你的吗?。"), ("human", "我最喜欢红色,帮我选一种适合圣诞节的礼物"), ("system", "圣诞帽"), ] '''fromlangchain_community.chat_modelsimportMessagesPlaceholder#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.10langchain-openai langchain-community python-dotenv1.代码(为了演示多轮对话,使用了函数)
importosfromdotenvimportload_dotenv# 补充:加载环境变量fromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportHumanMessage,AIMessage,SystemMessagefromlangchain_community.chat_message_historiesimportFileChatMessageHistoryfromlangchain.memoryimportConversationBufferMemory# 加载.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模型 ======================definit_qwen_plus_cn():returnChatOpenAI(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. 本地文件存储 ======================deffile_based_chat_demo():try:client=init_qwen_plus_cn()exceptValueErrorase: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("输入 '退出' 结束对话(输入内容会本地保存)")whileTrue:query=input("\n请输入你的问题:")ifquery.strip()in["退出","exit","quit"]:print(f"对话结束,历史已保存至:{history_file}")breakifnotquery.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)ifisinstance(response,str):ai_content=responseelse:ai_content=response.contentexceptExceptionase: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轮对话deftruncate_chat_history(chat_history:FileChatMessageHistory,max_rounds:int=3):all_messages=chat_history.messages keep_count=2*max_roundsiflen(all_messages)>keep_count:#接取最近的6条数据(即3轮)truncated_messages=all_messages[-keep_count:]#清除历史数据chat_history.clear()formsgintruncated_messages:chat_history.add_message(msg)returnlen(chat_history.messages)总结
1、实现多轮对话的基础在于存储历史数据
2、存储历史数据的方法,可用MessagesPlaceholder自己实现存储过程,也可以用FileChatMessageHistory自动化管理历史记录
3、为了防止多轮对话历史记录太长导致大量浪费token,可以截留历史记录