标签 ChatOpenAI 下的文章

这两年,大模型几乎成了开发者的“标配工具”:
写代码、查资料、做总结、当智能助手。

但你有没有认真想过一个问题:

我们真的必须把所有请求都发到云端 API 吗?

随着模型体积持续下降、硬件性能快速提升,以及 Ollama 这类工具逐渐成熟,
本地运行大模型,已经从早期的“极客尝鲜”,演进为一种可以在真实项目中落地的工程方案

这篇文章,我们就来完整走一遍:

如何使用 LangChain,基于最新 Runnable API,调用本地启动的 Ollama 模型,构建一个真正可用的本地大模型应用。

一、为什么选择 LangChain + Ollama?

先给结论:

Ollama 解决“模型怎么跑”,LangChain 解决“能力怎么用”。

这是目前本地大模型场景中,最自然、最稳定的一种组合方式


1️⃣ Ollama:本地大模型的“Docker”

你可以把 Ollama 理解为:
专门为大模型设计的一层运行时基础设施。

它解决的问题非常聚焦:

  • 统一模型的下载、管理与启动
  • 对外提供标准化 HTTP API(默认端口 11434
  • 支持 LLaMA、Qwen、Mistral、DeepSeek 等主流模型
  • Mac / Linux / Windows 全平台可用
  • 天然适合 Docker / 私有化部署

一句话总结:

Ollama 把“跑模型”这件事,做成了基础设施能力。

2️⃣ LangChain:AI 应用的“控制中心”

如果你只是想“问一句、回一句”,直接调 Ollama API 当然也没问题。
但一旦进入真实工程场景,需求会迅速复杂化:

  • Prompt 如何复用、版本化?
  • 对话上下文如何管理?
  • 如何组合多步推理?
  • 后续怎么接 RAG、Agent、工具调用?

这些正是 LangChain 擅长的事情:

  • Prompt 模板与结构化输入
  • Runnable / LCEL 编排能力
  • 对话历史(Memory)管理
  • Tool、RAG、Agent 的统一抽象
  • 可自然演进到 LangGraph

所以一个非常自然的分工是:

LangChain 负责“编排与逻辑”,Ollama 负责“模型与算力”。

二、准备工作:本地启动 Ollama 模型

1️⃣ 使用 Docker 部署 Ollama(推荐)

docker run \
-d \
--restart=always \
--name ollama \
--gpus=all \
-p 11434:11434 \
-v /home/data/ollama:/root/.ollama \
ollama/ollama

如果你对部署细节感兴趣,可以参考我之前的文章:

  • 《如何使用 Ollama 打造你的本地 AI 助手》
  • 《为本地部署的大模型添加 API Key 认证:Nginx 实现方案》

2️⃣ 拉取并运行模型

qwen3:8b 为例:

ollama pull qwen3:8b

简单测试:

ollama run qwen3:8b

如果可以正常对话,说明模型已经在本地成功运行。


三、LangChain 接入本地 Ollama(OpenAI 协议)

接下来进入核心部分:
如何用 LangChain 调用本地 Ollama?


1️⃣ 安装依赖

pip install langchain langchain-openai

这里我们使用 OpenAI 兼容协议,这是目前最稳定、生态最完整的一种方式。


2️⃣ 创建 Ollama LLM(ChatOpenAI)

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    name="ollama-ai",
    model="qwen3:8b",
    base_url="http://localhost:11434/v1",
    api_key="your api key",
    temperature=0.7,
    timeout=300,
)

几个关键点说明:

  • model 必须与 Ollama 中的模型名称一致
  • base_url 指向 Ollama,并注意使用 /v1 后缀
  • 这里使用的是 OpenAI 标准协议,不是 Ollama 私有 API

3️⃣ 最简单的一次调用

response = llm.invoke("用一句话解释什么是 LangChain")
print(response)

到这里,你已经完成了:

LangChain → 本地 Ollama → 本地大模型

这条完整调用链。


四、进阶用法:Prompt + Runnable(LCEL)

在真实项目中,几乎不会直接“裸调”模型。


