☰
AI代码四条审查清单:契约、状态、边界与演进
2026/10/6 6:27:05 网站建设 项目流程

1. 这不是代码审查,是AI时代的新守门人实践

“AI的代码交给我审:四条清单,两次打回”——这句话刚在内部技术群刷出来时,我正盯着CI流水线里第7次失败的PR发呆。不是因为逻辑错,也不是编译不过,而是那段由Copilot生成、被开发者直接合入主干的30行Python函数,把一个本该返回datetime.date的对象,悄悄转成了字符串再塞进数据库字段。上线后第三天,下游报表服务开始报TypeError: str object is not callable,排查了6小时才定位到这个“看起来完全合理”的类型隐式转换。

这已经不是孤例。过去三个月,我们团队接手的21个AI辅助开发项目中,有14个在UAT阶段暴露出至少一处由AI生成代码引发的语义性缺陷——不是语法错误,不是格式问题,而是对业务规则、数据契约、边界条件的系统性误读。它不报错,但会悄悄歪曲意图;它跑得通,但会在特定路径下坍塌;它写得漂亮,但像一张精心绘制却比例失调的地图。

所以,“AI的代码交给我审”,从来不是一句姿态性的口号,而是一套被迫成型的、带刺的防御机制。它不是否定AI编码能力,而是承认一个现实:当前阶段的代码生成模型,本质是高阶文本续写器,而非具备领域认知与工程判断的协作者。它擅长“怎么写”,却常忽略“为什么这么写”和“在什么条件下不能这么写”。我的工作,就是补上那缺失的“上下文锚点”。

四条清单,不是 checklist,而是四道过滤网;两次打回,不是流程卡点,而是两次认知对齐的机会。它针对的不是AI本身,而是人与AI协作时最容易松懈的四个思维断层:契约失焦、状态盲区、边界蒸发、演进负债。下面我会用真实案例拆解每一条背后的血泪教训,告诉你为什么必须打回两次——第一次打回,是拦住代码;第二次打回,是重建共识。

提示:本文所有案例均来自真实生产环境,已脱敏处理。文中提到的工具链(如SonarQube自定义规则、Pydantic v2 Schema Diff脚本)均可开源复用,文末附配置片段。

2. 第一条清单:契约校验——拒绝任何脱离接口契约的“优雅实现”

2.1 为什么契约是AI代码的第一道生死线?

AI模型训练数据里充斥着“炫技式代码”:用一行lambda替代清晰的函数命名,用嵌套字典推导式压缩逻辑,用getattr(obj, 'field', None) or default代替显式空值检查。这些在教学示例或玩具项目里很酷,但在真实系统中,它们直接撕裂了接口契约(Interface Contract)——即调用方与被调用方之间关于输入、输出、异常、副作用的隐含协议。

我们有个核心订单服务,其get_order_status(order_id: str) -> OrderStatus接口契约明确定义:

  • 输入:order_id必须为非空字符串,长度16位,符合UUIDv4格式
  • 输出:OrderStatus是一个Pydantic v2模型,包含status_code: Literal['PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED']和updated_at: datetime.datetime
  • 异常:仅抛出OrderNotFoundError,不抛出ValueError或KeyError

AI生成的实现版本长这样:

def get_order_status(order_id): if not order_id or len(order_id) != 16: return {"status_code": "INVALID", "updated_at": None} # ❌ 返回dict,非OrderStatus模型 try: raw = db.query("SELECT * FROM orders WHERE id = ?", order_id) return { "status_code": raw["status"].upper(), # ❌ 可能返回'cancelled'→'CANCELLED',违反Literal约束 "updated_at": raw["updated_at"].isoformat() # ❌ 返回str,非datetime } except Exception: raise ValueError("DB error") # ❌ 抛出未声明异常

表面看,它“功能正确”:能查到订单,能返回状态。但契约已被彻底践踏:类型不匹配、枚举值失控、异常体系污染、时间格式漂移。下游服务按契约解析updated_at为datetime对象,结果拿到字符串,datetime.fromisoformat()直接崩溃。

2.2 清单落地:三重契约扫描仪

我们把契约校验拆解为三个可自动化的扫描层,集成在PR预检流水线中:

