1. OpenClaw 2.0 升级后,本地模型会话为什么需要 SQLite 与 CLI 配合
OpenClaw 2.0 这次把会话和转录记录从原来的文件存储迁进了 SQLite,同时把本地模型接入从 node-llama-cpp 换成了托管的 llama-server,llama.cpp 默认上下文长度提到 64K。对个人开发者来说,这意味着两件事:一是你终端里的多模型会话终于有了结构化的查询入口,二是本地模型(llama.cpp / Ollama)和远端模型可以走同一套 CLI 工作流。
但升级之后很多人会卡在同一个地方:本地模型跑起来了,会话记录也进了 SQLite,可调用链路是断的——CLI 里切换模型要手动改配置,调用记录散在几个地方,想统计一下今天用了多少次本地模型、多少次远端模型,得自己写脚本去翻数据库。更麻烦的是,如果你同时用 Ollama 和 llama.cpp,两边的模型 ID 命名规则不一样,CLI 里传参很容易传错。
我试过把本地模型和远端模型统一到一个 Key/API 通道上,用 TaoToken 做统一入口,CLI 侧只维护一份配置,SQLite 侧只维护一张调用记录表。这样做的直接好处是:本地模型请求和远端模型请求在调用记录里是同一张表,字段结构一致,统计和排查都不用分两套逻辑。
这篇文章面向的是在终端里管理多模型会话的个人开发者。你会拿到三样东西:一份可以直接执行的 SQLite 表结构,一份 CLI 配置文件片段(含 Base URL、Key、Model ID 三件套),以及一次本地模型请求经统一通道的完整验证动作。目标不是讲概念,是把配置和调用链路跑通。
先说清楚 OpenClaw 2.0 在存储上的变化。旧版本会话是文件形式存的,回滚到旧版本前必须用当前 CLI 恢复归档的旧格式转录文件,而且迁移之后创建的会话在旧版本里根本不会出现。官方建议升级前先做一次经过验证的备份。这个破坏性降级路径是 2.0 的一个硬约束,所以你在动 SQLite 之前,先把备份做掉。
SQLite 在这里的角色不是替代 OpenClaw 自己的存储,而是给你一个额外的、可查询的调用记录层。OpenClaw 管的是会话和转录,你管的是「谁在什么时候用什么模型发了什么请求、走了哪条通道、返回了什么状态」。这两层分开,升级 OpenClaw 的时候你的调用记录不会跟着迁移,排查问题的时候也不会因为 OpenClaw 的 schema 变动而抓瞎。
CLI 工作流的核心是把模型选择、通道选择、记录写入这三件事串起来。OpenClaw 2.0 的引导式安装会扫描机器上已有的 AI 访问权限,能复用已经验证过的 Codex、ChatGPT 或 Claude CLI 登录,也能找出本机安装的 Ollama 与 LM Studio 模型。但扫描出来的模型和你要在 CLI 里实际调用的模型之间,还差一层映射。这层映射就是你要在配置文件里写死的东西。
本地模型走 llama.cpp 的时候,llama-server 默认监听一个本地端口,模型 ID 通常是你启动时指定的别名。Ollama 的模型 ID 是ollama list里显示的那个名字。这两个 ID 在 CLI 里如果直接透传,很容易和远端模型的 ID 冲突。统一通道的做法是:CLI 里只认一个 Model ID 字段,本地模型和远端模型都映射到这个字段上,由通道侧决定实际路由到哪个后端。
这样做的代价是你需要维护一份映射表,好处是 CLI 侧的逻辑变得极简。对于个人开发者来说,这个交换是划算的,因为 CLI 脚本一旦复杂起来,调试成本远高于维护一张映射表。
2. TaoToken 前置准备:统一 Key 与 API 通道的接入位置
在写 SQLite 表结构和 CLI 配置之前,先把统一通道这一侧准备好。TaoToken 在这里承担的是统一 Key 和 API 入口的角色,本地模型请求和远端模型请求都从这一个入口出去,CLI 侧不需要为每个后端维护一套鉴权逻辑。
你需要先拿到一个 API Key。入口在控制台的 API Keys 页面,地址是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。拿到 Key 之后,API 的基础地址是 https://taotoken.net/api ,注意这个地址不带 UTM 参数,直接用在配置文件里。
这里要区分两个地址:官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,用于了解产品和文档;API 调用地址是 https://taotoken.net/api ,用于实际请求。配置文件里写的是后者。
模型 ID 这一侧,你需要确认你要调用的模型在通道侧的名称。如果你打算把本地 llama.cpp 或 Ollama 的模型也接进来,通道侧需要能识别你传过去的模型标识。实际操作中,比较稳的做法是:远端模型直接用通道侧的标准 Model ID,本地模型用一个你自定义的别名,然后在通道侧或 CLI 侧做一次映射。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面会说明请求格式、鉴权头和模型列表的获取方式。建议在写配置之前先过一遍,特别是鉴权头的字段名,不同通道的写法有差异。
如果你用的是 Claude Code 这类工具,它的配置方式和普通 CLI 不太一样,需要单独处理。Claude Code 的接入说明在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code_anthropic&utm_campaign=rewrite ,里面会给出 Base URL 和 Key 的填写位置。这一篇主要讲通用 CLI 工作流,Claude Code 的细节你可以对照那份文档。
Coding Plan 适合长期编码和 Agent 场景,如果你打算把 OpenClaw 的 CLI 工作流跑成常态化的编码助手,可以看一下 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。模型对话的入口在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite ,用于验证模型是否正常响应。
前置准备的核心是三件套:Base URL、Key、Model ID。这三样在后面的 CLI 配置和 SQLite 记录里都会用到。Base URL 固定是 https://taotoken.net/api ,Key 从控制台拿,Model ID 根据你要调用的模型确定。本地模型的 Model ID 建议加一个前缀,比如local-或ollama-,这样在 SQLite 里查询的时候一眼能区分。
有一点要注意:不要把 Key 硬编码在会提交到版本库的文件里。CLI 配置建议用环境变量引用,SQLite 里只存 Key 的标识(比如 key 的前几位或一个自定义的 label),不存完整 Key。这是基本的安全习惯,和通道本身无关。
准备好这三样之后,就可以进入配置环节了。下一节给出可以直接复制的 SQLite 表结构和 CLI 配置片段。
3. 可复制配置:SQLite 表结构与 CLI 配置文件片段
这一节给两份可以直接用的配置。第一份是 SQLite 表结构,用于记录调用;第二份是 CLI 配置文件,用于把本地模型和远端模型统一到同一个通道上。
先看 SQLite 表结构。这张表的设计目标是:一次调用一行记录,字段覆盖通道、模型、请求状态、耗时和错误信息。本地模型和远端模型共用这张表,通过channel字段区分。
-- 调用记录表:本地模型与远端模型共用 CREATE TABLE IF NOT EXISTS llm_calls ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT NOT NULL DEFAULT (datetime('now')), channel TEXT NOT NULL, -- 'local-llamacpp' | 'local-ollama' | 'remote' model_id TEXT NOT NULL, -- 实际传给通道的 Model ID base_url TEXT NOT NULL, -- 本次请求使用的 Base URL key_label TEXT, -- Key 的标识,不存完整 Key request_id TEXT, -- 通道返回的请求 ID(如有) status TEXT NOT NULL, -- 'ok' | 'error' http_code INTEGER, latency_ms INTEGER, prompt_tokens INTEGER, output_tokens INTEGER, error_msg TEXT, session_id TEXT -- 关联 OpenClaw 会话(可选) ); -- 按时间和通道查询的索引 CREATE INDEX IF NOT EXISTS idx_llm_calls_created_at ON llm_calls(created_at); CREATE INDEX IF NOT EXISTS idx_llm_calls_channel ON llm_calls(channel); CREATE INDEX IF NOT EXISTS idx_llm_calls_model ON llm_calls(model_id); -- 模型映射表:CLI 侧别名 -> 通道侧实际 Model ID CREATE TABLE IF NOT EXISTS model_map ( alias TEXT PRIMARY KEY, -- CLI 里使用的别名 channel TEXT NOT NULL, -- 路由到哪个通道 target_model TEXT NOT NULL, -- 通道侧实际 Model ID base_url TEXT NOT NULL, note TEXT ); -- 初始化几条映射示例 INSERT OR REPLACE INTO model_map (alias, channel, target_model, base_url, note) VALUES ('local-qwen', 'local-llamacpp', 'qwen2.5-7b-instruct', 'http://127.0.0.1:8080/v1', 'llama-server 本地'), ('local-llama', 'local-ollama', 'llama3.1:8b', 'http://127.0.0.1:11434/v1', 'Ollama 本地'), ('remote-fast', 'remote', 'gpt-4o-mini', 'https://taotoken.net/api', '统一通道远端');这张表的关键设计点是channel和model_id分开。channel表示请求走哪条路(本地 llama.cpp、本地 Ollama、远端统一通道),model_id表示实际传给后端的模型标识。这样你在统计的时候可以按通道聚合,也可以按模型聚合,两个维度互不干扰。
model_map表解决的是别名映射问题。CLI 里你只写local-qwen这样的别名,实际请求的时候从这张表里查出target_model和base_url。本地模型和远端模型都走这套逻辑,CLI 侧不需要为本地模型写特殊分支。
再看 CLI 配置文件。这里用 TOML 格式,路径放在~/.config/openclaw/cli.toml。如果你的 OpenClaw 版本用的是别的配置路径,按实际路径调整,字段结构不变。
# ~/.config/openclaw/cli.toml # OpenClaw 2.0 CLI 统一通道配置 [channel] # 统一通道入口,本地模型和远端模型都从这里出去 base_url = "https://taotoken.net/api" # Key 从环境变量读取,不硬编码 api_key_env = "TAOTOKEN_API_KEY" # 默认模型别名,对应 model_map 表里的 alias default_model = "remote-fast" [channel.headers] # 鉴权头字段名以接入文档为准 Authorization = "Bearer ${TAOTOKEN_API_KEY}" Content-Type = "application/json" [local.llamacpp] # llama-server 默认监听地址 base_url = "http://127.0.0.1:8080/v1" # 启动参数里的上下文长度,2.0 默认 64K context_length = 65536 [local.ollama] base_url = "http://127.0.0.1:11434/v1" [storage] # SQLite 调用记录库路径 db_path = "~/.local/share/openclaw/calls.db" # 是否写入调用记录 log_calls = true [cli] # 会话默认超时(秒) timeout = 120 # 是否在终端打印请求摘要 verbose = true这份配置里,[channel]段是统一入口,[local.*]段是本地后端的地址。CLI 在发起请求时,先根据别名查model_map,如果channel是remote,就用[channel].base_url;如果是local-llamacpp或local-ollama,就用对应的本地地址。这样本地模型和远端模型在 CLI 侧是同一套调用逻辑,只是目标地址不同。
环境变量这样设置:
export TAOTOKEN_API_KEY="你的Key"如果你用的是 Codex 的auth.json方式管理凭据,可以把 Key 写进~/.codex/auth.json,CLI 侧读取这个文件。Cline MCP 的场景下,Base URL、Key、Model ID 三件套要写在 MCP 的配置里,字段名对照接入文档。CC Switch 的场景类似,切换配置的时候确保这三样同步切换,不要只换 Model ID 不换 Base URL。
配置写完之后,先不要急着跑请求。用一条 SQL 确认表结构建好了:
sqlite3 ~/.local/share/openclaw/calls.db ".tables"应该能看到llm_calls和model_map两张表。再看一下映射表里的数据:
sqlite3 ~/.local/share/openclaw/calls.db "SELECT alias, channel, target_model FROM model_map;"确认别名和实际模型 ID 对得上。这一步做完,配置环节就结束了。
4. 验证请求:一次本地模型经统一通道的完整调用
配置写好了,接下来跑一次真实请求,确认本地模型能经统一通道出去,并且调用记录能写进 SQLite。
验证分三步:先确认本地模型服务在跑,再发一次请求,最后查 SQLite 记录。
第一步,确认 llama-server 或 Ollama 在监听。llama.cpp 的 llama-server 启动后默认监听 8080 端口,Ollama 默认监听 11434。用 curl 探一下:
# 探 llama-server curl -s http://127.0.0.1:8080/v1/models | head -c 500 # 探 Ollama curl -s http://127.0.0.1:11434/v1/models | head -c 500如果返回里有模型列表,说明本地服务正常。如果连接被拒,先启动服务。llama-server 的启动命令大致是这样:
llama-server -m /path/to/qwen2.5-7b-instruct.gguf \ --host 127.0.0.1 --port 8080 \ --ctx-size 65536注意--ctx-size设成 65536,和配置文件里的context_length对齐。OpenClaw 2.0 把 llama.cpp 的默认上下文长度提到 64K,你的启动参数要跟上,否则 CLI 侧按 64K 发请求、服务端只开 8K,长上下文会截断。
第二步,发一次请求。这里用 curl 模拟 CLI 的行为,先走本地 llama.cpp,再走统一通道的远端模型,对比两次请求的差异。
# 本地 llama.cpp 请求 curl -s -X POST http://127.0.0.1:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5-7b-instruct", "messages": [{"role": "user", "content": "用一句话说明 SQLite 的 WAL 模式是什么"}], "max_tokens": 128 }' | head -c 800# 统一通道远端请求 curl -s -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "用一句话说明 SQLite 的 WAL 模式是什么"}], "max_tokens": 128 }' | head -c 800两次请求的返回结构应该是一致的,都是 OpenAI 兼容格式,choices[0].message.content里是模型输出。如果本地请求返回正常、远端请求返回 401,说明 Key 或鉴权头有问题,对照下一节的排查清单。
第三步,把这次调用写进 SQLite。CLI 侧如果开了log_calls = true,会自动写入。手动验证的话,用一条 INSERT 模拟:
sqlite3 ~/.local/share/openclaw/calls.db <<'SQL' INSERT INTO llm_calls (channel, model_id, base_url, key_label, status, http_code, latency_ms, prompt_tokens, output_tokens) VALUES ('local-llamacpp', 'qwen2.5-7b-instruct', 'http://127.0.0.1:8080/v1', NULL, 'ok', 200, 842, 28, 64); SQL然后查一下:
sqlite3 -header -column ~/.local/share/openclaw/calls.db \ "SELECT id, channel, model_id, status, latency_ms, created_at FROM llm_calls ORDER BY id DESC LIMIT 5;"应该能看到刚才插入的那条记录。到这里,本地模型经统一通道的调用链路就跑通了:CLI 读配置、按别名查映射、发请求、写记录。
如果你想验证远端模型也走同一条记录链路,把上面 INSERT 的channel改成remote、model_id改成gpt-4o-mini、base_url改成https://taotoken.net/api,再插一条。两条记录在同一张表里,按channel聚合就能看出本地和远端各调了多少次。
这一步做完之后,你可以把 CLI 的调用逻辑封装成一个 shell 函数,参数只传别名和 prompt,其余的都从配置和映射表里读。这样终端里的多模型会话管理就变成了「换别名」这一个动作。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth
配置和验证过程中,最常见的几类报错集中在这一节。每一条都给出触发条件和处理方式。
401 Unauthorized。触发条件通常是 Key 没设、Key 设错、或者鉴权头字段名不对。先确认环境变量:
echo $TAOTOKEN_API_KEY | head -c 8如果输出为空,说明环境变量没导出。如果输出有值但请求还是 401,检查鉴权头。不同通道对鉴权头的字段名要求可能不同,有的用Authorization: Bearer xxx,有的用x-api-key: xxx。以接入文档为准。另外注意 Key 前后不要有空格,从控制台复制的时候容易带上换行。
local proxy failed。这个报错通常出现在本地模型请求上,含义是 CLI 尝试连接本地服务但失败了。先确认本地服务在监听:
ss -tlnp | grep -E '8080|11434'如果没有输出,说明 llama-server 或 Ollama 没启动。如果端口在监听但请求还是失败,检查配置文件里的base_url是否带了/v1后缀。llama-server 和 Ollama 的 OpenAI 兼容接口都在/v1路径下,漏掉/v1会返回 404,有些 CLI 会把 404 报成 proxy failed。
reading choices 报错。这个报错的形式通常是cannot read property 'choices' of undefined或类似,含义是返回体里没有choices字段。触发条件有三种:一是请求根本没发出去,返回的是错误对象;二是返回体是流式格式,但 CLI 按非流式解析;三是通道返回了非 OpenAI 兼容的结构。先看原始返回:
curl -s -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' | head -c 1000如果返回里有error字段,按错误信息处理。如果返回正常但 CLI 还是报 reading choices,检查 CLI 是否开了流式模式而通道返回的是非流式,或者反过来。
OAuth 相关报错。OpenClaw 2.0 的引导式安装支持复用已经验证过的 Codex、ChatGPT 或 Claude CLI 登录。如果你在 CLI 里同时用了 OAuth 凭据和 API Key,可能会出现凭据冲突。表现是请求发出去了但返回 403,或者 CLI 提示凭据无效。处理方式是明确指定用哪一套凭据:如果用 API Key,就把 OAuth 相关的环境变量清掉;如果用 OAuth,就不要在配置里写api_key_env。
SQLite 写入失败。如果 CLI 报unable to open database file,检查db_path的目录是否存在。SQLite 不会自动创建父目录:
mkdir -p ~/.local/share/openclaw如果报database is locked,说明有另一个进程在写同一个库。OpenClaw 自己的存储和你的调用记录库建议分开,不要用同一个文件,避免锁竞争。
模型 ID 不匹配。本地模型的 ID 在不同后端下不一样。llama-server 的模型 ID 是你启动时-m参数对应的模型名,Ollama 的模型 ID 是ollama list里的名字。如果 CLI 传的 Model ID 和后端实际加载的不一致,会返回model not found。用model_map表把别名和实际 ID 对齐,不要靠记忆。
排查的顺序建议是:先确认本地服务在跑,再确认 Key 和鉴权头,再确认 Model ID,最后看 SQLite 写入。这个顺序能覆盖大部分问题,因为调用链路是从本地服务到通道到记录,前面的环节不通,后面的报错都是表象。
6. 把 CLI 工作流跑成常态:从单次验证到日常使用
单次验证跑通之后,接下来是把它变成日常可用的工作流。这一步不需要新配置,主要是把重复动作封装掉。
第一个封装是把请求逻辑写成一个 shell 函数,放在~/.bashrc或~/.zshrc里:
oc() { local alias="$1"; shift local prompt="$*" # 从 model_map 查别名对应的通道和模型 local row row=$(sqlite3 -separator '|' ~/.local/share/openclaw/calls.db \ "SELECT channel, target_model, base_url FROM model_map WHERE alias='$alias';") if [ -z "$row" ]; then echo "unknown alias: $alias" >&2 return 1 fi local channel model base IFS='|' read -r channel model base <<< "$row" # 发请求 local start=$(date +%s%3N) local resp resp=$(curl -s -X POST "$base/chat/completions" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"model\":\"$model\",\"messages\":[{\"role\":\"user\",\"content\":\"$prompt\"}]}") local end=$(date +%s%3N) # 写记录 sqlite3 ~/.local/share/openclaw/calls.db \ "INSERT INTO llm_calls (channel, model_id, base_url, status, latency_ms) VALUES ('$channel','$model','$base','ok',$((end-start)));" echo "$resp" | head -c 500 }这个函数做了三件事:查映射、发请求、写记录。调用的时候只传别名和 prompt:
oc local-qwen "解释一下 SQLite 的 WAL 模式" oc remote-fast "写一个 bash 函数,统计今天的调用次数"第二个封装是统计查询。日常用的时候,你可能想知道今天本地模型和远端模型各调了多少次、平均延迟多少。一条 SQL 就够:
sqlite3 -header -column ~/.local/share/openclaw/calls.db <<'SQL' SELECT channel, COUNT(*) AS calls, ROUND(AVG(latency_ms)) AS avg_ms, SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors FROM llm_calls WHERE created_at >= date('now') GROUP BY channel ORDER BY calls DESC; SQL这条查询按通道聚合,输出调用次数、平均延迟和错误数。本地模型和远端模型在同一张表里,对比起来很直观。
第三个封装是清理。调用记录会一直增长,建议定期归档或删除旧记录:
# 删除 30 天前的记录 sqlite3 ~/.local/share/openclaw/calls.db \ "DELETE FROM llm_calls WHERE created_at < date('now','-30 days');" # 回收空间 sqlite3 ~/.local/share/openclaw/calls.db "VACUUM;"如果你开了 WAL 模式,VACUUM之前先做一次 checkpoint:
sqlite3 ~/.local/share/openclaw/calls.db "PRAGMA wal_checkpoint(TRUNCATE);"WAL 模式对调用记录这种写入频繁、读取也频繁的场景比较合适,但要注意 WAL 文件会增长,定期 checkpoint 能控制大小。
日常使用中还有一个容易忽略的点:OpenClaw 2.0 的会话存储在它自己的 SQLite 库里,你的调用记录在另一个库里。两个库不要混用,也不要在 OpenClaw 升级的时候把你的调用记录库一起迁移。分开的好处是 OpenClaw 的 schema 变动不影响你的记录,你的记录清理也不影响 OpenClaw 的会话。
如果你用的是 Claude Code 或 Cline MCP,CLI 侧的封装逻辑类似,只是配置文件的路径和字段名不同。Base URL、Key、Model ID 这三件套在哪个工具里都是核心,换工具的时候先确认这三样,再调其他参数。
最后留一个实用技巧:在model_map表里给每个别名加一个note字段,写清楚这个模型适合什么场景。比如local-qwen适合长文本总结,remote-fast适合快速问答。终端里oc函数调用的时候,如果传了未知别名,把model_map里的所有别名和 note 打出来,相当于一个模型选择菜单。这个习惯能省掉很多「我上次用的是哪个模型」的翻找时间。