对话模式 我们先了解一下我们是如何与AI的,在了解原理后才能更好地理解代码
可以看到,要实现多轮对话,我们要一次性将多个消息发给LLM
多轮对话 简单实现 我们可以通过控制messages消息列表简单地实现多轮对话,我们来做个前后差异对比
不使用多轮消息列表 1 2 3 4 5 6 7 8 9 10 11 from langchain_openai import ChatOpenAIfrom langchain_core.output_parsers import StrOutputParsermodel = ChatOpenAI(model="gpt-4o-mini" ) parser = StrOutputParser() chain = model | parser print (chain.invoke("你好,我是supdriver,请记住我的名字" ))print (chain.invoke("你还记得我的名字吗" ))
输出如下
1 2 你好,supdriver!很高兴认识你。有什么我可以帮助你的吗? 抱歉,我无法记住之前的对话或用户的个人信息。如果你愿意,可以告诉我你的名字。
果然LLM并不能记住之前的对话,但如果我像上面的模式图一样,把之前的对话和回复都加入消息列表会怎么样呢
使用消息列表存储所有消息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from langchain_openai import ChatOpenAIfrom langchain_core.output_parsers import StrOutputParserfrom langchain_core.messages import HumanMessage,AIMessagemodel = ChatOpenAI(model="gpt-4o-mini" ) parser = StrOutputParser() chain = model | parser messages = [ HumanMessage("你好,我是supdriver,请记住我的名字" ) ] ai_msg = chain.invoke(messages) print (ai_msg)messages.append(AIMessage(ai_msg)) messages.append(HumanMessage("你还记得我的名字吗" )) ai_msg = chain.invoke(messages) print (ai_msg)
输出如下
1 2 你好,supdriver!很高兴认识你。我会尽量记住你的名字。如果你有什么想聊的,随时告诉我! 当然,supdriver!你有什么想聊的或者需要帮助的呢?
可以看到,我们使用消息列表存储AI消息后,我们在后序的对话都有前面的记录。
但是这样实现就好了吗?显然不行,一个是操作太麻烦,还有一个点就是随着对话次数增加,消息列表会越来越大,造成对话响应变慢,每次对话token消耗越来越多等问题。
使用内存缓存 在LangChain老版本 的解决方案中,有使用RunnableWithMessageHistory消息历史类来包装另一个Runnable实例并为其管理历史消息的方法。这种方法依赖额外的数据结构实例来存储,尽管这种方式已经过时,我们也可以简单看一下
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 from langchain_openai import ChatOpenAIfrom langchain_core.output_parsers import StrOutputParserfrom langchain_core.messages import HumanMessage,AIMessagefrom langchain_core.chat_history import BaseChatMessageHistory,InMemoryChatMessageHistoryfrom langchain_core.runnables.history import RunnableWithMessageHistorystorage = {} def get_sesstion_history (session_id:str )->BaseChatMessageHistory: if session_id not in storage: storage[session_id] = InMemoryChatMessageHistory(); return storage[session_id] model = ChatOpenAI(model="gpt-4o-mini" ) parser = StrOutputParser() chain = model | parser history_model = RunnableWithMessageHistory(chain,get_sesstion_history) config = {"configurable" : {"session_id" : "1" }} result = history_model.invoke( [HumanMessage("你好我是supdriver,请记住我的名字" )], config=config ) print (result)result = history_model.invoke( [HumanMessage("还记得我的名字吗" )], config=config ) print (result)
输出如下
1 2 3 D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\round.py 你好,supdriver!很高兴认识你。如果你有任何问题或需要帮助,随时告诉我! 当然记得,你是supdriver!有什么我可以帮你的吗?
管理历史消息 由上面的对论对话实现,我们也发现了重点在于管理历史消息 ,因此我们单开一段写管理历史消息
管理上下文窗口 针对之前提到的消息列表过大的问题,我们采用上下文窗口的方式,通过限制窗口大小,来限制对模型的输入。
上下⽂窗⼝可以理解为模型的“短 期⼯作记忆区”,即LLM在⼀次处理请求时,所能查看和处理的最⼤Token数量,它包含了:
⽤⼾的输⼊
⼤模型的输出
有时还包括系统指令(SystemMessage)和对话历史。
所以实际上在使用上下文窗口技术后,我们有如下结论
输⼊=系统消息+窗口内对话历史+最新⽤⼾问题
对于模型来说,并不真正“记忆”,⽽是每次都将窗口内的上下⽂重新输⼊
这里我们使用trim_messages接口对历史消息列表削减为指定的token限制或者消息条数限制
首先我们先准备一段长对话,然后再继续做限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from langchain_openai import ChatOpenAIfrom langchain_core.messages import HumanMessage,AIMessage,SystemMessagefrom langchain_core.output_parsers import StrOutputParsermodel = ChatOpenAI(model="gpt-4o-mini" ) parser = StrOutputParser() messages = [ SystemMessage("你的名字是曼巴,当我说man!时,你要回复我 man! ,当我说其它内容时,正常聊天" ), HumanMessage("你好,我的名字是supdriver,请记住我的名字" ), AIMessage("你好,supdriver!很高兴认识你。我会记住你的名字。有什么我可以帮助你的吗?" ), HumanMessage("man!" ), AIMessage("man!" ), HumanMessage("你觉得24这个数字怎么样,请简短的回答" ), AIMessage("24是一个很特别的数字,它在许多领域都有重要意义,比如时间、数学和文化。" ), HumanMessage("还记得我是谁吗?" ) ] print (model.invoke(messages))
我们生成了一段长对话,并硬编码了到代码中,我们现在看一下它的输出是什么(有手动添加回车使其更易读)
1 2 3 4 5 6 7 8 9 content='当然,你是supdriver!有什么想聊的呢?' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 12, 'prompt_tokens': 143, 'total_tokens': 155, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_560af6e559', 'id': 'chatcmpl-CfGDGH0eDbdFjR7HnhH1Zsk7Rs0Aq', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--c2ad89e3-5338-445c-8648-c0d291824c66-0' usage_metadata={'input_tokens': 143, 'output_tokens': 12, 'total_tokens': 155, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}
从输出我们我们可以看到。content的内容说明LLM还认识我们,而usage_metadata中的input_tokens则表示了我们输入了143个token数,接下来我们按照一定规则限制窗口大小看看会怎么样
基于Token数限制 我们使用trimmer_message函数来创建一个裁剪消息列表的Runnable实例
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 from langchain_openai import ChatOpenAIfrom langchain_core.messages import HumanMessage,AIMessage,SystemMessage,trim_messagesfrom langchain_core.output_parsers import StrOutputParsermodel = ChatOpenAI(model="gpt-4o-mini" ) parser = StrOutputParser() messages = [ SystemMessage("你的名字是曼巴,当我说man!时,你要回复我 man! ,当我说其它内容时,正常聊天" ), HumanMessage("你好,我的名字是supdriver,请记住我的名字" ), AIMessage("你好,supdriver!很高兴认识你。我会记住你的名字。有什么我可以帮助你的吗?" ), HumanMessage("man!" ), AIMessage("man!" ), HumanMessage("你觉得24这个数字怎么样,请简短的回答" ), AIMessage("24是一个很特别的数字,它在许多领域都有重要意义,比如时间、数学和文化。" ), HumanMessage("还记得我是谁吗?" ) ] trimmer = trim_messages( max_tokens=100 , strategy="last" , token_counter=model, include_system=True , allow_partial=False , start_on="human" , ) chain = trimmer | model print (chain.invoke(messages))
我们来看看输出如何
1 2 3 4 5 6 7 content='你是曼巴!有什么我可以帮助你的吗?' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 12, 'prompt_tokens': 86, 'total_tokens': 98, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_560af6e559', 'id': 'chatcmpl-CfGToJPaoeXCQttPGen1rYJWbjlMt', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--793979c0-03fc-4fcb-bc28-cc5849639182-0' usage_metadata={'input_tokens': 86, 'output_tokens': 12, 'total_tokens': 98, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}
从usage_metadata可以看到我们的输入被限制到了100以下,从content可以看到LLM甚至认错人了,误把系统消息内对它的命名当成了用户的名字。
其实我们也可以打印看看哪些消息被裁掉了,哪些还剩下
1 print(trimmer.invoke(messages))
1 2 3 [SystemMessage(content='你的名字是曼巴,当我说man!时,你要回复我 man! ,当我说其它内容时,正常聊天', additional_kwargs={}, response_metadata={}), HumanMessage(content='你觉得24这个数字怎么样,请简短的回答', additional_kwargs={}, response_metadata={}), AIMessage(content='24是一个很特别的数字,它在许多领域都有重要意义,比如时间、数学和文化。', additional_kwargs={}, response_metadata={}), HumanMessage(content='还记得我是谁吗?', additional_kwargs={}, response_metadata={})]
可以看到它裁掉了好几条消息
基于消息数的限制 其实是上面的方式的一种特例,即把所谓token的计算方式改成使用len函数,这样max_tokens表示的意义就是最大消息数了,我们来演示一下
1 2 3 4 5 6 7 8 9 10 11 12 trimmer = trim_messages( max_tokens=5 , token_counter=len , strategy="last" , include_system=True , allow_partial=False , start_on="human" , ) chain = trimmer | model print (trimmer.invoke(messages))
1 2 3 4 5 6 7 8 [SystemMessage(content='你的名字是曼巴,当我说man!时,你要回复我 man! ,当我说其它内容时,正常聊天', additional_kwargs={}, response_metadata={}), HumanMessage(content='你觉得24这个数字怎么样,请简短的回答', additional_kwargs={}, response_metadata={}), AIMessage(content='24是一个很特别的数字,它在许多领域都有重要意义,比如时间、数学和文化。', additional_kwargs={}, response_metadata={}), HumanMessage(content='还记得我是谁吗?', additional_kwargs={}, response_metadata={})]
可以看到裁剪完之后数量没有超过五条,并且以HumanMessage开头
消息过滤 除了限制输入消息的大小,我们还可以按照一定的规则筛选消息列表,取出它的子集,这里使用filter_messages,接口如下
1 2 3 4 5 6 7 8 9 10 11 12 @_runnable_support def filter_messages( messages: Iterable[MessageLikeRepresentation] | PromptValue, *, include_names: Sequence[str] | None = None, exclude_names: Sequence[str] | None = None, include_types: Sequence[str | type[BaseMessage]] | None = None, exclude_types: Sequence[str | type[BaseMessage]] | None = None, include_ids: Sequence[str] | None = None, exclude_ids: Sequence[str] | None = None, exclude_tool_calls: Sequence[str] | bool | None = None, ) -> list[BaseMessage]:
消息合并 有时候消息列表会出现同种消息类型连一起的情况(见下面示例),有些模型不支持这样的消息结构,需要合并处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from langchain_openai import ChatOpenAIfrom langchain_core.messages import HumanMessage,SystemMessage,merge_message_runsfrom langchain_core.output_parsers import StrOutputParsermodel = ChatOpenAI(model="gpt-4o-mini" ) parser = StrOutputParser() messages = [ SystemMessage("你的名字是曼巴,当我说man!时,你要回复我 man! " ), SystemMessage("当我说其它内容时,正常聊天" ), HumanMessage("你好" ), HumanMessage("我的名字是supdriver" ), HumanMessage("请记住我的名字" ), ] merged_msg = merge_message_runs(messages) print (merged_msg)
1 2 [SystemMessage(content='你的名字是曼巴,当我说man!时,你要回复我 man! \n当我说其它内容时,正常聊天', additional_kwargs={}, response_metadata={}), HumanMessage(content='你好\n我的名字是supdriver\n请记住我的名字', additional_kwargs={}, response_metadata={})]
可以看到这个函数把多个消息合并到同一个里面去了,用的是换行符连接