☰
LangChain+Ollama搭建完全离线AI助手:本地部署与RAG实践指南
2026/10/18 12:16:14 网站建设 项目流程

今天是我给自用AI助手“断奶”的第19天。前18天功能全靠云端模型接口,能用是能用,但心里一直不踏实。直到有一次给同事演示内部资料问答,网络一抖,整屏白掉,当场翻车,我下定决心:把模型彻底搬到本地。用LangChain接上Ollama之后,我总算拥有了一套完全离线的AI助手——断网照样多轮对话,本地文档照样能问,所有数据都不出这台机器。这篇把从零到一的完整过程写出来,包括选型理由、环境搭建、代码实现和五个实打实的坑,适合想在完全离线环境里搭AI助手的开发者和技术爱好者参考。

1. 为什么要把AI助手彻底离线:一次断网引发的“本地化”执念

1.1 云上AI的三种“不可用”:隐私、延迟、成本

先说动机,不然你很难理解后面为什么要折腾这一大圈。

我在这个项目里要处理的,一半是内部的说明文档、总结类文本,另一半是我自己攒的笔记和技术资料。这些东西放在云端的聊天框里看着方便,但只要一想到它们会被传到外部服务器,心里就不踏实。有些资料本身就有明确要求,数据不能出内网,领导看看可以,导出去绝对不行。这是隐私层面最直接的一堵墙,也是离线方案最值钱的地方。

其次是延迟。云端接口看起来快,但一次请求要经过网络、排队、限流,做简单问答还好,一旦批量总结几十页文档,速度非常不稳定。我实测过,高峰期接口排队能排到十几秒,交互体验远不如本地模型在CPU上慢悠悠但稳定的产出节奏。对一个天天要用的助手来说,“稳定地慢”比“偶尔地快”舒服太多了。

最后是成本。日调用量一上去,账单就开始离谱。我算了一笔账:按我这个项目的调用频率,一个月省下来的API费用足够买一台二手小主机专门跑本地模型。与其长期把数据交给别人还交钱,不如一次性投入把推理能力放在自己手里。

1.2 本地模型不是降级:现在的开源模型已经够用

很多人一听“本地模型”就想到小模型智商不高。说实话两年前确实如此,7B模型写几句代码就开始胡编。但最近这一两年开源模型进步非常明显,7B到14B这个量级的模型在中文理解、摘要、问答这些常见任务上,已经能保质保量地完成。

我做了一套简单评测,拿几十条工作场景问题分别问了云端模型和本地7B模型,结论是:日常问答和资料摘要类任务,本地模型的表现差距不大,只有在特别复杂的长文推理和高质量代码生成上才有明显差距。而对离线助手来说,稳定、私有、可复现往往比“偶尔聪明一点”更重要。

再加上嵌入模型也可以本地跑,整个链路——对话、向量化、检索——没有任何一个环节需要联网。这是真正意义上的“彻底离线”,而不是把几个组件简单换成自托管的伪离线。

1.3 先定原则再动手:我这套方案的四条铁律

  • 第一条:整条链路任何环节都不允许出现外部HTTP调用,违反这一条直接重写。
  • 第二条:优先中文友好的模型。我最终选定 qwen2.5 系列做对话,nomic-embed-text 做向量。
  • 第三条:少装重型组件。能用一个进程解决的事,不引入数据库和中间件。
  • 第四条:所有模型名、服务地址、上下文长度全部参数化,换模型不重写代码。

这几条看着简单,实际执行时帮我挡掉了很多诱惑。比如有人建议我再上个重型向量数据库、再套一层问答服务框架,我全拒绝了——离线场景下,组件越少,排障越容易,依赖越少,越不会出现“这台机器装不上”的尴尬。

2. Ollama这个模型运行器的正确打开方式:安装、拉模型与API初探

2.1 从安装到跑通第一个对话,五分钟搞定

Ollama本质上是一个本地模型运行器:它负责下载模型、管理量化权重、调度GPU/CPU推理,并对外暴露一个标准的HTTP接口。选它的核心原因就是省心,不用手动去配Python推理框架,也不用自己折腾显存分配那一大堆东西。安装也简单,官网按平台下载安装包即可,Linux下可以直接用安装脚本:

curl -fsSL https://ollama.com/install.sh | sh

装完之后验证一下:

ollama --version ollama pull qwen2.5:7b ollama run qwen2.5:7b