1️⃣ PromptTemplate

from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate(
    input_variables=["question"],
    template="你是一个资深后端工程师,请用简洁、专业的语言回答:{question}",
)

2️⃣ 输出解析(StrOutputParser)

from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()

显式的输出解析,是 LangChain 新 API 的重要特征:

  • 输出类型清晰
  • 便于后续切换为 JSON / Pydantic
  • 更适合工程化

3️⃣ Runnable 组合(推荐写法)

chain = prompt | llm | parser

response = chain.invoke({
    "question": "为什么本地部署大模型越来越流行?"
})
print(response)

这就是 LangChain 当前主推的 LCEL(表达式)写法
比早期的 LLMChain 更透明、也更可组合。


五、加入 Memory:真正的本地对话能力

⚠️ 一个非常重要的变化

在新的 Runnable 体系中,
Memory 不再是 Chain 的“隐藏参数”,而是显式的状态管理。


1️⃣ 定义对话历史存储

from langchain_core.chat_history import InMemoryChatMessageHistory

store = {}

def get_session_history(session_id: str):
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

2️⃣ Prompt 显式消费 history(关键)

from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate(
    input_variables=["history", "question"],
    template="""
         你是一个资深后端工程师。

         以下是之前的对话历史:
         {history}

         当前用户问题:
         {question}

         请基于上下文给出连贯、准确的回答。
    """.strip()
)
这是很多人第一次使用 RunnableWithMessageHistory 时最容易忽略的一点:
历史是否生效,取决于 Prompt 是否显式使用 {history}

3️⃣ 构建带记忆的 Runnable

from langchain_core.runnables.history import RunnableWithMessageHistory

chain = prompt | llm | parser

chat_chain = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="question",
    history_messages_key="history",
)

4️⃣ 调用(带 session_id)

config = {"configurable": {"session_id": "local-chat"}}

print(chat_chain.invoke(
    {"question": "什么是 Ollama?"},
    config=config
))

print(chat_chain.invoke(
    {"question": "它和 LangChain 有什么关系?"},
    config=config
))

到这里,你已经拥有了一个:

  • 支持上下文
  • 完全本地
  • 状态可控

的对话系统。

而且 所有数据都只存在你的本地机器上


六、这套方案适合谁?

非常适合:

  • ✅ 本地工具 / 桌面应用
  • ✅ 内部知识库 / 私有 RAG
  • ✅ 研发辅助工具(代码、文档、SQL)
  • ✅ 对数据安全敏感的企业场景
  • ✅ 学习大模型工程化的开发者

不太适合:

  • ❌ 超大并发场景
  • ❌ 极限性能 / 超大模型
  • ❌ 面向公网的 C 端产品

七、一些来自实践的工程建议

最后分享几点真实踩坑后的经验:

  1. 模型别贪大

    • 7B / 8B 是当前本地部署的性价比甜点位
  2. Prompt 比模型更重要

    • 本地模型对 Prompt 非常敏感
  3. LangChain 要“模块化使用”

    • Prompt / LLM / Parser / Memory 明确分层
  4. Memory 要可演进

    • InMemory → Redis → 数据库 → Checkpointer
  5. Ollama 非常适合私有化场景

    • Docker + 内网 + 权限控制,工程成本极低

结语

过去一年,我们讨论最多的问题是:

“该用哪个云端大模型?”

而现在,越来越多开发者开始认真思考:

“哪些能力,其实可以放回本地?”

LangChain + Ollama 并不是为了“替代云”,
而是为我们提供了一个:

真正可控、可组合、可落地的本地大模型方案。

如果你正在做:

  • 本地 AI 工具
  • 私有化大模型
  • Agent / RAG 工程实践

那么这套组合,非常值得一试。


如果你觉得这篇文章对你有帮助,欢迎 点赞 / 转发 / 收藏
下一篇,我会继续分享 LangGraph 在本地大模型场景下的实战用法

在构建问答Agent时,多轮对话的上下文记忆是核心需求——让Agent能记住历史对话内容,结合「历史问题+历史回答+当前问题」给出连贯回复,而非孤立回答每个问题。

