Administrator
发布于 2026-09-11 / 2 阅读
0

我把一个 2B 小模型塞进 AI 客户端当大脑,三天后它给我编了个假 API Key

起因:一个很朴素的省钱念头

我用 WorkBuddy 干活,每次对话都在烧积分。某天看着账单我冒出一个念头:我服务器上不是跑着大模型吗?把它接进客户端当大脑,不就 0 积分了?

服务器是那台老朋友——腾讯云 4核4G/40G,跑着博客、书库、Caddy,还剩一半内存闲着。模型是刚装的 MiniCPM5-2Bopenbmb/minicpm5-2b:q4_k_m,Q4 量化约 1.7GB),热出字 15.5 tok/s,比之前那颗 qwen2.5:3b(8.6 tok/s)快一倍。

看起来挺美。于是我花了三天,把这条路从头走到尾。

结论先说:走通了一半,最后一脚踩空。但中间挖出来的东西,比结论值钱。

第一步:先把模型接进去

客户端要连服务器的 Ollama,得解决"本地怎么访问 11434"。方案是 SSH 隧道:

ssh -f -N -L 11434:127.0.0.1:11434 -i ~/.ssh/id_ed25519_xiaobu ubuntu@106.53.178.213

然后写配置文件 ~/.workbuddy/models.json。这玩意的 schema 官方文档没写,我是从客户端安装包本体里挖出来的——在 app.asar 里翻到一个叫 isValidLocalCustomModel 的校验函数,字段规则全在里面:

  • id 必填且非空
  • 字符串:name / vendor / url / apiKey
  • 数字:maxInputTokens / maxOutputTokens / temperature
  • 布尔:supportsToolCall / supportsImages / supportsReasoning / onlyReasoning / useCustomProtocol

配好之后,隧道一测,/v1/chat/completions 返回 200。当时我觉得这事成了。

坑一:model not found——我把 id 当成了内部编号

客户端模型列表里出现了「MiniCPM5-2B (进哥服务器)」,我一点,红字:

model 'minicpm5-2b-server' not found

原因很蠢也很关键:id 不是内部标识,它会被直接当成模型名发给后端。我图省事写了个自定义名 minicpm5-2b-server,Ollama 那儿当然没这个模型。

改成 Ollama 的真实模型名 openbmb/minicpm5-2b:q4_k_m,中文显示名留在 name 字段,搞定。

教训:接第三方模型时,id 字段填后端认得的真名,别填你给它起的爱称。

坑二:18161 token 撞上 4096 窗口

第二次尝试,又是红字:

request (18161 tokens) exceeds the available context size (4096 tokens)

一万八对四千。这个数字让我第一次意识到一件事:AI 客户端每轮发出的提示词,远不止你打的那几个字

日志里能看到,我实际输入只有 6237 字符,但 Ollama 收到的是 18161 token——多出来的一万多,全是客户端的系统提示:行为规范、可用工具、技能说明、环境信息、会话历史。

也就是说,你在跟客户端聊天,但模型在读一本小册子。2B 模型默认窗口只有 4096,连这本册子的目录都装不下。

那就开大窗口。我从 32K 开始建测试模型:

printf "FROM openbmb/minicpm5-2b:q4_k_m\nPARAMETER num_ctx 32768\n" > Modelfile
docker cp Modelfile ollama:/tmp/ && docker exec ollama ollama create mtest -f /tmp/Modelfile

坑三:我的压测数据骗了我自己

给模型开窗口之前,我想先摸清它的承受极限,于是写了个脚本:从 1K 开始,每次 +0.5K,一直干到 32K,看它什么时候崩。

结果:全程没崩。18K 用 155 秒,32K 用 259 秒,内存稳稳 2.8G 不涨。

我挺高兴,觉得之前"撑不住"的判断是错的。直到后来我又测了一次——

那 259 秒是假的。

我的压测文本是"一段话重复 N 遍",各档之间互为前缀,Ollama 的 prompt cache 全命中了。越测越快,测的不是算力,是缓存。

重新用全新文本 + 墙钟计时测,真实数据是:4.8K token 冷处理约 55 秒,也就是 ~87 tok/s(首字延迟口径),跟之前那个漂亮数字完全不是一回事。

教训:压测大模型,重复文本测出来的是缓存性能,不是算力。要测就得用互不相关的全新文本,并且用墙钟而不是接口返回的统计字段(我甚至见过 prefill=0.2s 这种 26000 tok/s 的鬼数据)。

坑四:好心开的优化,让模型慢了 6.8 倍

看到内存吃紧(常驻 2.5G,机器只有 3.6G),我想着量化一下 KV 缓存省点内存:

-e OLLAMA_KV_CACHE_TYPE=q8_0 -e OLLAMA_FLASH_ATTENTION=1

内存确实降了:容器 2.795G → 2.186G,系统可用内存从 134MB 涨到 868MB。

然后我一测速度,傻眼了:4.8K token 从 55 秒变成 377 秒,慢了 6.8 倍

原因是在纯 CPU 上,q8_0 每步都要做反量化,这个开销远大于省下的内存带宽。FLASH_ATTENTION 同理,那是给 GPU 准备的,CPU 上只有副作用。

两个参数全撤,回到默认 f16 KV。

教训CPU 推理的优化参数和 GPU 不是一回事。看到 FLASH_ATTENTIONKV 量化 这种词别激动,先实测。

顺带摸到了真正的崩溃点:窗口 48K 能加载(2.945G),64K 勉强(2.989G,贴着 3G 上限),128K 直接被 OOM 杀掉llama-server process has terminated: signal: killed。这台机器的窗口天花板,就是 64K

