在本地部署大模型后,很多人会发现一个常见问题:模型能够回答当前问题,却“记不住”上一轮说过什么。原因并不复杂,Ollama 负责运行模型,但默认不会替应用维护完整会话状态;LangChain 则可以把历史消息组织起来,在每次请求时重新提交给模型。本文将完成 Ollama 与 LangChain 的接入,并加入对话记忆和上下文窗口裁剪,让本地聊天既连贯又不会无限堆积消息。🤖
一、整体实现思路
多轮对话并不是模型内部永久保存了聊天内容,而是应用在调用模型时,把系统提示、历史问答和本轮问题一起发送。LangChain 使用 SystemMessage、HumanMessage、AIMessage 等消息对象统一描述这些内容,具体定义可参考 LangChain 消息文档。
本例采用一套容易理解的流程:
- 通过 Ollama 在本机运行聊天模型。
- 使用 ChatOllama 将模型接入 LangChain。
- 按照 session_id 分别保存不同用户的消息历史。
- 调用模型前裁剪历史,只保留系统提示和最近对话。
- 模型返回答案后,将本轮问答写回历史。
二、准备 Ollama 与 Python 环境
先安装并启动 Ollama,然后拉取一个适合本机配置的聊天模型。模型名称应以本机实际安装结果为准,可通过 ollama list 查看。Ollama 的 LangChain 接入方式和安装说明可查看 官方集成文档。
ollama pull qwen2.5:7b
ollama list
pip install -U langchain langchain-core langchain-ollama
如果电脑内存或显存有限,可以选择参数规模更小或量化程度更高的模型。这里不建议盲目追求大模型,因为上下文越长、模型越大,通常越考验本机资源。⚙️
三、接入 ChatOllama
创建模型对象时,可以同时设置温度和 Ollama 服务地址。temperature 较低时回答通常更稳定,适合知识问答和工具型应用。
from langchain_ollama import ChatOllama
llm = ChatOllama(
model="qwen2.5:7b",
temperature=0.3,
base_url="http://localhost:11434"
)
ChatOllama 返回的是 AIMessage,因此可以直接与 LangChain 的消息体系配合。相比把全部内容拼成一个长字符串,消息对象能够明确区分系统指令、用户输入和模型回答,后续扩展工具调用时也更方便。
四、实现按会话隔离的本地记忆
最简单的记忆方案是使用字典保存消息列表。键是 session_id,值是该会话的历史记录。正式项目可以把它替换为 SQLite、Redis 或其他持久化存储。
from langchain_core.messages import SystemMessage, HumanMessage
sessions = {}
def get_history(session_id):
if session_id not in sessions:
sessions[session_id] = [
SystemMessage(content="你是一个简洁、可靠的中文助手。")
]
return sessions[session_id]
每个会话第一次访问时都会创建独立历史,这样不同用户之间不会串话。需要注意,内存字典会在进程重启后清空。如果业务要求恢复历史,应在每轮结束后把消息序列化到本地数据库,并在用户重新进入时加载。
五、加入上下文窗口裁剪
如果每轮都提交全部历史,输入会不断增大,最终可能超过模型上下文限制,还会拖慢推理速度。LangChain 提供 trim_messages,可按消息数或近似 Token 数缩减历史。官方建议通常保留 SystemMessage,并优先保留最近消息,详细参数可参考 trim_messages 参考文档。✂️
from langchain_core.messages import trim_messages
from langchain_core.messages.utils import count_tokens_approximately
def crop_history(messages):
return trim_messages(
messages,
strategy="last",
token_counter=count_tokens_approximately,
max_tokens=3000,
start_on="human",
include_system=True,
allow_partial=False
)
strategy="last" 表示优先保留最近消息,include_system=True 用于保存系统指令,start_on="human" 可以避免裁剪后从一条孤立的模型回答开始。max_tokens=3000 只是示例值,实际项目需要结合模型上下文上限、预留回答长度和提示词大小进行调整。
六、组合成可运行的对话函数
def chat(session_id, user_input):
history = get_history(session_id)
history.append(HumanMessage(content=user_input))
input_messages = crop_history(history)
response = llm.invoke(input_messages)
history.append(response)
return response.content
print(chat("user-001", "我准备学习 LangChain,请记住这一点。"))
print(chat("user-001", "我刚才准备学习什么?"))
这里需要区分保存历史和发送历史:sessions 中可以保存较完整的会话记录,而传给模型的 input_messages 应经过裁剪。这样既能留存原始记录,又能控制单次推理开销。对于长期对话,还可以定期把较早内容总结成一条摘要消息,再删除对应原始轮次,形成“近期原文加长期摘要”的两级记忆。
七、实际开发中的优化建议
- 预留输出空间:不要把上下文窗口全部用于历史消息,应给模型回答保留足够余量。
- 隔离会话:session_id 必须稳定且不可混用,服务端还要做好用户身份校验。
- 避免机械裁剪:用户姓名、任务目标和关键约束可以提取到结构化状态中,不应只依赖最近几轮。
- 记录裁剪日志:调试阶段记录裁剪前后消息数量,便于定位模型突然“失忆”的原因。
- 控制本地数据:聊天记录即使不上传云端,也应设置访问权限、保存期限和清理机制。🔐
总结
Ollama 解决了模型本地运行问题,LangChain 则负责组织消息、维护会话和控制上下文。实现稳定的本地多轮对话,关键不是无限追加历史,而是将会话隔离、持久化保存、近期消息裁剪和长期信息摘要结合起来。完成本文方案后,可以继续接入 Web 界面、SQLite 存储或 RAG 检索,逐步构建一个真正可用、资源可控且注重隐私的本地智能助手。✅