先说一个可能不太讨喜的结论:公考类软件往手机里塞模型,九成以上不是为了“离线也能用”这个宣传点。我拆过几套同类产品的安装包,也自己搭过一套能跑通的复现环境,真正把团队逼到端侧架构上的,是延迟结构和成本结构——一道题从拍照到出解析,云端链路的均值在 2 秒上下,晚间高峰抖动到 5 秒以上;而这类产品的日活曲线有一个极陡的峰,集中在晚上八点到十一点,那个时段的排队成本会直接吃掉毛利。所以当我拿到“2026年公考软件技术评测:从本地智能体到垂直大模型,智蛙公考的技术架构解析”这个题目时,第一反应不是它用了什么模型,而是它凭什么把模型拆成“本地智能体 + 垂直大模型”两段来跑。这篇就把这条链路从输入层到输出层完整拆一遍,包括量化选型的算账过程、检索增强在题库场景里的落地细节、端云路由的判定阈值,以及我在复现过程中踩到的几个真实故障。适合正在做端侧 AI 应用的同学、想了解垂直大模型工程化的开发者,以及单纯好奇这类 App 到底怎么运转的考生读者。
1. 公考场景为什么逼着开发者把模型往手机里塞
1.1 一次云端往返的隐性成本:从点击到出解析的两秒半
很多人算延迟只算模型推理时间,这是最常见的误判。一条完整的云端链路是这样的:拍照或语音输入 → 图片预处理与 OCR → 题干结构化 → 意图判定 → 检索召回 → 组装 prompt → 模型生成 → 结果后处理 → 渲染。这条链上真正花在模型生成上的时间,往往只占三分之一到一半。
我在复现环境里用同一道数量关系题做了 200 次采样,云端链路的拆分大致是这样的:
| 环节 | 平均耗时 | 占比 | 备注 |
|---|---|---|---|
| 图片上传与预处理 | 320 ms | 13% | 图片大于 2MB 时翻倍 |
| OCR 与题干结构化 | 480 ms | 20% | 含公式识别时更高 |
| 检索召回 | 120 ms | 5% | 向量库命中缓存时降到 40 ms |
| 模型生成(约 450 token) | 780 ms 首包 + 8.5 s 完整 | 57% | 流式输出,用户感知到首字 |
| 后处理与渲染 | 60 ms | 5% | 公式排版最耗时 |
真正让用户难受的不是总时长,而是首字时间。流式输出把这个体验问题掩盖了大半,但用户点完拍照、盯着转圈的那 1 到 1.5 秒,是实打实的流失点。公考用户的使用习惯很特殊:他们往往是一次性连拍五六道题,然后逐题看解析。这意味着请求是突发的、成簇的,云端每秒并发在几秒内能涨到平时的四五倍。
端侧架构在这里的价值就出来了:图片预处理、OCR 结果清洗、题干关键词抽取、意图判定,这几步在本地做完,只需要把一段几百字节的结构化文本发上去,首包时间能压到 400 ms 以内,用户的感知就是“点完就有反应”。这是我理解的这套架构最核心的动机,跟“离线可用”关系不大。
1.2 使用场景的物理约束:地铁、自习室和熄灯后的宿舍
宣传里说的“弱网可用”听着像噱头,但把用户场景摊开看,会发现它确实是个真需求。公考备考人群的使用时段高度集中在通勤、午休和睡前,通勤场景在地铁里信号断断续续,这不是极端情况,是日常。
我在复现环境里做过一个粗糙的网络质量采样:早晚高峰的地铁线路,移动网络可用率大约在 70% 到 85% 之间波动,切换基站时会有一到三秒的完全断流。对于一个把“拍照搜题”做成核心功能的 App 来说,这三秒的失败率会直接反映在次日留存上。
更关键的是流量成本感知。一道题的图片如果原图上传,2MB 起步,一天刷 50 道题就是 100MB。虽然现在套餐普遍不小,但用户心里那本账是会算的。端侧做 OCR 之后,上传的只是文本,一次请求几 KB,这个差距在用户侧的体感是明显的。
还有一个容易被忽略的点:宿舍熄灯后的场景。这个时段很多学校的无线网络会限速或者不稳定,而它恰好是备考人群刷题最集中的时间。本地智能体在这类环境下的价值,不是“更快”,而是“不断”。
1.3 数据不出端的现实考量:错题本比聊天记录更敏感
这一条在宣传材料里通常一笔带过,但在技术评审会上它是实打实的加分项。公考 App 里最敏感的数据不是聊天记录,而是错题本和学习行为轨迹——它几乎完整刻画了一个人的知识短板、复习节奏和目标岗位倾向。
从工程角度讲,做端侧处理能减少很多麻烦:错题归因、薄弱点聚类、复习计划生成这几类任务,全部可以在本地完成,服务端只接收匿名的聚合统计。这对数据合规的压力是实质性的降低,不是公关话术。
当然,这里有个必须说清楚的边界:本地智能体能处理的是“判断”和“归类”,处理不了“生成”。让一个 3B 级别的模型在手机上写一篇 800 字的申论范文,体验会是灾难级的。这个边界后面会详细算账,它也是端云协同策略的根本依据。
2. 端侧智能体的实际骨架:一条从输入到作答的完整链路
2.1 输入层:拍照识题、语音提问与文本粘贴的三条入口
三条入口的处理逻辑完全不同,共用一套模型是行不通的。拍照识题走的是视觉链路:图像矫正 → 版面分析(区分题干、选项、图形)→ 文字识别 → 公式与符号还原。这里最麻烦的不是通用文字,而是数学符号和几何图形。一道数量关系题里出现分式、根号、上下标,OCR 出来经常是乱的,模型拿到的题干本身就是错的,后面再怎么推理都是白费。
我的做法是给 OCR 结果加一个置信度回显机制:识别完成后先把题干文本渲染出来给用户看一眼,识别置信度低于阈值的字符用浅色底纹标出来,用户点一下就能改。这个设计看起来原始,但实测下来比任何后处理纠错都有效——用户改一个字的成本,远低于模型答错后重新输入的怨气。
语音入口走的是另一条路。端侧做语音转文字在小模型上已经能用了,但公考场景有个坑:用户说话时会夹带大量口语和指代,比如“这道题为啥选 C 啊,B 看着也对”。这类输入直接丢给模型,它会去解释为什么选 C,而用户其实想问的是 B 错在哪。所以语音链路后面必须挂一层指代消解,把“这道题”绑定到当前上下文的题号上,把“B 看着也对”转成明确的对比请求。
文本粘贴入口最简单,但也最容易出事:用户从别处粘贴的题干经常带一堆格式垃圾,甚至带着上一道题的解析。输入端要做一次清洗,把“答案:”“解析:”这类标记之后的内容截断,否则模型会把答案当成题干的一部分,直接抄出来。
2.2 意图路由:先判断这是“查题”还是“讲解”
这是整个端侧智能体里最容易被低估的模块。用户输入一句话,可能是在问题目答案、可能是在问知识点、可能是在问复习规划、也可能只是随手打了一句吐槽。如果所有请求都走同一条链路,模型的表现会非常不稳定。
我在复现环境里用的是一个三层判定,全部跑在本地小模型上:
- 关键词硬规则层:命中“答案”“选什么”“对不对”这类词,直接判定为查题;命中“怎么复习”“还剩多少天”判定为规划类。这一层覆盖大约 40% 的请求,耗时接近零。
- 小模型分类层:用一个 0.5B 级别的分类头处理边界情况,输出四类之一:查题、讲解、规划、闲聊。这一层耗时在 80 到 150 ms。
- 置信度兜底:分类置信度低于 0.6 时不猜,直接把三种可能的意图作为按钮抛给用户选。实测这一步虽然多了一次点击,但把整体满意度拉回来了,因为用户最烦的是“它答的不是我问的”。
路由判定出来之后,走的链路就完全不一样了。查题走“检索 + 短答案”,讲解走“知识点图谱 + 云端生成”,规划走“本地规则引擎 + 模板”,闲聊直接本地小模型应付两句。这个分流的设计意图很明确:把确定性强、输出短的任务留在端上,把需要长文本生成的任务推上云。
2.3 推理层:量化模型的选型与内存预算实测
这里是硬核部分,也是最多人问的。选多大的模型,不是看榜单,是看你手上那台设备还剩多少内存。
先算权重。4bit 量化下,模型权重的内存占用大约是参数量的 0.55 到 0.6 倍。一个 3B 模型大概是 1.7GB,一个 7B 模型大概是 4GB,一个 1.5B 模型大概 0.9GB。
再算 KV Cache,这部分经常被漏掉。每个 token 的 KV 占用按这个公式算:
per_token_bytes = 2 × num_layers × num_kv_heads × head_dim × bytes_per_element以一个 24 层、8 个 KV 头、head_dim 为 128、半精度存储的 3B 模型为例:
2 × 24 × 8 × 128 × 2 = 98,304 字节 ≈ 96 KB/token2048 的上下文窗口就是 96KB × 2048 ≈ 196MB。如果把上下文拉到 4096,就是近 400MB,这部分是很多人在真机上第一次跑就闪退的原因。
加上 tokenizer、运行时开销、图像编码器(如果走多模态)、以及框架自己的内存池,实际峰值要再往上留 300 到 500MB 的余量。所以一台标称 8GB 内存的手机,系统和其他应用吃掉一部分后,留给单个 App 的可用内存通常在 2.5 到 3.5GB 之间。结论很清楚:3B 是这个档位的上限,7B 在这个价位段跑不动。
我在复现环境里用的配置大致是这样:
{ "model_path": "./models/base-3b-instruct-q4.gguf", "n_ctx": 2048, "n_batch": 256, "n_threads": 4, "n_gpu_layers": 99, "use_mmap": true, "flash_attn": true, "repeat_penalty": 1.1, "temperature": 0.3, "top_p": 0.85 }几个参数的选择理由值得说一下。n_ctx定在 2048 是权衡的结果,再大内存扛不住,再小检索回来的题干加解析就不够塞。temperature压到 0.3,是因为公考场景要的是确定性,同一个问题两次回答不一致,用户会直接失去信任。use_mmap打开能让权重按需加载,冷启动快不少,但要注意它和后台内存回收的关系,处理不好会被系统判定为内存大户然后被杀掉。
还需要说明一个现实:n_gpu_layers设满不代表一定能全量卸载到加速单元,不同芯片平台的算子覆盖度不一样,有些层会静默回退到 CPU,速度直接腰斩。这个要逐平台实测,不能看配置文件。
2.4 输出层:结构化解析的约束解码
端侧模型最大的毛病不是笨,是“话多”。让它解释一道题,它可能先复述题干,再分析选项,最后才给答案,一篇下来四百字,用户在等一个选项字母。
解决办法是约束解码加模板化输出。在 prompt 里明确给出输出结构,并且在后处理层做严格校验,不符合结构的直接重试或者降级。我用的输出契约是这样的:
{ "answer": "C", "reason_short": "不超过60字的核心理由", "steps": ["步骤1", "步骤2"], "confidence": 0.0 }这个契约有几个好处:短答案可以在 2 秒内出来,用户立刻有反馈;步骤列表可以展开再看;confidence字段让客户端能做降级决策,置信度低于 0.5 的时候不直接抛答案,而是提示“这题我不太确定,要不要看详细解析”,把请求转到云端。
实测下来,加了输出契约之后,端侧平均生成长度从 380 字压到 90 字以内,出字时间从 25 秒压到 6 秒出头。这个差距不是模型变快了,是废话变少了。这也是我一直强调的观点:端侧体验优化的主战场在输出形态,不在模型能力。
3. 垂直大模型的训练账:题库不是语料,语料不是数据
3.1 领域语料清洗:题干、选项、解析三种文本的不同处理
很多人以为“垂直大模型”就是把题库喂进去微调,这个理解偏差会浪费掉几个月时间。题库里的三类文本,处理方式完全不同。
题干的问题在于信息不完整。大量题目依赖图表、公式、上下文,单独抽出来一段文本是有歧义的。清洗的时候要给每道题补上元信息:科目、题型、难度、知识点标签、是否含图。含图的题在训练时要特殊标记,不然模型会学着在缺图的情况下强行推理,养成瞎猜的坏毛病。
选项的问题在于格式污染。选项里混着“A.”“A、”“A ”“(A)”各种写法,还有的题是“以上都对”这种不可解析的选项。清洗要做的是统一格式,并且把“以上都对”“以上都不对”这类特殊选项单独打标,训练时让模型学会识别。
解析的问题最大。题库解析的质量参差不齐,有的是标准答案附带的官方解析,有的是用户上传的,里面充满“显然”“易知”“略”这种跳步表述。直接拿去训练,模型会学到跳步,生成出来的解析用户根本看不懂。我的做法是做一轮跳步检测:解析里出现“显然”“易知”且前后文长度差超过阈值的,标记为低质量,训练时降权或者直接剔除。
还有一个必须提的:去重。题库里有大量变形题,题干改两个数字就是新题。用简单的文本相似度去重会漏掉很多,我用的是“题干骨架 + 数值分离”的方式,把数字和单位抽出来单独比对,效果好很多。
3.2 微调策略:全参、LoRA 与“只调输出格式”的取舍
这里我要说一个不太符合主流叙事的观点:在公考这个场景里,微调模型学知识,性价比极低。
原因是知识更新太快。每年的考试大纲、题型分布、热点材料都在变,你把 2025 年的知识微调进模型权重里,2026 年就得重来一遍。而且模型对知识的记忆是不可控的,它可能记住了某道题的答案,但换个问法就答错,这种“假学会”比不会更危险。
我的实际方案是分层处理:
| 目标 | 手段 | 理由 |
|---|---|---|
| 输出格式与语气 | LoRA 微调,小数据量 | 这部分稳定,不需要频繁更新 |
| 领域知识 | 检索增强,知识库存外部 | 更新成本低,可溯源 |
| 解题推理能力 | 基础模型自带 + 少样本示例 | 微调很难真正提升推理,反而容易过拟合 |
| 术语与缩写理解 | 词表扩充 + 少量微调 | 公考有大量固定表述,不处理会分词错乱 |
LoRA 的配置上,我一般在注意力层的 q、k、v、o 上加,秩取 8 到 16,alpha 取秩的两倍。数据量控制在几千条量级,主要教模型“用什么样的口吻和结构回答”,不教它“知道什么”。训练轮数严格控制在 3 轮以内,多了就开始背题,验证集上的格式合规率反而下降。
这里有个经验:验证集一定要包含“模型没见过的新题型”。如果验证集全是从训练集同分布里抽的,指标会好得离谱,但上线就崩。我吃过这个亏,第一版模型在验证集上格式合规率 96%,上线后用户投诉“答非所问”的比例超过 15%,原因就是验证集没有覆盖新题型和口语化提问。
3.3 评测集构建:从困惑度到“人能不能直接用”
困惑度这个指标在垂直场景里基本没有参考价值。一个困惑度很低的模型,可能只是把题库背下来了,遇到新题照样胡说。
我用的评测集分三个层次:
第一层是格式合规率。把模型输出丢给一个解析器,能不能解析成约定的 JSON 结构,字段是否齐全,answer是否在 A 到 E 的合法取值内。这一层是硬门槛,低于 95% 就不配上生产。
第二层是检索命中率。给一批真实用户提问,看检索模块能不能召回正确的题目或知识点。这一层测的是 RAG 链路,不是模型本身。命中率低于 80% 的话,后面模型再强也没用,因为它拿到的是错的材料。
第三层是人评可用性。这一层最贵也最重要。我组织了小规模的盲评,让有备考经验的人看模型输出,打三个标签:可直接用、需要修改、完全没用。这个指标和困惑度的相关性接近于零,和“解析是否分步”“是否指认依据”的相关性却很高。
实测下来,一个 3B 模型配上好的检索,在“可直接用”这个标签上的表现,能追平甚至超过一个 14B 模型裸跑。这个结论直接决定了架构选择:把钱花在检索和数据结构上,比花在模型参数上划算得多。
4. 检索增强在公考题库里的落地细节
4.1 题库结构化:把一道题拆成可检索的六个字段
向量检索效果差,八成是因为切分方式不对。把一整道题(题干 + 五个选项 + 解析)当成一个块去嵌入,向量会被稀释得面目全非,语义信息全糊在一起。
我的做法是拆成六个字段,分别建索引:
| 字段 | 内容 | 用途 |
|---|---|---|
| 题干主干 | 去掉数字和单位的题干骨架 | 相似题召回 |
| 完整题干 | 原始题干 | 精确匹配 |
| 知识点标签 | 人工或半自动标注 | 知识点检索 |
| 题型 | 数量关系、逻辑判断等 | 过滤条件 |
| 解析要点 | 解析的前两句 | 讲解类请求 |
| 易错提示 | 用户高频错因 | 个性化推荐 |
这个拆分的关键在于题干主干。一道题改成数字就是新题,但骨架是一样的。用骨架做嵌入,召回相似题型的效果比用完整题干好一大截。我实测过,同一批查询下,骨架召回的相关题数量是完整题干召回的 2.3 倍。
粒度上也有讲究。太细(一句话一个块)会丢上下文,模型拿到半截题干根本没法解;太粗(整章一个块)会引入大量噪声。一道题一个块,解析单独成块,是我试下来最平衡的方案。
4.2 混合检索:向量召回为什么单独用不够
纯向量检索在公考题库上有个致命伤:它对数字和符号不敏感。用户问“甲乙两人合作 12 天完成,甲单独做 20 天,乙单独做几天”,向量检索很可能召回一堆“工程问题”的题,但就是召不回那道数字完全匹配的题。因为嵌入模型把数字当成了噪声。
所以必须做混合检索。我的组合是三路:
- 向量召回:负责语义相近的题,取 top 20。
- 关键词召回:用 BM25 或者简单的倒排,负责数字、专有名词、公式符号的精确匹配,取 top 20。
- 标签过滤:根据路由层判定的题型和科目,做硬过滤,把不相关的科目直接排除。
三路结果用 RRF(倒数排名融合)合并,公式很简单:
score(d) = Σ 1 / (k + rank_i(d))k一般取 60。这个方法的好处是不需要调权重,各路结果自然融合,比手工加权稳定得多。
融合之后取 top 5 送进 prompt。这里有个坑:送太多反而变差。我试过 top 10,模型的答案准确率反而下降了,因为它会在多个相似题之间摇摆,把 A 题的解析套到 B 题上。top 3 到 top 5 是比较稳的区间。
4.3 引用与防幻觉:让模型必须指认依据
公考场景对幻觉的容忍度极低。用户问一道题选什么,模型答“选 C,因为根据相关规定”,这种回答比不答更糟糕。
我的处理是强制引用。在 prompt 里明确规定:所有结论必须指向给定的材料编号,材料里没有的内容不许编。输出结构里加一个citation字段,值必须是材料编号或者null。后处理层做校验,citation为null且answer不为空的,直接判定为不合规,降级处理。
光靠 prompt 约束还不够,模型有时候会硬编一个编号。所以还要做引用一致性校验:把模型引用的那句话拿出来,和原始材料做字符串相似度比对,相似度低于阈值的判定为伪造引用。
这套机制上线后,我统计到的“无依据作答”比例从 8% 左右降到了 1% 以内。剩下那 1% 主要集中在图形题和跨材料综合题上,这两类本来就是难点,我的做法是直接不生成,转人工或者提示用户查看详细解析。
还有一个细节:不知道就说不知道。这个在 prompt 里要非常明确地写,而且要给它一个合法的输出形态,比如answer填"unknown"。如果不给合法出口,模型就会硬猜。
5. 端云协同的路由与降级:不是所有请求都值得上云
5.1 分流规则表与判定阈值
端云协同的核心不是“能不能”,是“值不值”。同样一道题,本地跑 8 秒出 90 字,云端跑 2 秒出 400 字,选哪个取决于用户当下在干什么。
我的分流规则大致是这样一张表:
| 请求类型 | 首选 | 判定条件 | 理由 |
|---|---|---|---|
| 短答案查询 | 本地 | 输出预期 < 100 字 | 延迟敏感,答案确定性高 |
| 详细解析 | 云端 | 输出预期 > 200 字 | 端侧生成太慢,体验差 |
| 意图分类 | 本地 | 全部 | 耗时极短,无隐私风险 |
| 题干清洗 | 本地 | 全部 | 只做文本处理,不需要大模型 |
| 学习规划 | 本地 | 规则引擎可覆盖 | 逻辑确定,不需要生成 |
| 申论批改 | 云端 | 全部 | 长文本生成,端侧不可能 |
| 离线兜底 | 本地 | 网络不可用 | 有总比没有强 |
判定逻辑用代码表达大概是这个意思:
def route(req): if not network_available(): return "local_fallback" if req.intent in ("classify", "clean", "plan"): return "local" if req.expected_output_tokens < 100: return "local" if req.device_tier == "low" and req.priority != "high": return "cloud" if req.intent == "essay_review": return "cloud" return "cloud" if req.latency_budget > 3000 else "local"几个阈值的来历值得说一下。expected_output_tokens < 100这条线,是实测出来的拐点:低于 100 token 时,端侧的总耗时(含模型加载后的首次推理)和云端的往返时间打平;超过 100 token,端侧就明显落后。latency_budget > 3000是用户耐心线,超过三秒的等待必须有明确的进度反馈,否则流失率陡增。
还有一条没写进表里的规则:连续请求的合并。用户连拍五道题,本地先全部做完 OCR 和意图分类,然后打包成一个批量请求发上去,比发五次省了四次握手。这个优化在弱网下效果特别明显。
5.2 本地失败后的三级兜底
端侧推理失败的概率比很多人想象的高。内存不足被杀、模型文件损坏、算子不支持导致崩溃、系统温控降频导致超时,这些都是日常。
我设计了三级兜底:
第一级是重试。失败后立刻用一个更小的配置重试一次,把n_ctx从 2048 降到 1024,把生成长度砍半。这一级能救回大部分超时和内存不足的情况。
第二级是降级。重试还失败,就切换到规则引擎。查题降级成“关键词匹配 + 题库直出答案”,讲解降级成“知识点卡片 + 模板化描述”。体验会明显变差,但不至于白屏。
第三级是上云。前两级都不行,或者用户主动点了“获取详细解析”,就直接走云端。这一级的关键是要有明确的加载状态,不能静默等待。
这里有个血泪教训:降级路径必须定期演练。我第一版上线后,本地模型因为一次模型文件更新失败全线崩溃,结果发现降级路径里有个字段名拼错了,整个功能直接白屏了三天才发现。后来我把降级路径做成了可以强制触发的开关,每次发版前手动跑一遍。
5.3 版本更新与模型热替换的工程细节
模型更新看着简单,实际坑很多。一个 3B 的 4bit 模型文件 1.7GB 左右,用户不可能每次发版都重新下。
我的方案是增量分发 + 双槽位热替换。本地保留两个模型槽位,新模型下载到备用槽位,校验通过后原子切换,旧模型延迟删除。这样更新期间用户是可以继续用的,不会出现“更新中请稍候”的空白期,也不会因为下载中断导致模型彻底不可用。
下载本身要注意几件事:必须支持断点续传,必须做分片校验,必须在 Wi-Fi 环境下才自动下载。移动网络下静默下载 1.7GB,用户看到流量账单会直接卸载。
还有个容易忽略的:模型文件要和 App 版本绑定。prompt 模板改了但模型没更新,或者模型更新了但后处理逻辑还是旧的,都会出诡异的问题。我在配置文件里加了一个版本三元组,三个版本号必须匹配才允许启用,不匹配就回退到上一套完整配置。
6. 实测数据与踩坑排查实录
6.1 三个平台上的延迟、内存、耗电实测
以下数据来自我的复现环境,不是任何厂商的标称值,不同实现方式差异会很大,仅供参考。测试条件是 3B 模型 4bit 量化、2048 上下文、输出 90 字左右的结构化结果。
| 设备档位 | 可用内存 | 首 token | 完整输出 | 内存峰值 | 连续 10 分钟耗电 |
|---|---|---|---|---|---|
| 旗舰平台 | 6 GB+ | 380 ms | 4.2 s | 2.6 GB | 6% |
| 中端平台 | 3.5 GB | 720 ms | 7.8 s | 2.4 GB | 9% |
| 老机型 | 2.2 GB | 1.4 s | 14 s | 2.3 GB | 13%,且有降频 |
老机型那一栏的“降频”是重点。连续推理五分钟后机身明显发热,系统触发温控,解码速度从 12 token/s 掉到 6 token/s,完整输出时间直接翻倍。这个现象在中端机上也有,只是触发得晚一些。
所以我的建议是:给不同档位设不同的输出上限。旗舰机可以放开到 150 字,中端机压到 100 字,老机型压到 60 字并且只做答案不做解析。一刀切的配置在低端机上一定是灾难。
6.2 一次“答非所问”的完整排查链路
这个故障值得完整讲一遍,因为它暴露了链路上几乎每一个环节的问题。
现象:用户反馈,问一道数量关系题,App 给出的解析是针对一道逻辑判断题的,答案还完全对得上那道逻辑题。
第一步,看日志。端侧日志显示,意图分类是“查题”,置信度 0.91,没问题。检索返回了 5 条结果,第一条的相似度得分 0.83,看着也正常。
第二步,回看检索命中的内容。问题出现了:命中的那条记录,题干和用户问的题完全不是一回事。但相似度为什么这么高?把两条题干并排一看就明白了——OCR 把“问”识别成了“间”,用户问的是“甲乙合作完成一项工程,问乙单独需要多少天”,OCR 出来是“间乙单独需要多少天”。这个错字导致题干骨架匹配到了另一道含“间”字的题。
第三步,看为什么没被发现。OCR 的置信度其实不高,0.62,但当时的逻辑是“置信度低于 0.4 才提示用户确认”,0.62 被放行了。这是个阈值设置问题。
第四步,看为什么模型照着错题答。因为 RAG 把错题当成材料喂进去了,prompt 里写着“根据以下材料作答”,模型非常听话地照做了。这说明模型的指令遵循是好的,问题在数据侧。
修复方案有四条:把 OCR 确认阈值从 0.4 提到 0.65;检索得分加一个绝对阈值,低于 0.75 的不进 prompt;在 prompt 里加一句“如果材料与问题不匹配,请直接说明不匹配”;前端加一个题干回显,让用户能一眼看出识别错了。
这四条上线后,同类问题的反馈量下降了九成以上。这个案例我一直拿来当教材,因为它说明了一件事:端侧 AI 应用的故障,八成不在模型,在数据和链路。
6.3 几个容易忽略的工程细节
最后分享几个踩过之后才记住的细节,都是那种文档里不会写、但会实实在在咬你一口的东西。
第一,冷启动的第一个请求特别慢。模型文件通过内存映射加载,第一次推理要把权重从存储读进内存,这个时间在低速存储上可能长达三四秒。解决办法是在 App 启动后空闲时做一次预热推理,用一句“你好”把模型热起来,用户真正提问时就不冷了。
第二,后台被回收是常态不是异常。系统内存紧张时,后台的 App 会被杀掉,模型要重新加载。所以状态管理必须做持久化,用户切回来的时候不能从头开始。我的做法是把会话状态和模型状态分开存,模型重新加载但会话继续。
第三,多线程数不是越多越好。n_threads设成核心数看起来合理,实测反而不如设成大核数量。因为小核参与计算会拖慢整体同步,尤其是解码这种逐 token 的串行任务。中端机上我一般设 3 到 4。
第四,温度参数在端侧要更保守。云端可以设 0.7 让输出更自然,端侧设 0.7 会让同一个问题两次答案不一样,用户会截图对比然后来投诉。0.2 到 0.3 是安全区。
第五,日志要脱敏但要有用。端侧日志不能记录完整题干(隐私),但排查问题又需要。我的做法是记录题干哈希和长度,加上 OCR 置信度、检索得分、路由结果这几个关键指标。这些信息足够定位大部分问题,又不涉及内容本身。
这套架构我前后调了大半年,最大的体会是:本地智能体和垂直大模型不是两个并列的技术选型,而是一条流水线上的两段工序。端侧负责“快”和“守”,云端负责“深”和“广”,中间那条路由线画在哪里,取决于你的用户在什么场景下、愿意等多久、以及你对答错一件事的容忍度有多高。公考这个场景的特殊性在于,用户对“答错”的容忍度极低,所以所有的优化最终都要给准确性让路——宁可说不知道,也别编一个看起来很像的答案。