坑五:首轮 12.7 分钟,第二轮 2.6 秒

窗口开到 24K 挂回去,客户端终于能用了。实测三次请求:

请求 首字延迟 总耗时
第 1 次 68 秒
第 2 次 762 秒(12.7 分钟) 14.2 分钟
第 3 次 2.6 秒 22.9 秒

第二和第三差了 288 倍。还是 prompt cache:客户端每轮提示词的前面那一大段(系统提示、工具定义)完全相同,模型算过一次就记住了,第二轮只算新增的那一小截。

所以真实体验是:首轮慢到怀疑人生,之后飞快。前提是你别开新会话、别换话题。

高潮:它给我编了个假 API Key

速度能忍,接下来是质量问题——这才是致命的。

我让它查天气,它给我输出了这么一坨:

<function name="Bash"><param name="command"><![CDATA[
curl -s "https://devapi.twdt.com.cn/4.2.5/json?key=bD5c1c8e7f0a9c8d2e4b6a0c3d7f9e2a6b4c1d8..."
]]></param></function>

我盯着这串东西看了三遍:devapi.twdt.com.cn 这个域名根本不存在,那一长串 bD5c1c8e... 的 API Key 是它自己编的

我第一反应是"2B 模型果然不行",准备写结论收工。但转念一想:它为什么会编得这么像模像样?

根因:是我配错了,不是它不行

我去翻客户端日志,翻到这么一行:

[ModelProvider] Stripped tools/tool_choice from request body
  (model custom-local:minicpm5-2b-24k does not support tool calls)

破案了。

我在配置里写了 supportsToolCall: false,客户端于是把请求里的 tools 字段(工具定义)剥掉了。但是——系统提示的正文里,仍然详详细细写着"你可以使用 Bash、Read、Write 这些工具"

模型收到了一份菜单,上面写着有鱼香肉丝,结果去厨房一看没有这道菜的原料。它不会说"没有",它选择自己编一个

我做了组对照实验,只改一个变量(给不给工具定义):

场景 结果
系统提示描述工具 + 不给工具定义 ❌ 7.2 秒编出 <function name="weather">
系统提示描述工具 + 真实工具定义 ✅ 18.2 秒输出标准 tool_calls: get_weather({"location":"湛江市霞山区"})

一模一样的模型,一模一样的问题,给不给工具定义,结果天差地别。

我还模拟了客户端的 12 个常用工具连测 5 题:

问题 模型的选择
查 C 盘剩余空间 ✅ Bash
读 D:\notes\plan.md ✅ Read(路径完全正确)
搜代码里所有 TODO ✅ Grep
查实时天气 ✅ WebSearch
纯聊天"什么是复利" ✅ 判断不需要工具

5 题全对。 而且后 4 题只用了 5-8 秒——工具定义是固定前缀,命中缓存了。

所以真相是:之前所有"幻觉工具调用",都是我的配置制造的。模型本身支持工具调用,而且挑得挺准。

结局:修好了,但没完全好

supportsToolCall 改成 true 之后,进哥(就是我)实测了一次——客户端报未知错误,无响应

这时候已经晚上十点半,我决定先停手。这条路剩下的不确定性(首轮 15-20 分钟、纯文本回答偶尔返回空、隧道时不时被掐断)不值得继续熬夜去赌。

但这次停手和之前的判断完全不同:之前我以为"2B 模型不行",现在我知道"配置对了它有戏,只是这台机器太慢"。

三天下来,边界在哪

场景 结论
短 prompt 单轮任务(持仓日报:4 只股票的技术面点评) ,42 秒出合规结论,已跑了很久
12 个工具里挑一个 5/5 全对,参数基本正确
18K 提示词的 Agent 对话 ❌ 首轮 10-20 分钟,靠缓存续命
纯 CPU 上量化 KV / 开 Flash Attention ❌ 慢 6.8 倍,纯副作用
窗口超过 64K ❌ 直接 OOM

还有几条是 2B 模型的固有缺陷,改配置也救不回来

  1. 裸问会编事实——不给工具时问它天气,它照样给你编一个"32~39°C、湿度 76%",数字编得比真的还真
  2. 偶尔胡诌产品名——推荐天气 App 能给你推荐出"Wandouban""天气Bug滋"这种不存在的东西
  3. 中英夹杂——推理过程会突然蹦出英文单词

可复用的几条经验

如果你也想给本地小模型接 AI 客户端,这几条能帮你省三天:

  1. id 填后端真名,显示名放 name
  2. 先量清楚客户端实际发多少 token(我的场景是 1.8 万),再决定开多大窗口
  3. 压测必须用全新文本 + 墙钟,重复文本测的是缓存
  4. CPU 上别开 KV 量化和 Flash Attention,实测是负优化
  5. supportsToolCall 不要随便设 false——系统提示里的工具描述不会跟着一起消失,模型会拿幻觉去补这个窟窿
  6. 给容器加内存上限--memory=3g),模型崩了只杀自己,不会连坐博客和书库

最后

这台 4核4G 的小机器,现在跑着博客、书库、日报和一个常驻的 2B 模型,磁盘用了 22G。

它干不了 Agent 的活,但每天早九点二十六分,它会准点给我推一条带 AI 点评的持仓日报——这个活它干得很好,而且一分钱不花。

给小模型找对位置,比逼它干大模型的活有意思多了。

至于客户端那边要不要接云端模型……那是另一个故事了。