ollama run会直接进入一个交互式对话界面,输入“你好”能看到模型开始逐字回复,到这里第一个模型就算跑通了。我这次的环境是Python 3.11、Ollama 0.3.x、langchain-ollama 0.2.x,后面给的所有代码都在这套组合下验证过。顺便说几个我高频使用的管理命令:

  • ollama list:查看本地已下载的模型。
  • ollama ps:查看当前正在内存里加载的模型,以及它们占用的上下文空间。
  • ollama rm 模型名:删除不用的模型。
  • ollama show 模型名:查看模型元信息。

2.2 量化等级和模型大小,内存不够时的取舍表

所有通过Ollama下载的模型基本都是GGUF格式的量化权重。量化可以简单理解为“用精度换体积”:权重从16位近似压缩到4位或8位,文件体积直接缩小好几倍,质量损失控制在可接受范围。Ollama拉模型时默认选的是Q4_K_M级别,这也是目前公认性价比最高的平衡点。

我自己实测的感受:Q4_K_M和Q8_0在日常问答里几乎听不出区别,但文件体积差了近一倍。所以除非机器内存非常宽裕,建议默认就用Q4_K_M。下表是我实际用过的几组模型参考占用情况:

模型量化级别文件大小(约)建议运行内存
qwen2.5:7bQ4_K_M4.7GB8GB以上
qwen2.5:14bQ4_K_M9.0GB16GB以上
qwen2.5:32bQ4_K_M19GB32GB以上
llama3.1:8bQ4_K_M4.7GB8GB以上
nomic-embed-textF16274MB1GB即可

注意,这里“文件大小”和实际运行时的内存占用不是一回事。模型加载进内存后还需要额外的上下文KV缓存,如果你把上下文设到16K甚至更大,内存占用会明显上涨。所以内存规划一定要留出余量,别卡着表格数字配机器,否则一开多轮对话就等着系统卡死。

2.3 藏在Ollama背后的HTTP API:为什么它是接入一切的钥匙

虽然Ollama自带交互终端,但它真正的价值是后台那个本地API服务,默认监听在127.0.0.1:11434。这意味着任何语言、任何框架都可以通过HTTP和本地模型通信,LangChain也只是它的一个客户端而已。我平时调试第一反应就是先打一行curl,亲眼确认模型没问题再往上查上层代码:

curl http://127.0.0.1:11434/api/chat -d '{ "model": "qwen2.5:7b", "messages": [{"role": "user", "content": "你好,请简单介绍一下自己"}], "stream": false }'

响应里会带着模型生成的内容、token统计、耗时等元信息。常用的接口还有:

  • GET /api/tags:列出当前已下载的模型。
  • POST /api/embed:生成文本向量。
  • POST /api/generate:老版本的单轮补全接口,现在多数场景用/api/chat即可。

如果想让局域网里的其他机器也能访问,启动时把监听地址放出来就行:

OLLAMA_HOST=0.0.0.0 ollama serve

这个能力我后面用上了——给同事提供离线问答入口时,不需要每台机器都装一套模型,一台主机当推理服务器就足够了。

提示:改监听地址前确认一下自己所在网络的防火墙策略,局域网开放服务一定要先想好谁能访问,别把API裸奔到不可控的范围。

3. 把LangChain接到本地模型的三种姿势:从直接调用到RAG再到大模型应用

3.1 为什么选LangChain:省掉的都是重复轮子

断网环境下你当然可以对着Ollama的HTTP API手撸对话服务,但一旦要开始做多轮记忆、提示词模板、文档检索,你很快就会发现自己在一遍一遍地写重复代码:拼接历史消息、切分文档、管理向量库连接……LangChain把这几个高频环节都抽象成了标准组件,接上之后确实省事。

它给Ollama提供了两个关键封装:ChatOllama负责对话补全,OllamaEmbeddings负责文本向量化。我安装的是官方拆分后的稳定包:

pip install langchain-ollama langchain-community langchain-core

这里插一句重要的:LangChain的版本迭代非常快,API变动也频繁,尽量优先用拆分后的子包,而不是一整套大杂烩。我身边已经不止一个人因为装了过度版本导致接口对不上、满屏过时警告,排查到怀疑人生。

3.2 最简单的姿势:ChatOllama直接对话

先看最基础的用法,把ChatOllama实例化出来:

from langchain_ollama import ChatOllama llm = ChatOllama( model="qwen2.5:7b", base_url="http://127.0.0.1:11434", temperature=0.7, num_ctx=8192, ) resp = llm.invoke("用一句话说清楚什么是RAG") print(resp.content)