LangChain中的ConversationBufferMemory轻量、易用的短期记忆组件,核心作用是按顺序缓存对话历史,并将历史内容注入到模型的输入提示中,实现问答Agent的短期记忆能力,适合中小长度的多轮对话场景。

本文将基于LangChain框架,从核心原理、完整可运行代码、关键细节、进阶优化四个维度,教你为问答Agent集成ConversationBufferMemory,支持OpenAI/国产大模型(通义千问/文心一言),代码可直接复用。

一、核心概念铺垫

1.1 ConversationBufferMemory 核心作用

  • 键值对形式按时间顺序存储对话历史(问题+回答);
  • 支持将对话历史格式化为字符串/消息对象,注入到LLM的输入提示中;
  • 提供清空记忆、获取记忆、修改记忆的便捷方法;
  • 轻量无依赖,无需额外存储,对话历史保存在内存中(会话结束即销毁,符合「短期记忆」定位)。

1.2 核心搭配

ConversationBufferMemory通常与ConversationChain(通用对话链)/RetrievalQA(知识库问答链)搭配使用,本文先实现基础问答Agent(基于ConversationChain),后续补充带知识库的问答Agent优化方案。

1.3 关键参数

参数名作用常用值
memory_key记忆在提示模板中的变量名(需与提示模板一致)chat_history(推荐)
return_messages记忆返回格式:True返回消息对象(HumanMessage/AIMessage)False返回拼接字符串False(基础场景)/True(复杂场景)
input_key输入问题的变量名input(默认,无需修改)
output_key输出回答的变量名output(默认,无需修改)

二、环境准备

安装LangChain核心依赖+大模型适配依赖(以OpenAI/通义千问为例,二选一即可):

# 核心依赖:LangChain核心+社区组件
pip install langchain-core langchain-community -i https://pypi.tuna.tsinghua.edu.cn/simple

# 可选1:OpenAI模型依赖(GPT-3.5/GPT-4)
pip install langchain-openai -i https://pypi.tuna.tsinghua.edu.cn/simple

# 可选2:国产大模型依赖(通义千问/文心一言/智谱清言)
pip install langchain-qianfan langchain-dashscope -i https://pypi.tuna.tsinghua.edu.cn/simple

三、完整实现:基础问答Agent+短期记忆

3.1 方案1:基于OpenAI模型(GPT-3.5/GPT-4)

# 1. 导入核心模块
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain.prompts import PromptTemplate
import os

# 2. 配置环境(OpenAI API密钥)
# 国内用户需配置代理:os.environ["HTTP_PROXY"] = "http://127.0.0.1:7890"
os.environ["OPENAI_API_KEY"] = "你的OpenAI API密钥"

# 3. 初始化LLM模型
llm = ChatOpenAI(
    model="gpt-3.5-turbo",  # 推荐gpt-3.5-turbo,性价比高
    temperature=0.1,        # 越低回答越稳定,适合问答场景
    max_tokens=2048
)

# 4. 初始化ConversationBufferMemory(核心:短期记忆)
memory = ConversationBufferMemory(
    memory_key="chat_history",  # 记忆变量名,需与提示模板中的{chat_history}一致
    return_messages=False,      # 返回字符串格式的对话历史,适合基础场景
    input_key="input"           # 输入问题的变量名,默认input即可
)

# 5. 自定义带记忆的提示模板(必须包含{chat_history}和{input})
# 模板说明:chat_history=历史对话,input=当前问题,让模型结合两者回答
prompt = PromptTemplate(
    input_variables=["chat_history", "input"],  # 必须包含记忆变量和输入变量
    template="""你是一个专业的问答助手,善于结合历史对话内容回答当前问题。
    历史对话:{chat_history}
    当前问题:{input}
    请简洁、准确地回答当前问题,无需额外赘述。"""
)

# 6. 构建带记忆的问答链(核心:将LLM、记忆、提示模板绑定)
conversation_chain = ConversationChain(
    llm=llm,
    memory=memory,
    prompt=prompt,
    verbose=True  # 开启详细日志,可查看输入的提示内容(含历史对话)
)

