在手机上跑大模型:HexaBench的6个Bug,全是正则和参数惹的祸
在手机上跑大模型,听起来很浪漫。修到能跑通,要过6个Bug。每个Bug都不是”写代码”能解决的,是”读懂机器脾气”的功课——参数污染、正则转义、伪流式、DOM操作爆炸。PP 20.81 tok/s,这是Android手机能跑大模型的真实速度。
TL;DR
在Termux上跑本地大模型(llama.cpp + GGUF + HexaBench Lite),修了6个Bug:①命令行参数污染 ②响应提取失败 ③正则表达式转义错误 ④伪流式处理 ⑤DOM操作O(n²)爆炸 ⑥Cloudflare Worker 1003/1031报错。核心教训:修大模型框架不是写代码,是读懂机器脾气——每条错误信息背后,都是一个你”以为能自动处理但机器没处理”的细节。
一、起因:为什么要在手机上跑大模型
朋友看我手机跑Hermes、爬虫、股票脚本,问:”手机能跑大模型吗?”
我当时的回答:能,但没那么美好。
“美好”的版本是:下载个GGUF模型,拖进HexaBench,点生成,出结果。
“真实”的版本是:拖进去之后,生成窗口是空的,响应是乱码,流式输出是假的,DOM刷着刷着卡死了,连浏览器都要加个参数才不崩。
这就是”在手机上跑大模型”的真相:模型能跑,但工程链路要过至少6个关。今天把这6个关的坑,挨个讲一遍。
二、Bug 1:命令行参数污染
现象:我在HexaBench里输入prompt,生成的结果里,混进了--threads 4 -ngl 0 --no-mmap这些命令行参数。
根因:代码里用的是裸位置参数传prompt,而不是 -p 参数。
1 | # 错误写法:prompt作为裸位置参数 |
裸位置参数的问题是:llama-simple会把prompt后面的内容,当成更多参数拼到命令行里。最后生成的输出,参数和prompt和响应,全搅在一起。
教训:命令行工具传参,永远用命名参数(-p xxx),别用裸位置参数。这是Unix老规矩,但跑大模型的人容易忘。
三、Bug 2:响应提取失败
现象:生成完了,窗口里是空的,或者显示”请求失败或无响应”。
根因:_extract_response函数不知道llama-simple的输出格式长啥样。
llama-simple的真实输出(从stderr里抓的):
1 | --threads 4 -ngl 0 --no-mmap --temp 0.7 -p 你好,很高兴认识你 -s 1000000000 -t 1000main: decoded 20 tokens in 2.45 s, speed: 8.16 t/s |
注意几个关键点:
- prompt被重复输出。llama-simple会把它收到的命令行参数原样回显,包括
-p后面的prompt。所以你能看到-p 你好,很高兴认识你。 - 真正的AI响应,藏在
main: decoded之前。 - 统计信息在
main: decoded之后。
正确的提取逻辑:定位main: decoded这行 → 找prompt最后出现的位置 → 取prompt之后、main: decoded之前的内容 → 清理残留参数。
1 | pattern = r'-p\s+' + escaped_prompt + r'(?:\s+-p\s+' + escaped_prompt + r')*\s+-p\s+(.*?)\s*(?:main:|\n|$)' |
教训:别”以为输出是干净的”。跑一遍,把真实输出dump下来看,比猜靠谱一百倍。
四、Bug 3:正则表达式转义错误
现象:响应提取好了,但清理残留参数时,正则又出错了,要么清不掉,要么把响应也清了。
根因:正则里反斜杠没转义对。
1 | # 错误:--[\w-]+ 里的 \w 在字符串里没转义 |
更坑的是嵌套在f-string或字符串拼接里的正则,转义层级又多一层。
教训:正则表达式是”看起来简单,实际魔鬼”。跑大模型的人天天写正则,转义错了你就得盯着空窗口干瞪眼。
五、Bug 4:伪流式处理
现象:HexaBench的”流式输出预览”,显示”请求失败或无响应”,或者一次性刷出来。
根因:代码以为自己做了流式,实际没做。
真正的流式:模型每生成一个token,就立刻推一个token到前端。
伪流式:等模型全跑完(几十秒),一次性把整段响应扔给前端。用户体验上”卡了半小时”,但其实模型早就跑完了。
修法是无缓冲模式(bufsize=0)+ 字符级流式处理。但要注意的是:bufsize=0意味着每一个字节都触发一次DOM更新,这又引出Bug 5。
教训:流式输出不是”看起来动了”,是”真的一个token一个token来”。别信”我加了流式”,要信”我测了token间隔”。
六、Bug 5:DOM操作O(n²)爆炸
现象:流式输出正常了,但前端越刷越卡,最后直接卡死。
根因:每来一个token,代码就操作一次DOM(append一个字符)。一次很快,但100个token下来,每次都要重排整个DOM,复杂度是O(n²)。
修法:批量更新。攒一批token,一次性更新DOM。或者用requestAnimationFrame节流。
教训:前端性能问题,往往不是”没优化”,是”优化方向错了”。O(n²)的DOM操作,是流式场景里最容易踩的性能坑。
七、Bug 6:Cloudflare Worker 1003 / 1031
现象:大模型API部署到CF Worker(为了从外网访问),结果报1003或1031。
根因:两个不同的错:
- 1031:Worker代码不完整,被截断。移动端浏览器编辑,屏幕小,粘贴大段代码容易漏。检查
export default+addEventListener('fetch')+ 闭合括号。 - 1003:Worker和Tunnel的端口上下文冲突,或WAF拦截。
这条是我上一篇CF Serverless那篇的延续。大模型部署到CF,部署链路本身就是坑。
教训:本地跑模型 ≠ 端到端可用。要把模型API对外提供,部署链路(CF/DNS/WAF)又是一条坑链。
八、真实速度:PP 20.81 tok/s
跑通之后,我在手机上实测的速度:
- Prompt Processing(PP):约20.81 tok/s
- Token Generation(TG):约8-12 tok/s(取决于模型大小)
这是什么概念?
- 手机上跑7B模型,生成速度约8 tok/s。一句话(50 token)要6秒。
- 手机上跑3B模型,生成速度约12 tok/s。一句话要4秒。
结论:手机跑大模型,能跑,但别指望速度。它是”离线可用、隐私安全、零成本”,不是”高速、低延迟”。
九、6个Bug的共同规律
回看这6个Bug:
- 参数污染 → 命令行传参的Unix老规矩
- 响应提取 → 别猜输出格式,dump真实输出
- 正则转义 → 正则的魔鬼在反斜杠
- 伪流式 → 信token间隔,不信”我加了流式”
- DOM爆炸 → O(n²)是前端性能第一坑
- CF 1003 → 部署链路也是坑链
共同规律:每一条都是”你以为能自动处理,但机器没自动处理”的细节。
这不是”写代码”的功课,是”读懂机器脾气”的功课。你越自信”框架应该能处理”,坑就越深。
十、从Bug到Skill:把6个坑沉淀成技能
这6个Bug,我修完之后,没让它们只留在脑子里。我把它们沉淀成了3个Hermes skill:
- hexabench-lite-troubleshooting:修”请求失败或无响应”和流式问题
- hexabench-lite-response-extraction:修响应提取(从llama-simple输出提取纯文本)
- llama-simple-output-processing:llama-simple输出处理与AI响应提取
每个skill里都带着:触发条件、修复步骤、正则表达式、验证命令、常见陷阱。
这就是”折腾”的价值:坑踩过了,沉淀成可复用的知识。下次再跑大模型,不用重新踩。
十一、结语:修大模型框架,是读懂机器脾气
在手机上跑大模型,最浪漫的是”零成本、隐私安全、离线可用”。最真实的,是要过6个关。
每个关,都不是”写代码”能解决的:
- 参数污染,是Unix传参老规矩。
- 响应提取,是别猜、要dump。
- 正则转义,是魔鬼在反斜杠。
- 伪流式,是信间隔不信口号。
- DOM爆炸,是O(n²)第一坑。
- CF 1003,是部署链路也是坑链。
修过这6个Bug之后,你就明白了:跑大模型,不是”拖个模型进去”,是”读懂机器脾气”。
机器脾气读懂了,PP 20.81 tok/s,就够你手机上离线跑一个私人模型了。
作者:小道 · 环境:Termux on Android 13 · 2026-09-27
关联阅读:《用CF搭建Serverless代理:0元搞定外网出口》 · 系列:AI Agent实战笔记(技术线)