几个参数值得展开讲:

  • model:对应Ollama里已经下载的模型名,换成qwen2.5:14b之类的名字即可切换。
  • base_url:Ollama服务的地址,默认本机是http://127.0.0.1:11434,远端部署时改成对应IP。
  • temperature:控制随机性。总结类任务我调到0.2,日常聊天0.7,创意类任务才会往上拉。
  • num_ctx:上下文窗口长度,这个在后半部分踩坑章节还会重点讲。

invoke返回的是一个AIMessage对象,里面的content字段才是模型生成的文本。新手容易直接打印整个对象,结果满屏都是token统计和调用元数据,误以为代码出了问题。

3.3 再进一步:LCEL、Embedding和本地知识库

单纯一句对话没什么意思,LangChain更擅长的是把多个组件串起来形成管线,官方叫LCEL。它的核心思路就是管道操作符|,左边组件输出直接喂给右边组件输入,代码非常好读:

from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个严谨的助手,回答尽量简洁。"), ("human", "{question}"), ]) chain = prompt | llm | StrOutputParser() print(chain.invoke({"question": "离线部署AI助手有什么好处?"}))

这里StrOutputParser把AIMessage对象转成纯文本,后面接其他组件时方便很多。接下来是Embedding:

from langchain_ollama import OllamaEmbeddings embeddings = OllamaEmbeddings(model="nomic-embed-text") vec = embeddings.embed_query("测试一下这句话的向量") print(len(vec)) # 输出 768

别小看这一步,本地知识库问答的核心就是把文本切片转成向量存起来,用户提问时再把问题转成向量去库里做相似度检索。嵌入模型不联网、体积小、速度快,非常适合离线跑。

3.4 三种接入方式的对比:什么时候用哪个

我把这段时间用过的接入方式整理成一张表,方便你按场景对号入座:

接入方式典型场景复杂度我的评价
ChatOllama直接invoke单轮对话、快速验证低最快跑通,但能力有限
LCEL模板链固定格式的任务,比如摘要、翻译中代码清晰,可控性好
RAG检索链 / Agent知识库问答、工具调用高能力最完整,排障也最难

实际项目中我经常同时用后两种:简单对话走LCEL,涉及内部资料的问题走RAG检索链。分工明确,互不干扰。等你熟悉了这套结构,再往上加Agent之类的复杂度也会顺很多。

4. 一套可以直接照抄的离线问答助手代码链路

4.1 整体架构:消息怎么走,文档怎么参与

这个完全离线的问答助手,我把它拆成了两条支路。

支路一是普通对话:用户输入进来,套上系统提示词,直接交给ChatOllama,返回结果。支路二是知识库问答:用户输入先不急着生成答案,而是先做向量检索,把向量库里最相关的几个文档片段捞出来,连同用户问题一起塞进提示词,让模型基于这些片段作答。

你可能会问为什么非要绕这一圈。原因很简单:7B模型没有能力记忆超大语料,直接问“手册里怎么重置密码”,它要么胡编,要么说不知道。而把相关片段塞进上下文之后,它就像拿到了一本翻开的书,照着念也能答准。这就是所谓RAG的基本逻辑。

完整链路是:文档加载、文本切分、向量化、存入向量库;查询时则是:问题向量化、相似度检索、提示词组装、模型生成。整个流程看下来很对称,这也是为什么本地模型方案在工程上特别清爽——每块都能独立替换。

4.2 本地知识库问答:文档切分、向量化、检索、生成

下面这段代码是我实际在项目的干净环境里跑过的,你照抄基本能通。先处理文档:

from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter loader = TextLoader("guide.txt", encoding="utf-8") documents = loader.load() splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, ) chunks = splitter.split_documents(documents) print(f"切分成 {len(chunks)} 个片段")

chunk_size=500表示每个片段大约500个字符,chunk_overlap=80表示相邻片段之间重叠80个字符。重叠的目的很关键:如果一条知识恰好被从中间切断,下一个片段还能保留它的前半段,检索时不容易漏掉。我实测过重叠参数从0调到80,检索命中率有明显提升。

然后生成向量并建库:

from langchain_community.vectorstores import FAISS from langchain_ollama import OllamaEmbeddings embeddings = OllamaEmbeddings(model="nomic-embed-text") vectorstore = FAISS.from_documents(chunks, embeddings) vectorstore.save_local("faiss_index")

FAISS是轻量级的本地向量库,不需要额外起服务,直接存成文件。这里特别提醒:一定要保存到磁盘,不然每次启动都要重新把文档向量化一遍,又慢又费电。接下来组装RAG检索链:

from langchain.chains import create_retrieval_chain from langchain.chains.combine_documents import create_stuff_documents_chain from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "请只根据下面提供的资料回答问题。如果资料里没有答案,就明确说“资料里没有提到”。\n\n资料:\n{context}"), ("human", "{input}"), ]) retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) combine_docs_chain = create_stuff_documents_chain(llm, prompt) rag_chain = create_retrieval_chain(retriever, combine_docs_chain) answer = rag_chain.invoke({"input": "指南里说重置密码的步骤是什么?"}) print(answer["answer"])

检索时k=4是让系统把最相关的4个片段都给模型。太少了容易漏答案,太多了模型会被无关内容干扰,4是我试下来比较稳的数字。这个提示词里“资料里没有答案就直说”也很重要,是压制小模型胡编最有效的一句话。

4.3 多轮记忆:离线下要不要做,怎么做最省

多轮对话时,模型本身是无状态的,上一轮说了什么它根本不知道。要让它“记得”,就得把历史消息主动拼进去。最省事的方式就是维护一个消息列表,每轮对话后把用户消息和模型回复追加进去,下次请求时带上最近几轮:

from langchain_core.messages import HumanMessage, AIMessage history = [] # 每轮做三件事 history.append(HumanMessage(content=user_input)) resp = llm.invoke(history[-6:]) # 只带最近6条消息,控制上下文占用 history.append(AIMessage(content=resp.content))

之所以只用最近6条,是因为7B模型虽然在长上下文下不会立刻报错,但距离太远的旧消息要么被忽略,要么挤占宝贵的上下文空间。对绝大多数离线助手场景,“记住最近几轮”比“完整记住整场对话”更实用也更稳。如果对话特别长,进阶做法是定期把历史总结成摘要再交给主模型,不过这个等真有需求再上也不迟。

4.4 前端体验:命令行版和API服务版,二选一

我先把最简单的命令行版本跑通了,核心就是个死循环:

while True: user_input = input("你: ") if user_input in ("quit", "exit"): break resp = llm.invoke(user_input) print("助手:", resp.content)

命令行适合自己调试,但同事要用就不能这么原始了。后来我用FastAPI套了一层,把问答应答暴露成POST /ask,请求体里带question和可选session_id,响应里返回答案和命中的文档片段来源:

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class AskRequest(BaseModel): question: str session_id: str = "default" @app.post("/ask") def ask(req: AskRequest): resp = llm.invoke(req.question) return {"answer": resp.content, "session_id": req.session_id}

这样浏览器、内网IM机器人、其他内部系统都能来调,整个离线服务就这么活起来了。别急着上太花哨的前端,先把这层API稳定住,后面想接什么入口都方便。

5. 离线化路上那些坑,按踩过的先后顺序讲给你听

5.1 默认上下文只有2048,聊到后面开始“失忆”

第一个坑是我在测试多轮对话时踩到的。前面两三轮还好,到第五六轮,模型开始答非所问,甚至把前面说过的话题忘得一干二净。一开始我以为是模型不行,后来查Ollama的API文档才发现,默认的上下文窗口其实很小,旧版本是2048,新版本放宽到4096,但对多轮对话和RAG来说依然偏小。超出了窗口范围,前面的内容直接被截掉,模型自然“失忆”。

解决方式是在初始化时把num_ctx显式设大。我在ChatOllama里设置了num_ctx=8192,RAG场景下资料、问题、历史一起塞也能装下。这里要提醒一句:上下文越大,KV缓存占的内存越多,8K上下文在7B模型上大概会额外占用1到2GB内存,别盲目上16K或32K。

5.2 换了Embedding模型,旧向量库直接废掉

这个坑相当隐蔽。我一开始用的是某个768维的嵌入模型,后来听说另一个嵌入模型效果更好,就直接改了OllamaEmbeddings里的model名。结果一查向量库,检索出来的内容乱七八糟,相似度得分完全没参考价值。

原因是两个嵌入模型的输出维度不同,语义空间也不同。向量库里已经存的是旧模型生成的向量,用新模型生成的问题向量去和旧向量做相似度计算,完全是鸡同鸭讲。解决办法只有一个:换模型就重建向量库,没有捷径。我后来在建库脚本里顺手把嵌入模型名写进了索引文件名的后缀,比如faiss_index_nomic_768,看到名字就知道里头是什么模型生成的。这个习惯强烈建议养成。

提示:建库时把模型名、维度、切分参数一起记下来。离线项目周期长,隔一个月你自己也不记得当时怎么建的库。

5.3 流式输出神秘消失:版本匹配的教训