# 7. 测试多轮问答(验证记忆效果)
if __name__ == "__main__":
    # 第一轮问答
    print("===== 第一轮 =====")
    res1 = conversation_chain.invoke({"input": "什么是大语言模型?"})
    print("回答:", res1["output"], "\n")

    # 第二轮问答(结合历史:问大语言模型的核心优势)
    print("===== 第二轮 =====")
    res2 = conversation_chain.invoke({"input": "它的核心优势是什么?"})
    print("回答:", res2["output"], "\n")

    # 第三轮问答(结合历史:问该优势的应用场景)
    print("===== 第三轮 =====")
    res3 = conversation_chain.invoke({"input": "这些优势能用到哪些领域?"})
    print("回答:", res3["output"], "\n")

    # 手动查看记忆中的对话历史
    print("===== 查看短期记忆 =====")
    print(memory.load_memory_variables({}))

    # 清空记忆(可选)
    # memory.clear()
    # print("清空记忆后:", memory.load_memory_variables({}))

3.2 方案2:基于国产模型(通义千问,国内用户推荐)

替换上述步骤2和步骤3即可,其余代码完全不变,适配性拉满:

# 2. 配置环境(通义千问API密钥,从阿里云DashScope获取)
os.environ["DASHSCOPE_API_KEY"] = "你的通义千问API密钥"

# 3. 初始化通义千问模型(替换OpenAI)
from langchain_dashscope import ChatDashScope
llm = ChatDashScope(
    model="qwen-plus",  # 通义千问轻量版,免费额度足够测试
    temperature=0.1,
    max_tokens=2048
)

3.3 运行结果与关键日志

核心输出(记忆生效)

===== 第一轮 =====
回答: 大语言模型是基于大尺度语料训练、具备强大自然语言理解与生成能力的人工智能模型,能完成文本创作、问答、翻译等多种自然语言处理任务。

===== 第二轮 =====
回答: 大语言模型的核心优势包括:1. 强大的上下文理解与语义分析能力;2. 灵活的自然语言生成能力,可输出流畅、贴合语境的文本;3. 泛化能力强,能处理未见过的新问题;4. 多任务适配,无需单独训练即可完成多种NLP任务。

===== 第三轮 =====
回答: 这些优势可应用在智能客服、内容创作、教育辅导、代码开发、数据分析、机器翻译、智能助手等领域,覆盖互联网、教育、金融、制造业等多个行业。

===== 查看短期记忆 =====
{'chat_history': 'Human: 什么是大语言模型?\nAI: 大语言模型是基于大尺度语料训练、具备强大自然语言理解与生成能力的人工智能模型,能完成文本创作、问答、翻译等多种自然语言处理任务。\nHuman: 它的核心优势是什么?\nAI: 大语言模型的核心优势包括:1. 强大的上下文理解与语义分析能力;2. 灵活的自然语言生成能力,可输出流畅、贴合语境的文本;3. 泛化能力强,能处理未见过的新问题;4. 多任务适配,无需单独训练即可完成多种NLP任务。\nHuman: 这些优势能用到哪些领域?\nAI: 这些优势可应用在智能客服、内容创作、教育辅导、代码开发、数据分析、机器翻译、智能助手等领域,覆盖互联网、教育、金融、制造业等多个行业。'}

Verbose日志(关键:验证历史对话注入)

开启verbose=True后,可看到模型的实际输入提示包含了历史对话,这是记忆生效的核心:

> Entering new ConversationChain chain...
Prompt after formatting:
你是一个专业的问答助手,善于结合历史对话内容回答当前问题。
    历史对话:Human: 什么是大语言模型?
AI: 大语言模型是基于大尺度语料训练、具备强大自然语言理解与生成能力的人工智能模型,能完成文本创作、问答、翻译等多种自然语言处理任务。
    当前问题:它的核心优势是什么?
    请简洁、准确地回答当前问题,无需额外赘述。
> Finished chain.

四、关键细节:避免记忆失效的核心要点

