首页 / 资讯中心 / 文章详情

AI编程时代的交付级代码:契约、不变量与状态机实战

AI编程时代的交付级代码:契约、不变量与状态机实战 ★ FEATURED ARTICLE
1. 一次代码评审让我开始怀疑“能跑能过测”的真实含义1.1 Vibe Coding的甜蜜与隐忧先说说那个让我坐不住的周末。团队刚把一个用AI辅助写出来的订单调度模块合进主干整体功能测试也过了我随手翻了翻代码发现一段处理并发订单的路径——入口有个全局锁中间却在改共享缓存出口没释放。我问负责的同事“这段并发模型是经过推敲的还是碰巧的”他想了半天回答“AI写的我看着像对的测试也过了。”这就是Vibe Coding时代的典型状态。AI生成代码的速度确实快得离谱你只需要把需求用自然语言描述清楚几分钟内它就能给你一版“活”的实现。但问题恰恰出在“活”和“对”之间。大多数人的关注点集中在“能不能跑通”很少有人追问这段代码在什么前提下成立它在什么条件下会静默地出错我用“隐式契约”这个词来描述这类隐患。AI写代码时它依据的是训练数据里沉淀下来的“程序员平均水平”的直觉——它默认调用方会按某种惯例传参默认某个字段不会为空默认重试不会破坏幂等性。大多数情况下这些默认成立可一旦业务数据的边界被打破这些没有言明的假设就会变成生产事故的引信。1.2 什么是交付级代码可验证、可审计、可演进我们想要的东西其实很朴素可交付的代码。所谓可交付我的标准有三条——可验证、可审计、可演进。可验证是说每次改动都有明确的检查方式可审计是说任何一段代码的行为边界都能被找出来、被讲清楚可演进是说下次有人或AI接手时不会因为不理解既有约束而改坏行为。而形式化方法Formal Method这个名字听上去高冷本质上就是在回答这三个问题。它用数学语言把代码的行为边界描述清楚用逻辑规则把“什么能发生、什么不能发生”钉死。很多人一听到“形式化”就联想到论文里的类型系统证明、模型检测、定理证明器觉得那是只能存在于学术界的东西。但我的观点是形式化方法是一个光谱光谱的一端确实是繁重的形式化验证另一端则是每个普通开发都能接受的轻量级实践——把代码的关键行为用明确的约束表达出来并让这个约束在开发、测试、运行的各个环节都发挥作用。在这篇文章里我想讲的不是如何把整套形式化方法搬到AI辅助编程里而是从里面偷师几样武器把它们整理成一套普通团队能立刻用的质量体系。这套体系不会消灭Vibe Coding的爽快感但能让AI写出来的代码从“看着对”变成“确实对”。2. Formal Method里真正值得“偷师”的三样东西2.1 Design by Contract给AI装上边界意识先说我个人最推崇的一个概念契约式设计Design by Contract。它最早来自面向对象领域的经典语言实践核心思路是把代码的调用关系想象成一份合同——调用方有义务满足某些前置条件被调用方有义务保证某些后置条件双方在合同边界内自由发挥。举个例子。一个人机交互较少的内部结算函数AI容易写出“传个空数组进来就崩”的版本。如果我在提示词里明确写前置条件为“金额数组元素均为非负小数”后置条件为“返回的汇总金额精确到分”AI在生成实现时就会额外考虑空数组、非法值和精度问题。它的训练数据里有大量“承接契约式规格说明”的样本一旦你给了它清晰的边界它的输出质量就会明显上升。这正是很多团队忽略的点。大多数人在让AI写代码时只告诉它“做什么”很少告诉它“在什么条件下成立、做完之后必须保证什么”。AI不是猜不到是你没给它划边界。契约式设计就是在给AI装边界意识——把它默认能力里偏弱的那部分通过显式的条件描述补上。2.2 不变量防止“修一处、坏一片”不变量Invariant这个概念更微妙它是系统运行过程中始终为真的性质。比如“账户余额始终不小于0”“库存扣减总量等于销售总量”“每个订单的状态只能按既定顺序流转”。它不像前后置条件那样描述单个函数的行为而是描述整个系统的状态约束。在AI辅助编程里不变量之所以重要是因为AI对“全局状态”的把控天生很弱。它能写对一条业务分支但当多个分支共享某个状态时它很难保证每个分支都维护同一条不变式。现实中大量AI生成的bug不是单点逻辑错而是某条分支绕过了不变量——比如退款流程里没检查订阅状态导致同一个订单被退两次。我现在的习惯是在让AI动手之前先单独写一段“业务不变量清单”给它。不必用严谨的数学符号一句话一条就行。然后明确要求它所有代码路径必须满足这些不变量如果某条路径无法满足必须在注释里标注或者在返回结果里显式报错。这么做的核心目的不是让AI不犯错而是让它的错误更容易被识别。不变量就是一种判据系统还健康吗一眼就能看出来。2.3 状态机视角把复杂调度变成有限个状态转换第三个值得迁到实践里的是状态机思想。很多复杂系统的逻辑混乱不是算法难而是状态太多且被“散装”地写在各式各样的标志位里。AI特别容易在这种地方翻车因为大量布尔标志位的组合会指数级增长它根本没法在生成代码时遍历所有组合。与其让AI在一个标志位泥潭里猜来猜去不如先跟它明确这个实体的生命周期有哪几个状态允许的状态迁移有哪些哪些迁移被禁止。订单就是一个好例子——创建后只能待支付、已支付、已取消已支付之后不能重新支付取消之后不能流转回支付中。把状态迁移图用文字描述清楚AI实现起来会顺手很多审查的人也更容易判断对错。我把这三样东西称为“轻量级形式化思维”。它们不需要你掌握高阶逻辑、不需要写证明脚本只需要你在让AI编写代码之前先用确定性的语言告诉它边界、全局约束和合法演化路径。3. 如何把形式化约束“翻译”给大模型3.1 提示词中的契约模板先谈边界再谈业务理论和实践之间隔着一道翻译的坎。形式化约束写得太学术AI读不懂写得太随意等于没写。我在实践里总结了一套提示词模板核心思路是“先边界、后业务、再验收”。一个相对完整的提示词结构是这样先用一句话说明要实现的模块目标和接口形态紧接着列出前置条件和后置条件然后列出必须满足的不变量最后才描述具体的业务流程。业务流程可以让AI去发挥但边界和约束是锁定死的。我随手列一下近期用过的一个模板片段请实现订单对账模块的核心函数 reconcileOrders(orders, payments)。 前置条件 - orders 和 payments 均为非空数组 - 两者中的金额字段均为非负小数单位统一为分 - orders 按创建时间升序排列 后置条件 - 返回结果中每个订单的状态字段只能是 settled 或 failed - 所有 settled 订单的金额合计与 payments 总额完全一致 - 函数返回后不允许对输入数组做任何修改 不变量 - 同一个订单在结果中只能出现一次 - 任何情况下库存余量字段不得小于0 - settled 订单数量 failed 订单数量 orders 总数量我承认这段提示词比平常大家习惯的要多花两分钟。但这两分钟相当值。它把模糊的自然语言翻译成了AI更容易对齐的规格说明。实测下来如此约束过的生成结果表面正确率未必显著提高但“严重缺陷率”明显下降——AI不会再制造那种“看着正常、实测炸裂”的隐蔽问题了。3.2 带约束的脚手架代码让AI在轨道上填空提示词是“临场约束”脚手架代码则是“结构性约束”。如果AI是在一张已经定好契约、定义好接口、写好了关键校验逻辑的骨架里填空它的自由度会被大幅收敛犯错空间也就跟着收敛。具体做法是我先手动写一版带契约的脚手架包括函数签名、入参出参类型、异常定义、前置条件校验、不变量断言点位然后把脚手架代码直接扔给AI“在这个框架内补充实现不要改接口签名和校验逻辑不要删除不变量断言”。我自己试下来这种做法的收益比单纯写提示词还要大。原因很简单AI对已经存在的代码结构有天然的遵从倾向你给它的骨架越完整它越倾向于在结构内做局部推理而不是从头发明一套逻辑。所谓的“轨道式生成”本质上就是用显式结构把AI的不确定性锁在一定范围内。有人担心这会不会削弱AI的创造性和效率。实际上不会。创造力的应用场景是在业务逻辑内部而不是在接口边界和状态约束上。约束留给框架创造留给填充这才是合理的分工。3.3 注释级规范让人和模型共读的接口说明书还有一类容易被忽视的工具是注释。代码注释最重要的作用不只是解释逻辑它还在给未来的阅读者传递“不可见约束”。在你用AI续写代码的时候这些注释就是它唯一能读到的历史上下文。我在代码里的注释风格从“解释这行代码在干什么”改成了“这行代码在什么前提下成立”。一个典型的改动对比# 常规注释 # 检查订单是否可退款 def can_refund(order): return order.status paid# 约束式注释 # 前置条件order 必须非空且 status 属于有效枚举值 # 不变量退款操作不可改变订单已结算金额字段 def can_refund(order): return order.status paid别小看这一行字。AI在后续改动时读到“不可改变已结算金额字段”这个约束后就会避免在退款逻辑里顺手修改那个字段。注释级规范的最大价值是持续性——它不只在生成那一刻生效在后续每一次修改、重构、续写时都在生效。这比一次性提示词长远得多。4. 质量闭环从生成到交付的四道闸门4.1 第一道闸门静态分析与类型系统接管常规错误有了契约和约束还不够AI生成代码之后质量体系必须介入。我把过程设计成四道闸门第一道交给静态分析和类型系统。这一步做的事很朴素不许带着低级错误往下走。比如未定义变量、可疑的隐式类型转换、明确违反类型约束的修改。跑静态分析和类型检查的成本几乎为零却能挡住相当一部分AI生成代码里最粗心的错误。更关键的是静态分析工具能作为“约束执行者”提示词里写了不变量、类型约束工具会把违反约束的代码直接标红。我现在的习惯是AI生成的代码第一件事不是人肉review而是丢进检查管道。如果这一关都没过后面的一切质量工作都无从谈起。人不应被浪费在看AI的低级错误上人的注意力应该集中在业务逻辑是否符合预期。4.2 第二道闸门围绕不变量写测试而不是围绕实现写测试这一步是我认为整个体系最核心的转变。大多数人让AI写测试的习惯是“给一个函数按输入输出补几个用例”这种做法验证的是实现本身而不是系统的承诺。我的做法是反向的先列不变量再写测试。每个不变量都对应至少一个测试用例无论当前的实现是什么。比如“同一个订单只能出现一次”这个不变量就对应三个用例正常输入时只有一次、重复输入时不会产生重复结果、极端数据下也不会绕过这个保证。这套写法的好处非常直接当AI后续修改代码时破坏不变量必然导致测试失败你就能立刻知道哪个约束被违反了。而传统的按实现写测试实现改了测试也跟着改相当于自己人审自己人发现不了边界问题。这里我提供一个小方法把每个测试用例标注它守护的不变量ID。比如test_orders_result_is_unique_invariant_001。看起来啰嗦但它让测试变成了一张可追踪的“约束地图”后续维护的人能立刻理解为什么要测这个场景。4.3 第三道闸门运行时契约监控让“带病”代码尽早暴露静态约束和单测终究是离线检查运行时的行为才是最终的真相。第三道闸门是运行时契约监控——在关键入口、关键状态变更点上加上断言或契约检查代码让违反条件的情况一发生就被发现而不是悄悄滑入后续流程。举个例子。在订单状态迁移这个方法上我会让AI加上一个状态机合法性检查def transition(order, next_status): allowed { created: {paid, cancelled}, paid: {refunding, completed}, cancelled: set(), refunding: {refunded}, } if next_status not in allowed[order.status]: raise ContractViolation( f非法状态迁移: {order.status} - {next_status} ) order.status next_status这种代码不增加业务复杂度但它把“非法状态迁移”从“悄悄发生”变成“立刻暴露”。生产环境里可以关掉这类检查以省开销但开发环境必须全开。我的经验是AI生成的代码里这种运行时契约检查比单元测试更容易发现诡异问题——因为它是真实数据在真实路径上跑出来的。4.4 第四道闸门审查清单把人的注意力留给AI盲区最后一道闸门是人工审查。但人工审查的姿势得换。传统审查是通读代码找逻辑漏洞这种活AI比人干得好。人应该盯的是那些AI天然不擅长的地方业务语义是否正确、隐式约束是否遗漏、技术债务是否被引入。我给自己定了一张审查清单只查四类问题第一不变量清单是否完整有没有遗漏业务规则第二契约边界是否写清楚了调用方的义务是否明确第三状态机的合法迁移是否覆盖了所有业务场景第四错误处理路径是否也遵守了不变量——这一步是AI尤其容易栽的地方它处理happy path时很在线处理异常路径时常常忽略约束。把人的注意力集中到这些点上审查就从“通读代码”变成了“检查约束体系的漏洞”。效率更高也更有针对性。5. 一次完整的改造实践从“跑得起来”到“推得出去”5.1 改造前的状态AI生成的调度模块表面正常我在一个模拟项目X里做过一次完整改造对象是一个AI在两小时内写出来的异步任务调度模块。功能上确实完整支持任务创建、优先级排序、并发执行、失败重试。测试环境跑了两天稳定性也看不出问题。但我给它做了约束分析之后立刻发现三处隐患。首先重试逻辑没有检查任务状态一个已经在执行中的任务可能再次被触发其次“失败重试次数不超过3次”这个不变量没有被任何代码路径严格保证——如果失败发生在状态更新之前重试计数会丢失第三任务取消没有清理待执行队列已取消的任务仍可能被执行。这些隐患不改动具体业务代码时根本看不出来属于典型的“能跑”但不“可信”的状态。我拿这个模块当作测试样本跑了一遍完整的质量体系改造。5.2 引入契约和不变量后的改动记录改造从三个层面切入。第一层在提示词层面重新描述这个模块的边界和约束。我给了AI一份新的规格说明明确列出任务状态只能从pending流转到running、succeeded、failed、cancelled任何路径下重试次数不得超过3取消操作必须移除队列中的待执行任务。第二层在脚手架层面手动加固了三个关键函数签名及其校验逻辑把状态迁移检查函数固定下来要求所有分支都必须经过它。第三层围绕三个不变量补了一批测试并给关键入口加了运行时契约检查。改动本身不大前后加起来大约一个下午的工作量其中一半时间还是花在调整提示词和检查AI生成的代码是否真正遵守了约束上。改完之后这个模块的行为边界变得非常清晰。你看代码时第一眼就知道哪些路径是允许的哪些是被禁止的。5.3 这套体系带来的实际收益与意外之喜改造之后的收益除了代码本身变可靠之外我还有几个额外的观察。第一测试的“解释能力”大幅提升。此前测试用例是散装的现在的测试一眼看过去就知道每一个用例在守护什么承诺。第二后续再让AI给这个模块加功能时约束注释和测试成了它的“自我校正器”生成的新代码自带不变量意识连带着后续改动的一次性通过率都上去了。最意外的是重构体验的改善。三个月后我因为性能问题要动这个模块的并发模型按照以往经验这种重构风险极高容易在改动中破坏旧行为。但这次不一样因为不变量测试都在状态机检查都在重构后跑一遍就能知道有没有破坏承诺。我敢说如果没有这套约束体系我大概率会推倒重写而不是安全重构。6. 别把形式化方法当成紧箍咒边界与成本6.1 哪些场景不值得上这套体系我必须坦白这套体系不是万能的也不是所有项目都值得上。判断标准其实很简单——你的代码需要活多久谁要读它它处在什么业务层级一次性数据分析脚本、验证想法的迷你原型、明天就删的演示demo确实没必要写不变量清单也不会有人审计它的约束。对这种场景上整套体系纯粹是自我安慰式的过度工程。AI编程最大的优势是快强行给每一行代码套上形式化枷锁反而把它最大的价值杀掉了。我之前碰到一个极端案例同事连一个内部工具脚本都要写前置条件注释结果写注释和维护注释的时间比写业务逻辑还多。这就是形式化思维用错了地方。方法论是为你服务的不是让你成为它的奴隶。6.2 渐进式落地从一条核心路径开始如果你认可这套思路但觉得一下子铺开不现实我建议走渐进式路线。别急着一上来就给整个系统建立不变量清单先挑一条核心业务路径最好是你最头疼的、AI生成代码后频繁出问题的那条路径。把那条路径的边界条件列出、不变量写清、测试补上、运行时检查加上然后观察这套体系带来的差异。你会立刻感受到两个变化一是代码的安全性确实变强了二是AI在这条路径里生成的代码质量明显更稳定。有了正反馈之后再逐步扩展到其他模块。从一条路径起步还有一个好处你能在真实项目里修正自己对“轻量级形式化”的理解。哪些约束值得写、哪些业务边界其实不关键、哪些不变量写出来只会增加噪声这些经验只有在你动手之后才能真正积累下来。6.3 警惕“过度契约化”AI编程的节奏感怎么找最后我想聊一个分寸问题。体系一旦建立任何方法论都有滑向教条的风险。我在实践中见过不少团队把不变量清单写得像法律条文一样厚每个函数都要配前因后果最终的结果是开发效率归零AI也没办法在这种重重约束下发挥能力。我的原则是“约束关键路径放开普通逻辑”。只有那些真正涉及核心数据一致性、资金状态、用户数据安全、系统可用性的关键路径才值得投入精力做契约化约束。其余那些无状态的计算、展示逻辑、常规CRUD放心让AI去发挥就好。这也是Vibe Coding和Formal Method共存的真正姿势不是互相消灭而是分工协作。AI提供速度和发散性形式化思维提供确定性和边界感。前者让代码写得快后者让代码靠得住。记住一个简单的判断标准——如果一行约束能拦住一类生产事故它就值得写如果它只是为了让代码“看起来更规范”那就删掉。我在实际操作中养成的一个小习惯或许可以从侧面说明这套体系的价值现在我开一个新模块第一件事不是让AI直接写业务实现而是先把接口契约、不变量和状态迁移写到一个单独的设计说明文件里然后让AI严格照着它实现。等于是让AI在一个我画好的逻辑笼子里跳舞。这个习惯最初只是为了管住AI后来我发现它也顺带治好了我自己“需求没想清楚就动手”的老毛病——因为如果我自己都不知道这道业务的不变量是什么我根本没法给AI写出约束清单。这大概就是Vibe Coding和Formal Method相遇之后最有意思的化学反应那些原本为了约束AI而被迫写下来的规格说明最后成了人对业务理解的一场系统性复盘。代码的质量体系修好了人对业务的认知也偷偷升了一级。
阅读完成 · 觉得有帮助?
咨询建站