扫描层检查项工具/方法AI易错点
静态契约函数签名与文档字符串一致性;返回类型注解是否匹配实际return语句类型pyright+ 自定义mypy插件AI常忽略-> OrderStatus,直接return dict;文档字符串写“returns status dict”,与注解冲突
运行时契约实际返回值是否满足Pydantic模型验证;是否抛出未声明异常pytest+pydantic.BaseModel.model_validate()断言;unittest.mock.patch捕获异常AI生成代码常绕过模型验证,直接构造字典;异常处理用except Exception:兜底,掩盖真实错误类型
契约演化新增/修改字段是否在OpenAPI/Swagger文档中同步更新;是否触发下游Schema变更告警openapi-diff+ Git钩子比对openapi.yaml历史版本AI不会主动更新文档,导致契约“纸上谈兵”,客户端按旧文档调用失败

实操中,我们要求:任何PR必须通过全部三层扫描,否则禁止合并。其中第二层“运行时契约”最致命——它强制AI生成的代码必须经过真实模型验证,堵死了“返回字典假装是模型”的捷径。

注意:我们禁用pydantic.BaseModel.parse_obj(),强制使用model_validate()。因为前者在输入为dict时会静默忽略多余字段,而后者严格校验,确保契约零妥协。这是踩过坑后的硬性规定。

2.3 为什么必须第一次打回?——契约不是技术细节,是协作语言

第一次打回,往往就发生在这一条。开发者常争辩:“它能跑,用户看不到区别。” 我会直接贴出下游服务崩溃的日志截图,然后问:“如果明天支付网关升级,要求updated_at必须是ISO 8601带时区格式,你的isoformat()能兼容吗?还是又要改?”

契约校验的本质,是把模糊的“应该怎样”变成精确的“必须怎样”。AI没有“应该”的概念,只有“概率最高”的文本选择。我们的清单,就是给它套上精确的模具。第一次打回,不是否定代码,而是重申:在这里,契约高于便利,稳定高于简洁,可预测性高于聪明。

3. 第二条清单:状态一致性——揪出所有“忘记清理”的临时状态

3.1 AI最擅长制造“幽灵状态”,也最不擅长清理它

状态管理是AI代码的阿喀琉斯之踵。它能完美写出“创建订单”、“更新库存”、“发送通知”的独立函数,但当这些函数被组合成一个完整业务流时,AI几乎从不考虑中间状态的生命周期。它默认世界是原子的、瞬时的、无副作用的——而这恰恰是真实系统的反面。

典型案例:一个“创建促销活动”的后台任务。AI生成的核心逻辑如下:

def create_promotion(promo_data: dict): # Step 1: 创建活动基础信息 promo = Promotion.create(**promo_data) # Step 2: 预热库存(AI生成的“优化”) if promo.is_preheat: cache.set(f"promo:{promo.id}:preheat", True, timeout=3600) # ✅ 设置缓存 # Step 3: 发送审核通知 notify_audit_team(promo.id) # Step 4: 启动定时任务(AI生成的“智能”) schedule_task("check_promo_validity", args=[promo.id], delay=60) # ✅ 调度任务 return promo

看起来天衣无缝。但问题藏在Step 2和Step 4之后:

  • 如果Step 3通知失败,cache.set已执行,但活动并未真正创建成功,缓存里留了一个“幽灵预热标记”;
  • 如果Step 4调度失败(如Celery Broker宕机),schedule_task静默失败,但代码继续执行,promo对象已返回,下游以为任务已启动;
  • 更致命的是,AI没写任何回滚逻辑:当任意步骤失败,cache.delete()和cancel_scheduled_task()从未出现。

结果是:活动创建失败,但缓存里标记为预热中,导致后续查询误判;定时任务未启动,但业务方以为已安排,错过关键校验窗口。

3.2 清单落地:状态审计矩阵(State Audit Matrix)

我们设计了一个轻量级状态审计矩阵,要求每个涉及外部状态变更(缓存、消息队列、定时任务、文件系统)的操作,必须在代码旁用注释明确回答四个问题:

