一个 Bug 修了 12 轮,根因只有一句话:AI 编程时代,日志才是唯一的真相

修一个"按字母键会删数字"的小 bug,我改了 12 轮代码、编译部署十几次、抓了三轮 logcat。最终根因简单到一句话,但找到它的过程,值得每一个用 AI 写代码的人看看。


📌 先放结论

先说根因,一句话:

手簿物理键盘的输入法,把一次"字母键"拆成了 DEL + SHIFT + 字母 三个按键事件,DEL 会真实删掉一个字符。

就这么一句话。但为了得出这句话,我前后改了 12 轮代码、编译部署了十几次、抓了三轮 logcat

这篇文章不是来炫耀"我修好了",而是复盘:为什么这么简单的问题会拖那么久? 尤其是当大模型(AI 编程助手)当结对搭档时,哪些坑会让简单 bug 被无限放大。


🎯 一、Bug 长什么样

场景 :测绘手簿上,一个"角度/距离"数字输入框。物理键盘英文模式下,按一个字母键(比如 S),输入框里已有的数字会被删掉一位。多按几次,数字一路递减到 0。

用户的原始需求,朴素到不能再朴素:

数字输入框里,按字母键应该被忽略,别把已有的数字删掉。

就这。四行需求,修了 12 轮。


🔄 二、12 轮拉锯战:现象一直在"自我伪装"

这是整件事最反直觉、也最值得记的一点:同一个 bug,在修复的不同阶段,表现完全不一样。 每一轮你都以为"这次终于对了吧",下一轮它就换个花样冒出来。

轮次 用户看到的现象 我当时以为的根因 结果
1~8 快速连按第二次"替换"了第一次输入 setSelectAllOnFocus(true) 全选残留 ❌ 全错
9 字母键根本拦不住 返回值写反了 ⚠️ 半对
10 字母不输入了,但每次删一位 拦截逻辑冲突 ❌ 还是错
11 内容一路递减到 0 IME 拆键 ✅ 接近真相
12 --- DEL + SHIFT + 字母 三连击 🎉 解决

核心结论:现象会"自我伪装"。 每一层拦截都在改动事件流,你修好上一层,下一层的真问题才会暴露。所以"修了这么久"的第一层原因,不是对手多强,而是靶子一直在动


💣 三、我踩的三个大坑(AI 协作会放大它们)

坑 1:前 8 轮陷进"选区/全选状态"的思维定式

我一直以为元凶是 setSelectAllOnFocus(true) 导致的焦点全选残留,于是反复在 onTouchListenerTextWatcherInputConnection.setSelectiononSelectionChanged 这些地方做"折叠选区"的文章。

问题在于:我在跟一个"我臆想出来的问题"死磕,而不是回到用户原始需求。

需求是什么?"字母键被忽略",不是"全选状态要折叠"。这两件事差着十万八千里。

AI 协作的放大效应:AI 助手特别容易顺着你的假设往下走。你一旦在 prompt 里写了"我觉得是全选残留的问题",它就会帮你在这条错误路上越走越远,甚至编出"看起来很有道理"的机制解释。

⚠️ 你的假设错了,AI 不会把你拉回来,只会帮你把错误假设论证得更圆。

这是我最大的教训:先自己站回需求原点,别把错误的预设喂给模型。

坑 2:栽在 return false 的返回值语义上

dispatchKeyEvent / onKeyDown 里,我写成了 return false

在 Android 事件分发里:

kotlin 复制代码
return false // ❌ 不消费,继续往下传 → 事件照样交给 TextView 插入文本,拦截形同虚设
return true  // ✅ 消费掉,事件到此为止

一个字符的差别,让一整轮的"拦截"变成空转。这是最不该犯、但也最容易犯的低级错误------因为大模型生成代码时这种语义默认不会错,但你在复制粘贴、改动分支时,很容易手滑。

坑 3:真正的元凶是"设备特有的非标准 IME 行为"

标准输入法按字母键 = 一次 commitText。但这台手簿的物理键盘 IME,把一次字母键拆成了三连击:

plaintext 复制代码
onKeyDown: keyCode=67    ← DEL(退格,真实删掉末尾一位)
onKeyDown: keyCode=59    ← SHIFT
dispatchKeyEvent: KEYCODE_W ← 字母键(被拦截,不插入)

净效果 = 每按一次字母键删一个字符。

