Local-first AI 工作流:把隐私和可控性放在前面
本地优先是一种产品取舍
我在 2025 年 Q4 开始把自己的日常 AI 辅助编码和笔记处理逐步迁移到本地模型。起因很具体:我负责的一个内部工具需要把客户的合同文本喂给 AI 做摘要提取,合同里有金额、条款和对手方名称。把这些内容发给任何云端 API,从合规角度都不合适。
这迫使我认真评估本地优先 AI 工作流的可行边界。不是「能不能跑起来」的问题——Ollama 装好之后拉个 7B 模型,十分钟就能在终端里跑通 demo。真正的难点在于:本地模型在什么场景下够用、什么时候必须退回云端、数据同步怎么做、资源开销怎么控制。这些问题没有标准答案,但我在反复调试中摸索出了一套可复用的判断框架。
这篇文章记录的就是这套框架。
「Local-first」到底是哪种隐私
2019 年 Martin Kleppmann 在论文《Local-First Software: You Own Your Data, in Spite of the Cloud》中提出了 local-first 的八项理想特性,核心主张是:用户应该拥有自己的数据,应用不应强制依赖服务器,软件应该能在离线状态下正常工作。到 2026 年,FOSDEM 已经开设了 local-first 专属 track,CRDT、Automerge、ElectricSQL 等同步基础设施日趋成熟,local-first 已经从学术概念变成了工程实践。
但 AI 场景下的「local-first」比 Kleppmann 原文讨论的更复杂。Nimbalyst 在 2026 年的一篇对比文章中做了有用的区分:
| 模式 | 含义 | 数据在哪里 | 模型在哪里 | 典型工具 |
|---|---|---|---|---|
| 本地优先数据 | 代码、会话、历史留在本机,模型可以调云端 | 本机 SQLite/文件 | 云端 API | Cursor Ghost Mode、Pieces |
| 本地模型执行 | LLM 在本机硬件运行,零字节离开设备 | 本机 | 本机 GPU/CPU | Ollama、LM Studio、llama.cpp |
| 自托管 | 组织在自有基础设施运行服务端 | 组织内网 | 组织内网 GPU 集群 | Tabby、Codeium Enterprise |
三种模式解决的不是同一个问题。如果你的诉求是「代码不能离开我的电脑」,你需要的是本地模型执行。如果只是「我不想让 AI 公司拿我的历史数据训练」,本地优先数据就够了。如果是公司层面的合规要求,需要的是自托管。混淆这三种模式,是选错方案最常见的原因。
我在合同摘要那个项目里需要的是第一种和第二种兼得:文件不出本机,模型也在本机跑。这直接排除了所有 BYOK(Bring Your Own Key)方案,只剩下 Ollama + llama.cpp 这条路。
端侧推理的工程现实
量化是核心约束
端侧推理能跑起来,完全依赖量化技术。一个 7B 参数的模型,FP16 精度需要约 14GB 显存,普通笔记本放不下。通过 GGUF 格式的 Q4_K_M 量化,同一个模型压缩到约 4.5GB,内存和推理速度都在可接受范围内。
但量化不是免费的。以下是我实际测试中的观察:
| 模型规模 | 量化方式 | 内存占用 | 代码补全质量 | 复杂推理能力 |
|---|---|---|---|---|
| 7B | Q4_K_M | ~4.5GB | 日常补全够用 | 多步推理经常出错 |
| 13B | Q4_K_M | ~8GB | 明显好于 7B | 简单架构设计可做 |
| 32B | Q5_K_M | ~22GB | 接近 GPT-4 水平 | 能处理中等复杂度任务 |
| 70B | Q4_K_M | ~40GB | 很强 | 接近未量化大模型 |
这些数据来自 M3 Max MacBook Pro(36GB 统一内存)上的实测。32B 模型是我在这台机器上能跑的最大可用规格,日常代码补全和文档摘要都够用。70B 模型需要量化到 Q3 才能装进内存,质量下降明显,实际可用性打折扣。
Red Hat 在 2025 年 9 月发布的对比报告中给出了更系统的数据:在 NVIDIA H200 上,vLLM 的峰值吞吐是 llama.cpp 的 35 倍(高并发场景),P99 首 token 延迟在 64 并发下仍然平稳。但这个数据只适用于多用户 GPU 服务场景。对于单用户本地推理,llama.cpp 的 CPU-first 设计反而是优势——它不需要 GPU,在消费级硬件上就能跑。
推理框架选择
| 维度 | llama.cpp | Ollama | vLLM | LM Studio |
|---|---|---|---|---|
| 设计哲学 | CPU-first 便携 | 开发者体验优先 | GPU 高吞吐服务 | 桌面 GUI 优先 |
| 需要 GPU | 否 | 否(有 GPU 加速) | 是 | 否(有 GPU 加速) |
| 并发支持 | 弱,排队模型 | 中等 | 强,线性扩展 | 单用户 |
| 适合场景 | 嵌入式/边缘/离线 | 本地开发/个人使用 | 生产级多用户服务 | 非技术用户试用模型 |
| API 兼容 | 自有格式 | OpenAI 兼容 | OpenAI 兼容 | OpenAI 兼容 |
| 量化格式 | GGUF | GGUF(底层 llama.cpp) | AWQ/GPTQ/FP16 | GGUF |
Ollama 在 2026 年 Q1 达到 5200 万月下载量,底层就是 llama.cpp。它提供了一层易用的封装,让你用 ollama run qwen3:8b 一行命令就能启动模型。但如果你需要压榨性能——比如在 RTX 5090 上把 Qwen3 Coder 32B 从 Ollama 的 30 tokens/s 提到 llama.cpp 直连的 52 tokens/s——就得绕过 Ollama 直接用 llama.cpp 的 C++ API。
我的选择是:日常开发用 Ollama 图省事,性能敏感的批处理任务走 llama.cpp 的 llama-server。
三个实际场景的踩坑记录
场景一:合同摘要的本地流水线
背景:法务同事每周需要处理 20-30 份合同,提取关键条款(金额、期限、违约责任、管辖法院)。数据敏感,不能出内网。
翻车过程:第一次尝试直接用 Ollama 跑 Qwen3 14B 做全文摘要,发现两个问题。第一,合同通常 20-50 页,远超模型的有效上下文窗口——14B 模型虽然声称支持 32K context,但实测超过 8K tokens 后摘要质量急剧下降,经常遗漏后半部分的关键条款。第二,单份合同处理时间约 90 秒,30 份合同串行处理需要 45 分钟,同事等不了。
修复方案:把流水线拆成三步——先用规则做文本分块(按章节标题切分),每块独立提取关键信息,最后用一次汇总调用合并结果。同时把模型从 14B 升级到 32B(Q5_K_M 量化,约 22GB),摘要准确率明显提升。
// ❌ 坏做法:把整份合同直接塞给模型
async function summarizeContractBad(fullText: string) {
// 全文 20-50K tokens,远超模型有效处理范围
// 后半部分条款会被遗漏
const response = await ollama.chat({
model: 'qwen3:14b',
messages: [{ role: 'user', content: `提取合同关键条款:\n${fullText}` }],
})
return response.message.content
}
// ✅ 好做法:分块提取 + 合并
async function summarizeContractGood(fullText: string) {
// 第一步:按章节分块,每块控制在 4K tokens 以内
const chunks = splitBySections(fullText, { maxTokens: 4000 })
// 第二步:逐块提取关键信息,并行处理
const extractions = await Promise.all(
chunks.map(chunk =>
ollama.chat({
model: 'qwen3:32b',
messages: [{
role: 'user',
content: `从以下合同片段中提取:金额、期限、违约责任、管辖法院。
未提及的字段返回 null。\n\n${chunk}`,
}],
})
)
)
// 第三步:合并去重,处理冲突
return mergeExtractions(extractions.map(r => r.message.content))
}关键差异在于:分块处理绕过了上下文窗口限制,32B 模型提升了提取准确率,并行处理把总耗时从 45 分钟压到约 8 分钟。
场景二:本地代码补全的质量边界
背景:我想在离线环境(飞行模式)下也有可用的代码补全。之前用云端 API,延迟和准确率都很好。切到本地后差距明显。
翻车过程:最初用 Qwen3 Coder 7B 做补全,单行补全(补全一个函数参数、一行 if 条件)质量不错,但多行补全经常跑偏——比如我写了一个 async function fetchUser(id: string),它补全的函数体经常用错 API 路径或者编造不存在的类型。更麻烦的是上下文感知能力:7B 模型对当前项目其他文件的代码结构基本没有感知,补全的变量名和已有代码风格不一致。
修复方案:做两件事。第一,换 32B 模型,多行补全质量直接上了一个台阶。第二,实现一个轻量的 RAG——在本地建一个当前项目文件的 embedding 索引,补全时把相关文件片段作为上下文注入 prompt。
# ❌ 坏做法:只传当前文件内容作为上下文
def complete_bad(current_file: str, cursor_pos: int):
prefix = current_file[:cursor_pos]
suffix = current_file[cursor_pos:]
# 模型对项目的其他代码一无所知
response = ollama.generate(
model='qwen3-coder:7b',
prompt=f'{prefix}\n// 继续补全:'
)
return response['response']
# ✅ 好做法:注入相关文件作为上下文
def complete_good(current_file: str, cursor_pos: int, project_index):
prefix = current_file[:cursor_pos]
suffix = current_file[cursor_pos:]
# 用当前文件的关键标识符(函数名、类型名、import 路径)
# 检索项目中最相关的 3-5 个代码片段
relevant_context = project_index.search(
extract_symbols(prefix + suffix),
top_k=4
)
prompt = f"""以下是项目中的相关代码:
{format_context(relevant_context)}
当前文件内容:
{prefix}
// 继续补全:"""
response = ollama.generate(
model='qwen3-coder:32b',
prompt=prompt,
options={'num_ctx': 8192}
)
return response['response']这个本地 RAG 实现很粗糙——用 sentence-transformers 做 embedding,结果存 SQLite + 余弦相似度检索。但它让 32B 模型的补全准确率从「勉强能用」提升到「大多数时候不需要手动改」。
场景三:同步冲突的处理
背景:我在三台设备上工作——办公室的 MacBook Pro、家里的 M2 Mac mini、出差时的 ThinkPad。本地优先意味着每台设备有自己的模型和索引,数据需要跨设备同步。
翻车过程:第一版同步方案直接用 Syncthing 同步整个 Ollama 模型目录和 SQLite 数据库。问题出在两台设备同时修改了同一个笔记文件时——Syncthing 产生了 .sync-conflict 文件,但 AI 生成的摘要和原始笔记之间的引用关系断了。更严重的是 embedding 索引:两台设备各自建了不同的索引,同步后索引和实际文件内容对不上,搜索结果开始出现不相关的条目。
修复方案:分两层解决。文件层用 Automerge(基于 CRDT 的文档合并库),文本冲突可以自动合并。索引层不跨设备同步——每台设备同步完文件后,用 file watcher 触发本地增量重建索引。AI 生成的内容(摘要、标注)带上设备 ID 和时间戳,合并时按时间戳取最新的。
// ❌ 坏做法:直接同步索引数据库
// 两台设备的索引合并后不一致,搜索结果出错
function syncIndexBad(localDb: Database, remoteDb: Database) {
// 直接复制行,忽略索引与文件内容的关联关系
remoteDb.indices.forEach(idx => {
localDb.indices.insert(idx) // 可能产生重复或过时索引
})
}
// ✅ 好做法:只同步文件,索引各自重建
function syncIndexGood(fileSync: FileSyncService, indexBuilder: IndexBuilder) {
// 监听文件变更事件
fileSync.on('change', async (event) => {
const { filePath, source } = event
// AI 生成内容带元数据,合并时按时间戳选择
if (event.hasConflict) {
const merged = automerge.merge(event.local, event.remote)
// 合并规则:AI 摘要取时间戳最新的版本
const latestSummary = [merged.localSummary, merged.remoteSummary]
.filter(Boolean)
.sort((a, b) => b.timestamp - a.timestamp)[0]
await fileSync.writeFile(filePath, { ...merged, summary: latestSummary })
}
// 文件稳定后增量重建索引
// 只处理变更文件,不做全量重建
await indexBuilder.rebuildForFile(filePath)
})
}核心原则是:文件是同步的原生类型,索引是文件的派生物,永远不要直接同步派生数据。
本地优先 AI 的系统架构
把上面的经验抽象出来,一个完整的 local-first AI 工作流长这样:
关键设计决策在「任务路由」这一层。不是所有任务都适合本地模型——我在实际使用中,大约 80% 的日常补全和摘要任务在本地完成,20% 需要复杂推理或处理多模态内容的任务走云端。路由规则可以很简单:检查网络状态(离线强制走本地)、检查内容敏感度(标记为敏感的不出去)、检查任务复杂度(prompt 超过一定长度或涉及多步推理的走云端)。
四种方案的对比
| 维度 | 纯本地模型 | 本地数据 + 云端模型 | 自托管服务 | 纯云端 |
|---|---|---|---|---|
| 数据隐私 | 最强,零字节离开设备 | 中等,数据发给模型提供商 | 强,数据不出内网 | 弱,依赖提供商策略 |
| 模型能力 | 受限于量化和设备资源 | 最强,始终用前沿模型 | 可选,取决于部署的模型 | 最强 |
| 离线可用 | 完全可用 | 不可用 | 内网可用 | 不可用 |
| 初始成本 | 高(需购买硬件) | 低 | 高(服务器 + GPU) | 无 |
| 边际成本 | 趋近于零 | 按 token 计费 | 固定运维成本 | 按 token 计费 |
| 适合谁 | 个人开发者/隐私极端敏感 | 大多数开发者日常使用 | 企业/团队 | 无特殊要求的场景 |
多数人在 2026 年的实际选择是第二种——本地存储文件和会话,日常补全走本地模型,困难任务用 BYOK 调 Claude 或 GPT。这不是妥协,是合理的工程取舍。
资源开销的精确控制
端侧推理最大的实际障碍是资源占用。一个 32B 模型常驻内存 22GB,如果开发环境本身需要 16-20GB(浏览器 + IDE + Docker),36GB 内存的机器刚好打满,开始 swap,推理速度也会下降。
| 策略 | 做法 | 效果 | 代价 |
|---|---|---|---|
| 按需加载 | 只在触发补全时加载模型,空闲超时卸载 | 内存占用降到 ~0 | 首次响应延迟 5-15 秒 |
| 小模型常驻 + 大模型按需 | 7B 模型常驻处理简单补全,复杂任务临时加载 32B | 平衡延迟和资源 | 需要两层模型管理逻辑 |
| CPU offload | 部分层放到 CPU,减少 GPU 内存占用 | 降低 GPU 显存需求 | 推理速度下降 30-50% |
| 量化降级 | 从 Q5 降到 Q4 甚至 Q3 | 内存占用降 20-40% | 质量有可见下降 |
# ❌ 坏做法:让模型常驻不释放,资源一直占满
ollama run qwen3:32b
# 启动后占 22GB 内存,即使长时间不用也不释放
# ✅ 好做法:设置空闲超时,自动卸载模型
# 在 ollama serve 的环境变量中设置
OLLAMA_KEEP_ALIVE=5m # 空闲 5 分钟后卸载模型
# 或者在 API 调用时指定
curl http://localhost:11434/api/generate -d '{
"model": "qwen3:32b",
"prompt": "...",
"keep_alive": "2m"
}'
# ✅ 更好的做法:用小模型常驻 + 大模型按需
# 常驻 7B 模型处理自动补全(占 4.5GB)
ollama run qwen3:8b --keep-alive 30m
# 复杂任务临时加载 32B
curl http://localhost:11434/api/generate -d '{
"model": "qwen3:32b",
"prompt": "...",
"keep_alive": "1m"
}'选型检查清单
在决定 local-first AI 方案之前,按以下清单逐项确认:
需求定义阶段
- 明确数据敏感度等级:哪些数据绝对不能离开设备,哪些可以走云端
- 确认离线需求比例:有多少工作时间处于无网络或部分网络状态
- 评估模型能力需求:日常任务(补全、摘要、分类)vs 复杂任务(架构设计、多步推理)的比例
- 确认合规要求:HIPAA、GDPR、等保等法规对数据处理的具体约束
- 确定并发需求:单人使用 vs 团队使用 vs 企业级部署
硬件评估阶段
- 确认设备可用内存(RAM):减去开发环境常驻后的剩余容量决定可运行的最大模型
- 确认 GPU 情况:有独立 GPU(NVIDIA/Apple Silicon)可以加速,没有也能跑(CPU 模式)
- 确认磁盘空间:模型文件通常 4-40GB,需要预留空间
- 如果需要多设备工作,确认同步方案(CRDT vs 文件同步 vs 手动)
工具选型阶段
- 根据隐私模型(本地数据/本地模型/自托管)缩小工具范围
- 测试候选模型在实际任务上的质量,不要只看 benchmark
- 确认工具是否支持 BYOK 混合模式,避免被锁死在纯本地或纯云端
- 评估运维成本:模型更新、索引重建、同步冲突处理的自动化程度
- 做一个为期一周的 dry run,记录质量不满足预期的场景,确认是否有降级/升级路径
部署运维阶段
- 设置模型空闲超时(
OLLAMA_KEEP_ALIVE),避免资源浪费 - 建立索引增量重建机制,不做全量重建
- 配置文件变更监控,确保索引与内容一致
- 设置混合路由规则:离线/敏感 → 本地,复杂/多模态 → 云端
- 定期对比本地模型和云端模型在你常用任务上的质量差距,评估是否需要升级硬件或调整模型规格
收束
回到最开始那个合同摘要的需求。最终方案是 Ollama + Qwen3 32B,分块提取流水线跑在本地,三台设备通过 CRDT 同步文件,索引各自重建。法务同事反馈摘要准确率约 90%,偶尔需要手动补充——比纯手工阅读快了 3 倍。
这套方案不完美。32B 模型在复杂条款推理上仍然不如 GPT-4,70B 模型在我的内存预算内跑不起来,多模态内容(合同里的表格、签章图片)本地模型基本处理不了。这些是当前硬件和模型能力的真实限制,不是架构设计能解决的。
但本地优先的核心价值不在于「什么都能在本地做」,而在于「至少有一个可靠的选择在本地」。当数据不能出去的时候,你有能力在本地完成工作,而不是被迫在合规和效率之间二选一。这个选项本身,就是 local-first 最大的意义。
参考资料
- Kleppmann, M. (2019). Local-First Software: You Own Your Data, in Spite of the Cloud. ACM SIGPLAN.
- Nimbalyst. (2026). Best Local-First AI Coding Tools 2026: 14 Compared.
- Red Hat. (2025). vLLM or llama.cpp: Choosing the Right LLM Inference Engine for Your Use Case.
- Ollama Official Documentation. https://ollama.com.
- FOSDEM 2026. Local-First, Sync Engines, CRDTs Track.
- Red Hat. (2026). vLLM 与 Ollama:不同场景下如何选择.
- Towards AI. (2026). I Tested Ollama vs vLLM vs llama.cpp.
- wal.sh. (2025). Local-First Software: Principles, Patterns, and Technologies.