1. 环境准备与基础概念

如果你和我一样,是个喜欢在本地折腾AI的开发者,那么Ollama和LangChain的组合,绝对是你不能错过的“黄金搭档”。简单来说,Ollama就像一个超级方便的本地模型管理器,让你一条命令就能把Llama、Qwen这些强大的开源模型“请”到自己的电脑上运行。而LangChain呢,它不是一个具体的模型,而是一个“框架”,或者说是一个“乐高积木工具箱”。它把调用模型、处理提示词、管理对话历史、连接外部知识库这些繁琐的步骤,都做成了标准化的组件。你只需要像搭积木一样把它们组合起来,就能快速构建出功能丰富的AI应用,而不用从零开始写一大堆胶水代码。

这个组合最大的魅力在于“可控”和“私密”。所有的计算和数据处理都在你自己的机器上完成,不用担心数据上传到云端的安全和隐私问题。对于想快速验证想法、开发内部工具或者学习大模型应用开发的朋友来说,这几乎是目前最顺滑的路径。接下来,我就带你从零开始,一步步搭建环境,并实现从简单聊天到复杂问答系统的全过程。我会把我在实际项目中踩过的坑和总结的最佳实践都分享给你,保证你能跟着做出来。

1.1 安装依赖:一步到位的配置清单

万事开头难,但配置环境其实可以很简单。我建议你直接使用condavenv创建一个独立的Python环境,这能避免后续各种烦人的包版本冲突。这里我以conda为例,如果你用venv,命令也大同小异。

首先,创建并激活一个新环境:

conda create -n langchain-ollama python=3.10
conda activate langchain-ollama

接下来是关键的一步:安装核心依赖包。这里我强烈建议你使用国内的镜像源,速度会快很多。下面这条命令是我测试过最稳定、兼容性最好的组合:

pip install langchain-ollama langchain langchain-community Pillow faiss-cpu -i https://pypi.tuna.tsinghua.edu.cn/simple

我来解释一下每个包是干什么的:

  • langchain-ollama:这是连接LangChain和Ollama的桥梁,是本次教程的绝对核心。没有它,LangChain就找不到你本地的Ollama模型。
  • langchain:LangChain框架的核心库,提供了链(Chain)、记忆(Memory)、提示模板(PromptTemplate)等基础抽象和实现。
  • langchain-community:社区维护的集成库,里面包含了大量与第三方工具(如FAISS向量数据库)的连接器。我们后面构建RAG系统时会用到它里面的FAISS模块。
  • Pillow:一个图像处理库。如果你后续想玩多模态模型,让AI“看图说话”,那就需要它来处理图片。
  • faiss-cpu:Facebook开源的向量相似度搜索库,我们用它来构建本地知识库的索引。注意,这里我安装的是CPU版本(faiss-cpu),对绝大多数入门和中等规模的应用来说完全够用,安装也更简单。如果你的机器有NVIDIA显卡并且想追求极致的检索速度,可以安装faiss-gpu,但前提是必须配置好CUDA环境,否则会报错。

安装完成后,你可以通过 pip list 命令检查一下这些包是否都已就位。有时候网络波动可能会导致某个包没装全,多试一次即可。

1.2 下载与运行模型:启动你的本地“大脑”

环境搭好了,我们还需要一个“大脑”,也就是大语言模型。Ollama让下载模型变得无比简单。打开你的终端(命令行),确保Ollama服务已经启动(通常安装Ollama后它会自动在后台运行),然后就可以拉取模型了。

目前社区里比较活跃且效果不错的模型有很多,比如Meta的Llama 3.1系列、阿里的Qwen 2.5系列,还有专为代码优化的DeepSeek-Coder等。对于初学者,我推荐从 llama3.1:8b 这个版本开始。它在效果和资源消耗上取得了很好的平衡,8B参数对大多数消费级显卡(如RTX 4060 8G)或强一点的CPU都足够友好。

下载命令非常简单:

ollama pull llama3.1:8b

这条命令会从Ollama的官方仓库下载模型文件到本地。下载速度取决于你的网络,模型大小一般在几个GB。完成后,你可以运行 ollama list 来查看本地已有的模型。

怎么验证模型真的能工作呢?你可以直接用Ollama的原生命令来一次快速对话测试:

ollama run llama3.1:8b

然后输入“你好”,看看它会不会用中文回复你。如果一切正常,恭喜你,你的本地大模型已经准备就绪了!接下来,我们就可以用LangChain这个更强大的工具来“驾驶”它了。

2. LangChain核心组件初体验

