Files
kangkang/docs/踩坑与排查记录.md
link2026 b79ae54b7b ```
feat(iOS): 更新MNN后端模型配置优化性能

将MNN主模型从Qwen3.5-4B(~2.64GiB)降级为Qwen3.5-2B(~1.1GiB),因为4B版本
实测运行过慢,影响用户体验。iPhone17+/SME2设备使用2B模型,保留MLX
兜底方案用于模拟器和备用场景,确保AI推理性能和存储效率的平衡。
```
2026-06-09 22:20:07 +08:00

10 KiB
Raw Blame History

康康 · 踩坑与排查记录

本地推理 / SwiftData / 端侧模型这类问题不好复现也不好搜,踩过的坑按统一模板记在这里,方便回查。 新增条目往最上面加(倒序),模板见文末。


2026-06-09 · 生成身体档案报告时,LLM 逐行复读死循环

现象

多轮「身体档案」对话点生成报告后,「## 关键指标」整段陷入死循环:同一行 ⚠️ 收缩压 (107 mmHg) 连续重复几十遍,最后被 maxTokens 截断成半行「⚠️ 收缩」。 (本质是小模型 repetition / degeneration loop,不是数据真有几十条。)

根因(确认)

采样器完全没有重复惩罚,叠加低温 → 几乎必然复读。两个后端都有问题:

后端 位置 原配置 问题
MNN(主) MNNLLMBridge.mm initWithConfigPathset_config temperature 0.3, topP 0.85 penalty
MLX(兜底) LLMSession.swift GenerateParameters temperature 0.3, topP 0.85 repetitionPenalty

关键细节(读 MNN 源码 transformers/llm/engine/src/):

  • llmconfig.hpp:mixed_samplers 默认 {topK, tfs, typical, topP, min_p, temperature} —— 不含 penalty; penalty / ngram_factor 默认 1.0(=全关)。
  • sampler.cpp configMixed:只会把 penalty移到链首(如果存在)」,不会自动插入。 所以光设 "penalty":1.1 没用,必须把 "penalty" 显式写进 mixed_samplers
  • sampler.cpp stepPenalty:repetition_penalty 对 logits 乘法惩罚;n-gram 命中整段重复时惩罚直接升到 max_penalty —— 这正是掐断「整行复读」最有效的开关。

为什么低温反而更糟:temperature 0.3 接近贪心,一旦吐出 收缩压 (107 mmHg)\n, 最高概率的后续就是再吐一遍同样的行,无惩罚就永远出不来。

排查过程(可复用思路)

  1. 看现象先判定是「数据重复」还是「生成复读」—— 被截断成半行 收缩 说明是 token 级复读,不是数据。
  2. grep -niE "penalty|temperature|top_?p|sampler" 康康/AI/ 一把定位两个后端的采样配置 → 都没 penalty。
  3. 不猜 MNN 配置键,直接读构建用的源码 MNN_SRC=/Users/xuhuayong/apps/MNN-srcllmconfig.hpp / sampler.cpp,确认键名、默认值、mixed_samplers 不自动插 penalty。
  4. MLX 侧读 SPM checkout 的 MLXLMCommon/Evaluate.swift,确认 GenerateParametersrepetitionPenalty: Float? + repetitionContextSize: Int

修复

  • MNN MNNLLMBridge.mm:set_config 显式开重复惩罚 + 把 penalty 放进 mixed 链首:
    {
      "jinja":{"context":{"enable_thinking":false}},
      "sampler_type":"mixed",
      "mixed_samplers":["penalty","topK","topP","temperature"],
      "temperature":0.3,"topP":0.85,"topK":40,
      "penalty":1.1,"n_gram":8,"ngram_factor":1.05
    }
    
    (注意:JSON merge-patch 对数组是整体替换,所以这里会覆盖掉默认 mixed_samplers,符合预期。)
  • MLX LLMSession.swift:GenerateParameters(..., repetitionPenalty: 1.1, repetitionContextSize: 64)

