白嫖 Claude Code!本地 Ollama 接入全流程 + 踩坑实录
本文记录了将 Claude Code VSCode 插件对接本地 Ollama 大模型的完整过程,包含架构设计、关键配置细节,以及逐一排查的 7 大高频坑点。适合有一定 Python/Linux 基础的开发者参考。
前言
Claude Code 是目前公认体验最好的 AI 编程助手之一,但官方 API 按量收费,长期使用成本不低。本文的目标很简单:用本地 Ollama 运行的开源大模型,替换掉 Claude Code 背后的 Anthropic API,实现完全本地、零费用的 AI 编程助手。
最终跑通的架构如下:
Claude Code (VSCode 插件)
↓ Anthropic /v1/messages 协议
anthropic_proxy.py(FastAPI,端口 4001)
↓ OpenAI /v1/chat/completions 协议
LiteLLM(端口 4000)
↓
Ollama(本地模型服务)
↓
qwen3.5:9b / qwen2.5-coder 等本地模型
环境准备
| 组件 | 说明 |
|---|---|
| 操作系统 | Windows 11,PowerShell |
| 显存 | 16GB(RTX 系列) |
| Ollama | 本地大模型运行时 |
| LiteLLM | 统一 LLM API 网关 |
| Python | 3.10+,用于运行自定义中间件 |
| Claude Code | VSCode 插件 v2.1.107+ |
第一步:安装并启动 Ollama
# 官网下载安装后,拉取模型
ollama pull qwen3.5:9b
# 或者
ollama pull qwen2.5-coder:14b
# 验证服务是否正常
curl http://localhost:11434/api/tags
⚠️ 坑 #1:PowerShell 下 curl 不是真正的 curl
Windows PowerShell 里的 curl 其实是 Invoke-WebRequest 的别名,不支持 -d 参数,执行以下命令会报错:
# ❌ 报错:找不到接受实际参数的位置形式参数
curl -X POST http://localhost:11434/api/chat -d '{"model":"qwen3.5:9b"...}'
✅ 解决方案:改用 Invoke-RestMethod
Invoke-RestMethod -Uri "http://localhost:11434/api/tags" -Method Get
或者安装真正的 curl(https://curl.se/windows/),然后使用 curl.exe(注意要加 .exe)。
第二步:配置 LiteLLM
LiteLLM 充当 API 格式转换网关,把 OpenAI 格式的请求转发给 Ollama。
安装(建议在虚拟环境中):
pip install litellm[proxy]
创建 litellm_config.yaml:
model_list:
- model_name: "claude-3-opus-20240229" # 对外暴露的模型名(给 Claude Code 看的)
litellm_params:
model: "ollama_chat/qwen3.5:9b" # 实际调用的本地模型
api_base: "http://localhost:11434"
api_key: "none"
model_info:
supports_function_calling: true
litellm_settings:
drop_params: true
启动 LiteLLM:
litellm --config litellm_config.yaml --port 4000
⚠️ 坑 #2:drop_params 必须设为 true
Claude Code 发出的请求会携带一些 Ollama 不认识的参数,比如 context_management、betas 等。如果不丢弃这些参数,LiteLLM 会直接报错:
litellm.UnsupportedParamsError: ollama does not support parameters: context_management
✅ 解决方案:在 litellm_settings 下加 drop_params: true,LiteLLM 会自动过滤掉不支持的参数。
⚠️ 坑 #3:模型前缀必须用 ollama_chat/,不能用 ollama/
LiteLLM 支持两种 Ollama 前缀:
| 前缀 | 特点 |
|---|---|
ollama/ |
使用旧版 Generate API,Streaming 有异常,工具调用不稳定 |
ollama_chat/ |
使用 Chat API,支持工具调用,推荐使用 |
✅ 解决方案:统一改为 ollama_chat/模型名。(编者注:这步配置大部分网上查询的资料以及询问元宝、千问ai都是说ollama,其实是不对的,用ollama_chat才正常)
第三步:配置 Claude Code
修改 C:\Users\你的用户名\.claude\settings.json:
{
"env": {
"ANTHROPIC_BASE_URL": "http://localhost:4001",
"ANTHROPIC_AUTH_TOKEN": "fake-key",
"ANTHROPIC_MODEL": "claude-3-opus-20240229",
"ANTHROPIC_SMALL_FAST_MODEL": "claude-3-opus-20240229"
}
}
注意这里指向的是端口 4001(我们自己写的中间件),而不是 LiteLLM 的 4000 端口。原因见下文。(编者注:b站视频等都是配完litellm做翻译就可以正常了,不知道是不是版本问题,实测下来没有这个中间件,大模型返回的数据会报错)
第四步:为什么需要自定义中间件?
你可能会问:LiteLLM 不是已经做了格式转换吗?直接指向 4000 不行吗?
不行。 这是本次踩坑最核心的地方。
(编者注:后续调试发现部分模型传输数据不正常,无法调用工具可以尝试这个中间件,不是非要用的。比如魔塔上找了个Qwen3 14B Thinking 2507 x Claude 4.5 Opus模型可以完美适配,调用工具,就不需要这个中间件了。)
Claude Code 使用的是 Anthropic 的 /v1/messages API 协议,而 LiteLLM 的 /v1/chat/completions 是 OpenAI 格式。LiteLLM 虽然也提供了 /v1/messages 端点,但在 1.82.x 版本存在 bug:
Ollama 模型通过
tool_calls字段返回工具调用时,LiteLLM 在转换为 Anthropic 格式时,会错误地将工具调用信息塞进text字符串,而不是生成正确的tool_useblock。
也就是说,Claude Code 收到的响应长这样(错误的):
{
"content": [
{
"type": "text",
"text": "{\"name\": \"bash\", \"arguments\": {\"command\": \"ls\"}}"
}
],
"stop_reason": "end_turn" // ← 错误,应该是 "tool_use"
}
而它期望的是这样(正确的):
{
"content": [
{
"type": "tool_use",
"name": "bash",
"input": {"command": "ls"}
}
],
"stop_reason": "tool_use" // ← 关键!
}
✅ 解决方案:自己写一个 FastAPI 中间件,接管 /v1/messages 端点,手动完成这个格式转换。
第五步:编写中间件 anthropic_proxy.py
完整代码如下(保存为 anthropic_proxy.py,用 uvicorn anthropic_proxy:app --port 4001 启动):
import json
import re
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import httpx
app = FastAPI()
def _try_parse_tool_call(text: str):
"""从文本/代码块中提取工具调用 JSON"""
# 直接解析
try:
parsed = json.loads(text.strip())
if isinstance(parsed, dict) and "name" in parsed and "arguments" in parsed:
return parsed
except json.JSONDecodeError:
pass
# 从 Markdown 代码块中提取
for match in re.finditer(r'```(?:json)?\s*(.+?)\s*```', text, re.DOTALL):
try:
parsed = json.loads(match.group(1).strip())
if isinstance(parsed, dict) and "name" in parsed and "arguments" in parsed:
return parsed
except json.JSONDecodeError:
pass
# 栈匹配:找 {"name": ... } 结构
json_start = text.find('{"name"')
if json_start != -1:
depth, start = 0, text.rfind('{', 0, json_start)
if start != -1:
for i in range(start, len(text)):
if text[i] == '{':
depth += 1
elif text[i] == '}':
depth -= 1
if depth == 0:
try:
parsed = json.loads(text[start:i+1])
if isinstance(parsed, dict) and "name" in parsed:
return parsed
except json.JSONDecodeError:
pass
break
return None
def _convert_openai_to_anthropic(litellm_data: dict, original_body: dict):
"""OpenAI 格式 → Anthropic /v1/messages 格式"""
choices = litellm_data.get("choices", [])
model = litellm_data.get("model", original_body.get("model", ""))
if not choices:
return {"type": "message", "role": "assistant", "model": model,
"content": [{"type": "text", "text": "No response"}],
"stop_reason": "end_turn", "stop_sequence": None}
message = choices[0].get("message", {})
tool_calls = message.get("tool_calls", [])
text_content = message.get("content", "")
anthropic_content = []
found_tools = []
# 工具名规范化映射(Qwen 模型经常乱起名)
tool_name_map = {
"shell": "bash", "local-exec": "bash", "exec": "bash", "run": "bash",
"glob": "glob", "grep": "grep", "search": "grep",
"read": "read", "cat": "read", "write": "write",
"edit": "edit", "patch": "edit",
"todos": "todo_write", "todowrite": "todo_write",
"fetch": "webfetch", "notebook": "notebook",
}
# 处理 tool_calls 字段(标准路径)
for tc in tool_calls:
func = tc.get("function", {})
name = func.get("name", "")
raw_args = func.get("arguments", "{}")
args_dict = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
found_tools.append(name)
anthropic_content.append({"type": "tool_use", "name": name, "input": args_dict})
# 处理 content 字段(Qwen 把工具调用写在文本里的情况)
if text_content and text_content.strip():
parsed = _try_parse_tool_call(text_content)
if parsed:
raw_name = parsed["name"].lower().strip()
final_name = tool_name_map.get(raw_name, parsed["name"])
found_tools.append(final_name)
anthropic_content.append({
"type": "tool_use",
"name": final_name,
"input": parsed.get("arguments", {})
})
print(f"[Proxy] 🔧 从 text 提取工具调用: {parsed['name']} → {final_name}")
else:
anthropic_content.append({"type": "text", "text": text_content})
stop_reason = "tool_use" if found_tools else "end_turn"
return {
"type": "message",
"role": "assistant",
"model": model,
"content": anthropic_content,
"stop_reason": stop_reason,
"stop_sequence": None,
"usage": {
"input_tokens": litellm_data.get("usage", {}).get("prompt_tokens", 0),
"output_tokens": litellm_data.get("usage", {}).get("completion_tokens", 0),
}
}
@app.post("/v1/messages")
async def anthropic_messages(request: Request):
body = await request.json()
# Anthropic → OpenAI 格式转换
messages = []
for msg in body.get("messages", []):
content = msg.get("content", "")
if isinstance(content, list):
parts = []
for block in content:
if block["type"] == "text":
parts.append(block["text"])
elif block["type"] == "tool_result":
parts.append(f"[Tool Result] {block.get('content', '')}")
content = "\n".join(parts)
messages.append({"role": msg["role"], "content": content})
openai_body = {
"model": body.get("model", "qwen3.5:9b"),
"messages": messages,
"stream": False,
}
if body.get("tools"):
openai_body["tools"] = body["tools"]
# qwen3.5 禁用 thinking 模式
if "qwen3" in body.get("model", "").lower():
openai_body["options"] = {"think": False}
async with httpx.AsyncClient(timeout=120.0) as client:
resp = await client.post(
"http://localhost:4000/v1/chat/completions",
json=openai_body
)
litellm_data = resp.json()
anthropic_response = _convert_openai_to_anthropic(litellm_data, body)
print(f"[Proxy] stop_reason={anthropic_response['stop_reason']}, "
f"content_types={[c['type'] for c in anthropic_response['content']]}")
return JSONResponse(content=anthropic_response)
启动命令:
pip install fastapi uvicorn httpx
uvicorn anthropic_proxy:app --host 0.0.0.0 --port 4001
⚠️ 坑 #4:缺少 usage 字段导致前端崩溃
早期版本的中间件没有返回 usage 字段,Claude Code 解析时报错:
undefined is not an object (evaluating '$.input_tokens')
✅ 解决方案:在响应中补充 usage.input_tokens 和 usage.output_tokens 字段,即使值为 0 也要有。
⚠️ 坑 #5:Qwen 模型把工具调用藏在文本里
Qwen 系列模型有时不走标准的 tool_calls 字段,而是把工具调用 JSON 直接写在 content 文本里,甚至包在 Markdown 代码块中:
我来帮你执行这个命令:
```json
{"name": "bash", "arguments": {"command": "ls -la"}}
**✅ 解决方案**:中间件的 `_try_parse_tool_call` 函数多重兜底解析:
1. 直接 JSON 解析
2. 正则提取 Markdown 代码块
3. 栈匹配算法定位嵌套 JSON 对象
---
**⚠️ 坑 #6:Qwen 生成非标准工具名**
Qwen 模型对工具名的发挥空间很大,同一个 `bash` 工具,它可能叫:`Shell`、`local-exec`、`Bash Execute a shell command`、`run`……
Claude Code 工具名大小写敏感,一旦对不上就不执行。
**✅ 解决方案**:在中间件加 `tool_name_map` 规范化映射,把常见变体统一映射到 Claude Code 原生工具名。
---
**⚠️ 坑 #7:Qwen3.5 的 Thinking 模式**
Qwen3.5 系列默认开启 "思维链" 推理(Thinking 模式),会在响应前输出大量 `<think>...</think>` 内容,严重拖慢响应速度,还可能干扰工具调用格式解析。
**✅ 解决方案**:在请求 options 中显式禁用:
```python
openai_body["options"] = {"think": False}
效果验证
中间件正常工作时,终端日志应该类似这样:
[Proxy] 🔧 从 text 提取工具调用: bash → bash
[Proxy] stop_reason=tool_use, content_types=['tool_use']
Claude Code 收到 stop_reason=tool_use 后,会触发本地工具执行,完成文件读写、命令运行等操作。
模型选择建议(16G 显存)
| 模型 | 显存占用 | 工具调用稳定性 | 推理质量 | 推荐指数 |
|---|---|---|---|---|
| qwen3.5:9b | ~6GB | ✅ 稳定 | ⭐⭐⭐ | 入门首选 |
| qwen2.5-coder:14b-instruct-q8_0 | ~15GB | ✅ 较稳定 | ⭐⭐⭐⭐ | 性价比最高 |
| qwen2.5-coder:32b-instruct-q3_K_M | ~14GB | ✅ 非常稳定 | ⭐⭐⭐⭐⭐ | 能力天花板 |
| mistral-nemo:12b | ~8GB | ✅ 格式规范 | ⭐⭐⭐⭐ | 速度/质量均衡 |
| llama3.3:70b-instruct-q2_K | ~15GB | ✅ 原生支持 | ⭐⭐⭐⭐⭐ | 高风险高收益 |
💡 推荐路径:先用
qwen3.5:9b验证架构跑通 → 升级到qwen2.5-coder:14b-instruct-q8_0提升质量 → 如有余力上32b q3冲性能。
完整启动流程
# 1. 启动 Ollama
ollama serve
# 2. 启动 LiteLLM(虚拟环境中)
litellm --config litellm_config.yaml --port 4000
# 3. 启动中间件
uvicorn anthropic_proxy:app --host 0.0.0.0 --port 4001
# 4. 打开 VSCode,Claude Code 即可使用本地模型
总结
| 坑点 | 根因 | 解法 |
|---|---|---|
| PowerShell curl 语法错误 | PS 的 curl 是 Invoke-WebRequest 别名 | 用 Invoke-RestMethod 或 curl.exe |
| LiteLLM 400 参数错误 | Claude Code 发送 Ollama 不认识的参数 | drop_params: true |
| Streaming 异常 | ollama/ 前缀使用旧 API |
改为 ollama_chat/ 前缀 |
| 工具调用格式错误 | LiteLLM 1.82.x 的 bug | 自写 FastAPI 中间件做格式转换 |
前端崩溃 undefined |
缺少 usage 字段 | 中间件补充 usage 映射 |
| 工具调用藏在文本里 | Qwen 模型输出位置不规范 | 中间件多重解析 + 代码块提取 |
| 工具名不匹配 | Qwen 自创工具名 | tool_name_map 规范化 |
| Thinking 模式拖慢响应 | Qwen3.5 默认开启推理链 | options: {think: false} |
整个调试过程挺折腾的,但架构跑通后非常丝滑——完全本地,零延迟,零费用。希望本文能帮你少走弯路。
如果你在复现过程中遇到其他问题,欢迎在评论区交流!
(编者注:除了所有编者注,其他文本均由workbuddy生成,本次调试过程也是workbuddy协助下完成,实测workbuddy大有可为,撒花完结^ ^)
参考环境:Windows 11 + Ollama 0.6.x + LiteLLM 1.82.x + Claude Code 2.1.107
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)