文章目录
- 一、背景:8GB 显卡的硬约束
- 二、方案一:Claude Code 接 Ollama(能跑,但先翻车)
- 2.1 坑一:401 Invalid API Key(不是 Ollama 报的)
- 2.2 坑二:Windows 的 .bat 不能有中文注释
- 2.3 此路小结
- 三、方案二:OpenCode(ollama launch opencode)
- 四、定位根因:问题不在配置,在模型的 function calling 能力
- 五、验证一个常见误区:增大上下文窗口没用
- 六、真正能落地的方案:自己写个 2–4 工具的迷你 agent
- 6.1 完整代码(mini_agent.py)
- 6.2 实测结果(沙箱端到端)
- 6.3 三个让 9B 更稳的技巧
- 七、备选方案:aider(不依赖 native tool calling)
- 八、结论与建议
- 附:关键命令速查
笔者显卡是笔记本上的RTX 5060 Laptop,显存只有 8GB。一直想用本地模型驱动一个 coding agent,让它能真正「写文件落盘」,而不是只在聊天框里把代码打印出来。结果一路踩坑:Claude Code 报 401、OpenCode 只说不动、以为是上下文太小结果也不是。本文把每一步的实测结论和能用的方案记下来,帮后来者省下几天折腾时间。
一、背景:8GB 显卡的硬约束
先摊开我的环境和硬件,结论其实被这张显卡焊死了:
- GPU:NVIDIA GeForce RTX 5060 Laptop GPU,总显存8151 MiB(约 8GB)
- Ollama:0.33.2(≥0.14,原生提供 Anthropic 兼容
/v1/messages与 OpenAI 兼容/v1/chat/completions) - Claude Code:2.1.116;OpenCode:1.2.26
- 已下载模型:
qwen2.5-coder:7b(4.7 GB)—— 编码专用qwen3.5:9b(6.6 GB)—— 通用/编码兼顾
目标很朴素:让本地模型驱动 agent,自动把代码写进文件。
| 模型 | 显存占用 | 装得下吗 |
|---|---|---|
| qwen2.5-coder:7b | ~4.7 GB | ✅ 轻松 |
| qwen3.5:9b | ~6.6 GB | ⚠️ 勉强(占满约 7–7.5 GB) |
| qwen3-coder:30b(~18 GB) | 远超 8 GB | ❌ 根本跑不起来 |
8GB 就是天花板。后面会看到,这个约束决定了「能干活」和「装得下」是两个互相打架的目标。
二、方案一:Claude Code 接 Ollama(能跑,但先翻车)
Ollama 从 v0.14 起原生提供 Anthropic 兼容接口,所以 Claude Code 可以直接把 Ollama 当后端。核心配置就三个环境变量:
$env:ANTHROPIC_BASE_URL ="http://localhost:11434"$env:ANTHROPIC_API_KEY ="ollama"# Ollama 不校验,任意值都行$env:ANTHROPIC_AUTH_TOKEN ="ollama"$env:CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC ="1"claude--model qwen2.5-coder:7b这里有两个真实踩过的坑,记下来免得你也中招:
2.1 坑一:401 Invalid API Key(不是 Ollama 报的)
只设ANTHROPIC_AUTH_TOKEN=ollama时,Claude Code 启动后报401 Please run /login。我实测过 Ollama 0.33.2连无 key 都返回 200,所以这个 401 是Claude Code 自己的鉴权层在拦——它实际发请求用的是ANTHROPIC_API_KEY变量,找不到就转去连真实 Anthropic API。补上ANTHROPIC_API_KEY即可。
顺带验证:
claude必须用脚本启动,直接敲claude不会带这些环境变量,会回到 401/超时。
2.2 坑二:Windows 的 .bat 不能有中文注释
把启动脚本写成.bat时,若含中文rem注释,CMD 默认用 GBK(CP936) 解析 UTF-8 无 BOM 文件,中文变乱码被当成命令执行,报'愯'、'MODEL'这类错。结论:给 Windows 用户生成启动脚本,默认只用 ASCII + CRLF。
2.3 此路小结
Claude Code 接本地 Ollama 是可行的,但本地 7B 同样「不会写文件」(原因见第四节),于是我转去试 OpenCode。
三、方案二:OpenCode(ollama launch opencode)
OpenCode 的 ollama provider 走本地http://localhost:11434/v1,不需要 API key,比 Claude Code 还干净。一键启动:
ollama launch opencode--model qwen3.5:9b# 或opencode-m ollama/qwen3.5:9bollama launch opencode会在~/.opencode.json生成配置,两个模型都标了"tools": true。
现象:模型在聊天框里说「我来创建 stars.py」、甚至把echo ... > hello.html当 bash 代码块打印出来,但文件一个都没写出来。直觉上像是「工具没开」,但tools:true明明开着。
四、定位根因:问题不在配置,在模型的 function calling 能力
直接拿 Ollama 的 OpenAI 兼容端点做真实 tool-call 测试,不给 agent 框架任何缓冲:
importurllib.request,json URL="http://localhost:11434/v1/chat/completions"tools=[{"type":"function","function":{"name":"write_file","description":"Write a file","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}}}]payload={"model":"qwen3.5:9b","messages":[{"role":"user","content":"create hello.py that prints hello"}],"tools":tools,"tool_choice":"auto","max_tokens":300}req=urllib.request.Request(URL,data=json.dumps(payload).encode(),headers={"Content-Type":"application/json"})r=json.loads(urllib.request.urlopen(req,timeout=60).read().decode())print(r["choices"][0]["finish_reason"],r["choices"][0]["message"].get("tool_calls"))对比不同「工具规模」下的表现:
| 场景 | qwen2.5-coder:7b | qwen3.5:9b |
|---|---|---|
| 2–4 个简单工具 | 把调用当文本吐(无 tool_call) | ✅ 正确返回tool_calls |
| opencode 真实规模(8 工具,含嵌套 edit、枚举 todo_write 等) | 吐文本 | ❌tool_calls = 0,退化成纯文本 |
铁证:直接查你机器上 OpenCode 的会话数据库~/.local/share/opencode/opencode.db,最近一次会话(D:\Test\claudetest)的part表共48 条,全部是text类型,0 个tool_call。也就是说 9b 从头到尾只是在聊天,一次真实工具调用都没发过。
关键认知:不是「模型会不会调工具」,而是「面对多少工具时还能稳定调」。同样一个 9b,在 2–4 个扁平工具下能稳定发 native tool_call;一旦工具多了、schema 复杂了,直接摆烂成文本。这是 ~9B 模型的 function-calling 能力上限,不是配置问题。
五、验证一个常见误区:增大上下文窗口没用
很多人搜「opencode 不写文件」,帖子建议改大num_ctx。我实测验证这个假设是否成立——在仿 opencode 的 8 工具 schema 下,对比num_ctx=4096与num_ctx=32768:
[qwen3.5:9b num_ctx=4096 23.0s] tool_calls=0 [qwen3.5:9b num_ctx=32768 49.3s] tool_calls=0结论:增大上下文窗口解决不了问题。原因:
- 瓶颈不是「窗口装不下工具定义」。Ollama 会自动扩容,我早测过把 system prompt 堆到 ~8000 token 也只 2.5s 正常返回,不存在溢出卡死。
- 瓶颈是模型能力。给了更大窗口,9b 还是
tool_calls=0,只是更慢(49s vs 23s)。 - 网上「改 num_ctx」的经验属于另一类场景:模型确实会调工具、但参数 JSON 被截断导致解析失败。你这台机器是「根本不发出 tool_call」,所以那条经验不适用。
六、真正能落地的方案:自己写个 2–4 工具的迷你 agent
既然 OpenCode 一次性塞一整套复杂工具会让 9B 崩,那反过来——只给模型 2–4 个扁平工具,9B 就能稳定发 native tool_call,由 Python 负责真正落盘,形成 agentic 循环。纯标准库、零依赖,Python 3.13 直接能跑。
核心思路:
- 模型只负责「决定调哪个工具 + 参数」
- Python 负责真正执行
write_file/read_file/run_bash并回传结果 - 工具集刻意只留 3 个(这正是能在 8GB 上成功的关键:少即是多)
6.1 完整代码(mini_agent.py)
#!/usr/bin/env python3# mini_agent.py —— 本地 Ollama 迷你 coding agent(纯标准库,零依赖)importjson,sys,subprocess,argparse,urllib.request,os URL="http://localhost:11434/v1/chat/completions"TOOLS=[{"type":"function","function":{"name":"write_file","description":"Create or overwrite a text file with the given content.","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}}},{"type":"function","function":{"name":"read_file","description":"Read a file.","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}},{"type":"function","function":{"name":"run_bash","description":"Run a shell command.","parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}},]SYS=("You are a concise coding agent. To create or modify files you MUST call write_file ""with the file path and full content. Do NOT just print code in chat. ""After finishing, reply with a short summary and NO tool calls.")defchat(model,messages):payload={"model":model,"messages":messages,"tools":TOOLS,"tool_choice":"auto","temperature":0,"stream":False,"max_tokens":1024}req=urllib.request.Request(URL,data=json.dumps(payload).encode(),headers={"Content-Type":"application/json"})returnjson.loads(urllib.request.urlopen(req,timeout=180).read().decode())["choices"][0]defexec_tool(call):name=call["function"]["name"]args=json.loads(call["function"]["arguments"]or"{}")ifname=="write_file":p,c=args["path"],args["content"]d=os.path.dirname(os.path.abspath(p))ifd:os.makedirs(d,exist_ok=True)withopen(p,"w",encoding="utf-8")asf:f.write(c)returnf"OK: wrote{len(c)}chars to{p}"ifname=="read_file":returnopen(args["path"],encoding="utf-8").read()[:4000]ifname=="run_bash":try:out=subprocess.run(args["command"],shell=True,capture_output=True,text=True,timeout=30)returnf"exit={out.returncode}\nstdout:{out.stdout}\nstderr:{out.stderr}"exceptExceptionase:returnf"error:{e}"returnf"unknown tool{name}"defmain():ap=argparse.ArgumentParser()ap.add_argument("task")ap.add_argument("--model",default="qwen3.5:9b")ap.add_argument("--base-url",default=URL)ap.add_argument("-y",action="store_true",help="auto-approve run_bash")a=ap.parse_args()globalURL URL=a.base_url messages=[{"role":"system","content":SYS},{"role":"user","content":a.task}]for_inrange(12):# agentic 循环,最多 12 轮resp=chat(a.model,messages)msg=resp["message"]calls=msg.get("tool_calls")or[]# 注意:不卡 finish_reason,有 tool_calls 就执行ifcalls:messages.append({"role":"assistant","content":msg.get("content")or"","tool_calls":calls})forcincalls:ifc["function"]["name"]=="run_bash"andnota.y:print(f"run_bash:{c['function']['arguments']}[y/N]")ifinput().lower()!="y":continueres=exec_tool(c)print(f"->{c['function']['name']}:{res[:200]}")messages.append({"role":"tool","name":c["function"]["name"],"content":res})else:print("=== final answer ===")print(msg.get("content")or"(empty)")returnif__name__=="__main__":main()6.2 实测结果(沙箱端到端)
cd D:\Test python mini_agent.py"用 Python 写一个 stars.py 模拟星空中彩色星星闪烁"--model qwen3.5:9b-y输出(节选):
-> write_file: OK: wrote 3178 chars to stars.py -> run_bash: exit=1 stderr: No module named 'tkinter' -> write_file: OK: wrote 3388 chars to stars.py # 自动改写成 ANSI 终端彩色版 === final answer === I've created stars.py that prints twinkling colored stars ...stars.py确实落在磁盘上(141 行)。这证明:限制 OpenCode 的不是「本地模型不能写文件」,而是「OpenCode 的工具集对 9B 太重」。
程序执行的效果如下图。
6.3 三个让 9B 更稳的技巧
temperature: 0:降低模型随机性(曾有一次抽风返回空回复)。- 不卡
finish_reason:Ollama 有时把工具调用放在finish_reason: "stop"的回复里,只要tool_calls字段非空就执行。 - 工具集保持 2–4 个扁平工具:越复杂越容易退化成文本。
七、备选方案:aider(不依赖 native tool calling)
aider 的原理和 OpenCode 相反:它让模型输出代码块/diff,由 aider 自己解析并落盘,完全不依赖模型发 native 工具调用。所以即便 7b/9b 不会 tool_call,aider 仍可能正常写文件——这是它在 8GB 上比 OpenCode 更可能成功的根本原因。
aider--model ollama/qwen3.5:9b安装坑(本机实测):在Python 3.13上pip install aider-chat会失败——aider 锁定tiktoken==0.4.0,而它没有 cp313 的预编译 wheel,pip 只能下源码包,构建又需要 Rust(ModuleNotFoundError: No module named 'setuptools_rust')。两种解法:
- 方案 A(推荐):用 Python 3.12 建 venv,
py -3.12 -m venv aider-env后直接pip install aider-chat(有 cp312 wheel,无需 Rust)。 - 方案 B:保留 3.13,先
winget install Rustlang.Rustup装 Rust,再pip install --no-build-isolation aider-chat。
八、结论与建议
把整轮探索浓缩成一张决策表:
| 模型 | opencode 能否写文件 | mini_agent(2-4工具) | 说明 |
|---|---|---|---|
| qwen2.5-coder:7b | ❌ 完全不会调工具 | ⚠️ 多数情况仍吐文本 | 能力太弱,基本 pass |
| qwen3.5:9b | ❌ 工具一多就退化成聊天 | ✅可用 | 8GB 上的现实最优解 |
| qwen3-coder:30b(需 18GB) | ✅ 稳 | ✅ | 但 8GB 显卡跑不起来 |
在 8GB 显卡上,真正能落地的只有两条路:
- 自写迷你 agent(推荐,零成本):像
mini_agent.py这样只给 2–4 个扁平工具,9b 稳定发 tool_call,Python 落盘。已经实测 stars.py / hello.py 成功。 - aider:换 Python 3.12 装好即可,靠代码块解析落盘,不依赖 native tool calling。
想用 OpenCode 那套完整 agentic 体验?唯一正解是把显卡换成≥16–24GB 显存,跑qwen3-coder:30b。在现显卡上,9b 就是天花板——这不是配置能救的。
附:关键命令速查
# 1) Claude Code 接本地 Ollama(需脚本带环境变量)$env:ANTHROPIC_BASE_URL="http://localhost:11434";$env:ANTHROPIC_API_KEY="ollama"claude--model qwen2.5-coder:7b# 2) OpenCode 接本地 Ollama(无需 key)ollama launch opencode--model qwen3.5:9b# 3) 迷你 agent 真正写文件(本文方案,零依赖)python mini_agent.py"写一个 hello.py 打印 hello"--model qwen3.5:9b-y# 4) 跑之前腾显存ollama stop qwen2.5-coder:7b一句话总结:8GB 显卡跑本地模型做代码生成,「装得下」和「能干活」互相打架。别指望 OpenCode + 小模型能自动写文件,也别指望改大上下文窗口能救——老老实实给模型 2–4 个扁平工具、自己写个迷你 agent,是这条窄路上唯一踩实的一步。