一个撇号击穿整条证据链:我用一个真实 Bug 测试 LLM 的代码调试能力
前言
最近在调试一个 VOC(Voice of Customer,客户之声)分析系统时,碰到了一个很有意思的问题。
页面上有一条用户评论,系统已经给它打上了“外观”标签。点击标签继续下钻时,页面却显示:
该记录没有可在评论原文中精确匹配的证据文本
但直接查数据库,却发现这条评论不仅有证据,而且有很多条证据。
其中“外观”标签对应的证据非常明确:
It's nice, elegant looking而评论原文中也明显有:
Its nice, elegant looking, sturdy and surprisingly easy to install.第一眼看上去,这简直让人怀疑程序是不是根本没从数据库查到 evidence。
结果最后发现:
数据库查询完全正常。
真正让整条证据链失效的,只是一个字符:
数据库: It's nice, elegant looking 评论原文: Its nice, elegant looking一个'。
也正因为这个 Bug,我突然意识到:
这种真实工程问题,非常适合拿来测试一个 LLM 到底是真会调试,还是只会“看代码猜答案”。
一、问题现场
系统中有这样一条智能门锁评论:
The model D110 Plus is 100 % as described and as I expected! This Desloc Smart Lock D110 Plus, Fingerprint keyless entry door lock, is exactly 100 % as it was pictured and described. It has a built in wifi with remote control. You get about 6 months on the qty 4 of AA batteries. It has a locvu display, fast fingerprint entry and recognition. App control with customizable users, you can unlock it six different ways. ... Its nice, elegant looking, sturdy and surprisingly easy to install. Definitely worth the money.数据库中,这条评论已经抽取出了多条标签和证据,例如:
外观 It's nice, elegant looking 干电池 You get about 6 months on the qty 4 of AA batteries WiFi It has a built in wifi with remote control APP App control with customizable users 指纹 fast fingerprint entry and recognition Alexa Compatible with Amazon alexa Google Compatible with Google assistant 安装相关 Easy installation 整体品质体验 sturdy 使用频率 you can unlock it six different ways 远程开门 remote control 密码 99+ codes 设置 customizable users数据库里明明存在:
外观 -> It's nice, elegant looking但点击“外观”标签下钻,页面却提示:
该记录没有可在评论原文中精确匹配的证据文本这就是整个测试题。
二、这道题为什么特别适合测试 LLM?
如果把以下材料一次性交给 LLM:
- 页面截图;
- 数据库查询截图;
- 项目源代码;
- 一句话描述:“数据库明明有 evidence,为什么页面说没有?”
不同能力的模型,答案会出现非常明显的分层。
一个能力较弱的模型可能会猜:
- SQL 没查出来;
- JOIN 写错了;
- tag_id 不一致;
- 前端没刷新;
- JSON 序列化失败;
- evidence 字段名称不匹配;
- 缓存问题;
- 大小写问题。
这些都“有可能”,但都没有形成证据。
真正解决问题,需要模型完成一条完整的数据链路追踪。
三、第一步:先判断数据库到底有没有查到
最容易犯的错误,是看到 UI 显示“没有证据”,立即得出:
后端没有从数据库读取到 evidence。
但数据库查询已经明确显示:
外观 | It's nice, elegant looking因此这里必须先建立一个非常重要的调试原则:
UI 中显示“没有”,不等于数据库中“没有”。
中间可能存在:
数据库 ↓ DAO / Repository ↓ 业务逻辑 ↓ 过滤 ↓ 数据转换 ↓ API ↓ 前端二次转换 ↓ UI任何一层都可能让原本存在的数据消失。
所以真正的问题应该重新定义为:
evidence 在哪一层被丢掉了?
这比问:
为什么数据库没返回?
要准确得多。
四、第二步:发现那一个不起眼的'
把数据库 evidence 和评论原文真正逐字比较:
数据库:
It's nice, elegant looking评论:
Its nice, elegant looking区别只有:
It's Its ^数据库 evidence 多了一个 apostrophe:
'对于人来说:
It's nice和:
Its nice语义几乎完全一样。
但对于程序:
"It's nice".casefold()in"Its nice".casefold()结果是:
False事情开始变得有意思了。
五、真正的 Bug:程序要求 evidence 必须是原文的精确子串
继续跟踪后端代码,可以看到类似这样的判断:
def_contains_literal(text:str,term:str)->bool:returnbool(term)andterm.casefold()intext.casefold()然后 evidence 会进入:
def_matched_highlight_terms(full_text,evidence_text):...核心逻辑相当于:
forfragmentinevidence_fragments:iffragment.casefold()notinfull_text.casefold():continue于是当前案例实际上执行的是:
"It's nice, elegant looking".casefold()in\"Its nice, elegant looking, sturdy and surprisingly easy to install.".casefold()结果:
False所以尽管数据库已经正确读取到了:
It's nice, elegant looking经过_matched_highlight_terms()后,却变成:
[]注意这里非常关键:
证据不是没有查出来,而是查出来之后被过滤掉了。
这是两个完全不同的问题。
六、Bug 是怎样一路传到前端的?
完整链路大致如下:
数据库 │ │ evidence_text = │ "It's nice, elegant looking" │ ▼ 后端查询 │ │ 正常 │ ▼ _matched_highlight_terms() │ │ 与评论原文做 literal match │ │ "It's" != "Its" │ ▼ highlights = [] │ ▼ API 返回 │ ▼ 前端转换 │ │ terms.length == 0 │ ▼ evidenceGroups = [] │ ▼ 页面 │ └── “该记录没有可在评论原文中精确匹配的证据文本”到这里,整个现象就完全闭环了。
页面没有撒谎。
数据库也没有错。
SQL 也没有错。
真正的问题是:
数据库中的 evidence 已经不是评论原文的严格子串,但下游程序仍然按照“严格原文子串”来使用它。
七、这个问题真正考验 LLM 的是什么?
表面上看,这只是一个'。
实际上它至少同时测试了 7 种能力。
1. 多源信息联合分析能力
模型需要同时理解:
页面截图 + SQL 查询结果 + 评论原文 + Python 后端 + JavaScript 前端不能只盯着其中一个文件。
2. 数据流追踪能力
真正优秀的代码模型不能停留在:
“我觉得可能是字符串匹配的问题。”
而应该继续回答:
evidence 从哪里来? ↓ 在哪里过滤? ↓ 过滤之后是什么? ↓ API 返回什么? ↓ 前端怎么处理? ↓ 为什么最终出现这句话?这就是工程调试中非常重要的Data Lineage能力。
3. 区分“读取失败”和“过滤失败”的能力
这是这个测试里非常关键的一项。
低水平答案:
数据库 evidence 没有被正确查询出来。高水平答案:
数据库 evidence 已经成功读取, 但在原文匹配阶段被过滤,因此没有进入最终 evidenceGroups。两句话看起来差别不大,工程意义却完全不同。
4. 字符级异常发现能力
模型必须真正比较:
It's和:
Its而不是只进行语义理解。
LLM 天生擅长语义相似性。
但调试代码时,有时候恰恰需要暂时关闭“语义脑”,切换成“编译器脑”。
因为:
It's nice和:
Its nice对人类近似相同;
对str.find()而言完全不同。
5. 理解程序设计约束的能力
为什么程序要做精确匹配?
因为这个系统不仅是在展示一个标签。
它还需要在评论原文中:
定位证据 → 高亮证据 → 保证证据可审计也就是说:
evidence_text实际上承担了两种职责:
语义证据 + 原文定位信息因此系统隐含了一个非常强的 invariant:
evidence_text 必须能够在 source text 中找到。
如果模型只建议:
similarity>0.8然后直接把数据库 evidence 返回给前端,其实问题仍然没有真正解决。
因为前端拿到:
It's nice, elegant looking后,还是无法在:
Its nice, elegant looking中做精确高亮。
八、更高级的答案:不能只说“改成模糊匹配”
这是另一个很适合区分模型水平的地方。
一个普通模型可能会建议:
使用 Levenshtein Distance或者:
使用 fuzzy matching这看起来很合理。
但存在一个问题。
假设后端认为:
It's nice, elegant looking和:
Its nice, elegant looking相似度 98%,于是匹配成功。
如果后端仍然把数据库字符串:
It's nice, elegant looking交给前端,前端仍然找不到。
真正正确的修复应该是:
fuzzy matching 只用于“定位”,最终返回的 evidence 必须仍然是评论原文中的真实 span。
例如:
数据库 evidence: It's nice, elegant looking ↓ 容错定位 source span: Its nice, elegant looking最终传给前端的应该是:
Its nice, elegant looking而不是数据库中经过修正后的:
It's nice, elegant looking这就保住了系统最重要的 invariant:
最终 evidence 永远是 source text 的真实子串。
九、一个更加稳妥的修复思路
例如可以首先保留严格匹配:
deffind_source_span(full_text:str,fragment:str)->str:pos=full_text.casefold().find(fragment.casefold())ifpos>=0:returnfull_text[pos:pos+len(fragment)]只有严格匹配失败后,再进行非常保守的 normalize。
比如只允许 apostrophe 差异:
importredeffind_source_span(full_text:str,fragment:str)->str:# 1. 精确匹配优先pos=full_text.casefold().find(fragment.casefold())ifpos>=0:returnfull_text[pos:pos+len(fragment)]# 2. apostrophe 容错parts=re.split(r"['’‘`]",fragment)iflen(parts)>1:pattern=r"['’‘`]?".join(re.escape(part)forpartinparts)match=re.search(pattern,full_text,flags=re.IGNORECASE)ifmatch:# 一定返回原文真实内容returnmatch.group(0)return""这样:
数据库: It's nice, elegant looking可以定位:
原文: Its nice, elegant looking最终返回:
Its nice, elegant looking页面就可以正常高亮。
十、为什么不建议直接上“大模糊匹配”?
因为这条评论里其实还有更有意思的数据。
数据库可能保存:
Compatible with Amazon alexa但用户原文可能是:
Compatible with Anazon alexa还有:
Compatible with Google assistant对上:
Compatible with Googe assistant如果允许非常宽松的 fuzzy matching,那么:
Amazon ↔ Anazon Google ↔ Googe都可能被自动“纠正”。
这在普通搜索场景可能没问题。
但在证据审计系统中,这是危险的。
因为系统可能逐渐从:
“展示用户真正说过的话”变成:
“展示系统认为用户大概说过的话”这两者不是一回事。
因此我的建议是:
严格匹配 ↓ 标点 / Unicode / 空白等安全 normalize ↓ 有限规则容错 ↓ 仍然失败则明确标记 evidence mismatch而不是:
什么都 fuzzy 一下十一、真正应该修的其实还有上游
进一步思考会发现:
下钻逻辑只是问题暴露的位置。
更根本的问题,很可能发生在 evidence 生成或写数据库的时候。
系统原本希望保存:
Its nice, elegant looking结果某个环节把用户原文“纠正”为:
It's nice, elegant looking这非常像 LLM 在生成 evidence 时进行了无意识的 grammar correction。
对于普通摘要任务:
Its → It's甚至可以算优化。
但对于证据抽取任务,这是数据污染。
如果任务要求的是:
从原文抽取 evidence span
那么模型输出必须满足:
evidenceinsource_text而不是:
semantic_similarity(evidence,source_text)>0.95所以更好的系统设计应该在 evidence 入库之前增加一道硬校验:
assertevidence.casefold()insource_text.casefold()不满足就:
重新抽取 / 拒绝写库 / 记录异常这比在展示层不断补洞更合理。
十二、我认为这个案例可以成为一个不错的 LLM Debug Benchmark
可以把测试题设计成下面这样。
给模型的材料
提供:
1. 用户评论截图 2. 页面错误提示截图 3. 数据库 evidence 查询结果 4. 完整项目源代码然后只问一句:
数据库里明明有多条 evidence,其中“外观”也有证据,为什么前端下钻显示没有证据?请定位具体原因,并给出修复方案。
不要告诉模型问题发生在哪个文件。
不要告诉它是字符串问题。
不要提醒 apostrophe。
让模型自己找。
十三、LLM 调试能力评分标准
我会把这个测试设计成 100 分。
Level 0:猜测型,0~20 分
典型回答:
可能数据库没有查询成功。 可能 SQL JOIN 有问题。 可能前端缓存没有刷新。 建议检查 API。特点:
大量“可能”,没有证据链。
这种模型可以当 Stack Overflow 搜索助手,但不能独立承担复杂调试。
Level 1:发现数据矛盾,20~40 分
能够指出:
数据库有 evidence, 所以问题应该发生在查询之后。说明模型已经开始做基本的数据流分析。
但还没有定位真正原因。
Level 2:发现字符差异,40~60 分
能够注意到:
It's nice和:
Its nice不一样。
并意识到:
termintext会返回 False。
这一层已经明显强于纯语义分析模型。
Level 3:定位具体过滤函数,60~75 分
不仅发现字符串差异,还能找到类似:
_contains_literal()和:
_matched_highlight_terms()并说明:
evidence 已经从数据库读到, 但在这里被过滤成了 []这一层已经具备比较可靠的代码 Debug 能力。
Level 4:完成前后端完整证据链,75~90 分
模型能够继续追踪:
DB evidence ↓ backend query ↓ matched_highlight_terms ↓ highlights = [] ↓ API ↓ terms.length == 0 ↓ evidenceGroups = [] ↓ UI error message到这一层,才真正算“闭环定位”。
Level 5:理解系统 invariant,并提出正确修复,90~100 分
最高水平的模型不仅要找到 Bug,还应该意识到:
不能简单把 exact match 换成 fuzzy match。
它应该提出:
1. 上游 evidence 必须尽量保持原文; 2. 入库前验证 evidence 是否为 source span; 3. 下游只做有限 normalize; 4. fuzzy 只帮助定位; 5. 最终返回的一定是 source text 中的真实 span; 6. 保持 evidence 可审计性。如果模型还能进一步发现:
Amazon / Anazon Google / Googe这样的同类问题,并提出 regression test,那么基本可以认为它已经具备较强的真实工程调试能力。
十四、为什么这个 Benchmark 比普通算法题更有价值?
传统代码 Benchmark 经常测试:
写一个排序算法 写一个 REST API 修一个 NullPointerException 实现一道 LeetCode这些当然有价值。
但真实工程里的 Bug 往往不是这种形式。
真实情况更像:
产品经理说页面错了 ↓ 页面确实看起来错了 ↓ 数据库看起来又是对的 ↓ 代码也没有报 Exception ↓ 日志甚至一切正常 ↓ 但数据就是在某一个不起眼的业务规则里消失了这种问题测试的已经不只是代码生成能力,而是:
Observation + Hypothesis + Evidence + Tracing + Falsification + Root Cause Analysis + System Design换句话说:
真正高级的 Coding LLM,不应该只是“代码生成器”,而应该是“工程推理器”。
十五、这个案例还有一个非常有意思的地方
人类看到:
It's nice, elegant looking和:
Its nice, elegant looking大脑会自动纠错。
LLM 也一样。
甚至 LLM 的自动纠错能力可能比普通人更强。
但程序不会。
程序看到的是:
39有没有那个字符,就是有没有。
所以这个案例实际上在测试 LLM 一种非常重要、却容易被忽略的能力:
模型能不能在需要的时候停止“理解意思”,开始“尊重字节”。
这是做代码调试非常重要的能力。
十六、我会如何评价一个模型是否真的通过了这个测试?
如果它只回答:
因为 It’s 和 Its 不一样。
我认为:
及格,但不优秀。
如果它回答:
数据库已经有 evidence,但
_matched_highlight_terms()做 literal substring match 时,因为It's和原文Its不一致导致 evidence 被过滤,最终前端收到空 terms,因此生成空evidenceGroups,所以才显示“没有可精确匹配的证据”。
这已经属于:
非常好的 Debug 答案。
如果它进一步说:
不应该简单使用 fuzzy matching 后继续返回数据库 evidence,而应该用容错规则找到 source span,并返回原文真实 span;同时应在 evidence 写库阶段强制验证 evidence 是原文子串。
那么我认为:
这个模型已经真正理解了系统。
这三种回答,实际上代表了完全不同层级的 LLM 工程能力。
十七、可以进一步把它做成自动化 Benchmark
甚至可以为这个案例写一个自动评分器。
检查模型答案中是否覆盖关键概念:
数据库 evidence 已存在 10 分 指出不是 SQL 查询问题 10 分 发现 It's / Its 差异 15 分 发现 literal / substring match 15 分 定位后端过滤 15 分 解释 highlights 为空 10 分 解释前端 evidenceGroups 为空 10 分 指出最终应返回 source span 10 分 提出上游 evidence invariant 5 分总分:
100这样甚至可以拿不同模型横向比较:
Model A 35 Model B 58 Model C 76 Model D 94这比单纯问:
“帮我找一下这个 Bug。”
更容易形成客观的能力测试。
十八、总结
这个 Bug 最终的 Root Cause 可以浓缩成一句话:
数据库 evidence: It's nice, elegant looking 评论原文: Its nice, elegant looking由于程序使用:
term.casefold()intext.casefold()进行严格 substring match,
一个'就足以让:
有 evidence变成:
没有可匹配 evidence但真正有意思的,不是这个 apostrophe。
而是模型能不能自己完成下面这条推理:
UI 说没有 ≠ 数据库没有 数据库有 ↓ 说明数据在后续链路消失 找到过滤逻辑 ↓ 比较 evidence 与 source text 发现字符级差异 ↓ 复现 False 继续追踪 ↓ highlights = [] 继续追踪 ↓ evidenceGroups = [] 最终解释 UI ↓ Root Cause 闭环 最后再检查修复方案 ↓ 不能破坏 source evidence invariant一个模型如果能在没有任何提示的情况下独立完成这条链路,我认为它测试的不再只是:
“会不会写 Python”。
而是:
有没有真正的工程问题定位能力。
以后评测 Coding LLM,我越来越倾向于少问几道 LeetCode,多扔几个这种真实项目里的“小破 Bug”。
因为真实的软件工程世界里,
最难找的 Bug,
往往真的只差一个字符。