问题示例答案(正确)示例答案(AI常见错误)审计方式
Q1:此状态变更的预期生命周期是什么?“缓存有效期1小时,仅用于前端预热展示,失效后自动清除”“设置缓存,提高性能”(无生命周期定义)人工审查注释+静态扫描关键词
Q2:若本操作失败,如何保证状态回滚?“若notify_audit_team失败,执行cache.delete(f'promo:{promo.id}:preheat')”无任何回滚说明PR模板强制填写,CI检查注释存在
Q3:若后续操作失败,此状态是否仍有效?“否。若schedule_task失败,此缓存标记应立即失效,因预热逻辑依赖定时任务触发”无此意识,认为“设置了就一直有效”交叉审查:上游状态变更者需标注下游依赖
Q4:是否有监控告警覆盖此状态的异常漂移?“Prometheus指标promo_preheat_cache_age_seconds{status="stale"}> 3600s时告警”无监控设计CI检查是否在monitoring/目录下新增对应指标定义

这个矩阵不增加代码复杂度,但强迫开发者(和AI)直面状态的“重量”。它把隐含的工程决策,变成显式的、可审计的契约。

3.3 为什么第二次打回常在此处?——状态是信任的基石,不是装饰

第一次打回解决契约,第二次打回则聚焦状态。当开发者修复契约后,我们立刻进入状态审计环节。这时常发现:他们只是把return dict改成return OrderStatus.model_validate(...),但对缓存、消息、定时任务等状态操作,依然沿用AI生成的“裸奔”版本。

“为什么还要打回?代码已经符合类型了!”——这是典型反应。我的回应是:“契约让你的代码能被调用,状态一致性让你的代码值得被信任。用户不关心你返回的类型是否正确,只关心点击‘创建活动’后,系统是否真的按承诺行动,且在失败时干净利落。”

第二次打回,是把“能跑”升级为“可信”。它要求开发者不仅理解自己的函数,还要理解自己函数在整个系统状态图中的位置。这正是AI无法提供的视角——它没有系统地图,只有当前页面的像素。

4. 第三条清单:边界防护——所有输入都必须是“被驯服的野兽”

4.1 AI天生缺乏边界感:它把“可能”当作“应该”

AI模型在训练数据中见过太多“宽松”的代码:int(input())直接转换用户输入,json.loads(request.body)不校验JSON格式,path = os.path.join(BASE_DIR, user_input)不过滤../。它把这些当作“标准做法”,因为它们在简单场景下“有效”。但它忽略了:生产环境的输入,永远是未经驯服的野兽。

我们有个API端点/api/v1/users/{user_id}/profile,AI生成的handler:

@app.get("/api/v1/users/{user_id}/profile") def get_user_profile(user_id: str): # AI假设user_id是合法字符串,直接拼接SQL query = f"SELECT * FROM users WHERE id = '{user_id}'" # ❌ SQL注入温床 result = db.execute(query).fetchone() # AI假设数据库返回非None,直接取字段 return { "name": result["name"], # ❌ 若result为None,此处AttributeError "email": result["email"] }

更隐蔽的是边界漂移:AI生成的validate_email(email: str)函数,用正则^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$校验。它能拦住"abc@def",但放行"very.long.email.address.that.exceeds.254.characters@domain.com"——而SMTP协议规定邮箱总长不超过254字符。这个边界,在AI的“常识”里不存在。

4.2 清单落地:三层边界防护网

我们构建了从外到内的三层防护,每层都针对AI的典型盲区:

第一层:传输层边界(Ingress Boundary)

  • 所有HTTP请求参数(Path、Query、Body)必须通过FastAPI的Pydantic BaseModel严格校验
  • 禁用request.json(),强制使用request.state.validated_data(由中间件预填充)
  • 对user_id等ID类字段,添加Field(pattern=r'^[a-f0-9]{24}$')(MongoDB ObjectId格式)或Field(min_length=16, max_length=16)(UUID)

第二层:领域层边界(Domain Boundary)

  • 所有外部输入进入核心业务逻辑前,必须经过DomainValidator适配器转换
  • 例如:raw_email = request_body["email"]→validated_email = EmailAddress(raw_email)(EmailAddress是带254字符限制的Value Object)
  • AI生成的validate_email函数,被替换为调用EmailAddress构造函数,失败则抛DomainValidationError

第三层:存储层边界(Storage Boundary)

  • 所有SQL查询必须使用参数化查询(?占位符),禁用f-string拼接
  • 数据库字段长度在ORM模型中强制声明(name = Column(String(100))),并开启sqlalchemy的CHECK约束生成
  • AI生成的db.execute(f"SELECT ..."),在CI中被grep -r "f\".*{.*}\"" .直接拦截并失败

