在手机上跑大模型: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
2
3
4
5
# 错误写法:prompt作为裸位置参数
cmd = [llama_tool, "-m", model_path, prompt]

# 正确写法:用 -p 参数
cmd = [llama_tool, "-m", model_path, "-p", 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

注意几个关键点:

  1. prompt被重复输出。llama-simple会把它收到的命令行参数原样回显,包括-p后面的prompt。所以你能看到-p 你好,很高兴认识你
  2. 真正的AI响应,藏在main: decoded之前
  3. 统计信息在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
2
3
4
5
# 错误:--[\w-]+ 里的 \w 在字符串里没转义
re.sub(r'--[\w-]+\s+\S+\s*', ' ', text)

# 正确:双反斜杠
re.sub(r'--[\\w\\-]+\\s+\\S+\\s*', ' ', text)

更坑的是嵌套在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:

  1. 参数污染 → 命令行传参的Unix老规矩
  2. 响应提取 → 别猜输出格式,dump真实输出
  3. 正则转义 → 正则的魔鬼在反斜杠
  4. 伪流式 → 信token间隔,不信”我加了流式”
  5. DOM爆炸 → O(n²)是前端性能第一坑
  6. 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实战笔记(技术线)