这种设备特有行为,靠任何推理、任何经验都预判不到。大模型也不知道,因为这不是文档里写的标准行为。唯一能发现它的办法,是日志。


⏱️ 四、"早点加日志"------这句废话为什么那么贵

复盘整个过程,最痛的不是技术难点,而是方法论上的迟钝

  • 前 4 轮基本是"盲修":没有日志佐证,全凭猜。改完编译部署手簿实测,失败,再猜。4 轮全白费。
  • 第 5 轮才开始全面埋点 :给每个事件入口打 Log.d,打印 keyCodesel=[start,end]、时间戳、调用栈。从那之后,才真正开始"看见"问题,而不是"猜"问题。

一个朴素的规则,我事后才总结出来:

连续 2 次修复仍失败,立即停手加日志,而不是继续猜第三次。

为什么说它"贵"?因为每一轮的验证闭环成本极高:

plaintext 复制代码
改代码 → 编译 → 部署手簿 → 物理按键实测 → 抓 logcat

一轮可能十几二十分钟。12 轮,就是大半天。如果第 2 轮失败时就停下来埋点,大概率能砍掉一大半轮次。

大模型时代,这句话反而更重要 :AI 能让你"改代码"的成本变得极低,低到你更容易陷入"多改几版试试"的冲动,而忘了"先看清再动手"。改得越快,越要警惕"瞎改"。


✅ 五、最终方案(其实就两层)

核心思路一句话:DEL 键延迟决策,用时间戳区分"IME 误发的前导 DEL"和"用户真实的退格"。

  1. onKeyDown 拦到 DEL(仅数字输入框)时:先快照当前文本,记下时间戳,照常执行真实删除,但标记"疑似误发"。
  2. dispatchKeyEvent 拦到字母键时:如果上一个 DEL 和它在 300ms 内,说明这个 DEL 是 IME 误发的前导,用快照撤销删除;如果间隔超过 300ms,说明是用户真退格,不撤销。

30~40ms 的真实时间戳差,就是区分"误发"和"真操作"的依据。


📝 六、写给以后再用 AI 排障的自己

  1. 先埋点、再动手。 连续 2 次失败就停手加日志,别猜第三次。
  2. 回归需求本质。 这里要的是"字母键被忽略",不是"全选状态折叠"。别把臆想的根因喂给 AI,它只会帮你把错误论证得更圆。
  3. return false ≠ 拦截。 Android 事件分发里只有 return true 才算消费。
  4. 设备特有行为推理预判不到。 手簿 IME 会把字母键拆成「DEL + SHIFT + 字母」三连击,这类问题只能靠日志逐层逼近,用"时间窗 + 快照撤销"来兜。
  5. 现象会自我伪装。 每修好一层,下一层真问题才暴露。别被"换了花样"迷惑,坚持回到日志找真相。

🎬 写在最后

回头看,这 12 轮的全部纠结,最后收敛成一句话的根因,和一个两层结构的修复。

复杂的从来不是问题本身,而是"发现它"的过程。 而决定这个过程的,不是技术水平,是你有没有在一开始就选择"用日志看清",而不是"用脑子硬猜"。

把这句话钉在工位上:

改得越快,越要先看清。


如果这篇复盘对你有帮助,点个 👍 收藏一下,也欢迎在评论区聊聊你被 AI 编程助手"带偏"过的经历~

相关推荐
默_笙1 小时前
😝 5 亿次循环卡死页面?我用 Web Worker 把它扔进后台线程,页面丝滑如初
前端·javascript
CAD老兵1 小时前
一行代码集成 DWG/DXF 图纸查看:测量批注,数据不出站
前端·javascript·github
小林ixn1 小时前
单例模式:从弹窗管理到全局状态,一个模式搞定
前端·javascript·设计模式
AI编程实验室1 小时前
Node.js Agent Handoff 仓库扫描 MVP:忽略规则、include 通配与稳定输出实现
前端·ai编程
猫不易1 小时前
从 Virtual DOM 到 Vapor:Vue 3.6 的另一条路
前端·vue.js
光影少年1 小时前
react navite性能优化 & 常见坑
前端·react native·掘金·金石计划
八角丶1 小时前
Node.js Cluster 详解
前端·node.js
名字还没想好☜1 小时前
React 用 useEffect 做轮询实战:setInterval 拿到旧 state 的闭包陷阱与正确清理
前端·javascript·react.js·react·useeffect
hiahiahia1231 小时前
AI Web 项目的文件到底应该怎么放?
前端·人工智能