
这台放在书房的电脑前阵子被我重新整理了一遍。现在它既不是单纯的游戏机也不是普通的代码调试服务器而是一套完整的“私有化 AI 全栈”本地模型推理、本地知识库搜索、自建网页搜索入口、聊天前端、Agent 任务调度全部在这台机器上跑。整条链路没有买任何云服务器也没有充过一分钱 API 额度除电费外基本是零成本。这篇文章把这次搭建从思路到实操完整记录一遍重点说清楚“模型层、搜索层、编排层、应用层”是怎么一层层串起来的。不管你是想给家里的电脑配一个私密 AI 助手还是想完整理解一个本地部署项目里的前后端技术栈都可以照着抄作业。先说三个结论避免你看完开头就划走第一没有 RTX 4090 也能跑8GB 显存或 16GB 统一内存即可入门第二不需要懂复杂的深度学习原理所有推理框架和模型权重都是免费现成的第三确实需要一点折腾精神但按步骤来一个周末基本能跑通全链路。1. 整套系统的架构设计与技术选型逻辑1.1 “本地 AI 全栈”到底由哪几层组成很多人一说本地部署 AI第一反应就是装个 Ollama 拉一个模型然后打开网页聊几句以为这就完事了。但实际上单个模型只是一个孤岛你很快会遇到三个尴尬问题模型不知道你硬盘里的私有资料模型的知识有截止日期问不到最新信息模型没有入口只能裸奔在终端里。所以我把整个系统拆成了四个层次每一层各管一件事模型层负责 LLM 推理和文本向量化相当于大脑。检索/搜索层负责从本地文件库里做语义检索或者通过自建搜索服务拿外部网页结果相当于记忆和耳目。编排层负责判断“这个问题该走哪条路、检索结果怎么拼进提示词、工具怎么调用”相当于决策中枢。应用层负责和用户交互可能是聊天界面也可能是 API 接口相当于脸面和手。这四层结构其实非常像传统全栈项目的“前端—后端—数据库—中间件”拆法。模型层对应数据库搜索层对应缓存和数据源编排层对应业务逻辑应用层就是前端页面。想通这一点你就在自己的电脑上完整复刻了一个现代 AI 应用的最小工程骨架。1.2 为什么坚持全部跑在家里而不是用在线服务很多人问过我云上大模型能力更强为什么非要本地搭一套我自己的理由有三点每一条都挺现实。第一是数据隐私。我用来做检索的语料里有大量个人笔记、技术文档、家庭账目这些内容我不希望经过任何外部服务。文件在本地切块、在本地向量化、在本地检索请求不出内网这个安全感是云 API 给不了的。第二是长期成本。在线 API 按 token 计费看起来单次很便宜但如果你真的让 AI 每天帮你整理资料、写周报、做会议摘要一年下来账单并不小。本地部署的模型权重一次下载终身使用显卡电费基本可以忽略。第三是可定制性。本地跑模型意味着上下文的长度、采样的温度、提示词模板、到底用哪个 embedding 模型全部由你控制。服务商只给你一个有限参数接口自己部署则可以调每一层的东西。但也要把丑话说在前面零成本不等于零硬件门槛也不等于零学习成本。本地模型的能力上限受制于你的硬件尤其在复杂推理和长文本理解上和云端超大模型还有差距。另外从下载模型到调通检索链路中间会遇到不少软件层面的问题心态上要做好准备。1.3 组件选型的核心原则我选组件的标准只有一条尽量选开源、社区活跃、接口标准化的项目。因为本地全栈项目最怕的就是“每个组件都很强但互相连不上”。最终确定的选型如下层次组件选型理由模型编排Ollama一条命令跑起模型服务自带 OpenAI 兼容 APILLM 模型Qwen2.5 系列中文能力强、开源协议友好、量化版本齐全Embedding 模型BGE-M3中英文混合检索效果好多语言场景不吃亏向量数据库Chroma轻量、本地文件持久化、API 简单适合单机自建搜索服务SearXNG开源元搜索引擎可以把公共搜索结果转成 JSON API后端编排FastAPI轻量异步框架写接口快适合家用级请求量前端界面Open WebUI / 自写 HTML现成方案省时间自写方案能看清前后端交互这套选型里最核心的决策就是“全部组件尽量提供 HTTP 接口”。Ollama 暴露 11434 端口SearXNG 暴露 8080 端口Chroma 虽然是嵌入式库但它能本地持久化FastAPI 统一封装对外。任何一层都可以单独重启和替换不会牵一发动全身。2. 模型层先让大模型在本地真正跑起来2.1 推理引擎对比为什么我选了 Ollama本地跑 LLM 的推理引擎其实不少主流的三条路线是 Ollama、llama.cpp 和 LM Studio。很多人不知道它们之间的区别这里先做一个直观类比llama.cpp 是发动机性能上限最高、可调参数最细但要自己动手拼装Ollama 是整车把发动机、模型管理、API 服务打包好了拧钥匙就能开LM Studio 则是带精美内饰的展示车适合纯鼠标操作但不太适合做后端服务。我最终选择 Ollama 作为“模型服务器”核心原因有三个。第一模型管理太方便了。ollama pull一条命令就能从模型仓库拉取量化好的权重不需要自己手动转换 GGUF 格式也不用管理散落各处的模型文件。第二它原生提供 OpenAI 兼容 API。这意味着我后面接任何前端框架、Agent 工具、测试脚本都可以用标准的 OpenAI SDK 来访问不需要为本地模型写特殊适配层。第三Ollama 会常驻后台提供服务可以被任意其他程序调用适合作为全家桶的中心节点。如果你追求极限推理性能以后可以换 llama.cpp 或者用带 vLLM 的服务但那是更大的工程家用场景下 Ollama 的收益成本比最高。2.2 选模型先算显存账量化等级和上下文长度模型的参数量是传统认知里的第一指标但在本地部署场景里真正决定能不能跑的是“量化后的权重体积 KV Cache 运行时开销”这张显存账。量化简单说就是把模型权重从高精度浮点数压缩成低精度整数让模型体积变小、推理速度变快代价是极小的质量损失。最经典的 GGUF 量化等级包括 Q4_K_M、Q5_K_M、Q8_0数字代表每个权重用多少 bit 表示。Q4_K_M 在当前生态里属于性价比最高的档位质量损失很小体积又控制得好。以 Qwen2.5 系列为例我整理了一个基于 8GB-24GB 显存的粗略参考表模型规格量化后体积建议显存/内存适合场景Qwen2.5:3b约 2GB4GB 起步轻量问答、意图分类Qwen2.5:7b约 4.7GB8GB 可用日常聊天、代码补全、摘要Qwen2.5:14b约 9GB16GB 可用复杂指令、长文本分析Qwen2.5:32b约 20GB24GB 以上高质量创作、复杂推理这里有一个新手最容易踩的认知误区以为模型量化后的体积小于显存就能跑。不对。权重只是其中一块开销模型运行时会额外申请大量显存来存 KV Cache也就是所有历史对话的注意力缓存。上下文窗口开得越长KV Cache 占用的显存越大。我实测过 7B 模型如果硬开 32K 上下文KV Cache 会多占好几 GB一旦超过显存系统不是报 OOM 就是被迫把部分层卸载到内存速度断崖式下跌。如果你用 Apple Silicon 的统一内存判断逻辑稍有不同显存和内存共用还要把操作系统和各路应用占的内存算进去。16GB 的 Mac 跑 7B Q4 比较稳32GB 可以尝试 14B但别抱着“内存 16GB 就能跑 32B”的幻想那会直接压缩到系统卡死。2.3 实操安装 Ollama 并拉取模型安装 Ollama 非常简单Linux 和 macOS 上用官方脚本一行完成curl -fsSL https://ollama.com/install.sh | shWindows 用户直接去官方网站下载安装包即可。安装完成之后Ollama 默认会在后台启动服务监听 11434 端口。然后拉取两个模型一个是对话用的大模型一个是后续做语义检索用的 embedding 模型# 对话模型按你显存情况选择规格 ollama pull qwen2.5:7b # 文本向量化模型用来做本地知识库检索 ollama pull bge-m3拉取完成后先不要急着打开任何前端直接在终端里验证模型是否能正常响应。用 curl 调一次 OpenAI 兼容接口curl http://localhost:11434/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5:7b, messages: [{role: user, content: 用一句话介绍你自己}] }如果返回一个包含choices字段的 JSON说明模型服务已经通了。从这一步开始这个模型就不再是终端里的玩具而是一个可以被任何程序调用的本地大模型服务。2.4 服务配置别忽略上下文和并行参数Ollama 默认配置对家用场景够用但有两个参数我强烈建议根据你的机器情况去调整。第一个是上下文长度。Ollama 默认会根据模型自动选择上下文长度但部分模型的最大上下文很高比如 Qwen 系列支持 128K硬件却扛不住。如果你发现推理很慢或者频繁报显存不足可以用环境变量把上下文限制到 8K 或 16K# Linux/macOS 启动服务前设置 export OLLAMA_CONTEXT_LENGTH8192 ollama serveWindows 上在系统环境变量里添加OLLAMA_CONTEXT_LENGTH即可。别追求一步到位开到最大家用助理场景 8K 已经足够覆盖大部分对话加上检索增强后的上下文通常不会超过几千字。第二个是并发参数。Ollama 默认允许一定数量的并发请求如果你的机器只有一块中端显卡多个请求同时进来会互相争抢显存和计算资源反而谁都很慢。我自己在跑自动化任务时会把并发限制到 1确保单请求延迟最低export OLLAMA_NUM_PARALLEL1当然如果你想在局域网里给多台设备提供 AI 服务可以适当调高但那时就要在更大的显存和更合理的并发之间找平衡。3. 搜索与检索让模型拥有“记忆”和“耳目”3.1 模型不能回答的两类问题很多人把大模型跑起来之后的第一反应是终于可以免费无限聊天了。然后用了三天就会发现这玩意儿只适合当高级聊天机器人真正干正事的时候很拉胯。原因很现实模型的知识来自训练数据训练数据有截止日期而且绝对不包含你电脑里的个人资料。要让本地模型真正有价值必须给它接上“搜索”能力。这里的搜索分两种第一种是针对私有资料的语义检索也就是常说的 RAG第二种是针对实时外部信息的搜索入口。当模型能访问这两类信息时它才能从“一个会接话的机器”变成“一个能随时随地回答问题的工作助理”。3.2 给本地文件建立向量索引RAG 的完整链路RAG 的工作流程用大白话说就是三步先把你的文档切成小段把每段文字变成向量存进向量数据库当用户提问时把问题也变成向量在数据库里找出语义上最相近的几个段落最后把这些段落拼进提示词让模型基于这些资料回答。第一步是准备 embedding 模型。前面我们已经拉了bge-m3这个模型对中文和英文都能生成高质量向量测试下来比很多小体积英文模型更靠谱。接着需要安装 Python 依赖pip install chromadb openai然后写一个脚本把本地文档切块并写入向量库。示例代码如下import os from openai import OpenAI import chromadb embed_client OpenAI( base_urlhttp://localhost:11434/v1, api_keynot-needed ) EMBED_MODEL bge-m3 def get_embedding(texts): resp embed_client.embeddings.create(modelEMBED_MODEL, inputtexts) return [item.embedding for item in resp.data] def split_text(text, chunk_size600, overlap150): chunks [] cursor 0 while cursor len(text): end cursor chunk_size chunks.append(text[cursor:end]) cursor end - overlap return chunks # 初始化 Chroma 持久化目录 vecdb chromadb.PersistentClient(path./home_kb) collection vecdb.get_or_create_collection( namehome_docs, metadata{hnsw:space: cosine} ) # 以单个 md 文件为例 content open(notes.md, encodingutf-8).read() chunks split_text(content) vectors get_embedding(chunks) for idx, chunk in enumerate(chunks): collection.add( ids[fnote_0_{idx}], embeddings[vectors[idx]], documents[chunk], metadatas[{source: notes.md}] )查询时逻辑反过来把问题向量化后搜最相似的几条def search_kb(question, top_k5): q_vec get_embedding([question]) results collection.query( query_embeddingsq_vec, n_resultstop_k, include[documents, metadatas, distances] ) return results有几个细节值得专门提醒。切块大小不要一刀切如果是 Markdown 笔记最好先保留标题层级再按标题块切分如果是一堆无格式文本600 字左右带 150 字重叠是比较稳的初始参数。重叠存在的意义是防止一个完整语义被从中间切断导致检索时漏掉关键上下文。3.3 自建搜索服务把网页搜索变成可用 API如果你的需求不只是查本地文件还要让 AI 回答“某个软件最新版本号是什么”这类实时问题就得给模型接入网页搜索。这一步我选的是 SearXNG一个完全开源的元搜索引擎可以聚合多个公共搜索引擎的结果而且它提供 JSON 格式输出方便程序化调用。Docker 部署是最省事的方式docker run -d -p 8080:8080 searxng/searxng启动后浏览器访问http://localhost:8080就能看到一个搜索引擎页面。默认设置下 JSON API 可能关闭需要修改配置。进入容器对应的配置目录编辑settings.yml把格式和限制调整成search: formats: - html - json safe_search: 0 server: public_instance: false limiter: false用 Python 调用测试import requests resp requests.get( http://localhost:8080/search, params{q: Ollama 最新版本, format: json}, timeout15 ) data resp.json() for item in data.get(results, [])[:5]: print(item.get(title), item.get(url))这里需要说明一点SearXNG 本身在家里跑但它要去抓取互联网上的公共搜索结果时仍然需要这台机器能正常访问外网。所谓“全部跑在家里”指的是系统和数据处理都掌控在自己手里不代表不依赖任何外部信息源。3.4 把检索结果拼进提示词别让模型瞎编有了知识库检索和网页搜索接下来最关键的一步是把结果正确“喂”给模型。很多人检索做得好好的结果回答还是一塌糊涂就是因为提示词没有约束模型只能基于材料回答。我常用的模板很简单但效果非常好你是我的本地资料助手。请优先根据下面的参考资料回答如果参考资料中没有相关内容请直接说“资料里没有找到”不要编造。 参考资料 1. [来源notes.md] 这里放检索到的文本片段 2. [来源网页搜索结果] 这里放搜索到的网页标题和摘要 用户问题XXX给模型的上下文不是越多越好。检索到的 top 5 段资料如果全部塞进去上下文会很长模型反而抓不住重点。我建议先按相似度排序取前 3-5 段每段控制在几百字内总长度限在 3000 字左右既给足信息量又控制推理延迟。4. 全栈串联API、前端与 Agent 的整合4.1 用 FastAPI 写一个统一后端当我们有了模型服务、向量库和搜索服务以后一个最常见的需求就是把这些东西暴露成统一接口给前端或手机端调用。这里我用 FastAPI 写一个极简后端把“检索知识库 搜索网页 调用模型”整合成一个/assistant/answer接口。from fastapi import FastAPI from pydantic import BaseModel import requests from openai import OpenAI import chromadb app FastAPI() llm_client OpenAI(base_urlhttp://localhost:11434/v1, api_keynot-needed) LLM_MODEL qwen2.5:7b vecdb chromadb.PersistentClient(path./home_kb) collection vecdb.get_or_create_collection(home_docs) class Question(BaseModel): question: str def search_kb(question: str): resp llm_client.embeddings.create(modelbge-m3, input[question]) q_vec [item.embedding for item in resp.data] results collection.query(query_embeddingsq_vec, n_results3) return results.get(documents, [[]])[0] def search_web(question: str): resp requests.get( http://localhost:8080/search, params{q: question, format: json}, timeout15 ) items resp.json().get(results, [])[:5] return [f{t[title]}: {t.get(content, )} for t in items] app.post(/assistant/answer) def answer(item: Question): kb search_kb(item.question) web search_web(item.question) context for i, doc in enumerate(kb): context f[知识库片段 {i1}] {doc}\n for i, doc in enumerate(web): context f[网页结果 {i1}] {doc}\n prompt f你是我的本地资料助手。请优先根据下面的资料回答资料没有的内容不要编造。\n\n{context}\n\n用户问题{item.question} resp llm_client.chat.completions.create( modelLLM_MODEL, messages[{role: user, content: prompt}] ) return {answer: resp.choices[0].message.content}启动方式一行命令uvicorn main:app --host 0.0.0.0 --port 9000这个后端就相当于整个系统的“方向盘”它决定了每个问题该走本地向量库还是网络搜索然后把拿到的资料交给大模型总结。以后无论你换任何前端只要对接这个接口就行。4.2 快速体验路线用 Open WebUI 白嫖现成前端如果你不想从零写前端Open WebUI 是目前最成熟的开源方案之一原生支持 Ollama、支持知识库上传、支持多用户。部署命令docker run -d -p 3000:8080 \ -v open-webui:/app/backend/data \ -e OLLAMA_BASE_URLhttp://host.docker.internal:11434 \ --name open-webui \ ghcr.io/open-webui/open-webui:main启动后访问http://localhost:3000注册一个管理员账号在设置里把 Ollama 服务地址填成本机地址。这样你能得到一个很漂亮的聊天界面直接在网页里和本地模型对话。但我的建议是如果你想要快速体验直接上 Open WebUI如果你想弄懂全栈链路还是得手写一遍上面的微型后端。因为现成前端帮你隐藏了“检索—拼接—调用模型”的过程很多问题黑盒化了出了问题很难排查。4.3 加一层 Agent 能力让 AI 决定调哪个工具当模型解决了“理解问题”和“生成回答”之后下一步要解决的是“执行任务”。这涉及到工具调用也就是让模型自己决定去调知识库、搜索网页或者执行一段代码。最标准的技术方案是 Function Calling。给模型定义两个工具一个搜知识库一个搜网页{ type: function, function: { name: search_kb, description: 在本地知识库中检索用户笔记和资料, parameters: { type: object, properties: { query: {type: string, description: 检索关键词} } } } }然后调用大模型时把tools参数传进去。模型如果觉得需要查询知识库会返回tool_calls而不是直接输出答案。程序拿到这个调用请求后执行具体的检索函数再把结果作为新消息回传给模型让它最终总结。不过说实话本地小模型的 Function Calling 稳定性并不总能达到云端大模型的水平。我自己的经验是7B 和 14B 的模型偶尔会虚构工具名或漏传参数。因此在真正的生产链路里我建议先做一个“意图路由”层用规则或更小的分类模型判断问题属于“知识库问答”“网页实时问答”还是“普通闲聊”再走各自的流程。这样比完全依赖模型自我决策更可控出了 bug 也更好定位。4.4 从开发到使用的工程化经验把整套服务串起来以后有几个工程习惯会让后续维护省心很多。每一个组件都单独跑一个进程或以容器方式运行不要用终端直接挂在前台建议用 systemd 或 Docker Compose 管理开机自动启动。我习惯的启动顺序是先是 Chroma 和 Ollama因为它们是基础和耗时件然后启动 SearXNG最后启动 FastAPI 后端和前端。如果哪次重启后回答不正常优先检查这些服务有没有都起来而不是直接怀疑模型本身。一个非常重要的安全提醒这些服务默认都没有鉴权千万不要简单粗暴地做端口映射暴露到公网。家用环境建议只监听内网如果需要从手机在外面访问至少加一层带用户名密码的反向代理。在配置 Ollama 或 FastAPI 时监听地址不要写0.0.0.0以外的任何公网地址除非你已经做了完整的身份认证。5. 踩坑实录常见问题排查与调优方向5.1 问题速查表我在搭这套系统的过程中踩了不少坑整理成一个速查表方便你遇到问题时直接对号入座症状常见原因处理方式调用 API 提示 model not found模型名写错或没下载完整执行ollama list查看真实名称和标签推理特别慢上下文开太大或模型超过显卡容量降低OLLAMA_CONTEXT_LENGTH或换更小模型频繁报显存不足权重加 KV Cache 超出显存换 Q4 量化、缩短上下文或加内存交换检索结果和问题完全无关embedding 模型不匹配语种中文库改用 BGE 系列模型检索总是返回同样几条切块太大或重叠不合理减小 chunk 到 400-600 字并增加重叠回答喜欢编造资料提示词没有约束来源强制要求只根据参考内容回答SearXNG 返回 JSON 格式报错JSON 格式被配置文件禁用了确认search.formats包含 json网页刷新后向量数据消失没有使用持久化目录Chroma 必须指定PersistentClient5.2 “慢”的调优心得本地模型的速度受硬件限制是硬道理但很多时候慢不是硬件不行而是配置不合理。我实测的几条经验如下。一是模型常驻显存。Ollama 默认会在空闲一定时间后把模型从显存中卸载如果每次提问都要重新加载会额外多出几十秒等待。可以在调用时设置keep_alive参数为一小时或更长确保连续使用时模型一直驻留显存。二是把 embedding 模型和大模型分开使用。有些人图省事直接用同一个大模型去生成向量那是巨大的浪费。embedding 生成用 bge-m3 这种小型专用模型又快又省资源对话才让 7B 大模型上场。三是 CPU 推理的用户别盲目追求高精度量化。如果只能靠 CPU 跑模型Q8 和 Q4 的速度差非常明显。我建议 CPU 用户从 Q4_K_M 开始试只要回答质量