告别无脑AI编程:建立可控、可审查、可维护的开发流程
“I‘m done coding with AI”这句话最近在开发者圈子里越来越常见。很多人一开始使用 Cursor、Copilot、通义灵码这类 AI coding 工具时觉得效率翻倍但用了一段时间后又遇到了代码质量失控、业务逻辑被“自作主张”改坏、依赖版本越升级越乱等问题。于是一部分开发者开始喊出“我不再用 AI 写代码了”。但事实上完全回到纯手写代码并不现实AI 编程已经成为日常开发的一部分。关键在于我们要搞清楚 AI coding 的能力边界、容易出现的问题以及怎样把它纳入一套可控制的工程流程。这篇文章就围绕“AI 编程”这个话题从实际开发视角梳理 AI 辅助编码的适用场景、踩坑点、一个完整的实战修复案例以及工程上的使用建议。整个过程不需要你有多深的 AI 基础只要会写 Python 和 Java能看懂接口和数据库就能跟着文章走一遍。文章偏实战重点不是“AI 能不能替代程序员”而是“如何让 AI 生成代码的质量可控、可审查、可维护”。1. 背景与核心概念AI 编程火了但问题也来了1.1 什么是 AI codingAI coding 指的是使用大语言模型LLM来辅助完成代码编写、补全、重构、测试和排查问题的过程。目前常见的落地形式包括编辑器内的代码补全工具例如 GitHub Copilot、通义灵码、CodeGeeX。对话式编程助手例如 ChatGPT、Claude、文心一言等可以直接通过对话生成代码片段。从需求直接生成代码的“Agent”类工具例如 Cursor 的 Agent 模式、各类 Coding Plan 服务这类工具可以读取项目目录、自动修改多个文件、执行命令甚至帮你提交代码。从工作方式上看AI coding 可以分为三个层次层次工具形态典型能力开发者参与度补全层IDE 插件根据上下文提示下一行或下一个函数高逐行确认对话层聊天窗口根据描述生成函数、脚本、测试用例中复制后自行修改Agent 层自动化工具自动读取项目、修改文件、执行命令低但需要强审查不管是哪个层次AI 编程的本质都是“概率性文本生成”。它并不理解你的业务也不理解你的系统架构它只是在海量代码样本的基础上预测最有可能出现在当前位置的代码片段。这个本质决定了 AI 生成的代码一定需要人工审查和修正。1.2 Vibe Coding 为什么让人既兴奋又担心Vibe Coding 指的是开发者只描述大致诉求让 AI 连续生成大段代码开发者跟着“感觉”往前走的一种编码方式。它很爽因为需求到代码的路径变得极短。但问题也很明显生成的代码表面上看没有语法错误但可能存在逻辑漏洞。AI 会使用它“见过”的 API但这些 API 可能已经过时甚至根本不存在。代码风格不稳定同一个项目中会出现多种写法和命名习惯。自动化程度越高开发者对代码的理解越弱后期维护成本越大。所以“I‘m done coding with AI”这句吐槽本质上不是否定 AI 编程本身而是表达了一种对不可控代码生成方式的厌倦。作者并不是说以后不碰 AI而是说不再让 AI 直接替自己“瞎写”。1.3 为什么我们需要重新审视 AI 编程从团队管理的角度看AI 编程最大的风险不是写不出代码而是写出了看似正确但无法维护的代码。一个 500 行的 AI 生成文件如果没有经过人工评审就合并进主干后续排错可能需要 5000 行代码的排查量。在业务迭代中AI 生成的代码往往会遇到三类问题依赖与版本混乱。AI 可能会给出过时的包名或 API在你的环境里根本编译不过。业务规则被忽略。AI 会写“通用”代码但不理解你的积分规则、优惠叠加规则、状态机流转规则。安全漏洞被隐藏。AI 生成的 SQL 拼接、鉴权逻辑、文件上传代码都有可能引入安全风险。因此这篇文章会重点讲如何在使用 AI 编程工具时仍然保持代码的可控性。2. 为什么越来越多人说“I‘m done coding with AI”2.1 代码质量不稳定这是最常见的吐槽点。很多开发者在用 AI 生成代码时会遇到“第一次生成能用第二次生成合理第三次生成一个完全不同的方案”的情况。原因很简单大模型的生成结果是概率性的同一个问题在不同上下文、不同措辞下会得到不同答案。例如用 AI 生成一个分页查询接口第一次def get_users(page: int, size: int): return db.query(User).offset((page - 1) * size).limit(size).all()第二次def get_users(page: int, size: int): return db.query(User).paginate(pagepage, per_pagesize).items第三次def get_users(page: int, size: int): stmt select(User).offset((page - 1) * size).limit(size) return session.execute(stmt).scalars().all()三个版本都能“跑”但风格完全不同依赖也可能不同。如果没有统一规范约束AI 就会把一个项目写得像四个人合作的产物。2.2 上下文越长越容易“跑偏”AI 编程工具在单文件、小函数场景下表现很好但一旦涉及跨文件、跨模块调用它就需要阅读大量上下文。大多数编辑器插件只能看到当前文件的内容无法真正理解整个项目的架构。于是会出现AI 调用了一个并不存在的 Service 方法。AI 修改了 A 文件但忘记修改 B 文件中的对应调用。AI 生成的代码破坏了原有接口的返回结构导致前端报错。AI 在配置文件中添加了不存在的配置项服务启动失败。这些问题在有经验的开发者手里还能快速发现但如果是新手很容易陷入“AI 写代码 → 报错 → 问 AI → AI 改 → 新报错”的循环。2.3 AI 不会主动告诉你“这里不该改”在 Cursor Agent 模式或 Coding Plan 类工具中AI 会自动读取项目文件并执行修改。它会列出一堆变更看起来非常专业但它不会告诉你这个变更可能影响其他模块可能破坏现有逻辑可能引入安全风险。例如AI 看到你有一个user_id字段自动为你加了一个索引。这个操作本身没错但如果在生产环境的大表上执行 DDL可能需要几分钟甚至几十分钟的锁表时间直接影响线上业务。AI 不知道这些“隐性的运维约束”。正是因为这些问题越来越多经历过 AI 代码事故的开发者开始重新强调代码审查、测试覆盖和人工把关。3. AI coding 的真实能力边界什么能做什么不能做3.1 AI 编程擅长的事情样板代码生成Controller、Service、Mapper、DTO 这类结构固定的代码AI 效率远高于手写。单元测试辅助根据现有函数生成基础测试用例覆盖正常输入和边界输入。正则表达式与字符串处理很多开发者不熟悉正则AI 能给出可用表达式但需要验证。脚本编写日志清洗、文件批处理、定时任务、数据迁移脚本等一次性脚本。接口文档生成根据代码生成 Markdown 格式的 API 文档基本可用。报错排查辅助把报错信息粘贴给 AI让它给出排查方向能节省不少时间。3.2 AI 编程不擅长的事情高并发与分布式事务设计。AI 能写出“看起来正确”的悲观锁、乐观锁、分布式锁代码但未必适合你的实际并发模型。业务规则状态机。例如订单状态从“待支付”到“已支付”再到“已完成”中间有哪些限制条件AI 无法深入理解。复杂 SQL 优化。AI 能生成 SQL但执行计划的解读、索引选择、是否命中缓存需要结合真实数据量和数据库版本判断。安全隐患排查。SQL 注入、越权访问、文件上传路径穿越等风险AI 有时候能意识到有时候会忽略。架构演进。AI 无法替你做“这个模块未来要拆分成微服务所以现在接口应该瘦一点”的决策。一句话总结AI 适合承担从 0 到 1 的代码生成但“从 1 到 N 的正确性”需要开发者自己负责。4. 完整实战用 AI 开发一个用户积分服务然后手动修掉它的坑下面通过一个真实感很强的例子来演示 AI 编程的完整流程。我们假设需求是开发一个用户积分查询接口支持按用户 ID 查询总积分并按积分变更记录分页展示。4.1 需求描述与项目结构技术要求Python 3.10FastAPI 框架。SQLAlchemy 2.x 作为 ORM。PostgreSQL 数据库。Redis 作为热点缓存。我们先设计一个最简目录point_service/ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── database.py │ ├── models.py │ ├── schemas.py │ ├── services.py │ └── redis_client.py ├── requirements.txt └── .env4.2 向 AI 描述需求得到初版代码向 AI 输入以下提示请用 FastAPI SQLAlchemy 2.x PostgreSQL 实现一个用户积分查询服务。 要求 1. UserPoint 表存储用户当前总积分。 2. PointLog 表存储积分变更记录。 3. 提供 GET /users/{user_id}/points 接口返回用户总积分带 Redis 缓存。 4. 提供 GET /users/{user_id}/points/logs 接口返回分页积分变更记录按时间倒序。AI 给出的初版代码如下。app/models.py# 文件路径point_service/app/models.py from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Index from sqlalchemy.orm import declarative_base Base declarative_base() class UserPoint(Base): __tablename__ user_points id Column(Integer, primary_keyTrue, autoincrementTrue) user_id Column(Integer, uniqueTrue, nullableFalse, indexTrue) points Column(Integer, default0, nullableFalse) updated_at Column(DateTime, nullableFalse) class PointLog(Base): __tablename__ point_logs id Column(Integer, primary_keyTrue, autoincrementTrue) user_id Column(Integer, nullableFalse, indexTrue) change_value Column(Integer, nullableFalse) reason Column(String(255), nullableFalse) created_at Column(DateTime, nullableFalse)app/services.py# 文件路径point_service/app/services.py from sqlalchemy.orm import Session from .models import UserPoint, PointLog def get_user_points(db: Session, user_id: int): record db.query(UserPoint).filter(UserPoint.user_id user_id).first() if record is None: return 0 return record.points def get_point_logs(db: Session, user_id: int, page: int, page_size: int): offset (page - 1) * page_size logs ( db.query(PointLog) .filter(PointLog.user_id user_id) .order_by(PointLog.created_at.desc()) .offset(offset) .limit(page_size) .all() ) total db.query(PointLog).filter(PointLog.user_id user_id).count() return logs, totalapp/main.py# 文件路径point_service/app/main.py from fastapi import FastAPI, Depends, HTTPException from sqlalchemy.orm import Session from .database import get_db from .services import get_user_points, get_point_logs from .redis_client import get_redis import json app FastAPI() app.get(/users/{user_id}/points) def read_points(user_id: int, db: Session Depends(get_db), redisDepends(get_redis)): cache_key fuser_points:{user_id} cached redis.get(cache_key) if cached is not None: return {user_id: user_id, points: int(cached)} points get_user_points(db, user_id) redis.set(cache_key, points, ex60) return {user_id: user_id, points: points} app.get(/users/{user_id}/points/logs) def read_point_logs( user_id: int, page: int 1, page_size: int 20, db: Session Depends(get_db), ): logs, total get_point_logs(db, user_id, page, page_size) data [ { id: log.id, change_value: log.change_value, reason: log.reason, created_at: log.created_at.isoformat(), } for log in logs ] return {total: total, page: page, page_size: page_size, items: data}单看这段代码结构是完整的有 model、service、api甚至还有 Redis 缓存。很多开发者拿到这样的代码后会直接跑起来测试。但如果放到真实业务里这个版本至少存在以下严重问题。4.3 人工审查发现 AI 生成代码中的隐藏问题问题一N1 查询问题在read_point_logs中先查询日志列表再统计总数这是两次查询。如果日志量很大这个接口每次可能扫描大量数据。更关键的是这段代码在获取日志列表后没有把total查询和列表查询合并也没有使用count的优化写法。在百万级数据量的积分日志表上这个接口会非常慢。问题二Redis 缓存与数据库数据不一致read_points接口先查缓存缓存不存在时查数据库然后写缓存。问题是积分发生变更时新增、扣减代码只更新了数据库没有删除 Redis 缓存。这会造成用户查询到的积分是旧值直到缓存过期。AI 生成的初版代码里完全没有处理缓存一致性。问题三数据库查询缺少锁保护积分查询本身是只读操作通常不需要加锁。但如果这个服务后续扩展为积分扣减接口直接复用get_user_points的查询逻辑在并发场景下会出现超扣问题。初版代码中没有任何锁或版本号的考虑。问题四异常处理缺失如果 Redis 挂了redis.get会抛异常导致整个接口 500。实际项目中缓存应该设置降级策略Redis 不可用时直接查数据库而不是让接口报错。问题五缺少缓存更新与失效策略对于积分这种高频变更业务建议采用“更新数据库后删除缓存”的策略并且对不同操作设置合理的过期时间。4.4 手工修复把 AI 生成代码改造成可上线版本下面我们手动修复上述问题得到一份更可靠的代码。首先修改services.py为积分查询增加缓存删除能力并优化日志分页查询# 文件路径point_service/app/services.py from sqlalchemy.orm import Session from .models import UserPoint, PointLog def get_user_points(db: Session, user_id: int): 返回用户当前总积分不存在记录时按 0 处理。 record db.query(UserPoint).filter(UserPoint.user_id user_id).first() if record is None: return 0 return record.points def get_point_logs(db: Session, user_id: int, page: int, page_size: int): 分页查询积分变更日志。 这里使用 limit offset 方式适合中小数据量。 如果日志量极大建议改为基于游标的分页方式。 offset (page - 1) * page_size total_stmt select(func.count(PointLog.id)).where(PointLog.user_id user_id) total db.execute(total_stmt).scalar_one() logs_stmt ( select(PointLog) .where(PointLog.user_id user_id) .order_by(PointLog.created_at.desc()) .offset(offset) .limit(page_size) ) logs db.execute(logs_stmt).scalars().all() return logs, total修改main.py增加缓存降级和删除策略# 文件路径point_service/app/main.py from fastapi import FastAPI, Depends, HTTPException from sqlalchemy.orm import Session from .database import get_db from .services import get_user_points, get_point_logs from .redis_client import get_redis import logging logger logging.getLogger(point_service) app FastAPI() app.get(/users/{user_id}/points) def read_points( user_id: int, db: Session Depends(get_db), redisDepends(get_redis), ): cache_key fuser_points:{user_id} try: cached redis.get(cache_key) if cached is not None: return {user_id: user_id, points: int(cached)} except Exception: # Redis 异常时降级为直接查数据库不影响主流程 logger.warning(Redis get failed, fallback to db, exc_infoTrue) points get_user_points(db, user_id) try: redis.set(cache_key, points, ex60) except Exception: logger.warning(Redis set failed, exc_infoTrue) return {user_id: user_id, points: points} app.get(/users/{user_id}/points/logs) def read_point_logs( user_id: int, page: int 1, page_size: int 20, db: Session Depends(get_db), ): if page 1 or page_size 1 or page_size 100: raise HTTPException(status_code400, detail分页参数不合法) logs, total get_point_logs(db, user_id, page, page_size) data [ { id: log.id, change_value: log.change_value, reason: log.reason, created_at: log.created_at.isoformat(), } for log in logs ] return {total: total, page: page, page_size: page_size, items: data}同时在积分变更逻辑中记住一个原则更新数据库后立即删除对应缓存。例如新增积分时def add_points(db: Session, redis, user_id: int, change_value: int, reason: str): 增加积分并清理缓存。 整个操作应在数据库事务内执行。 record db.query(UserPoint).filter(UserPoint.user_id user_id).first() if record is None: record UserPoint(user_iduser_id, points0) db.add(record) db.flush() record.points change_value log PointLog( user_iduser_id, change_valuechange_value, reasonreason, created_atdatetime.now(), ) db.add(log) db.commit() # 删除缓存下次查询时回源数据库 cache_key fuser_points:{user_id} try: redis.delete(cache_key) except Exception: logger.warning(Redis delete failed, exc_infoTrue) return record.points4.5 运行与验证准备虚拟环境和依赖cd point_service python -m venv venv source venv/bin/activate pip install fastapi uvicorn sqlalchemy psycopg2-binary redis pydantic数据库连接配置示例.envDATABASE_URLpostgresql://point_user:point_passwordlocalhost:5432/point_db REDIS_URLredis://localhost:6379/0启动服务uvicorn app.main:app --reload --port 8000调用接口curl http://localhost:8000/users/1001/points curl http://localhost:8000/users/1001/points/logs?page1page_size10第一次请求会查询数据库并写缓存第二次请求会命中缓存。如果中间通过管理端增加了积分缓存会被删除后续请求重新回源数据库。这样缓存与数据库的一致性就得到了基本保障。5. 使用 AI 编程时的常见问题与排查思路这部分整理 5 个高频问题每一个都是 AI coding 过程中真实踩坑的点。问题现象常见原因排查方案与解决思路AI 生成的代码引用了不存在的库或 API模型幻觉基于训练样本猜测方法名没有确认当前环境查看官方文档运行pip show/npm view验证包是否存在不要直接信任 AI 给出的 API 名AI 修改了无关文件Agent 类工具在读取项目上下文时无法准确识别“影响范围”使用 Git 提交前先git diff逐文件检查只保留必要变更必要时限制 AI 可读取的目录生成代码后原有的测试用例全部失败AI 重构了接口签名或返回结构导致调用方不兼容让 AI 在生成代码后同步更新调用方不满足时手动定位测试失败点同一个函数多问几次得到不同实现LLM 具有随机性提示词稍微变化结果就不同固定提示词模板在团队内统一 AI 编码规范核心代码以人工实现为准AI 生成的 SQL 在大数据量下性能极差AI 没有真实执行计划无法感知索引和数据分布在测试库中EXPLAIN ANALYZE验证 SQL使用真实规模数据测试缓存、事务、并发场景AI 经常漏处理AI 只能看到局部代码无法感知分布式环境的状态业务逻辑涉及事务与缓存时强制人工审查建议使用设计评审 checklist更具体的排查顺序可以这样做出现编译错误时先看报错堆栈不要立刻把报错复制给 AI。确认报错来自依赖版本还是语法逻辑分别处理。如果 AI 连续修改三次仍未解决停下来人工阅读相关代码。涉及数据库时先打印实际执行的 SQL而不是只看 ORM 代码。涉及缓存时检查缓存 key 是否与写入、删除逻辑一致。6. 正确使用 AI 编程的工程建议既然 AI 编程不可能彻底放弃那就要建立一套能让 AI 发挥价值、同时控制风险的工程规范。6.1 把 AI 当作结对编程的“初级开发者”不要把它当大神更不要把它当作“生成即正确”的工具。每一次 AI 生成的代码都要经过人工评审就像带一个实习生写代码一样。具体建议大块逻辑让 AI 给出方案不直接采用代码。小函数、工具类可以让 AI 直接生成但必须补充单元测试。核心业务逻辑、支付、权限、积分流转建议人工编写AI 只做辅助检查。6.2 通过提示词约束生成质量一份好的提示词能大幅提高 AI 输出质量。可以按以下要素组织角色设定你是一名有 5 年经验的 Python 后端工程师。需求描述请实现一个分页查询接口。约束条件使用 SQLAlchemy 2.x不使用第三方分页库返回格式必须为{code, data, message}。边界条件page 小于 1 时返回 400。输出格式先给代码再给调用示例。示例你是一名资深后端工程师。请实现一个用户积分查询接口使用 FastAPI SQLAlchemy 2.x。 要求 1. 查询用户总积分时先查 Redis缓存不存在时查数据库并写回。 2. Redis 异常时降级查数据库不能影响接口可用性。 3. 分页参数 page 从 1 开始page_size 最大 100。 4. 输出代码前先给出设计说明。6.3 建立代码审查 checklist推荐在合并 AI 生成代码前完成以下检查[ ] 是否只修改了目标文件没有无关改动。[ ] 是否包含事务控制事务边界是否合理[ ] 是否考虑缓存失效策略[ ] 是否存在 SQL 注入、越权、敏感数据泄露风险[ ] 是否有日志日志中是否包含敏感信息[ ] 是否有对应的单元测试[ ] 是否处理了参数边界异常[ ] 是否保持与现有代码风格一致6.4 数据库与生产环境安全在实际项目中AI 生成的数据库变更语句尤其需要谨慎严禁直接在生产环境执行 AI 生成的 DELETE、UPDATE、ALTER 语句必须先到测试环境验证。DDL 操作如加索引、加字段需要评估锁表时间建议在业务低峰期执行。涉及数据删除时必须确认 WHERE 条件并使用事务包裹必要时先备份表。建议遵循最小权限原则AI 工具或开发者账号不应拥有生产环境的 drop 权限。6.5 版本管理给 AI 的修改留好退路使用 AI 编程时推荐一个习惯在让 AI 修改代码之前先创建一个独立分支或提交一次“修改前快照”。这样 AI 如果改坏了可以随时回滚不用浪费时间手动撤销。特别是 Agent 类工具会自动修改多个文件回滚能力是必须的。git checkout -b feature/xxx-ai-modify git add . git commit -m chore: 保存 AI 修改前快照这样无论 AI 怎么改你都有最低限度的安全网。6.6 保持代码可维护性为了不让 AI 生成的代码变成技术债团队可以约定AI 生成代码必须符合项目的统一命名规范。模块内部依赖必须清晰禁止 AI 生成循环依赖。通用逻辑封装为独立函数或类避免重复代码。在代码注释中标记哪些部分是 AI 生成、哪些是人工修改便于后续维护。7. 总结不是告别 AI而是建立新的开发习惯回到文章标题“I‘m done coding with AI”。从开发者的真实反馈来看这句话表达的不是彻底放弃 AI 编程而是放弃那种不加思考、直接把 AI 输出当成最终代码的编码方式。AI 仍然是强大的工具但它的定位应该是“辅助者”而不是“决策者”。在本文中我们完成了以下事情解释了 AI coding 和 vibe coding 的基本概念。梳理了 AI 编程的优势与能力边界。通过一个积分服务的完整案例展示了 AI 生成初版代码后人工审查和修复缓存一致性、异常降级、分页优化等问题的全过程。给出了 AI 编程中的常见问题排查表。给出了工程层面的使用建议包括提示词约束、代码审查、安全边界、版本回滚和可维护性。如果你正在使用 AI 编程建议从今天开始给团队定两条底线AI 生成的代码必须经过人工审查。涉及数据库变更、缓存写入、事务操作时必须有测试环境验证环节。只要把这两条底线守住AI 编程就能真正成为效率工具而不是风险来源。至于“要不要和 AI 说再见”答案其实很简单不要和 AI 再见但要和“无脑接受 AI 代码”的自己说再见。

相关新闻