在喜马拉雅山上用离线 AI 智能体写代码
聊到远程工作,我们往往默认很多条件都具备,尤其是稳定的网络和电力。但有时候,远程工作意味着要在飞机上、漫长的火车旅途中,或者网络很差甚至没有网络的某个地方完成工作。一年前,我搬到了喜马拉雅山麓。我喜欢这里的宁静,但生活在这里也有它的挑战,比如断网和停电。以前不太在意的事情,现在都成了我安排工作时必须考虑的因素。我在 Thomson Reuters 担任首席工程师(Principal Engineer),大部分时间都在写代码和审查队友的代码。编码代理(coding agent)已经是我日常工作流的重要组成部分,这也让我好奇:离线状态下,我还能保留多少这样的工作流?在喜马拉雅山麓工作。我的工作区照片。如果你和我一样,既喜欢与技术打交道,又能在安静、偏远、网络时好时坏的地方找到平静,这篇文章就是写给你的。它也适合那些想知道编码代理能否完全在笔记本电脑上、在断网状态下运行的人。我试着在笔记本电脑上本地运行一个编码代理,看看离线状态下我究竟能完成多少工作。
离线时我们能做什么?
如果笔记本电脑上已经有一个项目,我们就有很多可做的事。我们可以让代理解释一个不熟悉的函数、排查失败的测试、重构一小段代码,或者帮忙完成某个功能。只要模型、项目依赖和测试工具都在本地,这些任务就可以离线运行。但下载新包或上网查资料仍然需要联网。
OpenCode 的用武之地
OpenCode 是一个开源编码代理,可以与你的项目配合工作。你给它一个任务,它就能读取文件、提出修改建议,并运行测试套件等命令。在我们的配置中,它的权限提示让我们可以在允许命令和更改之前先进行审查。我选择 OpenCode,是因为它让我可以在终端中保持这套工作流,同时连接到运行在自己笔记本电脑上的模型。Ollama 负责运行模型;OpenCode 负责处理与项目的交互。
Mac 上的 OpenCode,本地模型选了 Qwen3.5 9B。能这么玩,靠的是开放权重模型(open-weight model,比如 Qwen):它们的权重——也就是生成回答所依赖的那些学出来的参数——可以直接下载到本地运行。只要 Ollama 在同一台笔记本上跑着 Qwen,OpenCode 就能把请求发给本地服务。我们的做法是先把所有东西下载好,然后断网,再开始写代码任务。如果你更喜欢有引导的编辑流程,Aider 也是备选,它同样支持 Ollama。这篇教程从头到尾都用 OpenCode,这样配置和结果能保持一致。OpenCode 负责处理项目文件,并把模型请求发给同一台笔记本上的 Ollama,Ollama 则在本地运行 Qwen。
选一个开源模型
Qwen 有不同尺寸。我想要的是既能在本地跑、又能调用智能体(agent)工具的模型。先给点参考:下面是 Qwen 模型卡里公布的两档 Qwen 尺寸和另一个开放权重选项的成绩。
LiveCodeBench v6 测的是在编程题上生成代码的能力。BFCL-V4 评估函数调用(function calling)——当模型需要挑一个工具并给出正确参数时,这项能力就很关键。两项都是分数越高越好。
| 模型 | LiveCodeBench v6 | BFCL-V4 |
|---|---|---|
| Qwen3.5 4B | 55.8 | 50.3 |
| Qwen3.5 9B(本文使用) | 65.6 | 66.1 |
| gpt-oss-20b | 74.6 | 未公布 |
上表分数来自 Qwen 模型卡,不是这台笔记本上的实测。「未公布」表示来源没有给出该项成绩。
这篇教程用的是 Qwen3.5 9B。它公布的编码和工具调用成绩都比 4B 版本强,作为起点比较合适。这张表测不了笔记本上的运行速度,也测不了整套 OpenCode 流程;真正的检验还得靠我们离线的文件编辑和测试。
模型尺寸与内存
| 模型 | 大约下载大小 |
|---|---|
| Qwen3.5 4B | 3.3–4.0 GB |
| Qwen3.5 9B(本文使用) | 6.6–7.6 GB |
| Qwen3.5 27B | 17–20 GB |
Ollama 列出了所有可用尺寸;具体下载大小会因运行时格式而不同。下载大小不等于内存需求。Voltti 给 6.6 GB 的 Qwen3.5 9B 版本预留了 12–16 GB 内存。我们这套 OpenCode 配置开了 64k token 上下文,可能还需要更多,所以要给上下文和其他应用留出余量。
OpenCode 在基准测试中的表现
OpenCode 不只在小型演示中测试过。研究预印本《The Scaffold Effect in Coding Agents》用两个云端模型,在 50 个 Terminal-Bench Pro 任务上对比了 OpenCode、Goose 和 OpenHands-SDK。表 2 列出了各家的通过率。
| 编码智能体 | Qwen3.6 Plus | MiniMax M2.5 |
|---|---|---|
| Goose | 48% [34–62%] | 38% [24–52%] |
| OpenCode | 50% [36–64%] | 46% [32–60%] |
| OpenHands-SDK | 50% [36–64%] | 46% [32–60%] |
方括号内是 95% 置信区间。作者提醒,多数通过率差异在统计上并不显著。研究还指出,OpenCode 的 token 消耗明显更高:每解决一个任务,约是 Goose 的 40 倍。
这些测试用的是云端 API 和不同的模型,和我们本地环境不一样,因此不能据此推断本地 Qwen3.5 9B 的表现。
初始设置
趁着还能联网,先按 GitHub README 里的完整步骤做好配置。其中包含安装工具、下载项目以及安装 OpenCode 1.19.4。项目里已经为本地运行 Qwen 准备好了示例、测试和配置文件。配置完成后,在项目目录里保留 Terminal A 窗口。
动手之前,最好先搞清楚 opencode.json 的作用。它是 OpenCode 的项目配置文件。在我们的文件里,model 和 small_model 都选用了 Qwen3.5 9B,baseURL 指向 Ollama 的 http://127.0.0.1:11434/v1。只启用了本地提供方。此外,它在编辑文件或执行 shell 命令前会先询问,并且屏蔽了网络工具。完整配置见代码仓库。
离线测试时,我们照样会断开网络。Terminal A 用来运行 OpenCode 和我们的测试。在 Terminal B 里启动 Ollama,让它保持运行。这条命令让模型服务只监听 localhost(同一台 Mac 内部的地址),并关闭 Ollama 的云端功能。
OLLAMA_HOST=127.0.0.1:11434 OLLAMA_NO_CLOUD=1 OLLAMA_CONTEXT_LENGTH=65536 ollama serve
回到 Terminal A,下载 Qwen,等下载完成。断网之前,我们先确认智能体能够正常响应。
ollama pull qwen3.5:9b
从一个费用计算器开始
这个例子会加总五笔支出,其中包含一笔退款。正确总额是 25.50。
在终端 A 里运行起始程序 python3 expense_total.py,它输出 8.00,也就是最后一笔开销的金额。这个函数每次迭代都会覆盖掉自己的总额。让智能体修 bug 之前,先跑一下现有的测试:
python3 -m unittest -v
有三个用例失败了:小数运算、退款和示例开销。空输入和单项输入这两个用例已经通过。用现有测试能给智能体一个具体的目标,也能让我们在它的解释之外独立核对结果。
联网试用 OpenCode
联网状态下,直接在项目文件夹里打开 OpenCode。--pure 选项会禁用外部插件。项目配置把主模型和辅助模型都指向 Ollama;之后断网才是让这次运行真正离线的关键。
.tools/node_modules/.bin/opencode --pure
确认选中了 Qwen3.5 9B local。在 OpenCode 里输入这段诊断提示:
读取 expense_total.py 并运行测试。不要修改任何文件,解释这个 bug。
OpenCode 收到诊断提示,此时选中的是 Qwen3.5 9B local。它读完了源码,又去找测试文件。它一开始提议用 pytest,但这个项目用的是 Python 自带的 unittest。权限确认弹窗给了我一个机会,先看清命令再放行。下一个任务,我会把确切的测试命令直接写进提示里:python3 -m unittest -v。
智能体找到了测试,请求权限运行测试命令。本地服务返回结果后,退出 OpenCode。让 Ollama 继续在终端 B 里跑着。
/exit
关掉网络
关掉 Wi-Fi;如果接着网线或手机热点,也一并断开。localhost 依然可用,因为请求根本没离开这台 Mac。
为了做连通性检查,我关掉了 Wi-Fi。在终端 A 里检查 Ollama 的本地 API:
curl --noproxy '*' --fail http://127.0.0.1:11434/api/tags
Ollama 照常响应,列出了已下载的 qwen3.5:9b 模型。接着试一个外部地址:
curl --noproxy '*' --connect-timeout 5 --max-time 10 --head https://example.com
外部请求失败,报错 Could not resolve host。
这些检查确认了当时的情况:本地服务有响应,但外部查询失败。跑编码任务期间,要保持所有外部连接断开。
离线修复缺陷
用同一条直接命令在终端 A 里重新启动 OpenCode。
.tools/node_modules/.bin/opencode --pure
让 agent 修 bug。
修复 expense_total.py 里的 bug,让现有测试通过。不要改动测试,也不要安装任何东西。跑一遍测试,并解释你的修复思路。
录屏显示,Qwen 提出了一行改动,请求编辑权限并运行了测试。改动很小,很容易检查。
total = Decimal(amount) # 修改前
total += Decimal(amount) # 修改后
整个编辑与测试流程都被记录了下来:Qwen 提出修复方案,请求批准,然后成功跑完五个测试。
agent 完成后,先退出 OpenCode,再自己检查。
/exit
接着我们在终端 A 里手动跑测试。
python3 -m unittest -v
手动检查显示,五个测试全部通过。保存下来的源码里是 total += Decimal(amount),测试文件与初始版本保持一致。修复后,每个金额都会被累加进去。跑一下示例程序,应该返回 25.50。
还有两个编码任务可以试
我在配套项目里又加了两个练习:一个 CSV 报告,一个日志分析器。它们的离线 agent 运行我还没做完,所以上面展示的结果来自 expense 示例。接下来可以用同样的环境试试这两个任务。
CSV 报告示例从一个未完成的函数开始。让 OpenCode 按产品对行分组,计算数量和收入,输出可读的报告。它带有示例数据,以及七个测试,覆盖分组、十进制运算、排序和空输入。
CSV 报告:代码与操作步骤
参照 README 补全 examples/csv-report/csv_report.py。保持测试不变,不要安装任何东西。跑一遍这个示例的测试,并解释你改了什么。
日志分析器本身能跑,但错误计数和“找出最慢请求”两处有 bug。它的七个测试还覆盖了格式错误的行和空输入。
日志分析器:代码与操作步骤
参照 README 修复 examples/log-analyzer/log_analyzer.py。
保持测试不变,不要安装任何东西。运行它的测试并解释你的修复。
每个演练都包含确切的测试命令和预期结果。用同样的本地环境完成这些练习,然后检查改动和测试输出。
断网时哪些做法有帮助
项目和模型都要提前准备。
下载依赖,把需要的文档存到磁盘上。我们的示例只用 Python 标准库,所以测试不需要下载软件包。
联网时先把完整环境跑一遍。
离线重试时用同样的命令、模型和配置,这样运行时需要的组件已经就位。
把项目的测试命令交给智能体。
我们这个例子用的是 unittest,智能体一开始却建议用 pytest。明确给出测试命令,能少走弯路。
任务保持聚焦,然后检查结果。
只让它修一个 bug 或加一个小功能,看过 diff 之后自己再跑一遍测试。保留原有测试,这样测试通过才能真正说明预期行为没被破坏。
留意内存压力。
下载模型只是内存开销的一部分。上下文开得太大、后台还开着别的应用,都会挤占模型可用的内存。
需要人工介入的尝试也要记录下来。
一份有用的演练应该展示权限提示、重试和失败,而不只是最后测试通过的那段输出。
离线尝试的体会
关掉 Wi-Fi 后看到测试通过,回答了我最初的疑问:不联网也能让编程智能体读取本地文件、做出修改并运行检查。我仍然自己审查了改动、跑了原有测试。保持测试不变,给了我一个检验结果的好办法。
在网络中断或旅途中使用这套方案时,我会从小的、自成一体的任务开始。项目和它需要的一切,都必须在断网前已经存在笔记本上。
这次一行代码的修复,并不能说明模型面对大型代码库或不熟悉的框架时表现如何。下一个有意义的检验是那份 CSV 报告:它要求智能体补全一个函数,需要检查的东西比一行修复多得多。
如何用 OpenCode、Ollama 和 Qwen 搭建并运行一个离线编程智能体。