☰
白嫖 Claude Code!本地 Ollama 接入全流程 + 踩坑实录(TaoToken 统一 Key 版)
2026/10/1 20:01:04 网站建设 项目流程

1. 本地 Ollama 接入 Claude Code 到底难在哪

Claude Code 是目前公认体验最好的 AI 编程助手之一,但官方 API 按量计费,长期跑下来成本不低。很多人第一反应是:我本地有 Ollama,跑个开源模型不就行了?想法很美好,真动手就会发现——Claude Code 说的是 Anthropic 的/v1/messages协议,而 Ollama 只认 OpenAI 风格的/v1/chat/completions,两边根本对不上话。

这篇文章要解决的就是这个协议鸿沟。适合谁看:有 Python 和 Linux/Windows 基础、手里有 16GB 左右显存、想让 Claude Code 跑在本地免费模型上的开发者。我会把整条链路拆开讲清楚:Ollama 提供本地模型服务,LiteLLM 做第一层格式转换,再自己写一个 FastAPI 中间件补上 Anthropic 协议的最后一块拼图,最后把 Claude Code 的 Base URL 指过来。

整条链路长这样:

Claude Code (VSCode 插件) ↓ Anthropic /v1/messages 协议 anthropic_proxy.py(FastAPI,端口 4001) ↓ OpenAI /v1/chat/completions 协议 LiteLLM(端口 4000) ↓ Ollama(本地模型服务,端口 11434) ↓ qwen3.5:9b / qwen2.5-coder 等本地模型

为什么中间要塞两层?因为 LiteLLM 虽然能把 OpenAI 格式转成 Anthropic 格式,但在 1.82.x 版本里有个坑:Ollama 模型通过tool_calls字段返回工具调用时,LiteLLM 会错误地把工具调用信息塞进text字符串,而不是生成正确的tool_useblock。Claude Code 收到这种响应就懵了,工具根本执行不了。所以得自己写个中间件,手动完成这个格式转换。

我试过直接让 Claude Code 指向 LiteLLM 的 4000 端口,结果就是模型返回的数据格式报错,工具调用完全失效。后来加了这个 FastAPI 中间件才跑通。下面把每一步的配置和踩过的坑都写出来,你可以照着操作。

2. 环境准备与 TaoToken 统一 Key 通道对照

先说清楚本地这套方案和 TaoToken 的关系。本地 Ollama 方案的核心优势是零成本、数据不出本机,但受限于显存,模型能力上限有限。如果你需要更强的模型做对照验证,或者本地跑不动大参数模型,可以用 TaoToken 的统一 Key 通道作为补充——它提供标准的 Anthropic 兼容接口,Claude Code 改个 Base URL 就能切过去,不用改任何代码逻辑。

TaoToken 官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。它的作用是给你一个统一的 Key,通过标准 Anthropic 协议访问模型,省去自己搭中间件的麻烦。本地方案和 TaoToken 方案可以并存:日常轻量任务走本地 Ollama,复杂任务切到 TaoToken 通道,两边用同一套 Claude Code 配置,只改环境变量。

环境清单如下:

组件说明操作
操作系统Windows 11 / macOS / Linux本文以 Windows 11 + PowerShell 为主
显存16GB(RTX 系列)决定能跑多大的模型
Ollama本地大模型运行时官网下载安装
LiteLLM统一 LLM API 网关Python 3.10+,用于运行中间件
Claude CodeVSCode 插件 v2.1.107+在 VSCode 扩展市场安装

第一步,安装并启动 Ollama,拉取模型:

# 官网下载安装后,拉取模型 ollama pull qwen3.5:9b # 或者 ollama pull qwen2.5-coder:14b # 验证服务是否正常 curl http://localhost:11434/api/tags

这里有个 Windows 特有的坑。PowerShell 里的curl其实是Invoke-WebRequest的别名,不支持-d参数,你执行下面这条会直接报错:

# 报错:找不到接受实际参数的位置形式参数 curl -X POST http://localhost:11434/api/chat -d '{"model":"qwen3.5:9b"}'

