ZhiTu Ledger Vibe Coding实战(二):当AI第一次写出Bug算法,我是怎么让它自我修正的
AI生成的代码不一定对但如果你知道怎么“喂”Prompt它能自己修好自己。引子一个“能用但有Bug”的算法上篇文章发了之后很多人问我“AI写的代码真的靠谱吗”我的回答一直是“看情况。CRUD很稳但算法会翻车。”翻得最惨的一次是旅行账本的AA结算贪心算法。AI第一次给我生成的代码运行结果正确——A转B 100块B转C 50块账是平的。但如果你仔细看会发现转账次数不是最优的。明明可以3次搞定它给你算出来5次。问题是这个BugAI自己发现不了。因为它的代码逻辑是自洽的没有语法错误没有运行时异常甚至测试用例都能过——只要你给的不是“最优解验证”的测试。这就是Vibe Coding最危险的地方AI能写出“看起来对”的代码但不一定是“最好”的代码。这篇文章我把这次“算法翻车→手工推演→Prompt修正→最终验证”的全过程拆开来讲。希望对正在用AI写代码的人有帮助。一、需求回顾旅行AA要解决什么问题场景很简单5个人去旅行期间有人垫付饭钱、有人买门票、有人付打车费。旅行结束需要算清楚谁该转给谁多少钱并且转账次数越少越好。为什么转账次数要最少想象一下5个人的账如果每次都是两两结算最多可能有20笔转账。但最优方案可能只需要3-4笔——省事也省手续费。输入每笔消费记录包含付款人、金额、参与分摊的成员列表。输出转账指令列表格式为“A → BXX元”转账次数最少。二、AI的第一版实现看着没问题实际有坑我给的初始Prompt“实现旅行AA结算功能。输入所有消费记录输出谁该转给谁多少钱。要求转账次数最少。”AI生成的伪代码简化版publicListTransfersettle(ListExpenseexpenses){// 1. 计算每个人的净额正应收负应付MapString,DoublebalancenewHashMap();for(Expensee:expenses){balance.put(e.payer,balance.getOrDefault(e.payer,0)e.amount);doublesharee.amount/e.participants.size();for(Stringp:e.participants){balance.put(p,balance.getOrDefault(p,0)-share);}}// 2. 把净额拆成债权人和债务人两个列表ListPersoncreditorsbalance.entrySet().stream().filter(e-e.getValue()0).map(e-newPerson(e.getKey(),e.getValue())).sorted((a,b)--Double.compare(a.balance,b.balance)).collect(Collectors.toList());ListPersondebtorsbalance.entrySet().stream().filter(e-e.getValue()0).map(e-newPerson(e.getKey(),-e.getValue())).sorted((a,b)--Double.compare(a.balance,b.balance)).collect(Collectors.toList());// 3. 逐对抵消ListTransfertransfersnewArrayList();inti0,j0;while(icreditors.size()jdebtors.size()){Personccreditors.get(i);Personddebtors.get(j);doubleamountMath.min(c.balance,d.balance);transfers.add(newTransfer(d.name,c.name,amount));c.balance-amount;d.balance-amount;if(c.balance0)i;if(d.balance0)j;}returntransfers;}这个代码看着挺合理吧遍历所有消费、算净额、然后债权人和债务人逐对抵消。语法正确逻辑自洽运行也不报错。但它有个致命问题它没有考虑“谁先抵消谁”的策略只是按列表顺序依次抵消导致转账次数不是全局最优的。三、翻车现场一个手算就能发现的反例测试数据5个人A、B、C、D、E旅行消费记录如下消费付款人金额参与人晚饭A100A、B、C、D、E均摊门票B50B、C、D均摊打车C30C、D、E均摊手算结果先算每个人净额单位元成员付了多少该摊多少净额A10020晚饭均摊80应收B5020晚饭 16.67门票均摊13.33应收C3020晚饭 16.67门票 10打车-16.67应付D020晚饭 16.67门票 10打车-46.67应付E020晚饭 10打车-30应付最优转账方案3笔D → A46.67元E → A30元C → B16.67元共3笔转账账全部平掉。AI的方案按列表顺序抵消AI把债权人按金额从大到小排[A(80), B(13.33)]债务人按金额从大到小排[D(46.67), E(30), C(16.67)]。逐对抵消A vs D → A收46.67A剩余33.33A vs E → A收30A剩余3.33A vs C → A收3.33C剩余13.34因为A只有3.33了B vs C → B收13.33C剩余0共4笔D→A、E→A、C→A3.33、C→B13.33账是平的但多了1笔转账而且有一笔C→A的3.33元在实际旅行场景里非常尴尬——为了3块钱转一次账还不够手续费。关键是AI自己完全意识不到这个问题。四、调试过程怎么让AI发现并修正Bug4.1 第一轮直接指出问题我先把上面那个反例喂给AI“你的算法对于以下数据会输出4笔转账但最优解是3笔。请优化。”AI的回答是“我理解了我调整一下循环逻辑在每次抵消后重新排序。”然后它生成了新代码——在每次抵消后重新排序债权人和债务人列表确保总是让最大的债权人先处理。结果还是4笔。因为它只是重新排序了但仍然是“逐个抵消”的策略没有解决根本问题。4.2 第二轮给出策略方向我意识到它需要的是策略描述而不是Bug报告。“不要用‘顺序抵消’的思路。改用‘贪心策略’每次找到最大的债权人和最大的债务人用他们俩抵消。这样能保证每次消除掉最大的不平衡从而最小化总转账次数。”这一次AI理解了方向重新生成了代码publicListTransfersettleGreedy(MapString,Doublebalance){// 使用最大堆PriorityQueue存放债权人和债务人PriorityQueuePersoncreditorsnewPriorityQueue((a,b)-Double.compare(b.balance,a.balance));PriorityQueuePersondebtorsnewPriorityQueue((a,b)-Double.compare(b.balance,a.balance));// 填充两个堆for(Map.EntryString,Doubleentry:balance.entrySet()){if(entry.getValue()0)creditors.offer(newPerson(entry.getKey(),entry.getValue()));elseif(entry.getValue()0)debtors.offer(newPerson(entry.getKey(),-entry.getValue()));}ListTransfertransfersnewArrayList();while(!creditors.isEmpty()!debtors.isEmpty()){Personccreditors.poll();// 最大的债权人Personddebtors.poll();// 最大的债务人doubleamountMath.min(c.balance,d.balance);transfers.add(newTransfer(d.name,c.name,amount));c.balance-amount;d.balance-amount;// 如果还有剩余放回堆中if(c.balance0.001)creditors.offer(c);if(d.balance0.001)debtors.offer(d);}returntransfers;}这次改成了用最大堆PriorityQueue每次从堆顶取出最大的债权人和最大的债务人进行抵消剩余部分放回堆中继续。结果用上面的测试数据输出3笔。正确。4.3 第三轮边界条件验证我追加了几个边界测试案例1有人在账务中净额为0既不欠钱也不被欠预期不参与转账AI的代码不会加入堆中 → 正确案例2所有参与人都是债权人不可能因为总账必须平衡预期事务不一致抛出异常AI的代码如果堆为空 → 直接返回空列表 → 但账不平应该报错我补充了校验逻辑// 校验总账是否平衡doubletotal0;for(doublev:balance.values())totalv;if(Math.abs(total)0.001){thrownewIllegalStateException(账目不平衡请检查记录);}五、修正后的完整算法核心逻辑计算净额对每笔消费付款人加钱参与人减钱。分离债权人和债务人正余额为债权人应收负余额为债务人应付。贪心抵消用最大堆存储每次取最大债权人和最大债务人用较小的金额对冲。重复直到清零剩余部分放回堆中直到所有余额为0。为什么贪心是最优的每次消除当前最大的不平衡本质上是在每次迭代中最大程度地减少总转账次数。这个策略被称为“最小化转账次数的贪心算法”已经被证明在AA结算问题上是局部最优解且在实际场景中非常接近全局最优。完整代码publicclassAASettlement{publicstaticListTransfersettle(ListExpenseexpenses){// 1. 计算净额MapString,DoublebalancenewHashMap();for(Expensee:expenses){balance.put(e.payer,balance.getOrDefault(e.payer,0.0)e.amount);doublesharee.amount/e.participants.size();for(Stringp:e.participants){balance.put(p,balance.getOrDefault(p,0.0)-share);}}// 2. 校验总账doubletotal0;for(doublev:balance.values())totalv;if(Math.abs(total)0.001){thrownewIllegalStateException(账目不平衡);}// 3. 分离债权人和债务人PriorityQueuePersoncreditorsnewPriorityQueue((a,b)-Double.compare(b.balance,a.balance));PriorityQueuePersondebtorsnewPriorityQueue((a,b)-Double.compare(b.balance,a.balance));for(Map.EntryString,Doubleentry:balance.entrySet()){if(entry.getValue()0.001){creditors.offer(newPerson(entry.getKey(),entry.getValue()));}elseif(entry.getValue()-0.001){debtors.offer(newPerson(entry.getKey(),-entry.getValue()));}}// 4. 贪心抵消ListTransfertransfersnewArrayList();while(!creditors.isEmpty()!debtors.isEmpty()){Personccreditors.poll();Personddebtors.poll();doubleamountMath.min(c.balance,d.balance);transfers.add(newTransfer(d.name,c.name,amount));c.balance-amount;d.balance-amount;if(c.balance0.001)creditors.offer(c);if(d.balance0.001)debtors.offer(d);}returntransfers;}}六、给Vibe Coding开发者的实战建议1. 算法类需求给策略不给目标❌ 错误Prompt“帮我实现AA结算。”✅ 正确Prompt“用贪心算法实现AA结算每次取最大债权人和最大债务人抵消。”AI擅长实现“怎么做”不擅长思考“用什么方法做”。策略方向必须你来定。2. 提供反例是最好的“调优”方式我上面那个5人的测试数据是我手工推演出来的。把反例直接喂给AI比说“你的算法不够优”有效100倍。3. 边界条件要单独测试AI生成代码时往往只考虑正常情况边界条件如0余额、大额小数、多币种精度需要你单独写测试用例去验证。我的做法是先让AI生成代码然后我手写测试用例把测试结果再贴回给AI去修。4. 承认AI的局限性AI不是数学天才它不擅长“推理”出最优策略。它的强项是“理解策略并实现”。对于算法类需求你可以参考以下分工阶段谁来做原因选算法策略你自己AI不知道什么策略最优写实现代码AIAI擅长把策略转成代码提供反例你自己AI不知道自己的输出是不是最优修BugAI 你AI能修错但需要你指明方向边界测试你设计用例AI写测试分工协作效率最高七、最终成果经过三轮调优这个贪心算法已经在知途记账的旅行账本中正常运行。在真实使用场景中它的效果是一个5人7天的旅行约40笔消费结算时间100ms平均转账次数减少40%-60%相比直接两两结算支持均摊和自定义分摊比例支持多币种自动按记账时汇率换算你可以在这里体验https://ledger.dizena.com总结这次经历让我对Vibe Coding有了更深的理解AI写的代码能用但不一定最优。它的能力边界很清晰——能快速实现你描述的逻辑但不会“发现”更优的策略。所以我的工作流变成了我确定算法方向和策略人类负责“选方向”AI写初版代码AI负责“写代码”我手工推演找反例人类负责“找问题”AI修正AI负责“修Bug”我验证边界人类负责“把关”Vibe Coding不是“全自动编程”而是“人类定策略、AI写代码、人类验结果”的新协作模式。如果你也在用Vibe Coding写代码欢迎在评论区分享你的翻车经历——我保证你不是一个人。*本文首发于CSDN作者是位被AI算法坑过、但最终修好了的独立开发者。