ConversationBufferMemory使用简单,但容易因参数不匹配、提示模板错误导致记忆失效,以下是必须遵守的3条铁律:

4.1 提示模板必须包含memory_key指定的变量

比如memory_key="chat_history",则提示模板中必须有{chat_history},且输入变量列表要包含该变量:

# 正确:input_variables包含chat_history和input
prompt = PromptTemplate(
    input_variables=["chat_history", "input"],
    template="历史对话:{chat_history}  当前问题:{input}"
)

# 错误:缺少chat_history,记忆无法注入
prompt = PromptTemplate(
    input_variables=["input"],
    template="当前问题:{input}"
)

4.2 invoke入参必须是字典,且键为input_key

默认input_key="input",因此调用时必须传{"input": "你的问题"},而非直接传字符串:

# 正确
conversation_chain.invoke({"input": "什么是大语言模型?"})

# 错误:入参不是字典,记忆无法关联当前问题
conversation_chain.invoke("什么是大语言模型?")

4.3 避免手动修改对话历史(除非特殊需求)

ConversationBufferMemory自动追加每次的inputoutput到记忆中,无需手动修改:

# 自动追加:无需干预
conversation_chain.invoke({"input": "问题1"})  # 记忆中添加问题1+回答1
conversation_chain.invoke({"input": "问题2"})  # 记忆中追加问题2+回答2

# 手动修改(特殊需求时使用)
memory.save_context(
    inputs={"input": "手动添加的问题"},
    outputs={"output": "手动添加的回答"}
)

五、进阶优化:适配更复杂的问答场景

5.1 优化1:带知识库的问答Agent+短期记忆

实际场景中,问答Agent通常需要结合私有知识库(如PDF/文档),此时将ConversationChain替换为RetrievalQA,并搭配ConversationBufferMemory即可实现「知识库+多轮记忆」的问答能力:

# 新增:导入知识库相关模块
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_text_splitters import CharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain.chains import RetrievalQA

# 1. 加载知识库(以文本文件为例,可替换为PDF/Word加载器)
loader = TextLoader("your_knowledge_base.txt")  # 你的知识库文件
documents = loader.load()
# 分割文本为小片段
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
texts = text_splitter.split_documents(documents)
# 构建向量库
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(texts, embeddings)
retriever = db.as_retriever(search_kwargs={"k": 3})  # 每次检索3个相关片段

# 2. 初始化记忆(与基础版一致)
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=False,
    input_key="question"  # 注意:RetrievalQA的默认输入键是question,需修改
)

# 3. 构建带记忆的知识库问答链
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",  # 适合小片段文本,简单高效
    retriever=retriever,
    memory=memory,
    chain_type_kwargs={
        "prompt": PromptTemplate(
            input_variables=["chat_history", "context", "question"],
            template="""结合历史对话和知识库内容回答当前问题,若知识库无相关内容,仅结合历史对话回答。
            历史对话:{chat_history}
            知识库内容:{context}
            当前问题:{question}
            回答要求:简洁、准确,基于知识库内容,不要编造。"""
        )
    },
    verbose=True
)

# 4. 测试:结合知识库+历史对话的多轮问答
qa_chain.invoke({"question": "知识库中提到的大语言模型有哪些应用?"})
qa_chain.invoke({"question": "这些应用中,哪个在教育领域的落地效果最好?"})  # 结合历史

5.2 优化2:限制记忆长度(避免历史对话过长)

ConversationBufferMemory无限制追加对话历史,当对话轮数过多时,会导致提示词过长、推理成本增加、模型注意力分散

解决方案:使用ConversationBufferWindowMemory(窗口记忆),仅保留最近N轮对话,本质是ConversationBufferMemory的进阶版,参数完全兼容:

from langchain.memory import ConversationBufferWindowMemory

# 仅保留最近2轮对话,超出的自动丢弃
memory = ConversationBufferWindowMemory(
    memory_key="chat_history",
    k=2,  # 核心参数:保留最近k轮对话
    return_messages=False
)

# 用法与ConversationBufferMemory完全一致,无需修改其他代码
conversation_chain = ConversationChain(llm=llm, memory=memory, prompt=prompt)

