本地搭建 Claude Code + Ollama + Gemma 4 实战教程
面向场景:想把日常代码分析、测试生成、小范围重构、调试辅助放到本地跑,降低 API 成本,并减少私有代码外发。
>
原文:<https://www.kdnuggets.com/local-agentic-programming-on-the-cheap-claude-code-ollama-gemma4>
>
来源:KDnuggets,Shittu Olumide。本文是基于原文的实战化整理,并补充了更保守的安全边界与本地验证流程。
1. 你最终会搭出什么
本文目标不是简单跑一个本地聊天模型,而是搭一个能被 Claude Code 调用的本地 Agentic 编程环境:
Claude Code CLI
↓ Anthropic-compatible request
Ollama local server: http://localhost:11434
↓ custom model variant
Gemma 4 26B MoE: gemma4-claude
↓
读文件 / 写补丁 / 跑测试 / 根据错误继续修复完成后你应该能做到:
- 在本地启动 Claude Code;
- 让 Claude Code 请求本机 Ollama,而不是 Anthropic 云端;
- 使用带长上下文和低温度参数的
gemma4-claude模型变体; - 先通过脚本验证:Ollama 健康、模型存在、Messages API 可用、工具调用结构可用;
- 再在一个 demo 项目里完成一次“读代码 → 写测试 → 跑测试 → 修复”的闭环。
2. 适用边界
适合做:
- 单文件或少量文件代码分析;
- 生成 pytest 单元测试;
- 小范围重构;
- 根据测试失败信息修复局部问题;
- 本地私有代码的低风险辅助分析。
不建议一开始就做:
- 生产仓库大范围自动修改;
- 跨多个服务的架构重构;
- 需要强推理的大型 repo 全局理解;
- 无人工 review 的自动提交、自动部署、自动修复。
3. 前置要求
3.1 硬件建议
原文针对 gemma4:26b,模型约 18GB。建议:
- 内存:至少 32GB 系统内存;
- GPU:16GB–18GB 显存可尝试
num_ctx 65536; - 24GB+ 显存再考虑
num_ctx 131072; - 没有足够显存也能跑,但可能明显变慢,尤其是上下文变长后 KV cache 被换到磁盘。
3.2 软件要求
- Linux 或 macOS;Windows 建议 WSL2;
- Ollama;
- Node.js 18+;
- Claude Code CLI;
- Python 3.10+,用于本地验证脚本。
当前官方文档确认:Ollama Modelfile 支持
FROM、PARAMETER、SYSTEM;num_ctx、temperature、repeat_penalty、num_predict都是有效参数。Ollama API 默认监听http://localhost:11434,/api/tags可列出模型,/api/generate支持keep_alive。
4. 安装 Ollama
Linux/macOS 可按 Ollama 官方方式安装:
curl -fsSL https://ollama.com/install.sh | sh验证服务是否可用:
ollama version
curl http://localhost:11434期望看到类似:
Ollama is running如果端口不通,先手动启动:
ollama serve5. 拉取 Gemma 4 模型
拉取原文推荐的 26B MoE 模型:
ollama pull gemma4:26b
ollama list确认列表里出现 gemma4:26b。
注意:具体模型标签是否可用取决于你当前 Ollama registry。若
gemma4:26b拉取失败,不要直接换成别的模型继续照抄参数;先确认 registry 中真实存在的 Gemma 4 标签,再调整FROM。
6. 安装 Claude Code
确认 Node.js 版本:
node --version安装 Claude Code CLI:
npm install -g @anthropic-ai/claude-code
claude --version如果你在公司内网环境,建议先用内部 npm 镜像或离线包方案;不要把代理、Token、私有 registry 密码写进项目仓库。
7. 创建面向 Claude Code 的 Ollama Modelfile
不要直接让 Claude Code 使用 gemma4:26b 原始标签。原始标签不会携带你需要的长上下文、低温度和 Agent 行为提示。
创建目录:
mkdir -p ~/.ollama/Modelfiles写入文件 ~/.ollama/Modelfiles/gemma4-claude:
FROM gemma4:26b
PARAMETER num_ctx 65536
PARAMETER temperature 0.2
PARAMETER top_p 0.9
PARAMETER repeat_penalty 1.15
PARAMETER num_predict 4096
SYSTEM """You are a senior software engineer operating as a coding agent.
When working with code:
- Read files before editing them. Never assume file contents.
- Make one focused change at a time and verify it before proceeding.
- When a tool call fails, examine the error carefully before retrying.
Do not retry with identical parameters. Diagnose first.
- Prefer surgical edits over full file rewrites.
- Run tests after each meaningful change, not after a batch of changes.
- If you are uncertain about the codebase structure, read more files
rather than guessing.
Be precise and methodical. Avoid explaining what you are about to do
when you could simply do it."""构建模型变体:
ollama create gemma4-claude -f ~/.ollama/Modelfiles/gemma4-claude
ollama list做一次普通响应 smoke test:
ollama run gemma4-claude "What is the time complexity of binary search and why?"通过标准:能在可接受时间内返回清晰技术回答。
8. 配置 Claude Code 指向本地 Ollama
推荐先用项目级配置,不要一开始改全局配置。
在你的测试项目根目录执行:
mkdir -p .claude创建 .claude/settings.json:
{
"env": {
"ANTHROPIC_BASE_URL": "http://localhost:11434",
"ANTHROPIC_AUTH_TOKEN": "ollama",
"ANTHROPIC_MODEL": "gemma4-claude",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gemma4-claude",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemma4-claude",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gemma4-claude",
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1"
}
}建议同时把本地个人配置排除出 Git:
printf '\n.claude/settings.local.json\n' >> .gitignore如果你打算提交 .claude/settings.json 给团队,先确认:
- 不包含真实 API Key;
- 不包含内网 IP、私有代理地址、用户名;
- 团队成员机器上也有相同的
gemma4-claude模型名; - README 中说明如何切回云端模型。
9. 启动前验证脚本
只验证普通聊天不够。Claude Code 的关键能力是工具调用,所以需要验证 tool_use 结构。
下面脚本只依赖 Python 标准库,保存为 verify_local_agentic_setup.py:
#!/usr/bin/env python3
"""Verify Claude Code + Ollama + local model readiness."""
from __future__ import annotations
import json
import sys
import urllib.error
import urllib.request
from dataclasses import dataclass
from typing import Any
OLLAMA_BASE_URL = "http://localhost:11434"
MODEL_NAME = "gemma4-claude"
TIMEOUT_SECONDS = 120
@dataclass(frozen=True)
class CheckResult:
name: str
ok: bool
detail: str
def request_json(method: str, url: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
data = None
headers = {"Content-Type": "application/json"}
if payload is not None:
data = json.dumps(payload).encode("utf-8")
headers["x-api-key"] = "ollama"
headers["anthropic-version"] = "2023-06-01"
request = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) as response:
return json.loads(response.read().decode("utf-8"))
def request_text(url: str) -> str:
with urllib.request.urlopen(url, timeout=10) as response:
return response.read().decode("utf-8", errors="replace")
def check_ollama_health() -> CheckResult:
try:
text = request_text(OLLAMA_BASE_URL)
except urllib.error.URLError as exc:
return CheckResult("Ollama health", False, f"cannot connect: {exc}")
if "Ollama is running" in text:
return CheckResult("Ollama health", True, "localhost:11434 is reachable")
return CheckResult("Ollama health", False, f"unexpected response: {text[:100]}")
def check_model_available() -> CheckResult:
try:
data = request_json("GET", f"{OLLAMA_BASE_URL}/api/tags")
except Exception as exc:
return CheckResult("Model availability", False, f"cannot list models: {exc}")
models = [model.get("name", "") for model in data.get("models", [])]
normalized = {name.split(":", 1)[0] for name in models}
if MODEL_NAME in models or MODEL_NAME in normalized:
return CheckResult("Model availability", True, f"found {MODEL_NAME}")
return CheckResult("Model availability", False, f"available models: {models}")
def check_messages_api() -> CheckResult:
payload = {
"model": MODEL_NAME,
"max_tokens": 100,
"messages": [
{"role": "user", "content": "Reply with exactly: VERIFICATION_OK"}
],
}
try:
data = request_json("POST", f"{OLLAMA_BASE_URL}/v1/messages", payload)
except Exception as exc:
return CheckResult("Messages API", False, f"request failed: {exc}")
content_blocks = data.get("content", [])
text_blocks = [block for block in content_blocks if block.get("type") == "text"]
if not text_blocks:
return CheckResult("Messages API", False, f"no text block: {data}")
return CheckResult("Messages API", True, text_blocks[0].get("text", "")[:80])
def check_tool_calling() -> CheckResult:
payload = {
"model": MODEL_NAME,
"max_tokens": 256,
"tools": [
{
"name": "read_file",
"description": "Read the contents of a file at the given path.",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The file path to read.",
}
},
"required": ["path"],
},
}
],
"tool_choice": {"type": "any"},
"messages": [
{"role": "user", "content": "Read the file at /tmp/test.py."}
],
}
try:
data = request_json("POST", f"{OLLAMA_BASE_URL}/v1/messages", payload)
except Exception as exc:
return CheckResult("Tool calling", False, f"request failed: {exc}")
content_blocks = data.get("content", [])
tool_blocks = [block for block in content_blocks if block.get("type") == "tool_use"]
if not tool_blocks:
return CheckResult("Tool calling", False, f"no tool_use block: {data}")
tool_call = tool_blocks[0]
if tool_call.get("name") != "read_file":
return CheckResult("Tool calling", False, f"wrong tool: {tool_call}")
tool_input = tool_call.get("input", {})
if "path" not in tool_input:
return CheckResult("Tool calling", False, f"missing path: {tool_call}")
return CheckResult("Tool calling", True, f"tool_use input={tool_input}")
def main() -> int:
checks = [
check_ollama_health(),
check_model_available(),
check_messages_api(),
check_tool_calling(),
]
for result in checks:
status = "PASS" if result.ok else "FAIL"
print(f"[{status}] {result.name}: {result.detail}")
if all(result.ok for result in checks):
print("All checks passed. Local Claude Code agent setup is ready.")
return 0
print("Resolve failed checks before using this setup on a real codebase.")
return 1
if __name__ == "__main__":
sys.exit(main())运行:
python3 verify_local_agentic_setup.py期望输出形态:
[PASS] Ollama health: localhost:11434 is reachable
[PASS] Model availability: found gemma4-claude
[PASS] Messages API: VERIFICATION_OK
[PASS] Tool calling: tool_use input={'path': '/tmp/test.py'}
All checks passed. Local Claude Code agent setup is ready.如果第 4 项失败,不要进入真实代码库。Claude Code 依赖工具调用读文件、写补丁、执行命令;模型如果只能输出文字解释,agentic 编程会失败。
10. 用一个 demo 项目做闭环验证
不要直接拿生产仓库试。先创建一个最小 Python 项目:
mkdir -p ~/tmp/local-agent-demo/src ~/tmp/local-agent-demo/tests
cd ~/tmp/local-agent-demo
cat > src/user_service.py <<'PY'
from __future__ import annotations
class ValidationError(ValueError):
pass
class UserService:
def normalize_email(self, email: str) -> str:
if "@" not in email:
raise ValidationError("invalid email")
return email.strip().lower()
def display_name(self, first_name: str, last_name: str) -> str:
return f"{first_name.strip()} {last_name.strip()}"
PY
cat > pyproject.toml <<'TOML'
[project]
name = "local-agent-demo"
version = "0.1.0"
requires-python = ">=3.10"
[tool.pytest.ini_options]
pythonpath = ["."]
TOML如果本机没有 pytest,建议在隔离虚拟环境里安装:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install pytest启动 Claude Code:
claude给它一个明确、低风险、可验证的任务:
请分析 src/user_service.py,为 UserService 写 pytest 测试。
要求:
1. 先读取文件,不要猜测内容。
2. 只新增或修改 tests/test_user_service.py。
3. 覆盖 normalize_email 和 display_name 的正常与异常输入。
4. 写完后运行 python -m pytest -q。
5. 如果失败,基于错误信息修复测试或代码。
6. 不要修改 pyproject.toml,不要安装新依赖。
成功标准:pytest 全部通过。你希望看到的行为:
read_file("src/user_service.py")
write_file("tests/test_user_service.py", ...)
bash("python -m pytest -q")如果它只是解释“我会写测试”,但没有工具调用,说明本地模型没有进入可用的 agentic 状态。
11. 常见故障与处理
11.1 Claude Code 仍然提示 Anthropic API Key
检查:
pwd
ls -la .claude/settings.json处理:
- 确认你是在包含
.claude/settings.json的项目根目录启动claude; - 确认 JSON 格式合法;
- 确认
ANTHROPIC_BASE_URL是http://localhost:11434,不是http://localhost:11434/v1; - 如果 shell 里有真实
ANTHROPIC_API_KEY,先在新终端用干净环境测试,避免误走云端。
11.2 反复出现 Invalid tool parameters
症状:Claude Code 报工具参数无效,模型道歉后用相同参数重试。
处理顺序:
- 确认模型名是
gemma4-claude,不是原始gemma4:26b; - 确认 Modelfile 里
temperature 0.2生效; - 重新构建:
ollama create gemma4-claude -f ~/.ollama/Modelfiles/gemma4-claude- 仍失败时,把温度降到
0.1后重建。
11.3 多轮后速度突然很慢
可能是上下文太大导致 KV cache 换到磁盘。
先降上下文:
PARAMETER num_ctx 32768然后重建:
ollama create gemma4-claude -f ~/.ollama/Modelfiles/gemma4-claude可选优化:设置 KV cache 量化并重启 Ollama:
export OLLAMA_KV_CACHE_TYPE=q8_011.4 每次对话开头都冷启动
保持模型常驻:
export OLLAMA_KEEP_ALIVE=-1
curl http://localhost:11434/api/generate -d '{"model": "gemma4-claude", "keep_alive": -1}'Ollama 官方 FAQ 说明:keep_alive 可以是时长、秒数、负数或 0;负数表示保持加载,0 表示立即卸载。
11.5 Beta header 被拒绝
确认配置里有:
{
"env": {
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1"
}
}如果你通过 shell 设置,确认同一个 shell 中能看到:
echo "$CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"期望输出:
112. 建议的落地策略
第一阶段:只读验证。
- 让 Claude Code 只读 demo 项目;
- 要求它解释文件结构和测试缺口;
- 不允许写文件。
第二阶段:demo 项目闭环。
- 允许写
tests/; - 允许运行 pytest;
- 不允许改业务代码。
第三阶段:低风险真实项目。
- 只允许处理单个文件或单个测试文件;
- 明确禁止提交、push、部署;
- 每次修改后人工 review diff。
第四阶段:形成团队规范。
- 固化
.claude/settings.json模板; - 固化验证脚本;
- 固化失败处理清单;
- 明确哪些仓库允许本地模型介入,哪些仓库必须使用云端强模型或人工处理。
13. 最小验收清单
正式用于真实代码前,至少通过这些检查:
curl http://localhost:11434返回Ollama is running;ollama list能看到gemma4-claude;python3 verify_local_agentic_setup.py四项 PASS;- demo 项目中 Claude Code 能真实调用读文件、写测试、跑 pytest;
- 生成的 diff 可读、范围小、没有无关重构;
- 没有把 API Key、Token、内网地址写进仓库;
- 你知道如何切回云端模型或禁用本地配置。
14. 生产边界提醒
这套方案可以降低成本和提高隐私控制,但不要把它误解成“本地模型等于生产级自动工程师”。
更稳妥的定位是:
本地模型:高频、低风险、可回滚、可测试的小任务
云端强模型:复杂架构、跨仓库推理、关键设计评审
人工 review:所有进入生产分支的最终判断如果你把这套流程用于团队内部,建议把“只读默认、写入需明确授权、破坏性操作禁止、测试必须运行、diff 必须人工 review”写进项目级 Agent 使用规范。