Claude Code 本地 Ollama 集成:从零到跑通的完整记录

本文档记录了将 Claude Code(Anthropic 的 CLI 开发者工具)的底层大模型从远程 Anthropic Claude 替换为本地 Ollama (qwen3.5:9b) 的完整过程,包括遇到的每一个问题和解决方案。

一、项目背景

Claude Code 是 Anthropic 开发的一个终端 AI 编程助手,其技术栈为:

  • 运行时:Bun (TypeScript)

  • 终端 UI:React + Ink(终端中的 React 渲染引擎)

  • API:Anthropic SDK(流式响应)

  • 架构:包含交互式 REPL 模式和非交互式 print 模式

我们拿到的是通过 npm source map 暴露的源码归档,目标是让它完全使用本地 Ollama 模型运行,摆脱对 Anthropic 远程 API 的依赖。


二、第一阶段:让项目能跑起来

2.1 创建基础构建配置

源码归档中没有构建配置,需要手动创建:

package.json --- 定义依赖和启动脚本:

json

TypeScript 复制代码
{
  "scripts": {
    "claude": "bun run src/entrypoints/cli.tsx"
  }
}

bunfig.toml --- 配置 Bun 加载预处理文件:

toml

复制代码
preload = ["./preload.ts"]

preload.ts --- Bun 启动前的模块 polyfill:

TypeScript 复制代码
import { plugin } from 'bun'

// 定义编译期宏常量
;(globalThis as any).MACRO = {
  VERSION: '1.0.0-source',
}

plugin({
  name: 'claude-code-shims',
  setup(build) {
    // polyfill bun:bundle 的 feature() 函数
    build.module('bun:bundle', () => ({
      exports: { feature(_name: string) { return false } },
      loader: 'object',
    }))

    // polyfill react-reconciler/constants.js
    build.module('react-reconciler/constants.js', () => ({
      exports: { ConcurrentRoot: 1, ... },
      loader: 'object',
    }))

    // polyfill react/compiler-runtime
    build.module('react/compiler-runtime', () => ({
      exports: { c(size: number) { return new Array(size).fill(sentinel) } },
      loader: 'object',
    }))
  },
})

2.2 Stub 缺失的原生/内部包

源码依赖了多个不可用的包,需要逐一 stub:

包名 用途 Stub 策略
color-diff-napi 终端 diff 渲染 空的 ColorDiff/ColorFile 类
@anthropic-ai/sandbox-runtime 沙箱执行环境 完整的 SandboxManager mock
@ant/claude-for-chrome-mcp Chrome 浏览器集成 空实现
@ant/computer-use-mcp 电脑操作工具 空实现
@ant/computer-use-input 原生输入 API 返回 null/false
@ant/computer-use-swift macOS Swift 桥接 空对象
@anthropic-ai/claude-agent-sdk Agent SDK 空对象
modifiers-napi macOS 修饰键检测 始终返回 false

2.3 安装依赖并首次运行

bash

复制代码
bun install
bun run claude -- -p "hello"   # print 模式测试

首次运行即报错 --- 这开启了漫长的调试之旅。


三、第二阶段:替换底层模型为 Ollama