取值都偏保守:penalty 1.1 / ngram_factor 1.05 是业界常用档(MNN 自带 omni 默认 1.05), 低温 + 轻惩罚既能掐复读,又不破坏 JSON / 结构化输出的稳定性。

验证

  • xcodebuild ... -destination generic/platform=iOS 编译通过(两个后端均编进)。
  • ⚠️ 真机/模拟器跑一遍多轮导出生成报告,确认不再复读 —— 复读属推理期行为,单测覆盖不到,必须实跑。

预防 / 相关注意

  • 任何新增的「长文本生成」(非 JSON 抽取)都走同一套带惩罚的采样参数,别再裸 temperature。
  • 相关隐患(未修,留观):HealthExportService.retrieveDialogueSnapshot 取指标时 没有 prefix 截断(窗口检索版 retrieve 截了 prefix(20))。指标极多时 prompt 会膨胀、 也更易诱发复读。若复发,优先给 dialogue snapshot 也加上限。

以下几条据 W1W2(2026-05~06)记忆补记,细节以代码/提交为准。

2026-06-09 · MNN 路径 Qwen3.5 强制思考,只吐 <think> / JSON 解析失败

现象

MNN 真机路径上模型自检只显示 <think> 思考过程,AI 辅助拿不到 JSON(解析失败); 同样的 prompt 走 MLX 兜底却正常。

根因

模型自带 config.json(taobao-mnn 预转换件)写死 "jinja":{"context":{"enable_thinking":true}}, Qwen3.5 聊天模板据此每个 assistant 回合硬塞 <think>\n 开思考,吞掉 token 预算。 prompt 里的 /no_think 对 MNN 无效 —— 模板只读 enable_thinking,不看文本软开关。 只在真机爆是因为 MLX 经 swift-transformers 套模板时不传 enable_thinking → 走 else 空 think 块,天然不思考。 (这点从仓库代码看不出来,config.json 是下载/旁路导入的模型产物,不在 git 里。)

修复

MNNLLMBridge.mmcreateLLM 后、load() 前 merge-patch 关闭: set_config("{\"jinja\":{\"context\":{\"enable_thinking\":false}}}")。不改模型文件、不动字节校验。stripThink 保留兜底。

预防

再遇 MNN 只出思考 / JSON 解析失败,先查 config.jsonenable_thinking,别去调 /no_think 或加大预算。


2026-06-07 · 「记录指标·拍照识别」VL 直读化验单不稳 → 改 Vision OCR + LLM

现象

Qwen-VL 直读密集小字化验单经常返回 {"indicators":[]}(读不出指标)。

根因 / 决策

小模型 VL 对密集中文小字不稳。改链路:DocumentScanner 整页扫描 → Apple Vision OCR(zh-Hans/Hant/en) → Qwen3 LLM 解析(VLPrompts.indicatorsFromText)→ stripThink → parseIndicatorsJSON → 确认页人工校对 → 存。 Vision OCR 是系统框架、100% 本地,不违反隐私红线。

预防

这条路不要改回 VL 直读。VL 仍只用于「体检报告归档」整份解读,两者分开。OCR 行分组偶有错位,靠确认页人工校正兜底。


2026-06-01 · git 全量 push 撞 HTTP 413(历史里有 165MB 构建产物)

现象

git push 到 myv0(Gitea 反代有上传体积限制)报 HTTP 413

根因

旧 commit 误把 build/ 构建产物提交进库(最大单文件 xcarchive DWARF 165MB),后来虽 git rm --cached + .gitignore, 但对象仍留在历史 → .git 87MB,全量 push 超反代上限。

修复

对主仓库 git filter-repo --path build/ --invert-paths --force 从全历史剥离 → .git 87M→2.9M,不再 413。 注意:① 重写了所有 commit hash(内容不变),旧克隆需重新 clone;② filter-repo 会移除所有 remote,事后须重新 git remote add origin;③ 凭证不写入 .git/config

