01 / THE SIGNAL

我们发现了什么

我的 AI 智能体曾经…… 标题:Smash Stories:那个低语了两周、我才听见的 Bug URL 来源:https://dev.to/okeke_chukwudubem_5f3bf49/smash-stories-the-bug-that-whispered-for-two-weeks-before-i-heard-it-405n 发布时间:2026-07-18T18:51:38Z Markdown 内容:_这是由 Sentry (https://sentry.io/

  • 来源:DEV Community(发现于 2026-07-19)
  • 证据等级:D · 发现产品或需求信号,暂未获得可核验的商业证据。
  • 商业模式:待核验
  • 主题:独立产品
  • 初筛评分:14.5/100 · 收录 1 次
#独立开发#待验证#产品发现
02 / SOURCE & EVIDENCE

证据,比故事更重要。

发现产品或需求信号,暂未获得可核验的商业证据。

规则清洗与初筛,未经人工商业核验。原文语境、实际客户和付费情况仍需自行验证。

引用与数字披露

来源类型(原作者自述/第三方测算/媒体转引)需采集端标注,本版尚未落字段。

短句引用
作者
未标注
抓取日期
来源类型
未标注
数字口径
币种
未标注
口径
未标注
披露主体
未标注
披露日期
未标注

中文辅助译文(全文)

这是 DEV 主办的 Summer Bug Smash(由 Sentry 赞助的 Smash Stories)的投稿。

我的 AI Agent 读取金融数据时,每 5 次就有 1 次出错。没有崩溃,没有异常,只有安静又危险的错误数字。

有些 Bug 会大声尖叫——空指针异常、段错误、让屏幕像圣诞树一样亮起来的崩溃日志。

但这个 Bug 是低语的。

背景

我正在用 Gemma 4 构建一个可以控制 Android 手机的 AI Agent。自然语言指令被解析为结构化的 JSON,转换成 ADB 指令,并在设备上执行。没有云端,没有 API Key,只有一台听你指挥的手机。

到第 16 天时,我迎来了一个里程碑:Agent 完成了它的第一个多应用工作流——"复制我的银行余额,然后通过 WhatsApp 发给妈妈"。

三个应用:银行 → WhatsApp,七个步骤,成功。

我庆祝了一下,把代码发了出去,发布了构建日志,然后带着工程师的满足感入睡了。

第二天早上,我又测了一遍。再测一遍。又测一遍。

低语

在 50 次测试中,余额有 10 次是错的。错误率百分之二十。

Agent 并没有崩溃,也没有抛出异常。日志显示成功。但 "₦15,000" 悄悄变成了 "₦15.000"、"₦15,00" 或 "₦15000"。货币符号被认成数字,逗号被解析为小数点,末尾的零也被丢掉了。

错误的数字就这样发给了妈妈。而我整整两周毫不知情,因为没有任何失败大到足以引起我的注意。

根本原因

我把问题追溯到了一个错误的假设:我一直把 OCR 输出当作真实数据。

在我做的无障碍审查中,银行 App 的得分是 F:UI 标签为零。Agent 只能完全依赖光学字符识别(OCR)来读取屏幕上的任何内容。而银行 App 对 OCR 来说是非常恶劣的环境: · 为密集数据设计的紧凑字体,而不是为了可读性 · 灰底配更深一点点的灰字 · 货币符号(₦)经常被 OCR 模型误认为数字 · 大数字中的逗号有时会被 OCR 解析为小数点

一次截图,一次 OCR,把一个数字存进内存。没有校验,没有二次检查,没有合理性检查。我构建了一条管道,把金融信息托付给了一个单一且不可靠的数据源。

修复方法:纵深防御

我没有尝试让 OCR 变得更好——Tesseract 和 ML Kit 就是它们那个水平。取而代之,我构建了一个三层验证系统,把每一个金融数字都视为未经验证,直到证明它确实无误。

第一层:格式校验

一个数字必须匹配有效的货币格式才会被接受。正则会检查货币符号(₦、$、£、€)后面是否跟着正确格式的数字。任何不匹配的都会被立即拒绝。