3.1 创建 Ollama 适配器(src/services/api/ollama.ts

核心挑战:Anthropic SDK 的调用格式和 Ollama 的 OpenAI 兼容 API 格式不同,需要一个翻译层。

请求格式转换

text

复制代码
Anthropic 格式                    Ollama/OpenAI 格式
─────────────────                 ──────────────────
role: "user"                  →   role: "user"
content: [{type:"text",...}]  →   content: "plain string"
role: "assistant"             →   role: "assistant"
system prompt (独立参数)       →   role: "system" 消息

流式响应转换

text

复制代码
Ollama SSE 流                              Anthropic SSE 事件
────────────                               ─────────────────
{"choices":[{"delta":{"content":"Hi"}}]}  →  message_start + content_block_start
                                             + content_block_delta(text_delta)
                                             + content_block_stop + message_stop

关键优化

  • 截断 system prompt(默认限制 4000 字符)------ 本地模型处理不了 Claude Code 的超长系统提示

  • 不发送 tools 定义给 Ollama ------ 减少 prompt 体积,避免本地模型处理能力不足

  • 处理 qwen3.5 的 reasoning 字段 ------ 该模型在 delta.reasoning 中返回思考过程

3.2 修改 API 客户端路由(src/services/api/client.ts

typescript

TypeScript 复制代码
export async function getAnthropicClient({ ... }): Promise<Anthropic> {
  const { isOllamaEnabled, createOllamaClient } = await import('./ollama.js')
  if (isOllamaEnabled()) {
    return createOllamaClient() as unknown as Anthropic
  }
  // ... 原有 Anthropic 逻辑
}

3.3 修改模型名称解析(src/utils/model/model.ts

typescript

TypeScript 复制代码
export function getMainLoopModel(): ModelName {
  if (process.env.USE_OLLAMA === '1' || process.env.USE_OLLAMA === 'true') {
    return process.env.OLLAMA_MODEL || 'qwen3.5:9b'
  }
  // ... 原有逻辑
}

3.4 跳过 Anthropic 认证(src/utils/auth.ts

typescript

TypeScript 复制代码
export function isAnthropicAuthEnabled(): boolean {
  if (process.env.USE_OLLAMA === '1' || process.env.USE_OLLAMA === 'true') return false
  // ... 原有逻辑
}

3.5 环境变量配置(preload.ts

typescript

TypeScript 复制代码
process.env.USE_OLLAMA ??= '1'
process.env.OLLAMA_MODEL ??= 'qwen3.5:9b'
process.env.OLLAMA_BASE_URL ??= 'http://localhost:11434'

if (process.env.USE_OLLAMA === '1' || process.env.USE_OLLAMA === 'true') {
  process.env.ANTHROPIC_API_KEY ??= 'ollama-local'  // 防止 CLI 因缺少 key 而挂起
  process.env.DISABLE_ANALYTICS ??= '1'
  process.env.DISABLE_INSTALLATION_CHECKS ??= '1'
  process.env.DISABLE_TELEMETRY ??= '1'
  process.env.DISABLE_BUG_REPORTS ??= '1'
  process.env.DISABLE_AUTOUPDATE ??= '1'
  process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC ??= '1'
}

四、第三阶段:print 模式调试

问题 4.1:ColorDiff.render is not a function

现象 :运行 bun run claude -- -p "hello" 报错 new ColorDiff(...).render is not a function

原因color-diff-napi 的 stub 缺少 render()highlight() 方法

修复:完善 stub 中的类方法

typescript

TypeScript 复制代码
build.module('color-diff-napi', () => ({
  exports: {
    ColorDiff: class ColorDiff {
      constructor() {}
      diff() { return '' }
      render() { return null }  // ← 新增
    },
    ColorFile: class ColorFile {
      constructor() {}
      highlight() { return '' }  // ← 新增
      render() { return null }   // ← 新增
    },
    getSyntaxTheme() { return null },
  },
  loader: 'object',
}))

问题 4.2:SandboxManager 方法缺失

现象 :运行时报 SandboxManager.xxx is not a function 系列错误

原因 :最初的 stub 只有 start/stop/isRunning 三个方法,实际代码调用了 20+ 个方法

修复 :逐一补全所有被调用的方法(isSupportedPlatform, checkDependencies, wrapWithSandbox, getFsReadConfig 等)

问题 4.3:Ollama 模型名称不匹配

现象:Ollama API 返回 404

原因 :最初配置的是 qwen3:8b,但本地实际安装的是 qwen3.5:9b

修复 :更新所有模型引用为 qwen3.5:9b

结果

print 模式成功运行,Ollama 返回了响应!


五、第四阶段:交互式 REPL 模式调试(最艰难的阶段)

交互模式(bun run claude)比 print 模式复杂得多------它使用 React + Ink 渲染终端 UI,有完整的键盘输入处理、对话框系统、keybinding 系统等。

问题 5.1:REPL 启动后无响应(第一层)

现象:输入消息后,长时间无响应

原因:本地 Ollama 模型处理 Claude Code 的超长 system prompt(几万字符)+ 全部 tools 定义需要 60+ 秒

修复

  • 截断 system prompt 为 4000 字符

  • 不发送 tools 定义给 Ollama

问题 5.2:仍然无响应(第二层)------ 并发 API 调用

现象:优化 prompt 后仍无响应

原因generateSessionTitle() 在后台发起第二个 API 调用来生成对话标题。Ollama 是顺序处理的,这个后台调用阻塞了主查询。

修复src/utils/sessionTitle.ts):

typescript

TypeScript 复制代码
export async function generateSessionTitle(...) {
  // ...
  if (isOllamaEnabled()) return null  // 跳过标题生成
  // ...
}

问题 5.3:仍然无响应(第三层)------ 设置对话框阻塞

现象:API 层面已通,但 REPL 仍然无反应

原因:交互模式启动时会显示一系列设置对话框(Onboarding → Trust → MCP Approvals → API Key Approval),这些对话框在等待用户操作,但在 Ollama 模式下它们不应该出现。

修复

  1. 跳过 API Key 审批对话框src/interactiveHelpers.tsx):

typescript

TypeScript 复制代码
// 在 showSetupScreens 中,跳过 ApproveApiKey 对话框
if (process.env.USE_OLLAMA === '1' || process.env.USE_OLLAMA === 'true') {
  // skip API key approval dialog
}
  1. 手动创建全局配置~/.claude/settings.json):

json

TypeScript 复制代码
{
  "theme": "dark",
  "hasCompletedOnboarding": true,
  "customApiKeyResponses": {
    "approved": ["ollama-local"],
    "rejected": []
  },
  "projects": {
    "/Users/pengli/code/claude-code-main": {
      "hasTrustDialogAccepted": true,
      "allowedTools": []
    }
  }
}

问题 5.4:仍然无响应(第四层)------ npm 安装检查提示

现象 :提示 "Claude Code has switched from npm to native installer. Run claude install"

原因:安装方式检查逻辑干扰了正常启动

修复process.env.DISABLE_INSTALLATION_CHECKS ??= '1'

问题 5.5:仍然无响应(第五层)------ Session Hooks 挂起

现象:设置对话框都跳过了,但输入 hello 按 Enter 后没有任何反应

诊断方法 :在关键节点添加文件日志(/tmp/repl-debug.log),逐步缩小范围

发现processSessionStartHooks() 中的 loadPluginHooks() 尝试加载网络插件,永远挂起

修复src/utils/sessionStart.ts):

typescript

TypeScript 复制代码
export async function processSessionStartHooks(...) {
  if (process.env.USE_OLLAMA === '1' || process.env.USE_OLLAMA === 'true') {
    return []  // 跳过所有 session hooks
  }
  // ... 原有逻辑
}

问题 5.6:Enter 键不触发提交(第六层)------ 最隐蔽的 Bug

前面的问题都解决后,按 Enter 仍然没有反应。这是整个过程中最难调试的问题。

诊断过程(逐层深入):

步骤 添加日志位置 结果
1 REPL.onSubmit 没有被调用
2 handlePromptSubmit 没有被调用
3 PromptInput.onSubmit 没有被调用
4 ChordInterceptor.handleInput Enter 被接收!result.type=match, action=chat:submit
5 BaseTextInput.useInput Enter 到达!isActive=true
6 useTextInput.handleEnter 被调用!但 onSubmit 没有执行
7 在 handleEnter 内逐分支加日志 所有分支日志都没出现,onSubmit 日志也没出现
8 用 try/catch 包裹整个函数 捕获到异常!

根本原因

text

复制代码
Cannot find package 'modifiers-napi' from '/Users/pengli/code/claude-code-main/src/utils/modifiers.ts'

完整调用链:

text

复制代码
用户按 Enter
  → ChordInterceptor 识别为 chat:submit(wasInChord=false,不拦截)
    → BaseTextInput.useInput 接收(isActive=true)
      → useTextInput.onInput → mapKey(key.return) → handleEnter(key)
        → 跳过 backslash 检查('o' !== '\\')
        → 跳过 meta/shift 检查(都是 false)
        → 检查 env.terminal === 'Apple_Terminal'  ← TRUE!(用户使用 Terminal.app)
        → 调用 isModifierPressed('shift')
        → require('modifiers-napi')  ← 💥 包不存在,抛出异常!
        → onSubmit 永远不会被调用

修复preload.ts):

