AI时代的程序员修养:接手 AI 写的代码,先别急着骂

发布时间:2026/7/21 12:11:56
AI时代的程序员修养:接手 AI 写的代码,先别急着骂 《AI时代的程序员修养》前面两篇讲了测试和可观测性。测试让结果可复现观测让系统出事时可定位。这一篇讲更现实的场景你接手了一段 AI 写出来的代码已经能跑甚至已经有人在用。但它像一团线函数很长边界不清异常处理玄学改一处容易带出三处问题。这时先别急着骂。骂没有产出。真正有产出的是把它拆开、钉住、清理、重构让它从“能演示的原型”变成“能继续维护的代码”。不要一上来大改接手烂代码最危险的动作是立刻重写。AI 生成代码尤其如此。它可能有很多问题但也可能藏着一些已经被用户验证过的业务细节。你觉得多余的分支可能是在绕某个真实线上坑你觉得奇怪的字段可能被前端或者脚本依赖你觉得可以合并的函数可能实际上承担了不同副作用。所以第一步不是重构而是冻结行为。冻结行为的意思是先确认它现在到底做了什么再决定哪些要保留、哪些要删除、哪些要重写。一个简单原则没有测试保护的重构不叫重构叫赌博。先跑起来拿到基线接手 AI 代码先做三件事。能启动 能测试 能复现关键路径不要急着整理目录也不要急着改命名。先让项目在本地跑起来记录启动命令、环境变量、依赖版本和最小数据集。例如一个报告生成服务可以先建立这样的基线uv sync uv run pytest uv run uvicorn app.main:app --host 0.0.0.0 --port 8000再准备一组最小请求curl -X POST http://localhost:8000/reports \ -H content-type: application/json \ -d {tool_id:demo_tool,query:最近一周数据,format:summary}如果这些都跑不起来先补运行文档和 fixture。一个不能稳定启动的项目谈不上重构。AI 生成代码常见的问题是本地能跑一次但依赖隐含在全局环境里。接手时要把这些隐含条件显式化.env.example、测试数据、启动脚本、数据库迁移、外部服务 mock。用回归测试钉住现状AI 代码不可靠不能只靠读。读代码只能告诉你作者想表达什么测试才能告诉你系统实际承诺什么。接手阶段的测试不追求优雅先追求保护关键行为。比如报告服务至少要有这些回归测试def test_create_report_returns_queued(client): response client.post( /reports, json{tool_id: demo_tool, query: hello, format: summary}, ) assert response.status_code 200 assert response.json()[status] queued assert report_id in response.json() def test_create_report_rejects_unknown_tool(client): response client.post( /reports, json{tool_id: missing, query: hello, format: summary}, ) assert response.status_code 400 assert response.json()[error_code] TOOL_NOT_FOUND这类测试不一定完美但它能先保护入口行为。然后再补 bug 样例、边界样例和失败样例空 query 怎么处理 超长 query 怎么处理 重复提交是否幂等 第三方超时是否进入重试 权限不足是否返回稳定错误码 结果保存失败是否会重复扣费接手 AI 代码时测试不是为了证明你写得好而是为了防止你改坏现有行为。画出模块图很多 AI 代码难维护不是因为语法差而是因为边界糊。一个函数里可能同时做了参数校验、权限检查、数据库查询、第三方调用、结果格式化、扣费、日志、埋点。它能跑但任何改动都可能牵一大片。接手时可以先画一个粗模块图API Layer - Auth / Permission - Use Case / Application Service - Domain Rules - Repository - Provider Client - Queue / Worker - Billing / Audit这不是为了套架构名词而是为了判断职责在哪里。更具体一点API 层只处理 HTTP、schema、错误响应。 Use Case编排业务流程例如创建报告、取消报告。 Domain Rules纯业务规则例如状态机、金额、权限策略。 Repository数据库读写不包含业务决策。 Provider Client外部接口调用包含超时、重试、错误映射。 Worker异步执行不直接依赖 HTTP 上下文。 Audit/Billing关键副作用必须可追溯、可幂等。AI 生成代码常把这些揉成一个大函数。重构的目标不是“分层好看”而是让每个改动只影响它该影响的地方。看依赖方向判断代码是否难维护一个很实用的方法是看依赖方向。理想情况下核心业务规则应该少依赖外部东西。它可以被单测直接调用不需要启动数据库、HTTP 服务或第三方 SDK。例如def can_cancel_report(status: str, owner_id: str, actor_id: str) - bool: if actor_id ! owner_id: return False return status in {queued, running}这类函数最好是纯函数。输入明确输出明确没有数据库没有网络没有时间读取没有随机数。反过来下面这种代码就难测async def cancel_report(report_id: str, actor_id: str): report await db.get(report_id) user await auth.current_user() if user.id ! report.owner_id: await logger.warning(permission denied) return False await provider.cancel(report.provider_job_id) await db.update(report_id, {status: cancelled}) await send_notification(user.id) return True它混合了数据库、认证、权限规则、外部调用、状态变更和通知。要测试一个简单权限规则也要拖进一堆依赖。接手时可以先把纯规则抽出来def decide_cancel_report(report: Report, actor: Actor) - CancelDecision: if actor.id ! report.owner_id: return CancelDecision(allowedFalse, reasonnot_owner) if report.status not in {queued, running}: return CancelDecision(allowedFalse, reasoninvalid_status) return CancelDecision(allowedTrue, reasonok)外层再处理副作用decision decide_cancel_report(report, actor) if not decision.allowed: raise PermissionDenied(decision.reason) await provider.cancel(report.provider_job_id) await repository.mark_cancelled(report.id) await audit.record(...)这一步通常是 AI 代码从“能跑”变成“能测”的关键。圈复杂度和函数长度不是玄学重构时可以用一些很朴素的指标做雷达。函数太长、分支太多、嵌套太深通常意味着它承担了太多职责。圈复杂度可以粗略理解为代码里有多少条独立路径。if、for、while、except、逻辑运算都会增加路径数量。路径越多测试覆盖越难修改风险越高。比如def normalize_result(data, mode, user_type, fallback): if mode summary: if user_type vip: ... else: ... elif mode table: if fallback: ... else: ... elif mode chart: ... else: ...这类函数不一定错但很容易变成分支垃圾场。接手 AI 代码时可以先找这些信号单个函数超过 80-100 行 嵌套超过 3 层 一个函数里出现多种外部依赖 异常处理分散在多个层级 同一个状态字段被多个模块随手改 大量布尔参数控制行为这些不是绝对规则但足够帮你找风险点。扇入扇出决定改动风险还有两个很实用的概念扇入和扇出。扇入是“有多少地方调用我”。扇出是“我调用了多少东西”。高扇入很多地方依赖它改它要小心。 高扇出它依赖很多东西通常职责过重。 高扇入 高扇出危险核心先补测试再动。例如一个execute_tool()函数被 API、worker、定时任务、脚本都调用同时内部又访问数据库、缓存、第三方接口、扣费、日志、埋点。这种函数就是重构雷区。处理方法不是一刀切重写而是先加保护层1. 给现有 execute_tool 写黑盒回归测试。 2. 记录输入输出和关键副作用。 3. 把内部纯规则逐步抽出。 4. 把外部依赖包成接口。 5. 新逻辑走新路径旧逻辑保留一段时间。这就是小步重构。先隔离副作用AI 代码最大的问题之一是副作用到处飞。副作用包括写数据库 发 HTTP 请求 发消息队列 扣费 发通知 写文件 读当前时间 读随机数 写审计这些操作不是不能有而是不能散落在任何地方。一个可维护的流程通常应该把“决策”和“执行副作用”分开def plan_report_execution(request: ReportRequest) - ExecutionPlan: # 纯决策选择 provider、检查模式、计算预算、生成步骤 return ExecutionPlan(...) async def execute_report_plan(plan: ExecutionPlan, deps: Dependencies) - ReportResult: # 副作用集中在这里调用 provider、写数据库、写审计 ...这样做有两个好处。纯决策可以大量单测跑得快也不依赖外部环境。副作用集中后可以统一处理超时、重试、幂等、错误映射和审计。给 AI 的提示词里要明确这一点请把业务决策写成纯函数不要在纯函数里访问数据库、网络、当前时间或随机数。 外部副作用通过明确的 dependency interface 注入。删代码比改代码更重要AI 很擅长生成“看起来有用”的代码。备用分支、未使用参数、过度抽象、未来可能用到的扩展点、重复的错误包装、没人调用的 helper都可能堆在项目里。接手时要敢删但要有证据。可以按这个顺序处理搜索调用点确认没有引用。 跑测试和类型检查。 看构建产物或路由注册。 如果是 API 或脚本入口确认没有外部调用。 删除后保留一次独立提交方便回滚。有些代码不能只靠本仓库搜索判断例如被定时任务、外部脚本、用户 webhook 调用的入口。这类要先标记 deprecated打日志观察一段时间再删除。删除代码的收益很直接上下文变短AI 更不容易被旧逻辑带偏人 review 也更轻。重构要按接缝切重构不是把文件重新摆一遍。好的重构会沿着接缝切。接缝就是系统里天然可以分开的地方接口、数据库访问、外部 provider、队列任务、状态机、权限策略。例如一个混乱的执行函数execute() - parse input - check permission - select provider - call provider - normalize result - save history - bill user - emit telemetry可以先切成parse_execute_request() authorize_execute() select_provider() call_provider() normalize_provider_result() record_execute_history() charge_usage() emit_execute_events()注意不是每个函数都要变成一个类。很多时候几个清楚的函数就够了。AI 生成代码经常过度类化。你让它“优化架构”它可能立刻生成一堆 manager、handler、service、factory。接手时要压住这个冲动先把边界切清楚再决定是否需要抽象。用 Strangler Fig 迁移大块逻辑如果旧逻辑太大不适合一次性重写可以用 Strangler Fig 的思路新逻辑一点点包住旧逻辑逐步替换。做法大概是保留旧入口。 在入口前加一个 router。 小流量或指定场景走新实现。 新旧结果并行比较一段时间。 确认一致后扩大范围。 确认稳定后删除旧实现。伪代码async def execute(request: ExecuteRequest) - ExecuteResult: if should_use_new_executor(request): return await new_executor.execute(request) return await legacy_executor.execute(request)如果是纯计算逻辑还可以影子执行legacy_result await legacy_executor.execute(request) if shadow_enabled(request): new_result await new_executor.execute_shadow(request) compare_and_record(legacy_result, new_result) return legacy_result这种方法适合大系统。它慢但风险小。AI 代码越是已经上线越不应该用“我重写一版肯定更好”的心态处理。每次重构都要能解释收益重构不是审美活动。每次重构最好能说清楚收益把纯规则抽出来单测从集成测试降为毫秒级。 把 provider client 封装起来外部错误映射统一。 把扣费逻辑移到独立模块避免重复扣费。 删除未使用分支减少 400 行无效上下文。 拆开大函数让权限、执行、记录三类失败可以分别定位。如果说不清收益就先别动。工程里有一种常见错误把“不喜欢的代码”都叫技术债。真正的技术债是它会影响交付速度、稳定性、成本或团队理解。否则只是个人偏好。接手 AI 代码尤其要克制。AI 可能写得不符合你的习惯但只要边界清楚、测试充分、可观测性完整也未必需要马上重写。Code Review 要看行为不只看风格审查 AI 代码不要只盯格式。格式化工具能解决的东西不值得人花太多时间。人应该看这些业务规则是否明确 失败场景是否完整 事务边界是否正确 副作用是否幂等 外部调用是否有超时 错误码是否稳定 日志指标是否足够 敏感数据是否泄露 测试是否覆盖关键路径 模块依赖是否反向AI 生成代码最容易在“看起来合理”的地方出问题。比如它会捕获所有异常然后返回成功会在数据库事务里调用慢接口会把用户输入原样写进日志会在重试时重复写历史。这些才是 review 的重点。一份接手 AI 代码的检查清单可以把下面这份清单放进项目里1. 项目能否一条命令启动。 2. 是否有最小 fixture 和本地测试数据。 3. 核心入口是否有回归测试。 4. 已知 bug 是否有失败测试。 5. 大函数是否承担多个职责。 6. 核心规则是否可以脱离数据库和网络测试。 7. 外部副作用是否集中处理。 8. 事务内是否存在慢外部调用。 9. 重试是否幂等。 10. 错误码、日志、指标、Trace 是否完整。 11. 未使用代码是否可以删除。 12. 高扇入高扇出函数是否有保护测试。 13. 新旧实现是否需要灰度或影子比较。这份清单不是一次性做完。它是接手顺序。给 AI 的重构提示词重构也可以让 AI 帮忙但提示词要限制它的动作请先不要直接重写代码。 请审查这段代码并输出 1. 当前代码的主要职责列表。 2. 入口函数、外部副作用、数据库写入、网络调用和队列操作。 3. 高风险函数函数长度、分支数量、扇入扇出、是否混合副作用。 4. 现有行为还缺少哪些回归测试。 5. 可以先抽出的纯业务规则。 6. 可以删除的未使用代码但必须说明证据。 7. 一个小步重构计划每一步都要求测试通过。 8. 哪些地方不能直接改需要灰度或影子比较。 在我确认前不要生成最终实现。这段提示词的重点是让 AI 先分析不要上来就“优化完成”。AI 很适合做代码地图、调用链梳理、重复逻辑识别、测试样例生成。真正的边界判断、上线风险和取舍仍然要由人负责。原型不是罪没人维护才是很多 AI 代码一开始都是原型。原型本身不是问题。工程里很多好东西都是从原型长出来的。问题是把原型直接当长期资产却没有测试、没有边界、没有观测、没有删除无效代码。接手 AI 代码时最好的态度不是轻蔑也不是迷信。它能跑说明它有价值。它乱说明它还没完成工程化。程序员要做的就是把价值留下把风险拆掉。这件事听起来不酷但很重要。因为未来很多系统都会带着 AI 生成的痕迹。会接手、会判断、会清理、会小步重构的人会比只会抱怨“这代码谁写的”的人更有用。