你会不会也有这种别扭的感觉:大模型确实强,但每次需要把私人笔记、合同扫描件、聊天记录整理稿扔进网页版 AI 时,心里总有点不踏实。而且真到了断网、接口欠费、或者想反复微调提示词的时候,才发现自己对整套 AI 应用其实只有“使用权”,没有“掌控权”。
最近一个月,我把一套“本地 AI 全栈”塞回了家里:开源模型跑在旧主机上,向量检索、RSS 新闻池、后端调度、前端问答全部运行在局域网内。模型不依赖云 API 调用,搜索不指向外部私密服务,所有数据都待在自己的硬盘里。这篇文章会把整套架构怎么拆、怎么搭、花了多少资源、踩了哪些坑,全部展开聊一遍。可以直接抄作业,也可以照着思路改成适合自己的版本。
1. 为什么我非要把 AI 全栈塞进家里那台旧主机
先把动机说清楚,不然很多人会误以为这是一次“性能折腾”。我不否认自己确实有点折腾欲,但真正让我下定决心把模型、搜索、后端和前端全部放回本地的原因,只有三个:隐私边界、成本控制、以及“从模型到交互全链路可解释”的学习价值。
1.1 “零成本”不是零硬件,而是边际成本为零
先破个题。很多人看到“零成本”会说:我手里根本没有一块像样的显卡,怎么零成本?这是对“成本”的误解。
我用的设备是一台吃灰好几年的旧笔记本,16GB 内存、无独显、CPU 大概只有中端移动端的水平。如果把它卖二手,可能就值几百块,但在家庭 AI 全栈这件事上,它完全够用。零成本指的是:不需要新买显卡、不需要开通云 GPU、不需要为每次对话和每次搜索付费,模型权重是开源的,工具链是免费的,电费摊到每个月也就一顿早餐钱。
所以更准确的说法是:用家里已有设备的闲置算力,把固定成本清零。
1.2 全栈不是“会前端加后端”,而是从权重到页面都能搭
“全栈工程师”这个词在热搜里经常和“前端和后端”绑定出现,但放在本地 AI 场景里,全栈的含义会更长:模型层、服务层、数据层、工具层、应用层,这五层都在自己手里。
我见过很多朋友用 LangChain 写了个 demo,底层模型还是调远程 API;也见过有人把模型下载下来后只会在终端里聊天,完全没有把模型变成“服务”的概念。真正的全栈,是当用户在前端输入一句话,你知道这句话经过 HTTP 请求到了哪个后端函数,模型内部拿什么 prompt 结构生成了工具调用,检索模块用哪种相似度算法把本地文档捞出来,最终又是如何拼回上下文、限制生成长度、返回给浏览器的。
这一整条链路才是“全栈”的价值。下表是我这套系统里每一层的最终选型,后面会按层展开说明。
| 层级 | 用途 | 选用方案 |
|---|---|---|
| 模型层 | 生成回答、判断意图 | Ollama + 开源对话模型(7B/8B 量化版) |
| 服务层 | 提供统一调用接口 | Ollama 的 OpenAI 兼容 API |
| 数据层 | 本地文档、RSS 语料的索引与检索 | SQLite / 向量索引 / FTS5 |
| 工具层 | 让模型调用搜索、查笔记 | Function Calling + Python 后端脚本 |
| 应用层 | 前端交互界面 | Open WebUI / 自建轻量页面 |
2. 模型层选型:量化、内存与推理速度的真实权衡
模型层是整个系统的地基。如果模型没跑起来,后面所有内容都是空中楼阁。但本地方案里最不缺的就是“能跑的模型”,真正难的是找到“适合你这台机器内存的模型”。
2.1 为什么我最终选了 7B/8B 量化模型
市面上的开源模型很多,热搜里提到的 DeepSeek、Qwen、GLM 等系列我基本都试过一遍。这里要说一个很多新手容易犯的错:以为模型参数越大效果就一定越好,结果 14B 甚至 32B 的模型刚拉下来,旧电脑直接被吃得连系统都快卡死。
以我的旧笔记本为例,内存 16GB,操作系统本身大概占用 2GB,Ollama 的常驻服务占用几百 MB。留给模型的空间大概在 12GB 到 14GB。在这个容量下,7B/8B 级别的模型搭配 4bit 量化,是最平衡的选择。再往上走,14B 模型即使能加载,上下文一长就会触发内存交换,推理速度会断崖式下降,实际体验反而更差。
选型时可以用一个粗略公式估算:
模型文件大小(GB) ≈ 参数量(B)× 每参数所需字节数
以 7B 模型、Q4_K_M 量化为例,每个权重平均约 4.5 bit,模型文件大概在 4.7GB 左右。如果你还想给 8k 长度的上下文留出 1GB 以上的 K/V cache 空间,那么 16GB 内存的机器跑 7B 正好,跑 14B 会非常紧。
2.2 Ollama 部署与量化档位实测
模型运行框架我选的是 Ollama,原因只有两个:安装简单,而且自带 OpenAI 兼容接口。不用自己编译 llama.cpp,也不需要手工管理权重文件。
在 Linux 或 macOS 下,一条命令就能完成安装:
curl -fsSL https://ollama.com/install.sh | sh安装完成后,先拉模型:
ollama pull qwen2.5:7b-instruct-q4_K_M把模型拉下来后,直接测试是否能正常运行:
ollama run qwen2.5:7b-instruct-q4_K_M输入“你好”如果能正常回复,说明基础链路已经通了。
量化版本怎么选?我用实际体验做了个对比表:
| 量化档位 | 模型体积(约) | 效果损耗 | 运行速度 | 推荐场景 |
|---|---|---|---|---|
| q2_K | 3GB 左右 | 明显,逻辑易出错 | 最快 | 仅做关键词抽取 |
| q4_K_M | 4.7GB 左右 | 较小,综合性价比高 | 较快 | 个人问答、摘要、检索 |
| q5_K_M | 5.2GB 左右 | 很小 | 中等 | 有 16GB 以上内存可考虑 |
| q8_0 | 7.2GB 左右 | 基本可忽略 | 较慢 | 高质量写作/代码场景 |
如果从没跑过本地大模型,建议直接从 q4_K_M 开始。这个档位在质量和资源占用之间最平衡,也是最“不容易劝退”的选择。
2.3 没有独显时的硬核兜底方案
很多人下载好模型后,第一反应是去装 CUDA、看显存,结果发现自己根本没显卡,瞬间就泄气了。其实 CPU 推理完全可行,只是要把预期调整到“工具级速度”而不是“聊天级速度”。
实测下来,旧笔记本纯 CPU 跑 7B q4_K_M,输出速度大约在每秒 3 到 6 个 token。一秒钟三个字,听起来很慢,但只要不是要求它写长篇小说,实际用于搜索摘要、结构化提取、意图判断是完全够用的。
如果你的机器也跟我一样没有独显,有几个实测有效的优化点:
- 关闭无关后台程序,尤其是浏览器多开标签页;
- 把 Ollama 的并发数限制为 1,避免多个请求同时抢占 CPU;
- 控制请求中的
max_tokens,默认生成 2048 个 token 会让 CPU 一直满载,实际任务 300 到 600 token 往往就够了; - 如果主板支持,加一根同容量内存条组成双通道,CPU 推理的内存带宽瓶颈会明显缓解。
3. 从模型到服务:OpenAI 兼容 API 和常驻进程管理
模型能在终端里聊天,只是第一步。要想让后端、前端、检索模块都调用同一个模型,必须把它变成一个标准接口服务。这里最大的功臣是 Ollama 自带的 OpenAI 兼容端点。
3.1 OpenAI 兼容 API 为什么重要
本地模型最容易踩的坑,是每个框架都有自己的调用方式。今天你用 llama.cpp 的命令行,明天换一个框架又得重新学参数。但 OpenAI 的接口格式已经成为事实标准:POST /v1/chat/completions、messages数组、tools字段、tool_calls返回。几乎所有上层应用天然支持这套协议。
Ollama 启动后,默认监听http://127.0.0.1:11434,只需要把 base_url 指向这个地址,就可以用任何兼容 OpenAI SDK 的工具直接调用本地模型。
用 curl 做一次最小验证:
curl http://127.0.0.1:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5:7b-instruct-q4_K_M", "messages": [{"role": "user", "content": "你好,请用一句话介绍你自己"}], "max_tokens": 200 }'这个请求成功返回之后,你的本地模型就从一个“只能聊天的进程”,变成了一个可以被任意程序调用的服务。Open WebUI、Dify 这类开源项目也可以直接接进来。
3.2 用 Open WebUI 或 Dify 做第一层门面
如果你不想自己从头写前端,Open WebUI 是最省事的方案。它支持本地的 Ollama 服务,界面和主流聊天产品基本一致,还带了文档上传、知识库插件、多用户管理等功能。
有 Docker 环境的机器可以直接跑容器,没有 Docker 也可以直接通过 Python 的 pip 安装:
pip install open-webui open-webui serve打开浏览器访问http://127.0.0.1:8080,注册一个本地管理员账号,然后在设置里把 Ollama API 地址填成http://127.0.0.1:11434,就能看到已经拉取的所有本地模型。
如果想要的不是聊天界面,而是一个更接近“可视化工作流”的工具,Dify 的本地部署也值得试。它能让你以图形化方式编排模型、知识库和工具节点,后续想加搜索能力会方便很多。不过 Dify 默认依赖的组件更多,对旧机器的资源开销会略高,所以我只有在编排复杂 Agent 工作流时才会启动它。
3.3 让服务长时间稳定运行:进程管理经验
刚开始我直接在终端执行ollama serve,结果一旦关掉终端窗口,整个服务就停了。这在开发调试时没有影响,但如果你想把它当作家庭服务长期使用,就必须交给进程管理器。
Linux 下我建议用 systemd,写一个服务文件:
[Unit] Description=Ollama Local AI Service After=network-online.target [Service] ExecStart=/usr/local/bin/ollama serve Restart=always RestartSec=10 Environment="OLLAMA_HOST=127.0.0.1:11434" [Install] WantedBy=multi-user.target保存后执行:
sudo systemctl daemon-reload sudo systemctl enable ollama sudo systemctl start ollama这里有一个关键配置:OLLAMA_HOST只绑定到127.0.0.1,也就是只有本机能访问。如果你确实需要让局域网内其他设备访问,再改成0.0.0.0:11434。但我的经验是,保持在本地回环地址最安全,后端程序在同一台机器上完全够用,没必要把模型端口暴露到整个局域网。
4. 让搜索变成模型外脑:个人知识库与 RSS 语料池
模型回答得再流畅,知识也停留在训练截止日期。要让模型能回答“我上个月记在笔记里那个思路是什么”这种问题,就必须给它接上搜索能力。而我说的搜索,不是让模型去调用某个第三方搜索接口,而是自己在本地搭一套检索系统。
4.1 搜索不止是“上网查”,更是本地语料发现
很多人一听到“搜索”就联想到搜索引擎。但在本地 AI 全栈里,“搜索”包含两个场景:一是把我们自己积累的笔记、文档、PDF 变成可检索的知识库;二是把日常关注的公开网站通过 RSS 订阅抓取到本地,形成一个可持续积累的语料池。这两个场景的共同点是:数据都不需要外传,完全在自己硬盘里检索。
具体到我的实现,检索层拆成两条腿走路:
- 关键词检索:适合匹配型号、编号、专有名词这类精确信息,用 SQLite 自带 FTS5 全文索引就能做。
- 语义检索:适合“我当时好像写过一段关于滑窗滤波器的思路”这种模糊问题,需要把文本切成片段、转成向量,再做相似度匹配。
4.2 文档切分和向量化:亲手做一个能读懂文件的检索端
文本转向量是整个检索链路里最影响效果的一环。我先选了 embedding 模型,例如nomic-embed-text或bge-m3,这类小模型本地跑起来几乎无压力。然后按顺序做三件事:清洗文本、切分片段、计算向量。
切块这件事看着简单,其实最容易出问题。机械地每 500 个字切一刀,经常把一个完整思路从中间截断,检索时就怎么都匹配不上。我踩过坑后改用“段落边界优先”的策略:优先按标题或空行切分,如果一个段落太长再按句子边界二次切分,每个片段控制在 300 到 500 字,相邻片段保留少量重叠,避免同一个概念被切断在上一块末尾。
embedding 生成可以直接通过 API 完成:
curl http://127.0.0.1:11434/api/embeddings \ -H "Content-Type: application/json" \ -d '{"model": "nomic-embed-text", "prompt": "需要转成向量的文本内容"}'生成后的向量和原文一起存进 SQLite,查询时把用户问题也转成向量,然后计算余弦相似度。如果不想自己实现向量距离计算,用sqlite-vec这个扩展或者直接装个轻量级向量库都可以。
这里给一个实际的查询逻辑示意图:
用户问题 -> 生成查询向量 -> 语义搜索:从向量库召回 top30 片段 -> 关键词搜索:从 FTS5 召回含精确词的片段 -> 合并去重,按分数排序 -> 取 top5 片段作为上下文这里为什么要同时跑关键词和语义两路?因为语义搜索擅长处理同义改写,但容易忽视精确代号;关键词搜索则相反。合并两路结果后,模型拿到的信息更全,回答引用也更准确。
4.3 RSS 订阅池:给搜索模块提供“本地可读新闻库”
除了个人文档,我还想让模型能聊一些近期的公开信息。最省心的方案是用 Python 的feedparser定时抓取自己订阅的 RSS 源,把抓到的文章标题、链接、正文摘要存入本地数据库,然后也走切分和向量化流程。
这个模块相当于自己攒了一个“新闻池”。模型不直接联网,但能检索到最近几天抓取入库的公开文章。有一个很重要的经验是:抓取入库前一定要做去重和过滤。RSS 源里经常有重复内容,而且有些网页会自动刷新时间戳,导致同一条新闻反复入库。我在入库前会增加一层“标题加链接 MD5 去重”,一周内重复的内容直接跳过。这样才能避免搜索结果翻来覆去都是同一篇。
5. 把模型、检索、后端和前端串成一个完整问答应用
到这一步,模型有了,API 有了,检索模块也有了。但如果不做串联,这三者还是孤岛。真正像“全栈项目”的部分,是把它们暴露成一个可交互的问答应用。
5.1 最小项目结构:先从后端代码开始
如果你自己已经写过后端,那么这个项目并不复杂。我用 FastAPI 写了一个最小的后端,核心就两个接口:聊天接口和检索接口。
项目结构长这样:
local-home-ai/ api/ main.py # FastAPI 后端入口 agent.py # 工具调用循环和上下文组装 config.py # 模型名、地址等配置 search/ embed.py # embedding 相关逻辑 indexer.py # 文档入库、切分 rss_pool.py # RSS 抓取与去重 web/ index.html # 简易前端页面后端main.py的核心逻辑并不复杂:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class ChatRequest(BaseModel): message: str @app.post("/chat") def chat(req: ChatRequest): # 1. 把用户问题发给本地模型 # 2. 让模型决定是否调用 search_notes 工具 # 3. 如果触发工具,则检索本地知识库 # 4. 把检索结果拼入上下文,让模型生成最终回答 return {"reply": "local answer"}这里有一个关键点值得多说:不要让用户问题直接丢给“大模型完事”,也不要每次用户提问都先做一遍全库检索。最稳妥的思路是给模型一个工具调用入口,让模型自己判断是否需要检索、以什么关键词检索。这就是 Function Calling 的作用。
5.2 Function Calling:让模型自动决定何时搜索
在 OpenAI 兼容协议里,Function Calling 的定义方式如下:
{ "tools": [ { "type": "function", "function": { "name": "search_notes", "description": "从本地知识库中检索相关片段", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "用于知识库检索的关键词或问题" } }, "required": ["query"] } } } ] }模型收到用户问题后,如果判断需要检索,会返回一个结构化的tool_calls对象,包含函数名和参数。后端解析这个对象后,把query参数传给检索模块,拿到搜索结果,再以系统消息或用户消息的形式追加到对话里,让模型基于真实检索内容生成回答。
这个过程听起来玄乎,其实就是一个多轮消息拼接。核心经验是:检索结果一定不要把整篇文档全部塞进上下文。我一开始图省事,把 top5 片段全文塞进去,结果提示词被撑爆,模型开始胡言乱语。正确做法是先做摘录:每个片段只保留前 300 到 500 字,同时附上原文文件名和段落号,让模型在回答时能注明引用来源。
5.3 一个实际跑通的场景
举一个真实例子,我整理了一份设备采购记录,里面有一行写着“3 月购买了一台 27 寸 4K 显示器,价格 1699,渠道是京东”。
用户在前端提问:“我三月份买的那台显示器多少钱来着?”
后端把问题发给本地模型后,模型没有直接回答,而是触发了search_notes,检索 query 自动生成了“27寸 4K 显示器 采购记录”。检索模块先做关键词命中,找到了包含“27 寸 4K 显示器”的文档,再经过向量排序,把最匹配的片段返回。后端把这段内容拼到上下文里,模型最终给出的回答是:“根据 5 月设备采购记录,这台 27 寸 4K 显示器的价格是 1699 元。”同时附上了来源文件名。
整个过程在纯 CPU 机器上大概需要 20 到 40 秒。如果只是闲聊式提问,不触发检索,速度会快很多。这也是为什么本地全栈不能像云端那样追求毫秒级响应,而更适合“问事实、查资料、做总结”这类低延迟敏感任务。
前端页面我没有上重框架,一个静态 HTML 页面,用 fetch 调后端接口就够了。把页面放在局域网内,手机和电脑访问同一个地址,整个家庭服务就闭环了。
6. 这套零成本架构的资源账本与踩坑记录
文章最后一部分,我想把这套系统在真实运行中遇到的资源问题和工程坑全部整理出来。这些内容不是从文档里抄来的,是省时间最有效的地方。
6.1 实际资源占用与量化参考
以我的旧笔记本为例,16GB 内存、纯 CPU 推理、运行 7B q4_K_M 模型,资源分布大致如下:
| 项目 | 占用情况 |
|---|---|
| 操作系统与桌面环境 | 约 2GB |
| Ollama 常驻进程 | 约 0.5GB |
| 模型本体 | 约 4.7GB |
| 上下文 KV cache(8k) | 约 1GB 到 1.5GB |
| 检索与 embedding 模型 | 约 0.6GB |
| Web 前后端 | 约 0.3GB |
整体下来,内存总占用在 10GB 到 11GB 左右,16GB 的机器还能流畅上网。如果模型换到 14B,内存会立刻逼近极限,系统开始疯狂 swap,鼠标都飘。所以,别被“模型参数越大越好”这种话带偏,先看自己有多少内存。
embedding 模型和对话模型同时常驻会多占一部分内存在,所以我在检索模块里用完后会立刻释放 embedding 模型,或者在 Ollama 中设置keep_alive=0,避免闲置模型一直占着内存。
6.2 延迟的真相:为什么本地“感觉慢”
本地模型响应慢,有两个完全不同的原因,需要分开排查。
第一个原因是预填充阶段慢。用户输入很长,模型需要先对所有输入 token 做一次前向计算,这期间 GPU 或 CPU 都处于满载状态,用户感觉到的就是“发送后迟迟没有输出”。这个阶段可以用num_predict和num_ctx参数控制,把上下文窗口设置到合理范围,比如 4096 或 8192,不要盲目拉到 32k。
第二个原因是输出阶段慢,也就是逐 token 生成慢。CPU 推理在这个阶段尤其明显,因为内存带宽决定了每秒能生成多少个 token。遇到这类情况,与其升级硬件,不如改任务设计:让模型只做“提取要点、判断类别、生成短回答”这类低输出长度任务,而不要让它写一篇长文。
另外一个很容易被忽略的点:如果你用 Docker 跑 Ollama,在高负载下容器间的网络转发、文件系统开销也会影响速度。我在旧机器上最终选择直接用二进制跑服务,而不是套一层 Docker,实测首 token 耗时能缩短一些。
6.3 家庭运行中最容易被忽略的安全与备份问题
最后说两个可能被骂“啰嗦”但必须讲的方面。
第一是访问边界。这套系统里没有敏感数据,但知识库可能会包含个人文档和记录。我的做法是所有服务默认只监听127.0.0.1,只有 Open WebUI 页面为了给手机访问才单独绑定到局域网 IP。不要轻易用端口转发把服务暴露到公网,更不要把 Ollama 默认的 11434 端口直接映射到公网。本地模型虽然灵活,但不代表它没有漏洞,安全上谨慎是底线。
第二是数据备份。检索模块的向量库本质是个 SQLite 文件,我曾经因为清理系统时误删了数据库,导致三个月积累的索引全部丢失。后来我加了一个定时任务,每天把向量库文件、RSS 原文库和模型配置文件压缩打包到另一个磁盘分区。这个习惯帮我避免过至少两次“重建索引”的痛苦。真正经历过一次数据全丢之后,你才会明白,本地 AI 全栈的价值不在于模型多强,而在于这个系统里保存的数据有多难重建。
如果现在让我重新搭一遍,我不会急着追求更强更大的模型,而会把检索质量、引用来源、数据备份放在更优先的位置。模型只是最后那道“把检索结果变成人话”的工序,整条链路稳定扎实才是这套家庭系统真正能长期跑下去的原因。