5.3 优化3:记忆格式为消息对象(适合复杂提示)

当提示模板需要更精细的对话格式时,将return_messages=True,记忆会返回HumanMessage/AIMessage对象,而非拼接字符串,便于灵活格式化:

# 初始化记忆:返回消息对象
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True,  # 核心:返回消息对象
    input_key="input"
)

# 自定义提示模板:遍历消息对象格式化
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder

# 使用MessagesPlaceholder接收消息对象,无需手动拼接
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是专业的问答助手,结合历史对话回答问题。"),
    MessagesPlaceholder(variable_name="chat_history"),  # 匹配memory_key
    ("human", "{input}")  # 匹配input_key
])

# 构建链(用法不变)
conversation_chain = ConversationChain(llm=llm, memory=memory, prompt=prompt)

六、常用操作:记忆的增删改查

ConversationBufferMemory提供了便捷的方法操作记忆,满足个性化需求:

# 1. 查看记忆内容
memory.load_memory_variables({})  # 返回字典,键为memory_key

# 2. 清空记忆(会话结束/切换用户时使用)
memory.clear()

# 3. 手动添加记忆
memory.save_context(
    inputs={"input": "手动添加的问题"},
    outputs={"output": "手动添加的回答"}
)

# 4. 手动删除记忆(需先获取记忆,再修改,最后重新保存)
# 步骤1:获取记忆内容
chat_history = memory.load_memory_variables({})["chat_history"]
# 步骤2:修改/删除内容(如删除最后一行)
chat_history = "\n".join(chat_history.split("\n")[:-2])
# 步骤3:重新保存
memory.chat_memory.add_user_message("")  # 清空原有记忆
memory.chat_memory.add_ai_message("")
memory.save_context(inputs={"input": ""}, outputs={"output": chat_history})

七、总结

7.1 核心流程回顾

为问答Agent添加ConversationBufferMemory的核心步骤仅5步:

  1. 安装LangChain核心依赖+大模型依赖;
  2. 初始化LLM模型(OpenAI/国产模型);
  3. 初始化ConversationBufferMemory,指定memory_key
  4. 构建包含记忆变量的提示模板;
  5. 将LLM、记忆、提示模板绑定到对话链,调用invoke实现多轮问答。

7.2 记忆组件选型建议

记忆组件核心特点适用场景
ConversationBufferMemory无限制保存所有对话历史,轻量易用短对话、测试场景
ConversationBufferWindowMemory保留最近N轮对话,限制长度常规多轮问答场景(推荐)
ConversationSummaryMemory对长对话历史做摘要压缩,节省令牌超长对话、高成本模型场景
VectorStoreRetrieverMemory将对话历史存入向量库,按需检索相关历史需精准匹配历史对话的复杂场景

7.3 性能优化建议

  1. 优先使用ConversationBufferWindowMemory,并设置合理的k值(如3-5轮);
  2. 降低LLM的max_tokens,避免无意义的长回答;
  3. 开启verbose=False(生产环境),减少日志开销;
  4. 生产环境中,可将记忆与会话ID绑定,实现多用户隔离。

八、常见问题排查

问题1:记忆失效,模型不结合历史对话回答

  • 原因:提示模板缺少memory_key变量,或input_variables未包含该变量;
  • 解决:检查提示模板,确保包含{chat_history}(或自定义的memory_key),且input_variables列表包含该变量。

问题2:调用时提示「key error: input」

  • 原因:invoke入参不是字典,或键与input_key不匹配;
  • 解决:调用时传{"input": "你的问题"}(默认input_key="input"),若修改了input_key,则传对应键。

问题3:知识库问答Agent记忆失效

  • 原因:RetrievalQA的默认输入键是question,而非input,记忆的input_key不匹配;
  • 解决:初始化记忆时设置input_key="question"

问题4:对话历史过长,模型推理变慢

  • 原因:ConversationBufferMemory无限制追加历史;
  • 解决:替换为ConversationBufferWindowMemory,设置k值限制轮数。