做API服务之后我想把流式输出加上,让用户看到逐字生成的效果。结果发现前端一直转圈,要等全部生成完才一次性吐出整段文字。我先用curl直接打Ollama的API,stream: true模式下逐字输出正常,说明问题出在LangChain这一层。

顺着排查,发现是langchain-ollama的某个版本和当时Ollama服务端的流式响应格式存在兼容问题。我的解决方式比较粗暴但有效:先把两边都升级到当前稳定版,然后在代码里直接用stream()方法消费生成流:

for chunk in llm.stream("给我讲讲离线部署的好处"): print(chunk.content, end="", flush=True)

升级之后一切正常。这里给同样头大的同学一个方向:遇到LangChain的怪问题时,先用curl验证底层能力,再逐层往上定位。别一上来就怀疑模型不行,很多问题根本不在模型身上。

5.4 模型常驻内存,笔记本电脑几分钟后开始卡

多模型切换是另一个隐形杀手。我同时拉了好几个模型来回测试,笔记本内存很快就吃紧,系统开始疯狂交换。检查ollama ps发现,刚才用过的模型都还热乎乎地加载在内存里。Ollama默认会让模型在结束请求后驻留一段时间,这样下次调用就省去加载时间,代价就是内存一直被占着。

想释放内存有几种做法:调用API时传"keep_alive": 0让模型用完立即卸载;或者在启动服务时设置环境变量OLLAMA_KEEP_ALIVE=0。我最后用了折中方案,设为5m,既不频繁加载,又不会霸占内存太长时间。如果同时加载多个模型,还可以把OLLAMA_MAX_LOADED_MODELS设成1,让新模型启动时自动踢掉旧模型。

5.5 真正离线环境下的模型分发问题

最后一个坑发生在我准备往一台完全没外网权限的机器上部署时。那台机器连不上外网,ollama pull根本拉不了模型。这个问题的解法是在能联网的机器上先把模型拉好,然后把整个~/.ollama/models目录拷过去。注意不同平台目录位置不一样,Windows下是C:\Users\你\.ollama\models,Linux和macOS下是~/.ollama/models。

除了模型文件,Python依赖也要提前准备好。在有网的机器上用pip download把依赖包下载成whl文件,再拷贝到离线机器上离线安装。别到了现场才发现缺包,内网环境里想临时装什么都难。这个坑我花了整个下午才填平,现在提起来还觉得亏。

6. 离线AI助手的下一步:模型怎么扩、记忆怎么做、工具怎么用

6.1 工具调用的坑:小模型的JSON输出并不稳定

离线助手跑通之后,我开始想让它调用工具,比如查本地日程、执行简单脚本。结果7B量化模型在工具调用上频繁翻车:要么返回的JSON格式不完整,要么工具名拼错、参数少一半。这是小模型的通病——工具调用要求模型在特定位置输出严格格式的内容,而量化压缩首先损失的就是这种精确输出能力。

我试了几种应对方案,最简单有效的是别用JSON,设计一套更松散的文本协议。比如让模型输出“查询天气|北京”,后端用规则解析字符串而不是硬做JSON解析。格式越简单,小模型越不容易出错。真想用JSON,Ollama API也支持用"format": "json"强制输出JSON,但字段内容仍可能不符合预期,后端一定要做校验和兜底。

6.2 按任务分模型:一个Ollama,多套配置

跑了一阵子之后我的经验是:不要试图用一个大模型解决所有问题。我现在的配置是对话用qwen2.5:7b,代码相关的任务切到专门的代码模型,复杂长文问题切到14b版本。反正模型都放在Ollama里,按任务切换只是改一个字符串的事,成本极低,但体验提升非常明显。

切换时别在代码里写死模型名,我把模型名放在配置文件里,用环境变量管理。比如OLLAMA_CHAT_MODEL=qwen2.5:7b、OLLAMA_CODER_MODEL=某个代码模型这样,哪天想换模型,改配置重启服务就行,不用翻代码。

6.3 硬件升级的性价比顺序

最后说说硬件。很多朋友问我是不是必须买显卡才能玩离线模型。我的回答是:没有也能玩,体验差一点而已。纯CPU跑7B模型每秒大概几个token,慢,但看它一个字一个字吐出来其实也还行;16GB内存的机器跑7B Q4很流畅,只是上下文别开太大。

如果要升级,我的建议顺序是:先把内存加到32GB,再考虑上独立显卡。显存24GB级别基本可以跑32B量级的模型,体验会完全不一样。如果用的是近两年带统一内存架构的机型,那就更省心,内存大的版本直接跑中大规模模型都挺稳。按“内存优先、显存次之”的顺序来,性价比最高,也最不容易买完后悔。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询