公司动态
让LLM生成反例:把技术方案中的隐藏假设逼出水面
前几天评审一个内部模块的缓存方案方案看着很稳热点数据用 LRU过期时间设成 30 分钟读写都加了锁。我本来打算直接放行但临时让 LLM 帮我找找反例。它给出的场景完全不在我熟悉的业务范围内一张订单表被批量任务扫一遍几万条不重复的 key 在几分钟内全部流过缓存LRU 里原本的热点数据被全部挤出去后续真正的高频读请求全部落库。那一刻我突然意识到我一直在用正向案例证明方案可用却很少让一个完全不懂业务背景的模型来帮我拆台。这不是一个“工具好用”的故事。它背后涉及一个更底层的判断反例远比正例更靠近问题的真实边界。而 LLM 生成反例这件事最有趣的地方不在于它能给出标准答案而在于它可以把你推向自己专业领域之外的视角。一个做业务开发的工程师可以让 LLM 帮忙检查分布式锁里的时间问题一个只写过脚本的人可以让 LLM 帮忙检查浮点累加的精度边界。这些反例未必全部成立但它至少让我开始问一句我的方案在什么情况下会坏这篇文章想说的不是“让 LLM 帮你抬杠”。我想聊的是如何把 LLM 生成反例当成一种低成本、可复用、需要验证的认知工具。它改变的不只是效率更是一种工作方式从证明自己正确转向寻找自己何时不正确。1. 反例不是找茬而是把隐藏假设逼出水面1.1 正例只能证明“这一次成立”反例才能指出“哪一种情况不成立”在数学里一个命题要成立需要证明它对所有情况都生效而只要出现一个反例命题就被推翻。工程领域没有这么严格的形式化要求但逻辑是类似的当我们设计一个缓存、一个重试机制、一个数据同步流程时经常会下意识地用几个正常场景去验证跑通了就觉得“没问题”。可是正常场景只能说明“在这几个输入下没出问题”它无法覆盖输入分布、并发状态、故障时序和资源边界。反例的价值在于它能把隐藏假设暴露出来。比如“ LRU 缓存适合热点数据”这个说法隐含假设是“访问分布存在明显热点”。一旦请求变成全量扫描式工作负载LRU 的命中率就会显著下降。这种反例在教科书里很常见但在实际项目评审中如果没有人刻意问一句“如果 key 集合不是热点分布呢”很容易被忽略。原因也很简单正例是顺着我们的预期走的它让我们舒服反例是逆着预期来的它让我们不舒服。好的技术评审不能只收集赞成票更需要有人负责拆台。LLM 在这个环节里的角色就是一个不会看人脸色的拆台者。1.2 领域专家容易走熟路“外行”视角反而容易碰到盲区一个长期做订单系统的工程师谈到缓存时脑子里会自然浮现出“商品详情”“用户会话”“库存查询”这些场景。这些场景很真实但也意味着思考已经被业务经验约束住了。领域专家不是能力不够而是太熟悉当前路径容易把“我们通常这么用”和“这个机制原本适用的边界”混在一起。LLM 恰恰没有这个包袱。它的训练语料横跨大量领域它不会因为“订单系统从来不会出现批量扫描”就跳过这个反例。它更像一个读过很多技术资料、但没有实际业务体感的实习生可能会提出不切实际的假设但也可能从数据库、分布式系统、概率数据结构甚至物理模拟里搬来一个你从未想过的场景。这是“far outside ones area of expertise”的真正价值。当我们让 LLM 生成反例时我们不是在寻找一个比我们自己更懂业务的专家而是在寻找一个能跳出当前业务叙事、从更一般的技术规律里找漏洞的跨界视角。很多时候方案真正危险的地方恰恰来自我们因为专业而自动忽略的领域。1.3 LLM 让反例生产从偶然相遇变成了可调度流程以前找反例主要靠三种途径团队里有人恰好遇到过类似问题线上出了故障复盘或者做代码审查时某个较真的人多问了一句。这些方式都不是主动的反例什么时候出现取决于运气和团队构成。大多数情况下我们是在问题已经发生之后才得到真正的反例。LLM 改变了这个流程的启动成本。过去要让五个人开会头脑风暴一个小时才能列出几个边界场景现在把方案复制给 LLM几分钟内它可以生成几十个候选反例。虽然里面有不少重复和无效项但只要其中一两个能命中盲区就已经值回票价。这带来的不是“替代人工找反例”而是让“找反例”变成一个可以在方案早期反复执行的动作。以前只在代码审查或者测试阶段做一次现在可以在需求分析、设计评审、编码前、测试用例设计阶段各做一轮。反例生产从偶然事件变成了可以主动调度的流程。2. 生成反例不是简单提问而是先把方案拆成假设2.1 直接问“有没有反例”为什么效果差很多人第一次尝试是这么写的“以下是我的方案请帮我找一些反例。”模型确实会给出一些回应但结果往往很泛比如“需要考虑并发安全”“需要考虑缓存一致性问题”“建议加入分布式锁”。这些不是反例只是风险提示没有具体触发条件也没有执行路径。问题出在提问方式太模糊。反例要成立必须满足三个条件第一它不违反方案里明确写出的约束第二它确实会导致某个结论或预期行为失效第三它能被转成一个可以推演或验证的最小场景。如果模型不知道你的方案假设了什么它就只能根据自己的常识去猜猜出来的东西自然不痛不痒。所以我更建议把提问从“找反例”改成“拆假设”。让模型先列出你方案里所有隐含假设再针对其中一条假设构造反例。假设一旦写清楚反例就有了攻击目标。2.2 一套经过验证的提示词结构我常用的提示词模板大概是这样的请扮演一个专门寻找反例的评审员。 我会给你一个方案以及这个方案成立所依赖的假设。请你做四件事 1. 列出我方案中的所有隐含假设 2. 选择其中一个假设构造一个让方案失败的最小化反例 3. 说明这个反例的前提、触发条件和最终结果 4. 如果还有别的反例请按破坏程度从高到低排序。 约束 - 不要为了反例而虚构外部对象所有条件都必须来自方案本身 - 如果反例需要引入额外机制请先说明该机制是否常见 - 反例可以是小概率事件但必须逻辑自洽 - 请尽量避免泛泛的风险提示至少要给出一条可以执行或推演的时间线。这个模板的关键在两点。首先是“列出隐含假设”它强迫模型从方案原文里找出没有直接说出口的预设其次是“最小化反例”防止模型写出一个需要十步才能触发的复杂事故链。最小反例更容易验证也更容易转化为测试用例。实际使用时不一定每次都要完整模板。如果方案比较复杂可以先让模型列出假设再由我从假设里挑一条最可疑的继续追问。2.3 两个可以举一反三的实例第一个例子是分布式锁。假设我用 Redis 的SETNX加锁设置 10 秒过期最后在finally里删除 key。方案看起来直接但我让 LLM 找反例时它给出的核心场景很容易被我这种不常写分布式代码的人忽略线程 A 拿到锁后执行时间超过 10 秒锁自动过期线程 B 拿到锁进入临界区线程 A 终于执行完在finally里删 key结果删掉的是线程 B 的锁。从此刻开始B 的临界区不再有锁保护。这个例子里的关键不是 Redis 本身而是“锁的持有时间必须和业务执行时间匹配”这个假设被打破了。我不能说这个反例是 LLM 第一个想出来的但它确实只花了十几秒就给出了一条完整的事件序列而我当时的注意力还在“锁是不是唯一 key”“释放时有没有判断持有者”这些次一级问题上。第二个例子和浮点精度有关。假设一段循环对浮点数做累加最后判断累加结果是否等于某个期望值。我没有刻意去构造边界输入但 LLM 给出的反例很直接0.1 0.2 ! 0.3。如果是一个刚接触数值计算的开发者很可能想不到常见编程语言里的浮点数存在二进制表示误差。这个反例在工程里不算稀奇但它说明了一个模式只要你的方案里存在“数值相等判断”反例就可以从精度角度切入。这两个例子的价值不在反例本身而在于它们都能迁移。把“缓存策略”换成“分页查询”“消息重试”“数据迁移”“分布式锁”换成“乐观锁”“唯一索引”“状态机”“浮点累加”换成“金额计算”“评分求和”整套方法依然成立。关键是先把方案拆成假设再让模型对假设发起攻击。3. 拿到反例后先做四步验证再考虑改代码3.1 LLM 给出的反例天然存在三类问题LLM 生成反例时目标函数是“生成看起来合理的文本”而不是“生成经过证明的逻辑”。这意味着它给出的反例一定存在噪声。常见问题有三类第一幻觉。模型可能引用一个不存在的 API、一个不存在的配置项或者一个听起来像真的、实际完全错误的机制。比如它可能会说“当 Redis 主从切换时SETNX会自动重试”但你的客户端可能根本没有这个行为。第二新增假设。模型为了凑出反例会在你的方案之外偷偷加条件。比如你的分布式锁方案里根本没有“任务可以超过 10 秒”的说明但反例里默认了这个条件。如果方案里没有限制任务不能超过锁过期时间那么这个新增假设本身就是一个合法反例因为它暴露了未定义的前提但如果方案里已经写了“每个任务必须在 5 秒内完成”那反例就失效了。第三概念混淆。模型可能把两个相近但不完全相同的概念混在一起比如把“缓存淘汰策略”和“缓存一致性”混在一起或者把“幂等”和“并发安全”混在一起。这类反例看起来像批判其实是在论另一个问题。所以拿到反例后的第一反应不应该是“赶紧改方案”而是“先验证”。验证的过程不是走形式而是把反例从“模型说”变成“我确认”。3.2 反例验证四步法我一般这样执行我通常会按下面四步走每一步都简单直接。第一步把反例转成最小可运行场景。如果反例描述的是并发我会画出两条线程的时间线如果是数据逻辑我会构造一小段代表输入如果是配置问题我会写出一组参数。不要停留在自然语言描述里。自然语言看着合理但一旦转成具体对象很多漏洞立刻暴露。第二步运行或推演。能写代码的就写代码哪怕只是几十行的模拟脚本不能运行的就手推状态变化。重点不是要完整实现整套系统而是验证反例里说的因果链是否真的成立。第三步核对前提条件。回到最初的方案描述一条条检查反例是否满足所有约束。如果反例里出现了方案没有写明的机制要判断这个机制是否属于“常见且合理的补充”。如果属于说明方案里缺少对它的说明反例有价值如果不属于标记为伪反例。第四步给反例分类。我会分成三类真反例满足前提且确实导致目标失效伪反例靠新增假设才成立且假设本身不合理边界反例条件满足但需要极端的参数范围或极低的概率才触发。真反例要立刻处理边界反例值得记录伪反例直接丢弃。这个过程其实也在反向检查我自己对方案的理解。如果一个反例我无法判断是否成立那就说明我对这个环节的知识储备还不够需要去查资料或者请教更熟悉的人。这本身就是一个学习信号。3.3 如果复现失败按这条链路排查验证时经常遇到反例复现不了这时候不要急着换下一个先按下面的顺序排查。先看描述是否有歧义。你给模型的方案里可能用了“通常”“尽量”“最好”这类自由度很大的词模型可能理解为“允许任务执行时间超过锁过期时间”而你的本意是“业务必须很快结束”。再看模型是否引入了方案之外的机制。比如方案里没有“自动续期”反例却基于“锁会自动续期”展开这就是假设增量。排查方法是回到原文把反例里出现的每个机制都标注来源标不出来的就要警惕。再看是否是不同环境和版本导致。比如并行度、网络超时、Redis 版本、语言版本都可能产生影响。如果反例在本地环境复现不了不代表在预发环境不存在也可能说明它依赖特定并发窗口需要提高重复次数才能触发。最后再看是否把“问题”和“反例”混为一谈。反例要求有明确的因果路径和失败结果。如果模型给出的只是一句“可能存在风险”那它只是风险提示不是反例不需要费劲验证。这套排查链路对验证任何 LLM 输出都适用。核心思路是先怀疑输入描述再怀疑模型假设再怀疑环境差异最后才怀疑自己的实现。4. 把反例生成嵌入工作流从一次性提问到长期资产4.1 第一层针对单个方案做一次性反例检查最低成本的做法是在写完设计文档之后、提交代码评审之前用前文的提示词模板跑一轮反例生成。不需要做任何工程化改造只需要把方案描述整理成一段结构清晰的文本扔给模型然后人工筛选输出。这一层适合刚开始尝试的人。它的收益是即时的你可能会在一个设计稿里发现一个真正值得解决的问题。成本也低一次提问大概只需要几分钟。但缺点是反例结果不会被保存下次遇到相似方案时你还是要从头让模型生成一遍。如果你只打算记住一个动作那就记住这个每次设计评审前先让 LLM 生成三个反例。不用多三个就足够逼着你重新看一遍方案。4.2 第二层沉淀成“反例检查单”当一次性提问做到一定次数你会发现某些反例反复出现。比如缓存方案里总有“扫描式工作负载”分布式锁总有“锁过期和业务执行时间不匹配”消息队列总有“重复投递和消费幂等”。这些反例不是一次性的而是可以在同类方案里复用的。这时候可以做一份“反例检查单”。它不需要很复杂按技术领域分类即可。比如“缓存方案检查单”下面写是否存在顺序扫描或全量遍历导致热点数据被淘汰是否存在同一时间大量 key 一起过期导致缓存雪崩过期时间是否需要随机化如果并发请求同时 miss后端是否有合并回源或限流这份清单的来源可以是之前 LLM 生成的反例也可以是你自己线上故障的经验。每次评审同类方案时先过一遍检查单再让 LLM 根据当前方案生成“针对这份检查单的新反例”。这样反例生成就不再依赖模型临场发挥而是变成了一个人机协作的知识库。4.3 第三层把有效反例固化成自动化测试反例沉淀的最终形式是变成测试用例。一个反例如果被验证为真反例并且场景可以代码化就应该被写进测试集里。比如分布式锁的例子可以把反例转成一个确定性测试def test_lock_should_not_be_deleted_by_other_thread(): # 模拟线程 A 获得锁 # 模拟锁提前过期 # 模拟线程 B 获得锁 # 模拟线程 A 最终释放锁 # 断言线程 B 的锁仍然存在 ...这里不需要完全复现分布式环境关键是落成一个明确的状态序列。只要测试失败就说明方案里还存在这个边界问题。这一层还可以和 property-based testing 结合。如果方案里有一些需要随机输入的逻辑比如序列化、协议解析、批处理可以让 LLM 先生成一批异常输入再用 Hypothesis 或 fast-check 这类工具把异常输入作为种子值跑回归。这样 LLM 反例就不是一次性文本而成为了长期守护代码质量的资产。但要注意不要一开始就追求自动化。先手动验证几个反例确认它们稳定有效再固化成测试。否则一个错误的反例被写进测试集反而会带来维护成本。5. 适用边界LLM 反例是探索工具不是最终裁判5.1 适合用反例驱动的场景如果你在学一个新领域LLM 生成反例可以帮你快速建立“边界感”。比如你想了解布隆过滤器正向解释半天可能只是记住了“能判断元素是否可能存在”但一个反例会让你明白它不能精确判断存在误判率会随着元素数量上升。这种边界感是继续深入的前提。如果你在做技术选型或设计评审LLM 反例可以当作第一轮筛选器。它会用你不太熟悉的领域知识去攻击你的方案帮你快速暴露盲区然后再由你或者团队里更专业的人来判断。如果你是写测试用例的人LLM 反例是很好的候选输入来源。哪怕十句话里只有一句有用也能为测试设计提供新角度。5.2 不适合直接用 LLM 反例做决策的场景反例生成并不擅长处理安全关键系统、支付结算、数据一致性、形式化证明这类对逻辑完备性要求极高的场景。不是说不能用而是不能只用。一个模型生成的“反例”如果被写进正式决策流程必须经过专家评审、静态分析和线上演练。同样如果你的上下文里已经有一套严格的形式化规范比如 TLA 规范或形式化验证工具LLM 反例只能作为辅助灵感不能替代模型检测。反例这个动作本身没问题但工具链的可靠性要求决定了你不能把大模型当成验证器。还有一个容易被忽略的边界LLM 对某些专业领域其实并不稳定。它可能生成一个看起来极其专业的反例涉及一个你完全不熟悉的中间件或算法结果这个中间件的行为完全不是它描述的那样。越是你外行的领域越要对反例的验证保持谨慎。它不是错的但需要在真实环境或权威文档里确认。5.3 我最后会遵循的一条红线我把 LLM 生成的反例都当作“候选证据”而不是“最终结论”。它们的作用是帮我提出更好的问题而不是代替我下判断。只要这个定位不变反例生成就可以放心用在更多场景里。如果你也想开始我建议下一轮方案评审前先别急着写文档。先把方案里的假设列出来然后让 LLM 针对假设生成反例。验证完以后把有效反例写进你的检查单。坚持一段时间你会发现最明显的变化不是少踩坑而是你思考问题的方式开始变了你会习惯性地问自己这个方案在什么情况下会失效这种思维习惯一旦建立比任何单个反例都值钱。