如果你只是想在本机跑一个大模型聊聊天,现在的工具其实多到有点挑花眼;但如果你想要的是一个开源的、完全运行在本地、又能当正经生产力工具的 AI 平台——能管理模型、能挂知识库、能调用工具、能同时给几个人用、还能对外提供标准 API——你会发现市面上几乎没有现成答案。我花了大概八个月做了一个,取名 Hearth,核心目标就一句话:一台不出网的机器也能把这套东西跑起来,并且用得不难受。文章会把这套平台的架构、选型理由、核心模块、本地化优化的实战经验,以及开源之后被用户折腾出来的教训,从头到尾完整拆一遍。适合两类人读:一类是准备自己搭本地 AI 平台的技术负责人或独立开发者,另一类是手里有开源项目、想提前知道发布后会面对什么的朋友。
1. 从"本地能跑模型"到"本地 AI 平台",中间隔着一整个工程
1.1 我最初想解决的痛点
开始动手之前,我桌上已经摆了好几个现成工具,Ollama 负责拉模型,Open WebUI 做聊天界面,AnythingLLM 挂知识库。说实话,如果只是零散地聊聊天,这套组合完全够用。但真正把 AI 放进日常工作流之后,痛点一个接一个地冒出来:模型装多了没人管,GGUF、AWQ、GPTQ 各种格式混在同一个目录里,时间一长根本分不清哪个文件对应哪个版本;想给知识库加一批文档,得自己另外搭一套向量库和分块流程,中间任何一个环节不对,检索效果就稀烂;最难受的是想让自己写的脚本调用本地模型,结果发现各家接口根本不兼容,有的只支持 chat,不支持 embeddings,更别谈 function calling。这些零散问题加在一起,指向的其实不是"再找一个聊天前端",而是一个能自己掌控的完整平台。
1.2 "full fledged"到底指什么
"全功能"这个词很容易变成营销话术,所以我一开始就给自己划了一条明确的验收线,把能力拆成可逐项检查的条目。后面所有迭代都跟着这条线走,而不是凭感觉加功能。
| 能力维度 | 最低可用标准 | Hearth 的做法 |
|---|---|---|
| 模型管理 | 能装、能删、能切换 | 模型仓库 + 量化信息识别 + 热切换 |
| 对话引擎 | 多会话、流式输出、上下文可控 | 上下文压缩、token 级统计 |
| 知识库 | 能传文档、能检索、能标来源 | 分块 + 混合检索 + 重排,答案带引用 |
| 工具调用 | 模型能触发外部动作 | 原生 function calling + 插件机制 |
| API 网关 | 兼容主流 SDK | 实现 OpenAI chat / embeddings / models 接口 |
| 多用户 | 权限隔离、配额、审计 | 角色权限 + Token 配额 + 操作日志 |
| 离线部署 | 断网可用、可迁移 | 离线模型包 + 全量依赖 vendoring |
这张表后来变成了 README 的功能清单,也变成了每次发版前的验收标准。没有它,"全功能"很容易做成"什么都沾一点,什么都用不顺"。实际开发时,这张表还帮我挡掉了很多需求膨胀:凡是不能对应到表里某一行的功能,我一律先不做,等真的有人需要再说。
2. 架构取舍:为什么是 Python 后端 + 本地推理引擎 + 适配器层
2.1 核心数据流
整体是一个比较传统的单体加插件架构,没有一开始就上微服务。本地平台的用户量级决定了微服务带来的运维成本远大于收益,一套 docker compose 能拉起来的系统,没必要拆成七八个服务互相调用。请求路径大概是这样的:浏览器或外部 SDK 先到 API 网关,网关完成鉴权、配额和路由,把请求交给会话服务;会话服务负责取历史消息、计算 token 数、必要时压缩上下文;然后是模型路由,根据用户选的模型、当前负载和硬件资源,把请求发到对应的推理后端;推理结果以流式方式一路返回前端。
可见的部分分成三大块:FastAPI 写的后端服务、Vue3 写的前端界面、可插拔的推理适配层。存储层默认用 SQLite,配置项里可以一键切成 PostgreSQL——本地单机场景 SQLite 完全够用,还少一个常驻服务要维护。推理适配层是整套架构里最重要的抽象,它决定了平台能接多少种后端,也决定了未来更换运行时的时候,要不要动上层代码。
2.2 模型运行时三选一,我为什么没全押
本地推理的运行时选项很多,我实际对比过三类:直接内嵌 llama-cpp-python、外部 Ollama 进程、vLLM 服务。三者的取舍非常典型,很多人在选型时会犹豫很久。
- 直接内嵌:部署最简单,一个进程解决所有问题,跨平台支持好,但并发和长上下文表现一般,而且 llvm-cpp 的 Python 绑定在 Windows 上编译经常翻车。
- Ollama:模型管理和命令行体验一流,但作为外部进程,平台想拿到细粒度的运行参数、做动态显存调度,会比较绕。
- vLLM:吞吐和并发是王者,但要求 Linux 加足够显存,CUDA 依赖很重,普通用户的桌面环境很难伺候。
我的选择是:默认内置 llama-cpp-python,同时提供 Ollama 和 OpenAI 兼容端点的适配器。普通用户开箱即用,想上规模的人可以外接 vLLM,想用远端模型的人也能把 Hearth 当成统一入口。多维护一层适配器的代价,是每个新版本都要在不同后端之间做回归测试;但换来的是平台不被任何一个运行时绑架。选型这种事,最怕的就是"因为大家都在用"而跟风,一定要回到你自己的部署场景里去看。
2.3 前端为什么用 Vue3 而不是 Next.js
做本地平台,前端框架我也纠结过一段时间。最后选了 Vue3 + Naive UI,不是 React 不好,而是本地应用几乎不需要服务端渲染,Next.js 的一部分优势在这里发挥不出来。Vue3 的单文件组件和响应式系统写管理后台类界面效率很高,Naive UI 的组件风格偏简洁,适合做成 Chat 类产品的干净界面。流式对话用 WebSocket 推,前端按 token 逐字渲染,配合虚拟列表处理长对话,实测几千条消息的会话滚动起来也不卡。另一个考虑是之后套桌面壳方便,Vue3 构建出来的纯静态产物,不管塞进 Electron 还是 Tauri 都很省事,不会遇到 SSR 应用在桌面环境里的各种水土不服。
2.4 为什么不直接套 Open WebUI
这个问题被问过无数次。我实际试过在那些成熟的 WebUI 项目上做二次开发,聊天交互做得确实好,但它们的定位是"给 Ollama 用的聊天前端",不是为"平台"设计的:没有统一的模型抽象层,API 权限控制弱,插件机制相对封闭。我需要的是一套从模型管理、知识库到 API 网关都长在自己地盘上的系统,而不是在别人的前端里缝缝补补。自己写前端的代价是工作量大,但换来的是每一层都能按自己的需求裁剪。对开源项目来说,这种可控性在后期的迭代速度上体现得非常明显——改一个按钮位置不用去 fork 整个上游项目。
3. 让平台配得上"full fledged"的六个模块
3.1 模型管理:下载、校验、量化识别与热切换
模型管理我做了三层:模型仓库、运行时实例、请求路由。模型仓库负责目录和元数据,每个模型在磁盘上有一个独立目录,里面除了 GGUF 文件,还有一份 manifest 文件记录来源 URL、SHA256、参数量、量化格式、推荐上下文长度。这样的目录结构,让人哪怕直接打开文件系统,也能一眼看明白:
models/ └── Qwen2.5-7B-Instruct/ ├── Q4_K_M.gguf ├── Q8_0.gguf ├── config.json └── manifest.yaml下载走 HuggingFace 和 ModelScope 两个渠道,国内用户可以直接切 ModelScope,断点续传和 SHA256 校验都做了。量化识别这步特别容易被忽略,但非常重要:同一个模型可能同时有 Q4_K_M、Q8_0、FP16 三个文件,界面上必须明确区分,不然用户以为自己换了更好的模型,其实只是换了个更大的文件,速度还变慢了。热切换的意思是,用户切换模型只影响新请求,正在进行的会话完全不受影响。实现上就是路由层维护一个模型 ID 到当前实例的映射,切换只改映射,不杀进程,不中断已有连接。
3.2 知识库不只是"往向量库塞文本"
知识库模块是我踩坑最深的。很多人以为把 PDF 丢进去、切片、embedding、检索,就完事了,实际做出来的效果会很差。我最后落地的流程是:文档解析、清洗、结构化分块、混合检索、重排、带引用生成。分块策略上,按标题层级做切片,而不是按固定字符数硬切,因为按标题切能保留上下文边界;每块之间保留 80 到 120 字符的重叠,避免检索时把关键句拦腰截断。表格和代码块单独保留,不塞进普通文本块里,否则检索出来要么缺列头,要么缺缩进。
Embedding 模型用的 bge-m3,维度和多语言表现都够用。检索阶段做两路召回,一路 BM25 关键词,一路向量相似度,两路结果合并后交给 reranker 模型重新排一遍,最后把得分最高的几段拼进提示词。这里最核心的一条经验是:纯向量检索在专有名词、缩写、版本号面前表现很差,BM25 补位之后效果提升非常明显。引用来源是硬需求,所有生成回答必须标出材料出处,否则在正经工作场景里没人敢用。我自己在内部跑过一个测试,没有来源标注的回答,评审几乎全部驳回;加上来源之后,通过率直接翻倍。
3.3 工具调用与 Agent:让模型不再只是聊天窗口
工具调用是平台从"聊天机器"变成"执行器"的分水岭。我实现了 OpenAI 风格的 function calling,模型在回复里声明要调用的工具和参数,平台侧校验参数、执行、把结果送回模型,再由模型生成最终答案。内置工具包括:代码解释器(在隔离容器里跑 Python)、定时任务、系统信息查询,以及一个可扩展的插件机制。插件协议做得很轻,一个 manifest.yml 声明名称、描述、参数 JSON Schema,一个 Python 入口文件实现执行函数,用户不必改主程序就能给平台加技能。
安全方面有一条底线我特别坚持:默认所有工具都在沙箱里执行,代码解释器必须跑在独立容器中,不能直接碰宿主机文件系统。这个设计被一些用户嫌"多此一举",但等他们把平台部署到公司内网、要过安全评审的时候,这条底线反而成了最有力的卖点。工具调用的调试也比普通对话麻烦,我在界面上加了工具调用日志面板,模型传进来的参数、工具返回的结果、最终生成的内容,三个阶段分开展示,排查问题的时候能省掉大量时间。
3.4 兼容 OpenAI API 的对外网关
这是我认为"full fledged"和"自嗨"之间的分水岭:平台必须能当一个标准 API 服务来用,而不是只能在网页里聊天。我实现了 /v1/chat/completions、/v1/embeddings、/v1/models 这些主流接口,支持流式 SSE 和 JSON 结构化输出。外部任何用 OpenAI SDK 的项目,只要把 base_url 指到 Hearth,把 key 换成平台分配的 key,就能把本地模型接进原有系统。这个兼容层让我现有的工具链几乎没有改动就接上了平台,价值非常大。
这里有个很实际的兼容性细节:部分带思考模式的推理模型,在响应里除了 content 还会返回一个 reasoning_content 字段。如果你保存多轮对话时把这个字段丢了,下一轮请求就可能在服务端返回 400 错误。所以网关在落库消息时把思考内容和正文分开存,再次发送时按对端要求原样带回。这类细节不做实测是发现不了的,我当初是在接某个推理模型的时候被报错连番轰炸,才意识到消息落库的字段设计要更细。
3.5 多用户、配额与审计
本地平台不等于单机单人。小团队部署之后,最常见的使用方式是几个人共用一个推理服务,这时候用户体系就是刚需。我做了最轻的一版:管理员创建账号、分配角色,角色控制能看到哪些模型、能否管理知识库;每个用户每天有 token 上限,防止某个人一次性把显存占满,其他人全部排队。配额真正跑起来之后我才意识到,限制用户一天用多少 token 不只是成本控制,还能倒逼大家把提示词写得更精简,服务器负载明显下降。
审计日志这点被很多自部署用户低估。所有涉及模型切换、知识库变更、工具执行的敏感操作都写进日志,真到了公司内部要汇报数据流向、做安全审计的时候,这是最直接的证据。权限模型我尽量做成声明式配置,而不是把规则写死在代码里,这样管理员改角色权限的时候不用碰代码,直接在界面上勾选就行。
3.6 离线安装与断网使用
"本地可用"对我的严格含义是"断网也要可用"。Hearth 为此做了两件事:一是所有依赖都支持离线安装,提供带完整 wheel 包的内网安装包;二是模型可以提前打包成"模型包"导入,不需要现场从网上下载。系统启动时会检测网络状态,如果远端仓库不可达,就自动切换到本地缓存模式,所有下载入口变成"从模型包导入"。
这个功能在隔离网段里特别有用。我不止一次收到用户反馈说公司机器不允许连外网,之前他们只能放弃本地 AI 方案,现在终于可以有边界地解决了。离线模型包的格式我定义为 tar.zst 压缩包,里面包含模型文件、配置文件、许可证文本,导入时自动校验 SHA256,防止传输过程中文件损坏。这套机制看起来不起眼,却是企业用户决定要不要长期使用的关键因素。
4. 本地化真正的难点:显存、性能、依赖地狱与打包
4.1 24GB 显存也撑不起"什么都跑"
很多人以为买了大显存显卡就万事大吉,实际一跑起来才会发现,显存大头不只是权重。以 7B 模型 Q4_K_M 量化为例,GGUF 文件大约 4.7GB,看起来 24GB 显卡绰绰有余;但如果上下文开 32K,KV Cache 会多吃掉好几个 GB;再开并行支持多人同时用,每增加一路并发,KV Cache 又翻一倍。所以显存规划要按"权重 + 目标上下文 × 并发数"来算,而不是只看模型文件大小。
我因此在平台里加了一个显存估算器,用户选模型和上下文长度的时候,界面直接显示预计占用,避免生成到一半爆显存。实测下来,24GB 显卡比较舒服的组合是:7B 或 8B 模型 + 16K 上下文 + 2 到 4 路并发;往上跑 32B 模型就建议开 CPU offload,或者干脆换 48GB 以上的机器。很多人不愿意面对这个现实,总想着"反正显存有 24G,直接上最大的模型",结果要么被 OOM 折磨,要么速度慢到没法用。
4.2 CPU 推理怎么做到"能等得起"
不是所有人都有好显卡,所以纯 CPU 环境我也花了不少功夫。最重要的经验有三条:一是尽量用 Q4 量化模型,推理速度和内存占用比高阶量化好太多;二是上下文长度要克制,CPU 上开 32K 上下文简直是自虐,生成速度和稳定性都会明显下滑;三是利用 llama.cpp 的层数参数做部分 GPU offload,哪怕只有几层能放到 GPU 上,效果也聊胜于无。
另一个容易被忽略的点是 Apple Silicon 用户。llama.cpp 的 Metal 后端在 M 系列芯片上表现很不错,不需要 NVIDIA 显卡也能获得可以日常使用的速度。我在文档里专门放了一张按硬件推荐模型档位的表,很多用户照着选就不再纠结了。CPU 环境下首 token 延迟比生成速度更影响体验,所以我把这块的优化重点放在 prompt 缓存上,重复前缀的请求直接复用计算结果,实测能省掉不少重复计算时间。
4.3 依赖地狱:llama-cpp-python 编译失败的真相
本地 Python 项目最头疼的依赖就是 llama-cpp-python。这个包在 Windows 上从源码安装经常失败,报错末尾通常能看到类似 cl.exe failed with exit status 2 的信息。本质原因不是代码问题,而是 Windows 上缺少完整的 MSVC 编译环境,或者 CMake 版本不对,Python 正试图现场编译 C++ 扩展。我的处理方式分三层:第一,发布预编译 wheel,让绝大多数用户用 pip 直接装,不用碰编译器;第二,文档里给出 Windows 上安装 Visual Studio Build Tools 和 CMake 的精确步骤,给确实需要自己编译的人留一条路;第三,安装脚本默认从预编译源走,只有用户显式开启本机构建才会去现场编译。
另一个依赖坑是 PyTorch。很多 embedding 方案离不开它,但对一个推理平台来说,为了一个 embedding 模型拖进两三个 GB 的 PyTorch,实在不划算。我后来把 embedding 和 reranker 都改成了 ONNX Runtime 或 llama.cpp 原生支持,默认安装路径完全不需要 PyTorch,只有用户主动开启某些高级功能才会装上。这个改动把基础安装包体积直接砍掉了一大截,对国内网速不稳定的用户来说,体感差异非常明显。
4.4 打包分发:三种安装方式
本地平台如果只有一个 git clone 教程,基本等于没发布。Hearth 现在提供三种安装途径:Docker Compose 一键起服务,适合部署到 NAS 或家庭服务器;官方安装脚本,适合 Linux 和 macOS 上愿意自己折腾的人;桌面版用 Tauri 套壳,适合 Windows 用户双击安装。三种方式共享同一套后端,只是包装不同。
打包过程中我吃过两个教训:一是 Windows 长路径问题,模型目录层级深了之后很容易超过 260 字符,安装器里必须默认开启长路径支持,否则用户解压模型包到一半就报错;二是杀毒软件误报,早期用 PyInstaller 打的包经常被 Windows Defender 拦下来,后来换成 Tauri 套壳并做了代码签名,误报率才明显下降。这类问题在开发机上完全遇不到,只有真的发给大量用户之后才会集中暴露。
4.5 我的实测性能参考
整理一份测试环境的数据供参考,这些数字只是典型的"可用"水平,不代表上限,不同模型和量化档位差异很大:
| 硬件环境 | 模型 | 量化 | 上下文 | 生成速度 |
|---|---|---|---|---|
| RTX 3090 24GB | 7B | Q4_K_M | 8K | 约 50-70 tokens/s |
| RTX 3090 24GB | 32B | Q4_K_M | 8K | 约 12-18 tokens/s |
| 纯 CPU i7-12700 | 7B | Q4_K_M | 4K | 约 4-6 tokens/s |
| Apple M3 Max | 7B(Metal) | Q4_K_M | 8K | 约 25-35 tokens/s |
生成速度只是体验的一半,prompt 处理的 prefill 速度决定了首 token 延迟。很多人只盯着生成速度,结果首 token 等半天,体感还是崩盘。我在界面上把首 token 延迟和生成速度分开展示,用户才能真正判断瓶颈在哪个环节。这部分数据对性能调优的指导意义,比任何跑分软件都大。
5. 开源之后:从 issue、许可证和 CI 里学到的教训
5.1 最高频的 issue 其实与技术无关
项目发出去之后,我原本以为最难的会是模型效果或者性能,结果收到最多的 issue 是安装失败和使用困惑。很多人在 Windows 上装不对 Python 版本、装不上 CUDA 依赖、或者不知道下载哪个模型。这件事让我意识到一个很现实的问题:开源项目最大的成本不是写代码,而是把"新用户从零跑起来"这件事做到位。
后来我把安装文档重写成按操作系统分支的方式,每个分支带截图和完整命令,还加了一个诊断工具,用户跑一下就能把环境信息一次性贴进 issue。命令大概长这样:
hearth doctor它会检测 Python 版本、CUDA 驱动、磁盘空间、端口占用,并输出一份标准化报告。这个改动之后,真正无效的 issue 少了很多,能复现的问题也更容易定位。开源维护者如果不想被"装不上"类 issue 淹没,一定要尽早把诊断工具做出来。
5.2 License、模型协议与合规
开源代码是一回事,模型是另一回事。平台代码用 Apache-2.0,但模型本身有各自的许可证,Llama 系、Qwen 系、其他社区模型的条款都不一样,混用的时候很容易踩坑。我在平台里做了一个模型详情页,下载哪个模型就展示对应许可证摘要,并且不允许在没有用户确认的情况下自动下载商用条款不明的模型。这点在开源社区非常加分,因为很多企业用户第一件事就是确认许可证。
另外,项目自身的仓库里我没有捆绑任何大模型文件,仓库体积保持精简,也避免了模型许可证对代码许可证的污染。如果有用户需要定制化预装模型的发行版,就走独立的构建流程,把模型和代码分开交付。许可证这件事,没出事的时候觉得无所谓,真出事就是法律风险,一定要在一开始就处理干净。
5.3 我最后悔没早点做的事
回看整个开发周期,如果重来一遍,我会更早做三件事。第一,写详细的技术决策记录,把每个模块为什么这样选型写下来,省得三个月后自己都忘了当初的考虑,也让贡献者知道不是随便一拍脑袋定的。第二,建一个跨平台的自动测试矩阵,在 CI 里把 Windows、macOS、Linux 三种环境的安装和核心流程都跑一遍,很多 bug 其实早该在合并前被拦住。第三,把错误信息写得像人话,而不是把 Python traceback 直接甩给用户。这三个都不是看起来最酷的工作,但它们是开源项目能持续活下来的地基。
最后再说一点个人体会。做本地 AI 平台这两年,我最大的感受是"本地"并不是一个限制,反而是一种解放。不依赖外部服务,就不受配额、限流和接口变动的影响;数据在自己手里,很多部署障碍自动消失;模型可以随便换,不用迁就任何供应商。这个项目现在已经是我日常工作的默认生产力入口,知识库、代码解释器、API 网关天天在跑。如果你也在酝酿做一个本地优先的 AI 项目,我的建议很简单:先把"一个人离线环境下用它完成一件真实工作"走通,再谈功能和开源。想交流具体实现细节的朋友,欢迎在项目仓库的讨论区找我。