typescript

TypeScript 复制代码
build.module('modifiers-napi', () => ({
  exports: {
    prewarm: () => {},
    isModifierPressed: (_modifier: string) => false,
  },
  loader: 'object',
}))

六、最终文件修改清单

文件 修改类型 说明
preload.ts 核心配置 环境变量 + 8 个原生包 stub
src/services/api/ollama.ts 新增 Ollama 适配器(340 行)
src/services/api/client.ts 路由 Ollama 客户端分发
src/utils/model/model.ts 模型 返回 Ollama 模型名
src/utils/auth.ts 认证 跳过 Anthropic 认证
src/utils/sessionTitle.ts 优化 禁用标题生成
src/utils/sessionStart.ts 优化 跳过 session hooks
src/interactiveHelpers.tsx UI 跳过 API Key 对话框
~/.claude/settings.json 配置 预设 onboarding/trust 状态

七、关键经验总结

7.1 大型项目的替换工作是"剥洋葱"

每解决一层问题,下面还有新的一层。从最初的"模型替换"到最终的"原生包 stub",一共剥了 6 层

  1. API 格式转换

  2. 超长 prompt 截断

  3. 并发 API 调用冲突

  4. 启动对话框阻塞

  5. Session hooks 挂起

  6. 原生包缺失导致异常被吞没