现在,我们的“赛车”(Ollama模型)和“赛道”(Python环境)都准备好了,是时候上手LangChain这个“方向盘”了。别被“框架”这个词吓到,LangChain的设计理念就是让复杂的事情变简单。我们先从最基础的几个组件玩起,感受一下它的便捷性。

2.1 你的第一个对话链:与模型打个招呼

让我们写第一个脚本,用LangChain的方式调用Ollama模型。创建一个Python文件,比如叫 first_chat.py

from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate

# 1. 初始化模型“司机”
model = ChatOllama(model="llama3.1:8b", temperature=0.7)
# temperature参数控制创造性,0.7是个不错的起点,既有创意又不至于胡言乱语。

# 2. 设计“任务指令卡”(提示模板)
template = """
你是一个风趣幽默的AI助手,喜欢用比喻和例子来解释问题。
请用简单易懂的方式回答用户的问题。

问题:{question}
"""
prompt = ChatPromptTemplate.from_template(template)

# 3. 组装“流水线”(链)
chain = prompt | model  # 这个“|”管道符是LangChain的灵魂,代表“然后”

# 4. 启动流水线
question = "如何向一个10岁的孩子解释什么是人工智能?"
result = chain.invoke({"question": question})

print("AI的回答:", result.content)

运行这个脚本,你应该能看到一个生动有趣的解释。这里发生了什么?我把它拆解一下:

  • ChatOllama:这是我们对本地Ollama模型的封装。通过它,我们可以用统一的接口调用模型,而不需要关心底层HTTP请求的细节。
  • ChatPromptTemplate:提示模板。你可以把它理解为一个填空题模板。{question} 就是一个占位符,我们传入的字典 {"question": "..."} 会把占位符替换成真实的问题。用好提示模板是构建稳定AI应用的关键。
  • 链(Chain)prompt | model 这行代码创建了一个最简单的链。它的工作流程是:接收输入 -> 用模板格式化 -> 送给模型 -> 输出结果。这种声明式的组合方式,让逻辑非常清晰。

踩坑提醒:如果你在这里遇到连接错误,比如 ConnectionError,请首先确认Ollama服务是否在运行。在终端执行 ollama serve 可以启动服务。另外,检查一下 model= 参数里的模型名是否和你用 ollama list 看到的完全一致。

2.2 实现流式输出:让回答“打字”出来

你有没有用过ChatGPT,看着答案一个字一个字地蹦出来?这种体验比干等十几秒然后突然出现一大段文字要好得多。这就是流式输出。在构建聊天应用时,这个功能几乎是必选项。

用LangChain实现流式输出非常简单,它已经帮我们封装好了。看下面的代码:

from langchain_ollama import ChatOllama

model = ChatOllama(model="llama3.1:8b", temperature=0.7)

# 构造对话消息。LangChain通常使用消息列表作为输入。
messages = [
    ("human", "请用一段话描述夏天夜晚的星空。"),
]

print("AI正在思考...:")
for chunk in model.stream(messages):
    # chunk是一个数据块对象,我们取它的content属性
    print(chunk.content, end='', flush=True)
# 注意这里的 end='' 防止换行,flush=True 确保内容立即显示

运行这段代码,你会看到回答像流水一样逐步打印在屏幕上。model.stream() 方法返回的是一个生成器(generator),它不会等模型生成完整句子,而是收到一点就返回一点。这对于构建WebSocket或SSE(Server-Sent Events)接口的实时聊天应用是基础。

性能小贴士:流式输出不仅能提升用户体验,在生成很长文本时,它也能让你提前判断内容是否跑偏,有机会中途停止(比如按Ctrl+C),节省时间和算力。

2.3 赋予模型“工具调用”能力

大模型很聪明,但它也不是万能的。比如,它可能不擅长精确计算,或者不知道最新的股票价格。这时,“工具调用”功能就派上用场了。我们可以教模型,当遇到特定类型的问题时,去调用我们写好的一个Python函数,然后把函数执行结果作为参考,再生成最终回答。

这听起来很复杂,但LangChain让它变得直观。假设我们想让AI能进行简单的四则运算:

from langchain_ollama import ChatOllama
from langchain_core.tools import tool
from pydantic import BaseModel, Field

# 1. 用Pydantic定义工具输入参数的“说明书”
class CalculatorInput(BaseModel):
    operation: str = Field(description="运算类型,只能是 'add', 'subtract', 'multiply', 'divide' 中的一个")
    x: float = Field(description="第一个数字")
    y: float = Field(description="第二个数字")