`python pattern = r'[₦$£€]?\s?\d{1,3}(?:,\d{3})*(?:\.\d{2})?' matches = re.findall(pattern, extracted_text) if not matches: return False, "", "No currency values found"

光是这一步就能捕获大约一半的错误——乱码文本、部分读取以及 OCR 有时返回的非数字数据。

第二层:双读确认

Agent 会抓取两张独立的截图,并对两者分别运行 OCR。如果数字一致,就以高置信度接受;如果不一致,就再抓一张截图,三选二。 Read twice text1 = extracttext(screenshot1) text2 = extracttext(screenshot2)

num1 = extractnumber(text1) num2 = extractnumber(text2)

if num1 == num2: return True, num1, "High confidence" Tiebreaker text3 = extracttext(screenshot3) num3 = extractnumber(text3)

if num1 == num3: return True, num1, "Medium confidence (2/3)" elif num2 == num3: return True, num2, "Medium confidence (2/3)"

这能捕获偶发的 OCR 错误——截图之间像素级的差异会导致不同的读数。代价是每次金融读取多花 4 到 6 秒,换来的是能捕获那些原本会悄悄污染数据的错误。

第三层:范围校验

即使一个数字格式正确且经过双重确认,仍然可能是无意义的。余额为 "₦0" 可能意味着账户是空的,也可能意味着 OCR 完全失败、什么也没读到。余额为 "₦999,999,999,999" 几乎可以肯定是错误。

if 1 <= value <= 1000000_000: return True else: return False # Suspicious. Re-read.

这能捕获那些通过了格式校验和双读校验,但在上下文中明显错误的边缘情况。

庆祝时刻

实现这三层之后,我重新跑了那 50 次测试。错误率从 20% 降到了 6%。在特定的 "₦15,000" 测试用例中,借助双读确认,20 次读取全部正确。

现在,Agent 在多应用金融任务中,20 次能成功 19 次。仅有的那一次失败是超时(旧设备上银行 App 加载太慢),而不是读数错误。更关键的是,当验证失败时,Agent 现在能意识到自己失败了:它会报告低置信度并中止任务,而不是悄悄地把错误数据发出去。

这件事教会我的

Bug 不在 OCR 引擎里,而在我的架构里。我构建了一个把金融信息托付给单一、不可靠数据源的系统。这不是 Tesseract 的问题,而是设计上的问题。

修复方案不是更好的模型,而是更好的流程。三个便宜又快速的检查加在一起,建立起任何单一检查都无法建立的信任。这就是应用于数据完整性的纵深防御。

这改变了我构建每一个功能时的思维方式。我现在会问:"如果这个组件悄悄返回了错误数据,我能知道吗?如果不能,校验在哪?"

代码

完整的验证系统已在 GitHub 上开源:

👉 github.com/Dexter2344/phone-agent

vision.py 中包含 verifyfinancialdata() 函数。agent.py 在把任何金融值存入任务内存之前都会调用它。

构建日志

我已经在 Dev.to 上公开记录这个项目 17 天了。每一个突破,每一个死胡同,每一次凌晨一点的"这玩意儿怎么又坏了"的调试。

这个 Bug 是第 17 天出现的——一个几乎被发布出去的 Bug,一个会低语的 Bug。

它不是我抓到的最响亮的 Bug,却是最让我受益的那一个。

这是我参加 Dev.to Smash Stories 比赛的投稿。Agent 的指令解析和推理由 Gemma 4 模型驱动,验证层则由我独立完成。

译文由上游机器翻译生成,可能有误;判断请以英文原文为准。

英文原文(来源本站未改写)

This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.

My AI agent was misreading financial data 1 in 5 times. No crashes. No exceptions. Just quiet, dangerous wrong numbers.

Some bugs scream. Null pointer exceptions. Segmentation faults. Crash logs that light up your screen like a Christmas tree.

This one whispered.

The Setup

I'm building an AI agent that controls Android phones using Gemma 4. Natural language commands get parsed into structured JSON, translated into ADB instructions, and executed on-device. No cloud. No API keys. Just a phone that does what you tell it.

By Day 16, I'd hit a milestone: the agent completed its first multi-app workflow. "Copy my bank balance and send it to Mom on WhatsApp."

Three apps. Banking → WhatsApp. Seven steps. Success.

I celebrated. Shipped the code. Published the build log. Went to sleep feeling like an engineer.

Then I tested it again the next morning. And again. And again.

The Whisper

Out of 50 test runs, the balance was wrong 10 times. Twenty percent.

The agent wasn't crashing. No exceptions were thrown. The logs showed success. But "₦15,000" was quietly becoming "₦15.000" or "₦15,00" or "₦15000." A currency symbol mistaken for a digit. A comma parsed as a decimal. A zero dropped from the end.

The wrong number was being sent to Mom. And I had no idea for two weeks because nothing was failing loudly enough to make me look.

The Root Cause

I traced the problem to a single bad assumption: I was treating OCR output as ground truth.

Banking apps scored F on my accessibility audit. Zero UI labels. The agent had to rely entirely on optical character recognition to read anything on screen. And banking apps are a hostile environment for OCR: · Condensed fonts designed for dense data, not readability · Grey text on slightly-darker-grey backgrounds · Currency symbols (₦) that OCR models frequently confuse with digits · Commas in large numbers that OCR sometimes parses as decimal points

One screenshot. One OCR pass. One number stored in memory. No verification. No double-check. No sanity check. I had built a pipeline that trusted a single, unreliable data source with financial information.

The Fix: Defense in Depth

I didn't try to make the OCR better. Tesseract and ML Kit are what they are. Instead, I built a three-layer verification system that treats every financial number as unverified until proven otherwise.

Layer 1: Format Validation

Before a number is accepted, it must match a valid currency pattern. The regex checks for currency symbols (₦, $, £, €) followed by properly formatted digits. Anything that doesn't match gets rejected instantly.

`python pattern = r'[₦$£€]?\s?\d{1,3}(?:,\d{3})*(?:\.\d{2})?' matches = re.findall(pattern, extracted_text) if not matches: return False, "", "No currency values found"

