本地跑大模型这件事,我从去年折腾到现在,前后换过三台机器、重装过不下十次环境,踩的坑足够写一本小册子。最开始的想法特别朴素:我只是想要一个能离线用、数据不出本机、还能按自己需求改的 AI 学习工具。市面上的在线服务要么按量计费,要么把对话记录留在别人服务器上,要么功能被砍得只剩一个聊天框。于是我干脆自己动手,用开源模型加本地推理引擎搭了一套完整的学习软件,从模型加载、对话管理到知识库检索全部跑在本地,代码也全部开源出去了。
这篇文章不讲空泛的概念,只讲我实际做出来的东西:它由哪些模块组成、每个模块为什么这么选、本地推理的性能怎么调、知识库检索怎么落地、以及我在真实使用中遇到的那些文档里不会写的坑。无论你是刚接触本地大模型的新手,还是已经跑过几轮推理想进一步做应用的老手,都能从下面这些内容里找到能直接抄作业的部分。核心关键词就三个:AI、开源、本地运行,全文围绕它们展开。
1. 为什么我坚持把 AI 学习软件做成本地运行
1.1 在线服务的三个硬伤,逼我转向本地
先说清楚动机,不然很多人会觉得本地跑模型是脱裤子放屁。我最初也是在线服务的重度用户,直到遇到三件让我彻底改变想法的事。
第一件是数据归属。我用 AI 辅助整理一些内部技术笔记和项目复盘,这些东西虽然不算机密,但也不适合长期躺在别人的服务器上。在线服务的隐私条款写得再漂亮,数据终究是离开了我的硬盘。第二件是可用性。有段时间我需要在没有外网的环境里查资料、做总结,在线服务直接歇菜,而我手头明明有一块能跑推理的显卡。第三件是可控性。我想调整系统提示词、想换一个更适合中文的模型、想接入自己的文档库,在线服务要么不支持,要么得加钱上企业版。
这三件事叠加起来,结论就很明确了:我需要一个数据不出本机、断网可用、能随意改造的 AI 工具。本地运行不是目的,而是满足这三个需求的唯一路径。
1.2 本地运行到底解决了什么问题
很多人对本地运行有个误解,以为只是为了省钱。省钱只是顺带的,真正的价值在于三点。
其一是数据主权。所有对话、上传的文档、生成的向量索引,全部存在本机磁盘上。我可以随时删除、随时备份、随时迁移,不需要向任何第三方申请导出。对于需要处理敏感资料的人来说,这一条就值回票价。
其二是离线可用。模型权重下载到本地之后,推理过程完全不依赖网络。我在高铁上、在会议室断网的环境里都用过,体验和联网时没有区别。这一点对经常出差或者工作环境网络受限的人特别友好。
其三是可改造性。开源意味着我能看到每一行推理逻辑,能替换模型、能改提示词模板、能加自定义工具调用。我后来给这套软件加了一个专利文档辅助检索的模块,在线服务根本不可能让我这么干。
1.3 这套软件的定位:学习工具而非聊天玩具
需要明确一点,我做的是一个学习软件,不是又一个聊天框。两者的区别在于:聊天框只负责把问题丢给模型、把回答显示出来;学习软件要解决的是"如何让模型基于我的资料回答问题"以及"如何让我的使用过程可积累、可检索、可复用"。
所以这套软件的核心能力包括:本地模型推理、多轮对话管理、本地知识库检索增强、对话记录持久化、以及一个能让我快速切换模型和参数的配置层。下面几节会逐个拆开讲。
2. 技术选型:推理引擎、模型与前端怎么搭
2.1 推理引擎为什么选 Ollama 而不是自己写
本地推理的引擎选择其实不多,主流的就那么几个:llama.cpp 直接调用、Ollama、以及一些 Python 侧的封装库。我最终选 Ollama 作为默认引擎,理由很实际。
llama.cpp 是最底层的,性能最好、控制最细,但它的接口偏底层,模型格式转换、量化参数、上下文长度这些都要自己处理,对新手不友好。而 Ollama 在 llama.cpp 之上做了一层封装,把模型下载、加载、量化、服务化都包好了,一条命令就能拉起一个本地推理服务,暴露的是标准的 HTTP 接口。这意味着我的前端代码不需要关心底层是 llama.cpp 还是别的什么,只要按接口调用就行。
提示:Ollama 默认监听本机的 11434 端口,只对本机开放,不需要额外配置防火墙规则。如果你的机器上有多个用户,注意这个端口默认没有鉴权,别随意改成对外监听。
当然 Ollama 也有代价,它的抽象层会带来一点点性能损耗,而且模型格式受限于它支持的 GGUF。但对我这种以应用为主、不追求极限性能的场景来说,这点损耗完全可以接受。如果你追求极致吞吐,可以后期把引擎换成直接调 llama.cpp 的 server,接口层不用动。
2.2 模型选择:中文能力、显存占用与量化等级的平衡
模型选择是本地运行里最纠结的一环。我的筛选标准有三条:中文能力要过关、显存占用要能塞进我的显卡、量化之后质量不能崩得太厉害。
先说量化。GGUF 格式的模型有 Q4、Q5、Q8 等不同量化等级,数字越大精度越高、体积越大。Q4_K_M 是社区里公认的甜点,4bit 量化后 7B 模型大概占 4 到 5GB 显存,13B 模型大概 8GB 左右。我实测下来,Q4_K_M 在日常问答和文档总结场景里和 Q8 的差距肉眼几乎看不出来,但显存占用少了一半。
下面是我实际用过的几个模型对比,供参考:
| 模型规模 | 量化等级 | 显存占用 | 中文表现 | 适用场景 |
|---|---|---|---|---|
| 7B | Q4_K_M | 约 4.5GB | 良好 | 日常问答、短文档总结 |
| 7B | Q8_0 | 约 8GB | 优秀 | 对精度要求高的推理 |
| 13B | Q4_K_M | 约 8GB | 优秀 | 长文档理解、复杂问答 |
| 13B | Q5_K_M | 约 10GB | 优秀 | 平衡精度与占用 |
我的建议是:显存 8GB 以下选 7B 的 Q4_K_M,8GB 到 12GB 选 13B 的 Q4_K_M,12GB 以上可以考虑 13B 的 Q5 或者更大的模型。别一上来就追求最大最强的模型,跑不动等于零。
2.3 前端形态:为什么我放弃了纯网页方案
前端我一开始做的是纯网页,浏览器打开就能用。但用了一段时间发现两个问题:一是浏览器标签页一多就容易误关,对话记录丢失;二是网页方案很难做系统级的快捷键唤起和文件拖拽。
后来我改成了本地 Web 服务加桌面壳的方案。核心逻辑还是一个本地 HTTP 服务,前端用轻量的框架渲染,外面套一层桌面容器。这样既保留了网页开发的灵活性,又有了桌面应用的稳定性和系统集成能力。文件拖拽、全局快捷键、托盘常驻这些都能做。
如果你不想折腾桌面壳,纯网页方案也完全够用,只要把对话记录存在本地数据库而不是浏览器 localStorage 里就行。localStorage 会被清理,这是很多人踩过的坑。
3. 本地知识库检索增强的落地细节
3.1 为什么光有模型不够,必须加检索
大模型有个致命问题:它的知识截止到训练数据的时间点,而且它不知道你的私有文档里写了什么。你直接问它"我上周那份项目复盘里提到的性能瓶颈是什么",它只会一本正经地胡说八道。
解决办法就是检索增强生成,也就是常说的 RAG。思路很直白:把你的文档切块、转成向量、存进本地向量库;用户提问时,先把问题也转成向量,在向量库里找出最相关的几个文档块,连同问题一起塞给模型,让模型基于这些材料回答。
这样一来,模型回答的内容就有了依据,而且这个依据完全来自你本地的文档,不涉及任何外部传输。
3.2 文档切块策略:切多大、怎么切
切块是 RAG 里最容易被忽视但影响最大的环节。切太大,检索出来的块里噪音多,模型容易被无关内容带偏;切太小,一个完整的语义被切断,检索出来答非所问。
我试过几种策略,最后稳定在按语义段落切、目标块大小 500 到 800 字、块之间保留 100 字左右重叠。按段落切是因为中文文档的段落本身就是语义单元,比按固定字数硬切要合理得多。保留重叠是为了防止一个关键信息正好卡在两块的边界上被切断。
具体实现上,我先把文档按换行拆成段落,然后贪心地往当前块里塞段落,塞到接近目标大小就封口,同时把最后一段的一部分作为下一块的开头。代码逻辑大概是这样:
def split_into_chunks(text, target_size=600, overlap=100): paragraphs = [p.strip() for p in text.split("\n") if p.strip()] chunks = [] current = "" for para in paragraphs: if len(current) + len(para) <= target_size: current += para + "\n" else: if current: chunks.append(current.strip()) # 保留重叠部分 tail = current[-overlap:] if len(current) > overlap else current current = tail + para + "\n" if current.strip(): chunks.append(current.strip()) return chunks注意:overlap 不要设得太大,否则向量库里会有大量重复内容,检索时容易返回一堆相似的块,反而稀释了有效信息。100 字左右是个比较稳的值。
3.3 向量化与本地向量库的选择
向量化就是把文本转成一串数字向量,语义相近的文本向量距离也相近。这一步需要一个嵌入模型。我选的是本地能跑的中文嵌入模型,体积小、速度快,几百字的块毫秒级就能出向量。
向量库我用的是轻量级的本地方案,数据直接存成文件,不需要额外起数据库服务。对于个人使用场景,几万条向量的规模,这种方案完全够用,而且备份就是复制文件,特别省心。如果你要处理几十万条以上的向量,再考虑上专业的向量数据库。
检索的时候有个细节:我默认返回 top 5 个最相关的块,但会做一个相似度阈值过滤,低于阈值的块直接丢掉。这样当用户问的问题和知识库完全无关时,不会硬塞一堆不相关的材料给模型,避免模型被误导。
4. 对话管理与本地持久化的实现
4.1 对话记录为什么必须落盘
前面提过 localStorage 会被清理的坑,这里展开说。浏览器存储有几个问题:容量有限、清理策略不透明、换浏览器就没了。我有个朋友用网页版工具整理了两个月的学习笔记,结果一次浏览器更新全没了,欲哭无泪。
所以对话记录必须落到本地文件或者本地数据库。我用的是轻量的本地数据库,每条对话存成一条记录,包含时间戳、模型名、完整的消息列表、以及引用的知识库块。这样既能按时间检索,也能按关键词搜索历史对话。
4.2 多轮对话的上下文管理
多轮对话不是把历史消息一股脑全塞给模型。模型的上下文窗口是有限的,塞太多会超出限制,而且历史越长推理越慢、显存占用越高。
我的策略是滑动窗口加摘要。最近几轮对话完整保留,更早的对话压缩成一段摘要。具体来说,保留最近 6 轮完整消息,再往前的对话用模型自己总结成一段话放在最前面。这样既保留了长期上下文,又控制了 token 数量。
这里有个实操心得:摘要的生成不要每轮都做,那样太浪费算力。我的做法是当历史消息超过 10 轮时才触发一次摘要,把最老的几轮压缩掉。实测下来这个频率比较合理。
4.3 对话导出与迁移
既然是学习工具,对话记录的导出就很重要。我做了两种导出格式:Markdown 和 JSON。Markdown 方便人阅读和整理成笔记,JSON 方便程序处理和迁移到其他工具。
导出的时候我会把引用的知识库来源也带上,这样回头看的时候能知道当时模型是基于哪份文档回答的。这个细节很多人不做,但实际用起来特别有用,尤其是做技术调研的时候。
5. 性能调优:让本地推理跑得更快更稳
5.1 显存不够时的分层加载策略
显存不够是最常见的问题。模型加载不进去,要么报错,要么被迫用 CPU 推理,速度慢到无法忍受。
Ollama 支持把一部分层放在 GPU、一部分放在 CPU,通过参数控制 GPU 加载的层数。层数越多越快,但显存占用也越高。我的经验是先把 GPU 层数设成一个保守值,跑起来看显存占用,再逐步往上加,加到接近显存上限但还留一点余量为止。
提示:留余量很重要。显存跑满会导致系统卡顿甚至推理进程被杀。一般留 500MB 到 1GB 的余量比较稳妥。
5.2 上下文长度对性能的影响
上下文长度是个隐形杀手。很多人把上下文设成 8192 甚至更大,觉得越大越好,结果发现推理慢得离谱。原因是注意力机制的计算量随上下文长度增长得很快,上下文翻倍,计算量可能翻好几倍。
我的建议是按需设置。日常问答 2048 到 4096 足够,长文档总结再临时调到 8192。而且要注意,上下文长度是预分配的,即使你实际只用了 500 个 token,设成 8192 也会占用对应的显存。所以别没事就拉满。
5.3 并发请求与队列控制
本地推理通常是单请求串行的,同时来两个请求会互相抢资源,结果两个都变慢。我的做法是在服务层加一个简单的队列,请求进来先排队,一个一个处理。虽然牺牲了一点并发性,但保证了每个请求的响应速度稳定。
如果你确实需要并发,可以考虑起多个推理实例,每个实例绑定不同的端口,前端做负载分发。但这会成倍占用显存,一般个人使用没必要。
6. 实际使用中踩过的坑与排查过程
6.1 模型加载失败:从报错到定位的完整链路
有一次我换了个新模型,加载直接失败,报错信息很模糊,只说加载出错。我的排查链路是这样的:
第一步,确认模型文件完整性。下载中断会导致文件损坏,用校验工具对一下哈希值。第二步,确认量化格式是否被当前引擎版本支持。有些新量化格式需要更新引擎版本。第三步,看显存是否真的够。有时候报错不是显存不足,但实际是显存碎片导致的分配失败,重启一下推理服务就好了。第四步,看模型文件路径有没有中文或空格,某些底层库对路径字符很敏感。
最后定位到是量化格式太新,引擎版本没跟上。更新引擎后问题解决。这个链路我后来整理成了排查清单,遇到加载问题就按顺序过一遍,基本能覆盖九成情况。
6.2 中文乱码与编码问题
中文乱码在本地处理文档时特别常见。表现是检索出来的内容是一堆问号或者方块。根因通常是文件编码不是 UTF-8,而读取时按 UTF-8 解析了。
解决办法是在读取文件时先探测编码,或者统一转成 UTF-8 再处理。我在文档导入环节加了一个编码检测步骤,遇到非 UTF-8 的文件先转换再入库。这个坑看起来小,但不处理的话整个知识库都是废的。
6.3 检索结果不相关的调优过程
有段时间我发现检索出来的块经常和问题不相关。排查下来有三个原因:一是切块太大,块里混了太多无关内容;二是嵌入模型对中文的语义理解不够好;三是相似度阈值设得太低,把不相关的块也放进来了。
对应的调整是:把块大小从 1000 字降到 600 字,换了一个中文表现更好的嵌入模型,把相似度阈值从 0.3 提到 0.5。三步做完,检索准确率明显提升。这个过程说明 RAG 的效果不是一蹴而就的,需要根据实际数据反复调。
7. 开源之后的一些体会
代码开源出去之后,陆续有人提 issue、提 PR,也有人在评论区分享自己的用法。有几件事让我印象挺深。
一个是有人把模型换成了更小的版本,跑在了一台老笔记本上,虽然慢但能用,他说这比没有强。这让我意识到本地运行的价值不只是性能,还有可及性。另一个是有人给知识库模块加了 PDF 解析,直接把我没做的功能补上了。开源的好处就在这里,你做一个骨架,别人帮你长出血肉。
如果你也想做类似的东西,我的建议是先把最小可用版本跑通,别一上来就追求功能齐全。我第一版只有模型加载和对话两个功能,能跑起来之后才逐步加的知识库、导出、多模型切换。先让它能用,再让它好用,这个顺序别搞反。
最后分享一个我一直在用的小技巧:把常用的系统提示词存成模板文件,切换场景的时候直接加载对应模板,比每次手敲提示词效率高得多。我目前存了技术问答、文档总结、代码解释三个模板,日常够用了。