# 2. 创建具体的工具函数,并用 @tool 装饰器标记
@tool(args_schema=CalculatorInput)
def simple_calculator(operation: str, x: float, y: float) -> float:
    """执行简单的加减乘除运算。"""
    if operation == "add":
        return x + y
    elif operation == "subtract":
        return x - y
    elif operation == "multiply":
        return x * y
    elif operation == "divide":
        if y == 0:
            return "错误:除数不能为零"
        return x / y
    else:
        return "错误:未知的运算类型"

# 3. 初始化模型,并“绑定”工具
llm_with_tools = ChatOllama(
    model="llama3.1:8b",
    temperature=0,
).bind_tools([simple_calculator]) # 关键一步:把工具“装”到模型上

# 4. 提问一个需要计算的问题
result = llm_with_tools.invoke("请计算一下,一千万乘以二再加上五百等于多少?")
print("模型原始响应:", result)

# 5. 检查模型是否“决定”调用工具
if result.tool_calls:
    print("\n模型决定调用工具!")
    for tool_call in result.tool_calls:
        # tool_call.name 是工具函数名,tool_call['args'] 是参数字典
        print(f"调用的工具: {tool_call.name}")
        print(f"传入的参数: {tool_call['args']}")
        # 这里在实际应用中,你需要根据name找到对应的函数并执行
        # 然后将结果作为新的上下文,再次传给模型得到最终回答

运行后,你会看到模型的响应内容里,除了常规的文本,可能还会包含一个特殊的 tool_calls 字段。这表示模型“思考”后认为,要准确回答这个问题,需要先调用计算器工具。在实际的完整应用里,我们会解析这个请求,执行真正的 simple_calculator 函数,把结果(比如20000000)和原始问题一起再送回给模型,让它组织成最终的自然语言答案:“一千万乘以二再加上五百等于两千零五百万。”

这个模式非常强大,意味着你可以把数据库查询、调用天气API、发送邮件等任何功能封装成工具,让大模型学会在合适的时候使用它们,从而突破其自身能力的限制。

3. 构建多轮对话与记忆系统

基础的问答是一次性的。但真正的对话是有上下文的,AI需要记住之前说过什么。比如你问“梅西是谁?”,它回答后你再问“他效力于哪个俱乐部?”,一个没有记忆的AI会一脸茫然。LangChain提供了多种“记忆(Memory)”组件来解决这个问题。

3.1 使用ConversationChain管理对话历史

ConversationChain 是LangChain里一个开箱即用的高级链,它把模型、提示模板和记忆组件打包在了一起,用起来非常省心。

from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain_ollama import OllamaLLM

# 初始化模型。注意这里用了OllamaLLM,它是ChatOllama的一个更基础的版本,同样兼容。
llm = OllamaLLM(model="llama3.1:8b")

# 初始化记忆组件。ConversationBufferMemory会像记事本一样记录所有历史对话。
memory = ConversationBufferMemory()

# 创建对话链
conversation = ConversationChain(
    llm=llm,
    memory=memory,
    verbose=True  # 设为True可以看到链的思考过程,调试时非常有用
)

# 开始多轮对话
print("第一轮:")
response1 = conversation.predict(input="你好,我想学习编程,有什么建议吗?")
print("AI:", response1)

print("\n第二轮(我接着上面的话题问):")
response2 = conversation.predict(input="Python和Java,我应该先学哪个?")
print("AI:", response2)

print("\n第三轮(我换了个话题):")
response3 = conversation.predict(input="对了,你刚才提到的那个在线学习平台叫什么来着?")
print("AI:", response3)

# 我们甚至可以查看记忆体里具体存了什么
print("\n--- 当前记忆内容 ---")
print(memory.buffer)

运行这段代码,你会看到在第三轮中,AI准确地回忆起了第一轮中它可能推荐过的某个学习平台名称(比如Coursera)。这就是记忆在起作用。ConversationBufferMemory 的工作原理很简单:把每一轮的“人类输入”和“AI输出”都拼接成一个长字符串,下次提问时,把这个长字符串作为历史上下文一起送给模型。

局限性:这种方法虽然简单有效,但有个明显问题——模型有上下文长度限制(比如4096个token)。如果对话进行得非常长,这个“记事本”就会超长,导致最开始的对话被“挤出去”或者直接触发模型长度限制而报错。

3.2 更智能的记忆管理:总结记忆与窗口记忆