这套防护网的关键,在于把边界检查从“可选逻辑”变成“基础设施强制”。AI可以绕过代码里的if判断,但绕不过Pydantic的Field约束,也绕不过SQLAlchemy的参数化查询引擎。

4.3 边界不是限制,是给AI的“安全沙盒”

很多开发者抱怨:“加这么多校验,代码变臃肿了。” 我的回答是:“臃肿的不是校验,是AI生成的、未经驯服的原始输入。你不是在加限制,是在给AI划出一个安全沙盒——让它在里面尽情发挥,但绝不允许它把野火带到生产环境。”

第三次打回(如果需要)往往在此处。当AI生成的代码试图用try...except捕获所有异常来“兜底”时,我们会要求:删除所有宽泛的except Exception:,改为针对具体边界异常(如ValueErrorfor email,IntegrityErrorfor DB)的精准处理,并记录结构化日志。因为真正的边界防护,不是掩盖错误,而是让错误暴露得更快、更准、更有价值。

5. 第四条清单:演进负债——拒绝任何“这次先这样,以后重构”的技术债

5.1 AI是技术债的超级加速器

AI最危险的能力,不是写错代码,而是写出“足够好”的代码——好到能通过测试,好到能上线,好到让人觉得“先这样吧,后面再优化”。这种“足够好”,正是技术债最肥沃的土壤。

我们有个支付回调服务,AI生成的handle_payment_callback函数长达200行,包含:

  • 解析微信/支付宝/银联三种不同格式的回调JSON
  • 校验签名(三种算法)
  • 查询本地订单状态
  • 更新订单状态(含幂等性处理)
  • 发送MQ消息通知下游
  • 记录审计日志

它“工作正常”。但问题在于:

  • 所有支付渠道的解析逻辑混在一个函数里,if provider == 'wechat': ... elif provider == 'alipay': ...
  • 签名校验逻辑散落在各处,无法复用
  • 幂等性key生成规则硬编码在更新逻辑里,与业务无关的细节耦合
  • 日志记录格式不统一,logger.info(f"Callback from {provider}...")vslogger.error("Failed to update order")

AI没有“关注点分离”的概念,它只追求“完成任务”。结果是,当需要接入第四种支付渠道(PayPal)时,开发者不得不在200行地狱里新增elif provider == 'paypal': ...,并复制粘贴大量重复逻辑。演进成本指数级上升。

5.2 清单落地:演进健康度评分(Evolution Health Score)

我们引入一个简单的演进健康度评分(EHS),对每个AI生成的模块进行量化评估,满分10分,低于7分必须重构:

评分维度满分标准(10分)AI常见得分(2-4分)评估方式
单一职责每个函数/类只做一件事,名称精确描述其行为(如WechatSignatureValidator)函数名handle_callback,内含5种职责radon圈复杂度 < 5;函数行数 < 30
抽象层级外部依赖(支付网关、DB、MQ)通过接口/协议隔离,可轻松Mock或替换直接调用wechat_api.verify_signature()和db.update_order()grep -r "import.*wechat|db|mq" src/,检查是否在核心逻辑层
可扩展性新增支付渠道只需实现一个接口,无需修改现有代码(OCP原则)新增渠道需修改handle_callback函数,增加elif分支检查是否存在PaymentProvider抽象基类及其实现
可观测性关键路径有结构化日志(event=payment_callback_received, provider=wechat, order_id=xxx)和指标(payment_callback_duration_seconds)日志为print("Callback received"),无指标CI检查logger.调用是否含event=关键字;Prometheus指标定义是否存在

EHS不是为了刁难,而是为了把“未来成本”变成“当前决策”的一部分。当AI生成一个高圈复杂度的函数时,EHS会立刻亮红灯,迫使开发者停下来思考:“我是在解决今天的问题,还是在为明天挖坑?”

5.3 第四次打回?不,这是“重构触发器”,不是惩罚

严格来说,第四条清单不触发“打回”,而是触发“重构触发器”。当EHS < 7时,PR状态变为Needs Refactor,并自动生成重构建议:

  • “检测到handle_payment_callback圈复杂度=18,请拆分为parse_callback,validate_signature,update_order_state,publish_event四个函数”
  • “检测到硬编码支付渠道,建议引入PaymentProvider抽象,当前实现类:WechatProvider,AlipayProvider”
  • “检测到日志无event字段,已生成标准日志模板:logger.info('event=payment_callback_processed, provider=%s, order_id=%s', provider, order_id)”