解决办法有两个:改用Invoke-RestMethod,或者安装真正的 curl 后用curl.exe(注意要加.exe):

Invoke-RestMethod -Uri "http://localhost:11434/api/tags" -Method Get

第二步,安装 LiteLLM 并创建配置文件。建议在虚拟环境里装:

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

这里有两个必须注意的点。第一,drop_params: true一定要开。Claude Code 发出的请求会携带一些 Ollama 不认识的参数,比如context_management、betas等,不丢弃这些参数 LiteLLM 会直接报UnsupportedParamsError。第二,模型前缀必须用ollama_chat/,不能用ollama/。两者区别如下:

前缀特点
ollama/使用旧版 Generate API,Streaming 有异常,工具调用不稳定
ollama_chat/使用 Chat API,支持工具调用,推荐使用

网上很多资料和 AI 问答都说用ollama/,实测下来是不对的,用ollama_chat/才正常。

3. 可复制配置:FastAPI 中间件与 settings.json

这一节是整篇文章的核心。LiteLLM 虽然也提供了/v1/messages端点,但在 1.82.x 版本存在 bug:Ollama 模型通过tool_calls返回工具调用时,LiteLLM 转成 Anthropic 格式会把工具调用信息塞进text字符串,而不是生成正确的tool_useblock。Claude Code 期望的是这样:

{ "content": [ { "type": "tool_use", "name": "bash", "input": {"command": "ls"} } ], "stop_reason": "tool_use" }

实际收到的却是:

{ "content": [ { "type": "text", "text": "{\"name\": \"bash\", \"arguments\": {\"command\": \"ls\"}}" } ], "stop_reason": "end_turn" }

stop_reason错了,工具就触发不了。所以需要自己写一个 FastAPI 中间件,接管/v1/messages端点,手动完成格式转换。完整代码如下,保存为anthropic_proxy.py:

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

接下来配置 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 端口。如果你要切到 TaoToken 通道做对照验证,只需把ANTHROPIC_BASE_URL改成https://taotoken.net/api,ANTHROPIC_AUTH_TOKEN换成你在 TaoToken 控制台申请的 Key,模型名换成对应模型 ID 即可,中间件和 LiteLLM 都不用启动。三件套(Base URL + Key + Model ID)配齐就能用。

4. 验证请求与成功结果

配置完成后,按顺序启动所有服务:

# 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 即可使用本地模型

先单独验证中间件是否正常工作。用 curl 发一个 Anthropic 格式的请求:

curl -X POST http://localhost:4001/v1/messages \ -H "Content-Type: application/json" \ -H "x-api-key: fake-key" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-3-opus-20240229", "max_tokens": 1024, "messages": [{"role": "user", "content": "列出当前目录文件"}], "tools": [{"name": "bash", "description": "执行 shell 命令", "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}}}] }'

中间件正常工作时,终端日志应该类似这样:

[Proxy] 从 text 提取工具调用: bash → bash [Proxy] stop_reason=tool_use, content_types=['tool_use']

看到stop_reason=tool_use就说明工具调用格式转换成功了。Claude Code 收到这个响应后,会触发本地工具执行,完成文件读写、命令运行等操作。

如果返回的是stop_reason=end_turn且content_types=['text'],说明模型把工具调用写在了文本里但没被正确提取,检查_try_parse_tool_call的解析逻辑,或者换个工具调用能力更强的模型。