为了解决长对话问题,LangChain提供了更高级的记忆组件。我常用的是 ConversationSummaryMemoryConversationBufferWindowMemory

  • ConversationSummaryMemory:它不会事无巨细地记录每一句话,而是会定期(或每次交互后)让模型自己总结一下之前的对话要点,然后只保存这个总结。这样就能用很少的token保留对话的核心信息,非常适合超长对话。
  • ConversationBufferWindowMemory:它像一个“滑动窗口”,只保留最近K轮的对话。比如设置 k=3,它就只记住最新的3组问答,更早的自动丢弃。这在关注近期上下文、不依赖远古历史的场景下(如客服对话)非常高效。

这里给你一个 ConversationSummaryMemory 的示例:

from langchain.memory import ConversationSummaryMemory
from langchain_ollama import ChatOllama
from langchain.chains import ConversationChain

llm = ChatOllama(model="llama3.1:8b")

# 使用总结记忆,需要传入llm,因为它要靠llm来做总结
memory = ConversationSummaryMemory(llm=llm)

conversation = ConversationChain(
    llm=llm,
    memory=memory,
    verbose=False
)

# 进行多轮对话...
# memory.buffer 里存储的将不再是原始对话,而是经过提炼的总结文本。

选择哪种记忆方式,取决于你的应用场景。如果对话逻辑连贯、需要引用很久之前的信息(比如讨论一部复杂小说的情节),总结记忆更好。如果只是简单的多轮澄清(比如订餐:选菜品 -> 选口味 -> 确认地址),窗口记忆就足够了。

4. 实战:搭建你的第一个RAG问答系统

前面我们都是在和模型的“通用知识”对话。但如果我想让AI回答我私人文档里的内容,或者公司内部的知识库呢?这就需要 RAG(检索增强生成) 技术了。它的核心思想是“先检索,后生成”:当用户提问时,先从你的知识库(比如一堆PDF、TXT文档)里找到最相关的几段资料,然后把问题和这些资料一起交给模型,让它基于这些“证据”来生成答案。这样既能利用模型的推理和语言能力,又能确保答案的准确性和时效性。

下面,我们就用LangChain和Ollama,手把手构建一个最简单的RAG系统。

4.1 准备知识库与创建向量索引

RAG的第一步,是把文本知识变成模型能快速查找的格式。我们使用“向量嵌入(Embedding)”技术,把句子转换成一组数字(向量)。语义相似的句子,它们的向量在空间里的距离也近。搜索时,我们把问题也转换成向量,然后快速找到知识库里和它向量最接近的文本块。

首先,确保你安装了 faiss-cpu 和下载了嵌入模型:

ollama pull nomic-embed-text # 这是一个小巧高效的嵌入模型

然后,我们开始写代码:

from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 1. 初始化嵌入模型。它负责把文字变成向量。
embeddings = OllamaEmbeddings(model="nomic-embed-text")

# 2. 准备你的知识库文本。这里我们用一个字符串模拟,实际中可以从文件读取。
knowledge_text = """
LangChain 是一个用于开发由大语言模型驱动的应用程序的框架。
它让应用程序具备以下能力:上下文感知(将语言模型与上下文源连接),推理能力(依赖语言模型进行推理)。
Ollama 是一个本地大模型运行和部署工具,支持一键拉取和运行多种开源模型,如 Llama 3、Qwen 等。
RAG,即检索增强生成,是一种通过检索外部知识源来增强大语言模型生成答案准确性和相关性的技术。
"""

# 3. 分割文本。模型有输入长度限制,大文档必须切分成小块。
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=150,  # 每个文本块的最大字符数
    chunk_overlap=30   # 块与块之间的重叠字符,防止把完整句子切碎
)
chunks = text_splitter.split_text(knowledge_text)
print(f"将知识库切分成了 {len(chunks)} 个文本块。")
for i, chunk in enumerate(chunks):
    print(f"块 {i+1}: {chunk[:60]}...") # 打印前60字符预览

# 4. 创建向量存储(索引)。这是最核心的一步。
vectorstore = FAISS.from_texts(chunks, embeddings)
print("向量索引创建成功!")

# 5. 将向量存储转换为检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 2}) # 检索最相似的2个块

关键参数解析

  • chunk_size:不宜过大,一般200-500之间。太大可能包含无关信息,太小则可能丢失上下文。
  • chunk_overlap:设置重叠可以避免一个完整的知识点被硬生生切成两半,检索时信息更完整。
  • search_kwargs={"k": 2}:表示每次检索返回最相似的2个文本块。k值需要权衡,太小可能漏掉信息,太大则会给模型带来无关噪音并增加处理负担。

4.2 组装RAG链:检索与生成的完美结合

