ollama run qwen3.5:9b在 16G M1 Pro 上跑 Qwen3.5-9B 并不难,难的是接上 lm-evaluation-harness 跑 GSM8K 和 DROP 之后,Exact Match 直接 0.0,模型输出空字符串,Hugging Face 还时不时 Timeout。给 Claude Code 配 TaoToken 之前,先去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=claude_code_lmeval 创建一把 Key,后面用它让 Claude Code 读samples_*.jsonl和错误栈,把空输出拆成接口类型、2048 Tokens 截断、停止词三类问题。整条链路里 Ollama 和 lm-eval 都不换,TaoToken 只负责给 Claude Code 提供模型通道,让它帮你对照日志改参数。
16G M1 Pro 的内存不算宽裕,Qwen3.5-9B 用 4-bit 量化跑推理可以,但 lm-eval 的 few-shot 模板很长,GSM8K 默认 5-shot,DROP 的上下文也不短。如果只看终端最后一行Exact Match 0.0,很容易误判成模型能力问题。真正的线索在--log_samples生成的 .jsonl 里,在 Claude Code 读得到的resps、filtered_resps、target和错误栈里。下面按排障顺序来:先复现空输出,再配 Claude Code 的 TaoToken 通道,最后回到 lm-eval 改接口类型、max tokens 和 stop 参数。
1. 0 分空输出先别怪 Qwen3.5-9B:拆开 GSM8K 与 DROP 的报错链
1.1 16G M1 Pro 上的复现命令与两个典型现象
先把本地链路跑起来。Ollama 拉起 Qwen3.5-9B,确认 OpenAI 兼容端点有响应,再让 lm-evaluation-harness 通过local-completions或local-chat-completions去请求。常见命令大致是这样,模型名和 tokenizer 按你本地实际情况替换:
ollama run qwen3.5:9b另开一个终端确认端点:
curl http://localhost:11434/v1/models如果这里返回模型列表,说明 Ollama 的兼容层已经起来。接着跑 lm-eval,一开始很多人会写成:
lm_eval \ --model local-completions \ --model_args base_url=http://localhost:11434/v1,model=qwen3.5:9b,tokenizer=YOUR_TOKENIZER_PATH_OR_NAME \ --tasks gsm8k,drop \ --num_fewshot 5 \ --batch_size 1 \ --output_path ./logs/qwen35_9b_ollama \ --log_samples跑完看到两个典型现象:一是 Hugging Face 数据集下载超时,任务还没开始就退出;二是任务跑完了,但 Exact Match 0.0,打开 .jsonl 发现resps是空字符串。前者是数据集获取问题,后者是请求或生成配置问题,不要混在一起查。
1.2 Hugging Face Timeout 与空字符串不是同一类错
Hugging Face Timeout 通常发生在 lm-eval 首次拉取 GSM8K、DROP 数据集时。如果本地~/.cache/huggingface/datasets已经有缓存,可以先用离线模式确认:
HF_DATASETS_OFFLINE=1 HF_HUB_OFFLINE=1 lm_eval \ --model local-chat-completions \ --model_args base_url=http://localhost:11434/v1,model=qwen3.5:9b,tokenizer=YOUR_TOKENIZER_PATH_OR_NAME \ --tasks gsm8k,drop \ --num_fewshot 5 \ --batch_size 1 \ --output_path ./logs/qwen35_9b_ollama_offline \ --log_samples离线模式能跑通,说明数据集不是主要矛盾,空输出另有原因。空字符串一般来自三种情况:请求打到了错误的端点,prompt 被 2048 Tokens 截断,或者 stop 序列把答案开头直接切掉。这三种都会让resps看起来像模型什么都没说,但日志里的doc、filtered_resps、target会留下不同痕迹。
注意:不要因为空输出就把 Qwen3.5-9B 换成更大的模型。16G M1 Pro 能跑动 9B 量化版已经不容易,先把请求格式对齐更划算。
2. 让 Claude Code 读 .jsonl:把 lm-evaluation-harness 日志变成排查线索
2.1 --log_samples 生成的 samples_*.jsonl 里看哪几个字段
--log_samples会在输出目录下生成samples_gsm8k_*.jsonl和samples_drop_*.jsonl。每行是一条样本,重点看四个字段:
doc:原始题目,里面的question和answer能判断 prompt 有多长。filtered_resps:实际送给模型的 prompt 或对话消息。如果是 chat 端点,这里可能是消息数组。resps:模型返回的文本。空字符串就是最直接的 0 分信号。target:标准答案。GSM8K 常见形式是#### 数字,DROP 是短答案。
先不要急着改参数,把前 10 条 .jsonl 片段交给 Claude Code。让它做统计,而不是让它替你跑评测。Claude Code 可以读本地文件、解释 JSON、对照错误栈,但lm_eval命令仍然由你在本地终端执行,再把输出贴回对话。
2.2 给 Claude Code 的提示词不要写“帮我跑评测”
更有效的提示词是限制它只做日志分析:
读取 ./logs/qwen35_9b_ollama_chat/samples_gsm8k_*.jsonl 的前 10 条记录。 统计 resps 为空字符串的条数。 对每条记录,输出 doc.question 的字符数、filtered_resps 的字符数、target 的值。 不要执行 lm_eval,不要改文件,只根据日志判断: 1) 空输出是否集中在长 prompt 样本; 2) filtered_resps 里是否出现了聊天模板多余的特殊 token; 3) target 是否被 stop 序列提前截断。Claude Code 接上 TaoToken 之后,消耗 Token 去读这些 .jsonl 和错误栈,比你手动翻几十条记录快得多。它给出的结论不一定全对,但能把“空输出”缩小到几个可验证的假设。下一步就是让它对照假设,改 lm-eval 的接口类型、max tokens 和 stop 参数。
3. 把 Claude Code 接到 TaoToken:settings.json 与最小验证
3.1 创建 Key 与模型 ID 以模型广场为准
TaoToken 在这里的角色很明确:给 Claude Code 提供统一 API 通道。打开 TaoToken 官网 注册并创建 API Key,Key 用占位符YOUR_API_KEY表示。模型 ID 不要凭记忆写,去模型广场看当时列表,以页面显示为准。填进 Claude Code 的 Base URL 固定用https://taotoken.net/api,末尾不要加/v1。
提示:官网落地页只用来注册、创建 Key、看模型广场和用量;真正填进工具的是
https://taotoken.net/api。两者不要混。
3.2 ~/.claude/settings.json 的 env 写法
Claude Code 可以通过环境变量读取 Anthropic 兼容配置,也可以写进~/.claude/settings.json的env字段。文件内容像这样:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "YOUR_MODEL_ID" } }保存后重开终端或重启 Claude Code,让环境变量生效。如果你更喜欢在 shell 里临时导出:
export ANTHROPIC_BASE_URL=https://taotoken.net/api export ANTHROPIC_AUTH_TOKEN=YOUR_API_KEY export ANTHROPIC_MODEL=YOUR_MODEL_ID3.3 可选 CLI:taotoken cc 直接拉起 Claude Code
如果不想手动改 settings.json,也可以用 TaoToken CLI。它适合在终端里快速切换 Key 和模型:
npm install -g @taotoken/taotoken taotoken cc -k YOUR_API_KEY -u https://taotoken.net/api -m YOUR_MODEL_ID注意-u后面只写https://taotoken.net/api,不要加/v1,也不要加任何查询参数。CLI 只是帮你把 Claude Code 的环境变量指过去,Ollama 和 lm-eval 完全不受影响。
3.4 发一条最小请求确认通道
配置改完,先在 Claude Code 里发一条最小请求,比如输入“只回复 OK”。返回 OK,说明 Key、Base URL、模型 ID 三者至少能通。如果返回 401,先检查ANTHROPIC_AUTH_TOKEN是不是还留着YOUR_API_KEY;如果返回 404,检查ANTHROPIC_BASE_URL是不是误写成了https://taotoken.net/api/v1。通道不通就不要急着查 lm-eval,先把 Claude Code 的请求调通。
4. 回到 lm-eval:/v1/chat/completions 与文本续写错位
4.1 local-completions 与 local-chat-completions 的区别
Ollama 的 OpenAI 兼容层通常提供/v1/chat/completions,它接收的是 messages 数组;而 lm-eval 的local-completions默认走文本续写思路,把 prompt 当成一段纯文本发给/v1/completions。两者错位时,Ollama 可能收到不符合预期的 payload,返回空内容或直接报错。表现就是任务跑完、请求有记录,但resps是空字符串。
4.2 改写 lm_eval 命令为 chat 模型
如果确认 Ollama 暴露的是 chat 端点,lm-eval 侧应改用local-chat-completions,并让base_url指向http://localhost:11434/v1。示例命令:
lm_eval \ --model local-chat-completions \ --model_args base_url=http://localhost:11434/v1,model=qwen3.5:9b,tokenizer=YOUR_TOKENIZER_PATH_OR_NAME \ --tasks gsm8k,drop \ --num_fewshot 5 \ --batch_size 1 \ --gen_kwargs max_gen_toks=512 \ --output_path ./logs/qwen35_9b_ollama_chat \ --log_samples改完再跑同一批任务,观察resps是否开始出现正常文本。如果仍然为空,继续看下一节的 2048 Tokens 和停止词。把新日志片段贴给 Claude Code,让它对比两次运行的filtered_resps,通常能看出端点类型改对了没有。
5. 2048 Tokens 截断:Ollama num_ctx 与 lm-eval max_gen_toks 要一起看
5.1 Ollama Modelfile 把 num_ctx 拉到 8192
Ollama 默认上下文窗口可能只有 2048 Tokens。GSM8K 的 5-shot prompt 加上 DROP 的长文档,很容易超过这个数。prompt 被截断后,模型看到的题目不完整,输出空字符串或无关内容。用 Modelfile 建一个更大上下文的变体:
FROM qwen3.5:9b PARAMETER num_ctx 8192 PARAMETER num_predict 512然后创建并运行:
ollama create qwen3.5:9b-8k -f Modelfile ollama run qwen3.5:9b-8klm-eval 里的model参数也要跟着改成qwen3.5:9b-8k。这一步不替换 Ollama,只是把本地模型的上下文设置调大。
5.2 lm-eval 的 max_gen_toks 不要留默认值赌运气
生成侧也有上限。lm-eval 可以用--gen_kwargs max_gen_toks=512给 GSM8K 和 DROP 留出足够的推理步数。太小会导致模型刚写到关键步骤就被切断,resps看起来不完整甚至为空。建议先设 512,再根据 .jsonl 里resps的长度调整。注意max_gen_toks不是越大越好,16G M1 Pro 上生成越长越慢,先保证答案能写完。
5.3 用 Claude Code 对照 .jsonl 判断是 prompt 截断还是生成截断
把新旧两份 .jsonl 的前 10 条贴给 Claude Code,提示它对比filtered_resps的字符数和resps的长度。如果filtered_resps在旧日志里明显更短,说明 prompt 被截断;如果 prompt 完整但resps很短且没有结束标点,说明生成上限或 stop 序列在捣乱。Claude Code 只做判断和给出修改建议,ollama create和lm_eval仍由你在本地执行。
6. Answer: 与 Question: 停止词陷阱:GSM8K 和 DROP 的 stop 不能乱加
6.1 为什么把 Answer: 加进 stop 会让答案变空
GSM8K 的答案格式通常包含#### 数字,有些模板会引导模型输出Answer:。如果你为了“干净”把Answer:加进 stop 序列,模型刚说出Answer:就被截断,后面的数字一个都留不下。DROP 里也可能出现Question:或Answer:开头的格式,误加 stop 会让resps变成空字符串。表面看是模型不输出,实际是停止词把输出掐掉了。
6.2 正确做法:保留任务模板,只加换行或模型结束符
先不要在--gen_kwargs里硬塞until=Answer:。让 lm-eval 使用任务自带的模板和默认停止条件,只通过max_gen_toks限制长度。如果确实需要停,优先用换行或模型自己的结束符,不要用题目里要求模型写出的关键词。改完命令像这样:
lm_eval \ --model local-chat-completions \ --model_args base_url=http://localhost:11434/v1,model=qwen3.5:9b-8k,tokenizer=YOUR_TOKENIZER_PATH_OR_NAME \ --tasks gsm8k,drop \ --num_fewshot 5 \ --batch_size 1 \ --gen_kwargs max_gen_toks=512 \ --output_path ./logs/qwen35_9b_ollama_no_stop \ --log_samples6.3 改完再跑一次,看 Exact Match 是否变化
重新跑 GSM8K 和 DROP,然后打开新的samples_*.jsonl。如果resps里开始出现完整的推理和最终数字,Exact Match 即使不高,也不会再是 0.0 空输出。把新日志给 Claude Code,让它统计resps非空比例、平均长度和被截断的比例。到这里,接口类型、2048 Tokens、停止词三条线基本能分清。TaoToken 负责的是 Claude Code 读日志、改脚本这一侧,不参与 Ollama 本地推理。
7. 跑通之后去控制台对一下这次调用
7.1 在模型对话里用同一把 Key 复测
Claude Code 能正常返回之后,去 TaoToken 模型对话 用同一把 Key 发一条测试消息,确认模型 ID 和 Base URL 没填错。这一步和 lm-eval 的本地评测互不干扰,只是为了确认 Claude Code 消耗 Token 的通道稳定。如果你在 .jsonl 分析里换了模型,也顺便在模型广场确认该模型 ID 仍然可用。
7.2 看 Coding Plan 与创建 Key
如果你准备长期用 Claude Code 分析评测日志、改 lm-eval 脚本,可以打开 Coding Plan 看套餐是否够用。Key 仍然在 控制台 API Keys 创建和轮换,Claude Code 的环境变量对照可以看 Claude Code 接入文档。回到你刚跑完的那次 lm-eval,如果 Claude Code 侧有调用记录,说明整条排查链路已经闭环;如果记录为空,先回头检查ANTHROPIC_BASE_URL是否还是https://taotoken.net/api,而不是带/v1或其他路径。