模型选择建议(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 冲性能。

5. 本篇常见报错排查

这一节把踩过的坑集中列出来,对照真实报错定位问题。

报错一:401 Unauthorized / invalid api key

Claude Code 报 401,通常是ANTHROPIC_AUTH_TOKEN没配或配错。本地方案里这个值随便填(比如fake-key),因为中间件不校验;但如果你切到了 TaoToken 通道,必须填真实的 Key。检查settings.json里的ANTHROPIC_AUTH_TOKEN和ANTHROPIC_BASE_URL是否匹配——本地是http://localhost:4001,TaoToken 是https://taotoken.net/api。

报错二:local proxy failed / connection refused

中间件没启动,或者端口被占用。先确认uvicorn anthropic_proxy:app --port 4001在跑,再用netstat -ano | findstr 4001检查端口。如果 LiteLLM 的 4000 端口没起来,中间件转发时会报连接失败,按顺序先起 Ollama、再起 LiteLLM、最后起中间件。

报错三:reading 'choices' / undefined is not an object

这是早期中间件缺少usage字段导致的。Claude Code 解析响应时会读usage.input_tokens,如果响应里没有这个字段就报undefined is not an object (evaluating '$.input_tokens')。解决办法是在响应中补上usage.input_tokens和usage.output_tokens,即使值为 0 也要有。上面的完整代码已经包含这部分。

报错四:OAuth error / authentication failed

Claude Code 默认走 OAuth 登录流程,如果你没在settings.json里显式配置ANTHROPIC_AUTH_TOKEN,它会尝试 OAuth 认证然后失败。确保env块里三个变量都配齐:ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_MODEL。

报错五:UnsupportedParamsError: ollama does not support parameters

LiteLLM 报参数不支持,说明drop_params: true没开。Claude Code 会发一些 Ollama 不认识的参数,必须在litellm_settings下加drop_params: true。

报错六:工具调用不执行 / stop_reason 一直是 end_turn

模型把工具调用写在了content文本里,而不是标准的tool_calls字段。Qwen 系列模型经常这样,甚至包在 Markdown 代码块里。中间件的_try_parse_tool_call函数做了三重兜底解析:直接 JSON 解析、正则提取 Markdown 代码块、括号匹配算法定位嵌套 JSON 对象。如果还是不行,检查模型是否支持 function calling,换qwen2.5-coder系列试试。

报错七:工具名不匹配

Qwen 模型对工具名的发挥空间很大,同一个bash工具,它可能叫Shell、local-exec、Bash Execute a shell command、run。Claude Code 工具名大小写敏感,对不上就不执行。中间件里的tool_name_map做了规范化映射,把常见变体统一映射到 Claude Code 原生工具名。

报错八:Thinking 模式拖慢响应

Qwen3.5 系列默认开启思维链推理,会在响应前输出大量...内容,严重拖慢速度,还可能干扰工具调用格式解析。解决办法是在请求options中显式禁用:openai_body["options"] = {"think": False}。

6. 本地与 TaoToken 双通道的取舍

整套链路跑通后,你会发现本地方案的边界很清晰:完全本地、零延迟、零费用,但受显存限制,模型能力有天花板。9b 模型做简单代码补全和文件操作够用,复杂重构和长上下文推理就吃力了。

我的实际用法是双通道并行。日常轻量任务走本地 Ollama,配置就是上面这套,ANTHROPIC_BASE_URL指向http://localhost:4001。遇到需要更强推理的复杂任务,把settings.json里的ANTHROPIC_BASE_URL改成https://taotoken.net/api,ANTHROPIC_AUTH_TOKEN换成 TaoToken 控制台申请的 Key,模型 ID 换成对应模型,重启 Claude Code 就切过去了。中间件和 LiteLLM 都不用动,因为 TaoToken 本身就是标准 Anthropic 协议,不需要格式转换。

如果你还没申请 Key,可以去 https://taotoken.net/api-keys 创建,接入文档在 https://taotoken.net/doc 。想先验证模型效果,用 https://taotoken.net/models 的对话功能试几个 prompt 再决定。长期跑编码和 Agent 任务的话,Coding Plan(https://taotoken.net/coding-plan )比按量计费更划算。

最后提醒一个容易忽略的点:本地方案的settings.json里ANTHROPIC_MODEL填的是 LiteLLM 配置里model_name的值(比如claude-3-opus-20240229),不是 Ollama 的真实模型名。这个映射关系在litellm_config.yaml里定义,改模型时两边要同步改,否则会报模型找不到。切到 TaoToken 通道时,ANTHROPIC_MODEL则要填 TaoToken 支持的模型 ID,别把本地的映射名带过去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询