索引建好了,现在我们需要构建一个链,它能自动完成“接收问题 -> 检索相关片段 -> 组合提示 -> 生成答案”这一整套流程。

from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough

# 1. 初始化用于生成答案的大模型
llm = ChatOllama(model="llama3.1:8b", temperature=0.1) # RAG中temperature可以设低点,更忠实于资料

# 2. 设计一个给模型的“指令模板”
template = """
请你严格根据以下提供的上下文信息来回答问题。如果上下文里没有答案,就直接说“根据提供的资料,我无法回答这个问题”,不要编造信息。

上下文信息:
{context}

问题:{question}

请根据上下文给出答案:
"""
prompt = ChatPromptTemplate.from_template(template)

# 3. 组装RAG链(这是最精妙的一步)
rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
)

# 4. 提问!
question = "Ollama 是什么?"
answer = rag_chain.invoke(question)
print(f"问题:{question}")
print(f"答案:{answer.content}\n")

# 再问一个知识库里没有的问题
question2 = "LangChain 是用什么编程语言写的?"
answer2 = rag_chain.invoke(question2)
print(f"问题:{question2}")
print(f"答案:{answer2.content}")

这段代码的精华在于 rag_chain 的组装。我用一个字典 {"context": retriever, "question": RunnablePassthrough()} 作为链的起点。它的意思是:对于输入的问题,retriever 组件会去执行检索操作,得到 context(上下文);而 RunnablePassthrough() 则表示让 question 原封不动地传递下去。然后,contextquestion 这两个值会被填入到 prompt 模板中对应的位置,形成完整的提示词,最后送给 llm 生成答案。

运行一下,你会看到对于“Ollama是什么”这个问题,AI能准确引用我们知识库里的定义来回答。而对于“LangChain用什么语言写的”这个我们没提供的信息,它应该会老实地说无法回答。这就是RAG的核心价值:让模型学会“知之为知之,不知为不知”,大大减少了“幻觉”(即胡编乱造)问题。

4.3 进阶优化:提升RAG效果的实用技巧

一个基础的RAG系统跑起来后,你可能会发现答案有时候不够精准,或者检索不到相关内容。别急,这是正常的。RAG系统的效果取决于“检索”和“生成”两个环节的质量。这里分享几个我实践中总结的优化点:

1. 优化文本分割策略: RecursiveCharacterTextSplitter 默认按字符分割,可能会切断句子。对于中文,可以尝试用 CharacterTextSplitter 并指定分隔符,或者使用更高级的 SpacyTextSplitter(需要安装spacy和中文模型)。

from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
    separator="。", # 按句号分割
    chunk_size=200,
    chunk_overlap=50,
    length_function=len,
    is_separator_regex=False,
)

2. 优化检索过程:

  • 调整 k:多试试 k=1, 2, 3 的效果。
  • 使用MMR(最大边际相关性)检索:它不仅考虑相似度,还考虑检索结果之间的多样性,避免返回内容重复的片段。
    retriever = vectorstore.as_retriever(
        search_type="mmr", # 使用MMR算法
        search_kwargs={"k": 3, "fetch_k": 5} # 最终返回3个,从最相似的5个中挑选
    )
    
  • 为检索器添加元数据过滤:如果你的文档有标签、日期等元信息,可以在检索时进行过滤,让结果更精准。

3. 优化提示模板: 提示模板是指挥模型如何利用检索到的上下文的关键。一个更强大的模板可以这样写:

template = """
你是一个严谨的问答助手。请严格遵循以下步骤:
1. 仔细阅读以下上下文:
{context}
2. 思考上下文是否包含了回答以下问题的足够信息。
3. 如果信息足够,请用简洁明了的语言回答问题。
4. 如果信息不足或完全不相关,请明确告知用户:“根据已知信息,我无法回答这个问题。”

问题:{question}

请开始你的回答:
"""

通过给模型更清晰的指令步骤,可以引导它更好地分析和利用上下文。

4. 处理超长上下文: 如果检索返回的多个文本块加起来太长,超过了模型的上下文窗口,你需要进行压缩。LangChain提供了 ContextualCompressionRetriever,可以与 LLMChainExtractor 结合,先用一个小模型去总结每个检索到的片段,再把总结后的精简内容送给大模型,这是一个高级但非常有效的技巧。

构建RAG系统是一个迭代优化的过程。从最简单的版本开始,然后根据实际问答的效果,从分割、检索、提示这几个环节逐一排查和优化,你的智能问答助手就会变得越来越聪明、越来越可靠。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