7.2 文件日志是终端 UI 调试的唯一可靠手段

React + Ink 渲染终端 UI 时,console.log 会干扰渲染。我们使用了:

typescript

TypeScript 复制代码
require('fs').appendFileSync('/tmp/repl-debug.log', `${new Date().toISOString()} [TAG] message\n`)

在每个关键节点逐步添加,通过二分法定位问题。

7.3 异常被静默吞掉是最难调试的

最终的 modifiers-napi 问题之所以难以定位,是因为:

  • Ink 的 useInput 回调中的异常被框架静默捕获

  • 函数入口的日志能打印,但后续代码因异常中断,没有任何错误信息

  • 只有用 try/catch 包裹整个函数体才能发现

7.4 Apple Terminal 的特殊逻辑

Claude Code 对 Apple Terminal (Terminal.app) 有特殊处理------因为它不支持自定义 Shift+Enter 键绑定,所以代码用 modifiers-napi 原生检测 Shift 键状态。这个仅在 Terminal.app 中触发的代码路径成为了最后的障碍。


八、运行方式

bash

复制代码
# 确保 Ollama 运行中且模型已下载
ollama pull qwen3.5:9b

# 交互模式
bun run claude

# 非交互模式
bun run claude -- -p "你好"

附录:环境变量速查

变量 默认值 说明
USE_OLLAMA 1 启用 Ollama 模式
OLLAMA_MODEL qwen3.5:9b 使用的模型名称
OLLAMA_BASE_URL http://localhost:11434 Ollama API 地址
DISABLE_INSTALLATION_CHECKS 1 跳过安装检查
DISABLE_TELEMETRY 1 禁用遥测

本文档记录了完整的集成过程,希望能为类似项目的本地化改造提供参考。

相关推荐
x-cmd9 小时前
[x-cmd] 让 OpenClaw 操作浏览器自动在 知乎、CSDN 编辑发布文章,告别手动复制、粘贴 | agent-browser
自动化·浏览器·claude·x-cmd·agent-browser·openclaw
winoooops1 天前
一个 AI Agent 的核心循环到底在干什么?Claude Code 的 queryLoop浅析
claude
sigmarising1 天前
AI 时代正在加速模因污染 — Claude Code CLI 源码泄露之外
ai编程·claude
洛卡卡了1 天前
别人开盲盒我开源码:我的 Claude Code 宠物是怎么变成金色传说龙的
agent·ai编程·claude
戴国进1 天前
Claude Code + GLM-5 + Superpowers
claude·superpowers
haibindev1 天前
写了10年代码的人,在AI编程时代反而最值钱
c++·ai编程·claude
字节逆旅2 天前
多Agent工作流开发
agent·claude
易安说AI2 天前
国内用 Claude Code 就三条路:官方订阅、API 中转、国产模型,一篇帮你选对
claude
odoo中国2 天前
Claude Code 架构总览
架构·claude·自动编程·claude cdoe
ANii_Aini2 天前
Claude Code源码架构分析(含可以启动的源码本地部署)
架构·agent·claude·claude code