核心内容摘要
测试工程师的显微镜:AI作曲侵权事件全链路解析
随着 LangChain 的快速迭代LCEL (LangChain Expression Language) 逐渐成为主流。
然而在很多现有的生产系统和教程中我们依然频繁看到经典Legacy组件的身影。
本文将带你深入了解 LangChain 最经典的对话组合ConversationBufferMemory与ConversationChain。
ConversationBufferMemory最纯粹的记忆
1 什么是 ConversationBufferMemoryConversationBufferMemory是 LangChain 中最基础的记忆组件。
它的逻辑非常简单粗暴它把你说过的每一句话、AI 回复的每一句话都原封不动地塞进一个变量里。
2 工作原理想象一个无限长的记事本用户说“Hi” - 记事本写“Human: Hi”AI 回复“Hello” - 记事本写“AI: Hello”下次对话时LangChain 会把记事本里的所有内容复制粘贴到 Prompt 的{history}位置。
3 优缺点分析优点无损完全保留了对话的所有细节。
简单易于理解和调试。
缺点Token 爆炸随着对话变长历史记录会越来越长最终超出 LLM 的 Context Window上下文窗口限制导致报错或费用激增。
ConversationChain开箱即用的对话链
1 什么是 ConversationChainConversationChain是一个预封装好的 Chain。
它帮你把以下三样东西组装在了一起LLM(大语言模型)Memory(记忆组件)Prompt(提示词模板)你不需要自己写 Prompt Template不需要自己处理history变量注入。
它内置了一个默认的 Prompt通常是“The following is a friendly conversation…”。
2 代码实战让我们通过一个简单的 Python 脚本 (src/examples/memory/demo_conversation_buffer_memory.py) 来演示它们的配合。
fromlangchain_classic.memoryimportConversationBufferMemoryfromlangchain_classic.chainsimportConversationChainfromsrc.llm.gemini_chat_modelimportget_gemini_llm#
初始化 Memory# 它负责在内存中维护一个不断增长的字符串memoryConversationBufferMemory()#
初始化 Chain# 自动将 Memory 挂载到 LLM 上conversationConversationChain(llmget_gemini_llm(),memorymemory,verboseTrue# 开启 verbose 可以看到它到底发给了 LLM 什么)#
第一轮对话conversation.predict(inputHi, my name is Alice.)# Memory 更新: Human: Hi, my name is Alice.\nAI: Hello Alice!#
第二轮对话conversation.predict(inputWhat is my name?)# Memory 再次更新...
3 运行结果解析当我们运行上述代码时ConversationChain会自动构建如下 Prompt 发送给 LLMThe following is a friendly conversation between a human and an AI... Current conversation: Human: Hi, my name is Alice. AI: Hello Alice! Human: What is my name? AI:这就是为什么 AI 能够回答 “Your name is Alice”因为它在 Prompt 里“看到”了之前的对话记录。
进阶如何查看记忆内容你可以随时调用load_memory_variables来查看当前 Buffer 里存了什么print(memory.load_memory_variables({}))# 输出:# {history: Human: Hi...\nAI: Hello...}
进阶如何限制历史消息上限用户提问ConversationBufferMemory可以限制历史消息的上限吗回答不可以它默认保存所有历史。
如果你需要限制历史记录为了节省 Token 或防止 Context Window 溢出你需要切换到它的兄弟组件ConversationBufferWindowMemory: 按轮数限制如只保留最近 5 轮。
ConversationTokenBufferMemory: 按 Token 数限制如只保留最近 2000 token。
ConversationSummaryMemory: 自动调用 LLM 对旧历史进行摘要
总结。
进阶如何持久化到数据库用户提问ConversationBufferMemory只能把消息保存到内存吗数据库是否可以回答它可以支持数据库但需要配合chat_memory参数。
默认情况下ConversationBufferMemory在内部创建了一个临时的内存列表 (ChatMessageHistory)。
一旦程序重启数据就丢失了。
如果你想把它保存到数据库如 Redis, Postgres你需要替换掉这个临时的内存列表换成一个连接数据库的 History 对象。
代码示例 (伪代码)fromlangchain_community.chat_message_historiesimportRedisChatMessageHistoryfromlangchain_classic.memoryimportConversationBufferMemory#
创建一个连接 Redis 的 History 对象# 这不是普通的 list而是一个读写 Redis 的代理message_historyRedisChatMessageHistory(urlredis://localhost:6379/0,session_idmy-session)#
将这个 History 对象传给 BufferMemory# 此时 Memory 不再把数据存在 RAM 里而是直接读写 RedismemoryConversationBufferMemory(chat_memorymessage_history)# 后续用法完全一样但数据会自动持久化到 RedisconversationConversationChain(llmllm,memorymemory)原理ConversationBufferMemory是一个逻辑层负责把消息格式化为 Prompt而chat_memory是存储层负责物理存取。
将存储层替换为数据库实现即可。
新旧对比本质区别是什么除了使用方式的不同两者在架构设计上有着本质的区别
架构模式实例绑定 (Stateful) vs 动态注入 (Stateless)用户提问ConversationChain不是也可以传入不同的 Memory 吗为什么说它是耦合的回答这里的耦合是指运行时的实例级绑定。
ConversationChain (旧)当你初始化chain ConversationChain(memorymem)时这个chain对象就和特定的那份mem对象绑定死了。
后果这个chain实例变成了“有状态”的对象。
它里面存着“用户A”的聊天记录。
你不能把这个chain实例拿去服务“用户B”否则用户B会看到用户A的记录。
并发问题在 Web 服务中你必须为每个用户new一个新的 Chain 实例无法通过全局单例复用。
RunnableWithMessageHistory (新)它本身不持有任何具体的 Memory 对象。
它持有的只是一个工厂函数get_session_history。
后果这个 Wrapper 对象是“无状态”的。
它不知道也不关心当前在服务谁。
动态性每次调用invoke时它才根据传入的config{session_id: B}动态地去工厂里找“用户B”的记录。
并发优势你可以创建一个全局单例的runnable让它同时并发服务成千上万个用户完全线程安全。
逻辑灵活性硬编码 vs 组合ConversationChain (旧)它的内部逻辑Load - Prompt - LLM - Save是硬编码在 Python 类里的。
如果你想在“保存历史”之前加一个“敏感词过滤”步骤你必须继承并重写这个类。
RunnableWithMessageHistory (新)它遵循 LCEL 组合原则。
内部的 Chain 可以是任意复杂的 DAG有向无环图。
你可以在任何环节插入自定义逻辑Wrapper 只负责最外层的历史管理。
RunnableWithMessageHistory (新)Chain 对象本身是无状态的。
状态记忆并不存在 Chain 对象里而是根据config{session_id: ...}动态加载的。
这意味着同一个 Chain 实例可以并发服务成千上万个用户只要 session_id 不同。
这是生产环境的高并发神器。
对比