This alone catches about half the errors garbled text, partial reads, and non-numeric data that OCR sometimes returns.

Layer 2: Double-Read Confirmation

The agent captures two separate screenshots and runs OCR on both independently. If the numbers match, the value is accepted with high confidence. If they differ, a third screenshot is captured and the best-of-three wins. Read twice text1 = extracttext(screenshot1) text2 = extracttext(screenshot2)

num1 = extractnumber(text1) num2 = extractnumber(text2)

if num1 == num2: return True, num1, "High confidence" Tiebreaker text3 = extracttext(screenshot3) num3 = extractnumber(text3)

if num1 == num3: return True, num1, "Medium confidence (2/3)" elif num2 == num3: return True, num2, "Medium confidence (2/3)"

This catches transient OCR errors—pixel-level variations between screenshots that produce different readings. The cost is 4-6 extra seconds per financial read. The benefit is catching errors that would otherwise silently corrupt data.

Layer 3: Range Validation

Even a correctly formatted, double-confirmed number can be nonsensical. A balance of "₦0" might mean an empty account—or it might mean the OCR failed completely and returned nothing. A balance of "₦999,999,999,999" is almost certainly an error.

if 1 <= value <= 1000000_000: return True else: return False # Suspicious. Re-read.

This catches the edge cases—numbers that pass format and double-read checks but are clearly wrong in context.

The Celebration

After implementing all three layers, I re-ran the 50-test battery. The error rate dropped from 20% to 6%. For the specific "₦15,000" test case, 20 out of 20 reads were correct with double-read confirmation.

The agent now succeeds 19 times out of 20 on multi-app financial tasks. The one failure is a timeout (banking app loads slowly on an old device), not a misread. And critically, when verification fails, the agent now knows it failed. It reports low confidence and aborts the task instead of silently sending wrong data.

What This Taught Me

The bug wasn't in the OCR engine. It was in my architecture. I had built a system that trusted a single, unreliable data source with financial information. That wasn't a Tesseract problem. That was a design problem.

The fix wasn't a better model. It was a better process. Three cheap, fast checks that together create confidence where no single check could. Defense in depth applied to data integrity.

This changed how I think about every feature I build. I now ask: "If this component silently returns wrong data, will I know? If not, where's the verification?"

The Code

The full verification system is open source on GitHub:

👉 github.com/Dexter2344/phone-agent

vision.py contains the verifyfinancialdata() function. agent.py calls it before storing any financial value in task memory.

The Build Log

I've been documenting this project publicly for 17 days on Dev.to. Every breakthrough, every dead end, every "why is this broken" debugging session at 1 AM.

This bug was Day 17. The one that almost shipped. The one that whispered.

It's not the loudest bug I've ever caught. But it's the one that taught me the most.

This is my entry for the Dev.to Smash Stories contest. The Gemma 4 model powers the agent's command parsing and reasoning. The verification layer is my own work.

出处https://dev.to/okeke_chukwudubem_5f3bf49/smash-stories-the-bug-that-whispered-for-two-weeks-before-i-heard-it-405n抓取日期 · 采集源 DEV Community

03 / EVIDENCE GAPS

这条还缺什么证据?

下面每条都由本条已有字段推出(等级、理由、商业模式、来源次数、是否演示), 本站不生成推测性结论;通用验证方法放在方法论页。

  • 可核验的收入或付费证据查官网定价页与付费口径;第三方数据源(如 GetLatka)只作旁证,需标注来源与时点。
  • 商业模式未定确认按席位/按用量/授权还是开源托管版收费;开源项目另查 LICENSE 与是否存在付费版。
  • 只有单一来源找一手站点或其他渠道是否重复出现同一产品;社区热帖数量不等于商业进展。

通用验证清单(谁有这个问题/谁愿意付费/一个人能交付哪一小步)见我们的筛选方法。

04 / SIGNAL HISTORY

发现时间线