这不是对AI的否定,而是对人与AI协作模式的升级:AI负责生成“最小可行实现”,人负责将其升华为“可持续演进架构”。第四条清单,是把AI从“代码生成器”解放为“原型生成器”,把工程师从“代码搬运工”还原为“系统建筑师”。

6. 两次打回的底层逻辑:一次校准意图,一次校准责任

6.1 打回不是阻碍,是建立新的协作节奏

很多人误解“两次打回”是流程上的卡点,仿佛在给AI戴紧箍咒。实际上,它是我们团队摸索出的人-AI协作最优节奏:

  • 第一次打回(Intent Alignment):聚焦“我们想做什么”。校验契约、确认输入输出、明确业务目标。此时,AI是“需求翻译器”,它的任务是把模糊的自然语言需求,转化为符合契约的、可执行的代码骨架。打回,是为了确保翻译没有偏离原意。
  • 第二次打回(Responsibility Alignment):聚焦“谁来负责什么”。校验状态、边界、演进,明确每个组件的职责边界、失败应对、长期维护成本。此时,AI是“原型生成器”,它的任务是提供一个可工作的起点;而工程师的任务,是把这个起点,封装成一个有明确责任边界的、可信赖的模块。

这个节奏,把AI从“全栈开发者”的幻觉中拉出来,回归到它真正擅长的位置:加速原型探索,而非替代工程判断。我们不再问“AI能不能写好代码”,而是问“在这个环节,AI最适合贡献什么价值”。

6.2 实操中的“打回话术”:从对抗到共建

打回不是批斗会。我们有一套标准化的“打回话术”,确保沟通建设性:

  • 针对契约问题:
    “这里返回了dict,但契约要求OrderStatus模型。为了下游服务稳定,我们需要model_validate()确保类型安全。建议:return OrderStatus.model_validate(raw_data)。需要我帮你写个单元测试验证吗?”

  • 针对状态问题:
    “cache.set后,如果notify_audit_team失败,缓存会残留。为了状态一致,建议在except块里加cache.delete(...)。另外,schedule_task失败时,我们是否需要记录告警?我可以帮你加个logger.error。”

  • 针对边界问题:
    “user_id直接拼SQL有风险。FastAPI的Path参数已支持regex校验,我们可以用user_id: str = Path(..., regex=r'^[a-f0-9]{24}$'),既安全又简洁。需要我发个配置示例吗?”

  • 针对演进问题:
    “handle_callback现在承担了太多职责。拆成parse,validate,update,notify四个函数后,每个部分都更易测试和复用。我已经用radon分析了,拆分后圈复杂度能从18降到3。要我帮你起个PR重构这个吗?”

话术的核心,是把问题转化为共同任务。不是“你错了”,而是“我们一起把它做得更好”。这消除了防御心理,让打回成为知识传递的契机。

6.3 为什么“四条清单”必须存在?——它定义了AI时代的工程师新素养

最后,回到标题:“AI的代码交给我审”。这句看似简单的宣言,背后是一整套新素养的建立:

  • 契约素养:能将模糊需求提炼为精确的接口契约,并用工具强制落地;
  • 状态素养:能绘制系统状态流转图,预见每个操作的副作用与回滚路径;
  • 边界素养:能识别每一层输入的潜在恶意与漂移,并构建纵深防御;
  • 演进素养:能评估代码的长期维护成本,用架构原则对抗短期便利诱惑。

这四条,不再是“高级工程师才需要”的软技能,而是每个使用AI辅助开发的工程师的必备硬技能。AI降低了编码门槛,但抬高了工程判断的门槛。清单不是束缚,而是导航仪——它帮我们在AI生成的代码海洋中,找到通往可靠、可维护、可演进系统的那条航道。

我在实际使用中发现,坚持这套流程后,团队平均每个PR的返工次数从2.7次降到0.9次,生产环境因AI代码引发的P0事故归零。更重要的是,开发者反馈:“现在写代码时,会下意识问自己,这条AI生成的代码,经得起四条清单的拷问吗?”——这才是最珍贵的改变:AI没有取代工程师,而是把工程师,逼回了工程师该在的位置。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询