预防

build/ 必须在 .gitignore;别把构建产物 / 大二进制提交进库。


2026-05-31 · 快拍 VL 识别时 App 自动退出(jetsam OOM,非崩溃)

现象

iPhone 15 Pro Max 上 VL 识别时 App 直接退出。

根因

不是代码崩溃(catch 只切 warning 屏,Swift 报错不会杀进程),是 OS 内存超限 jetsam kill。三因叠加: ① 无 entitlement(8GB 设备默认单 App 上限 ~3GB,VL ~3GB 常驻冲过);② 从不卸载模型(LLM ~1GB + VL ~3GB 同驻 → 4GB+);③ 没设 MLX cache 上限。

修复

① 新建 康康.entitlementscom.apple.developer.kernel.increased-memory-limit=true; ② AIRuntimeunloadLLM/unloadVL常驻互斥(两大模型永不同时驻留)+ actor 内串行推理闸门(GPU 同一时刻只一个解码/加载); ③ GPU.set(cacheLimit: 256MB),启动调一次。

验证

编译 + 单测通过。⚠️ 真机 OOM 是否真消失仍需 iPhone 15 Pro Max 实测(本机无法跑真机)。


2026-05-30 · 每次重打包 SwiftData 数据被清空

现象

W2 期每次重新打包安装,本地数据全没了。

根因

KangkangApp.swiftModelContainer 创建失败的 catch 块原本直接删 store 文件。 SwiftData 只对纯增量改动自动轻量迁移;一旦 schema 改动超纲(最常见:给已存在 @Model 新增「非可选且无内联默认值」属性)→ 迁移抛错 → 进 catch → 删库。

修复

catch 改为把旧 store(含 -wal/-shm)挪到 Application Support/StoreBackups/<时间戳>/ 再重建,不删除。

预防

给已存在 @Model 加属性一律给可选或内联默认值(如 var x: String = "daily"),才走轻量迁移。正式发布前升级为 VersionedSchema + SchemaMigrationPlan


(无明确日期)· 编辑 Localizable.xcstrings 炸出上万行噪声 diff

现象

Localizable.xcstrings 新增 3 个 key,却产生 ~15000 行 diff。

根因

仓库里该文件是 Xcode 规范格式("key" : { 冒号两侧带空格、2 空格缩进、key 按 Xcode 排序、结尾无换行); 用 python json.dump(indent=2) 重写会把分隔符变成 ": " 且顺序不同 → 几乎每行都 diff。

修复 / 正确做法

基于 HEAD 原始文本做文本插入:把新 key 块按 Xcode 格式( "<key>" : + separators=(',', ' : ') 的 value)拼到 strings 段末尾,保持结尾无换行。不要整文件 json.dump 回写


附:命令行编译方式(排查时拿真实错误/警告)

  • 系统默认是 Command Line Tools,裸 xcodebuild 不可用,需显式指向完整 Xcode: export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
  • 必须用独立 derivedDataPath(如 -derivedDataPath /tmp/kk-derived-xxx),否则和 Xcode 抢同一把 build.db 锁报 database is locked(不是代码错)。
  • 增量编译会吞警告:要看某文件警告先 touch 它强制重编,再 grep error:|warning:|BUILD (SUCCEEDED|FAILED)
  • 工程是 Swift 5 + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;跨到 nonisolated 调 MainActor 成员的隔离警告(标 "error in Swift 6 mode")在 Swift 5 下不阻塞构建。

模板(复制下面这段新增条目)

## YYYY-MM-DD · 一句话标题

### 现象
(用户看到什么 / 怎么触发)

### 根因(确认)
(定位到的真正原因,不是猜测;贴关键文件:行)

### 排查过程
(怎么一步步定位的,方便下次复用思路)

### 修复
(改了什么,贴 diff 要点或配置)

### 验证
(怎么确认修好了;不能单测的要写明需实跑)

### 预防 / 相关注意
(怎么避免再犯;顺带发现的隐患)