1. 先把定位想明白:ChatGPT 编写代码到底强在哪一环
我把 ChatGPT 真正接进日常编码流程,是从一次很普通的批量数据处理任务开始的。当时要写一个带失败重试、带并发上限、带超时控制的小工具,逻辑不复杂,但边角料特别多:异常分类、退避时间、日志格式、参数校验,写起来零零碎碎能耗掉小半天。我试着把需求按结构描述给它,第一版代码出来之后我只改了三个地方就能跑。从那以后,ChatGPT 编写代码这件事在我这里就不再是"玩具",而是变成了随手可用的生产力工具——它加速开发的部分并不在"写出惊天动地的算法",而在"把你脑子里已经想清楚的东西快速落成可运行的样子,并且顺手帮你把容易漏的错误兜住"。
先把话说透:这个工具解决的问题是从"想清楚"到"写出来"之间的落差。你有清晰的输入输出、有明确的技术栈、有已知的边界条件,那么它非常快;你要是自己都没想清楚要干什么,它只会把你的混乱翻译成一段更难读的混乱。所以这篇文章适合三类人看:一是刚开始写业务代码、还处在"知道要做什么但不确定怎么写才规范"阶段的新手;二是有几年经验、想把手上的重复劳动压缩掉的老手;三是团队里负责代码质量、想搞清楚该怎么定规矩再放开用的人。不管你属于哪一类,核心结论是一样的:它的价值上限由你的提问质量决定,它的风险下限由你的验收习惯决定。
1.1 它在编码流程里实际承担的三种角色
很多人对它的期待是"帮我写完这个功能",这个期待太大,容易失望。我实际操作下来,它在三个具体角色上表现最稳。
第一种是需求翻译器。你手里有一段含糊的产品描述、一份看不懂的第三方接口文档、一段别人留下的没有注释的代码,你需要先把它变成结构化的、能落地的东西。这时候让它做"翻译"而不是"创作",准确率会高很多。比如把一段中文需求翻译成接口签名、字段定义、状态机转移表,这件事它做得又快又稳,因为本质上是在做语言之间的映射,而不是在发明新逻辑。
第二种是样板代码生成器。CRUD、参数校验、DTO 转换、单元测试骨架、正则表达式、SQL 建表语句、日志埋点、配置文件的组装,这类工作有固定套路、几乎没有创造性、但极其耗时。把它们交出去,你省下的是最枯燥的那部分时间,而且它生成的样板代码通常比你手敲的更整齐,因为它不会因为困了就少写一个判空。
第三种是审查者与解释器。这个角色被严重低估了。你把一段代码贴给它,问"这段在并发下有没有问题"、"这个异常分支会不会吞掉错误"、"帮我看看有没有更简单的写法",它给出的意见经常能点醒你。它不会疲倦,不会因为你问了蠢问题而不好意思,也不会因为代码是同事写的就嘴下留情。我在做代码自查的时候,这个用法出现的频率已经超过了"生成代码"本身。
1.2 为什么"加速"和"减少错误"能同时成立
表面看这是矛盾的:写得快通常意味着错得多。它之所以能两头兼顾,原因在于它压缩的是机械劳动时间,而不是思考时间。人写代码的时候,真正容易出错的地方往往不是逻辑设计,而是那些高度重复、注意力最容易涣散的环节——第五个字段忘了加非空校验、第三个分支忘了关闭资源、第七个接口忘了处理超时。人在做第 N 次重复动作时出错率是上升的,而机器做第 N 次和第 1 次是一样的。
另一个原因是它默认会写得比你啰嗦。这个特点很多人嫌烦,但在减少错误这件事上恰恰是优点:它倾向于显式判空、显式捕获异常、显式写日志、显式声明类型。你自己写的时候为了快会省掉这些"看起来没用"的东西,而错误往往就藏在被省掉的那几行里。我现在的习惯是让它先写一版啰嗦的,然后我再决定哪些可以精简——做减法的成本远低于做加法,这个顺序不能反。
注意:它生成的"啰嗦"里也包含大量无意义的防御性代码,比如对一个已经确定为非空的主键再判一次空。这类冗余要删,否则代码会越来越难读。判断标准很简单:这个判空在当前调用链上有没有可能真的发生。
1.3 能力边界:哪些活给它,哪些活碰都别碰
我把任务分成三档。绿灯区:样板代码、格式转换、正则、测试骨架、报错解释、代码注释与文档、小范围重构、SQL 语句初稿、脚本工具。这些任务的共同点是"结果可以立刻验证",跑一下就知道对不对。黄灯区:业务核心算法、涉及金额与权限的判断、并发与事务控制、第三方库的深度用法。这些不是不能问,而是必须逐行读懂再改,绝不能直接合入。红灯区:涉及密钥与凭证的处理逻辑、生产环境的运维脚本、加密与鉴权实现、数据库迁移脚本。这些一旦出错代价太大,而且模型生成的内容你很难从表面看出问题。
还有一个硬边界必须清楚:它不能运行代码,也不能看到你的运行环境。它不知道你的 Java 是 8 还是 17,不知道你的依赖里那个库已经升到不兼容的大版本,不知道你的数据库字段实际是什么类型。这些信息必须由你在提示词里补齐,或者干脆把报错原文贴给它。指望它"猜对"你的环境,是新手最常见的翻车原因。
2. 提示词怎么写:代码质量其实在第一句话就定了调
我见过太多人说"这东西写的代码不能用",仔细一看他的提问是"帮我写个登录功能"。这种提问得到的必然是网上随处可见的教学级代码,因为模型只能按最通用的场景去猜。你把约束给足了,它的输出质量会有质的变化。我的经验是:同一个人、同一个模型,提示词从一句话变成结构化五段,代码可用率能从三成提到八成以上。
2.1 五段式结构:把"猜"的空间压到最小
我现在写提示词基本固定五段,顺序也不怎么变。第一段交代角色与背景:这段代码在什么项目里、上下游是谁、跑在什么环境。第二段说目标与输入输出:函数的签名、参数含义、返回值结构、调用方期望什么。第三段列技术约束:语言版本、框架版本、允许用哪些依赖、禁止用哪些写法。第四段写边界与异常:空值怎么办、超时怎么办、失败重试几次、日志打什么级别。第五段给验收标准:怎么算写对了,需要配套哪些测试。
| 段落 | 作用 | 常见缺失导致的问题 |
|---|---|---|
| 角色与背景 | 锁定场景,避免通用教学代码 | 给出与项目风格完全不匹配的写法 |
| 目标与输入输出 | 锁定接口契约 | 参数名、返回结构反复返工 |
| 技术约束 | 锁定版本与依赖 | 用了项目里没有的库、用了过时语法 |
| 边界与异常 | 锁定健壮性要求 | 空值不判、异常被吞、日志缺失 |
| 验收标准 | 锁定完成定义 | 不知道什么时候算写完,无限迭代 |
这里有个反直觉的点:约束条件比需求描述更重要。需求描述决定它写什么,约束条件决定它不能写什么。而实践中最容易出问题的恰恰是后者——它用了你项目里没有的工具类、它写了一个你的语言版本不支持的语法、它把配置硬编码在代码里。与其事后一行行改,不如一开始就把"只能用标准库""不要引入新依赖""必须兼容某个版本"这些话写进去。
2.2 让它先给方案,再给代码
直接要代码的最大问题是:你拿到手发现思路就不对,前面全白费。我改成了一个两步走:第一步让它先说方案——数据结构怎么设计、分几步、每步的输入输出是什么、有哪些坑;第二步我确认或者修正方案,再让它落成代码。多花的这一轮对话,通常能省掉后面三四轮返工。
这个做法还有个副作用是好的:方案阶段的文字是你能读懂的。代码你可能要逐行看,但一段中文方案你扫一眼就知道有没有跑偏。对新手尤其推荐,因为你在这个过程中顺便学到了"这个问题通常怎么拆",而不是只拿到一个黑盒结果。
2.3 三个可以直接抄的模板
下面几个是我用得最顺的,改改就能用。第一个是生成函数:
背景:一个内部数据同步任务,Python 3.9,只用标准库,禁止引入第三方包。 目标:实现 fetch_all(urls, max_workers=8, timeout=5, retries=2), 返回 [(url, 状态, 内容或错误信息)] 的列表。 约束:并发数不得超过 max_workers;单个请求超时 5 秒; 失败重试 2 次,退避时间 0.5s、1s;不得因单个失败中断整体。 异常:网络错误、超时、非 2xx 状态码分别记录不同错误类型。 验收:给出可运行的完整代码,附 3 个单元测试用例,覆盖全成功、 部分失败、全部失败三种情况。第二个是排错:
下面这段代码在我本地报错,完整报错信息附在后面。 环境:Python 3.9,依赖版本已固定,不能升级。 请先判断根因,再给两种修法(改动最小的 / 更规范的), 最后说明为什么会出现这个报错。 代码:…… 报错:……第三个是审查:
请以代码评审的视角看下面这段代码,只关注四件事: 1. 可能的空值与越界;2. 异常是否被正确传播;3. 并发安全; 4. 资源是否释放。每条问题指出具体行号、说明触发条件、 给出修改建议。不要评价命名风格和缩进。第三个模板里的"不要评价命名风格"很重要。不加这句,它会给你一堆无关痛痒的格式建议,把真正要命的问题淹没掉。提问时把范围收窄,输出才有密度。
提示:如果它给的答案明显跑偏,不要在原对话里反复修补,直接开新对话重写提示词。旧对话里的错误上下文会持续污染后续回答,越改越乱。
3. 完整实操:从一句需求到跑通的模块
光说方法太虚,我拿一个真实用过的小工具走一遍全流程。需求是:批量请求一批接口地址,收集结果,失败的要重试,整体要有并发上限,最后输出一份汇总。这个场景足够通用,你换成批量发消息、批量导数据、批量校验文件,逻辑是同一套。
3.1 第一步:先把接口契约钉死
我没有直接让它写代码,而是先让它输出函数签名和返回结构。这一步的价值在于:签名一旦定了,后面所有代码都是围绕它填肉,不会出现写着写着发现返回结构不对、整段推倒重来的情况。我最终确定的签名是fetch_all(urls, max_workers, timeout, retries),返回一个三元组列表,分别是地址、状态、内容或错误。
这里我特意要求"错误信息也作为正常返回值,而不是抛异常"。理由很实际:批量任务里单个失败是常态,如果每个失败都抛异常,调用方就得写一堆 try,而且拿不到"哪些成功了"的完整结果。把失败当成一种正常结果返回,调用方处理起来最省事。这类决策模型不会主动替你想,你得先想清楚再告诉它。
3.2 第二步:生成骨架,先跑通再优化
第一版代码它给得很快,结构大致是线程池加信号量控制并发,每个任务内部做了重试循环。我拿到之后先做了一件事:不看代码,先跑一遍。跑通说明大方向没错,跑不通就把报错贴回去。这一步不要急着逐行读,因为很可能你读半天的地方它下一版就改掉了,浪费时间。
跑通之后我才开始读,重点看三处:并发数是不是真的被限制住了、重试的退避时间是不是按预期递增、超时是不是作用在单次请求而不是整个任务。这三处是这类代码最容易出问题的地方,也是"看起来对但实际不对"的高发区。
3.3 第三步:把参数算清楚,别拍脑袋
参数这块我踩过坑,值得单独说。重试次数和退避时间的关系是:总耗时上限约等于单次超时乘以尝试次数,再加上所有退避时间之和。按超时 5 秒、重试 2 次(总共 3 次尝试)、退避 0.5 秒和 1 秒算,最坏情况下单个地址耗时是 5×3 + 0.5 + 1 = 16.5 秒。如果你的上游调用方设置的整体超时是 10 秒,这个配置就是错的,会先把调用方拖死。
并发数也是同理。假设你要处理 800 个地址,并发 8,单个平均耗时 200 毫秒,那么理论总耗时约 800÷8×0.2 = 20 秒。如果对端有每秒请求数限制,比如限制在 20 QPS,那并发 8 在 200 毫秒响应下正好顶到 40 QPS,直接超限。这时候要么降并发到 4,要么加令牌桶限流。我通常选后者,因为限流比调并发更精确,不会因为响应变慢就浪费吞吐。
import time class TokenBucket: def __init__(self, rate, capacity): self.rate = rate # 每秒补充的令牌数,即允许的 QPS self.capacity = capacity # 桶容量,允许的瞬时突发 self.tokens = capacity self.last = time.monotonic() def acquire(self, n=1): while True: now = time.monotonic() self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate) self.last = now if self.tokens >= n: self.tokens -= n return time.sleep((n - self.tokens) / self.rate)这段限流器是我让它生成的第三版。前两版分别用了time.time()和固定间隔 sleep,前者在系统时间被调整时会出问题,后者在突发流量下会误伤。第三版换成monotonic加令牌桶之后就稳了。这个迭代过程本身就是"减少错误"的典型样本:错误不是靠一次写对避免的,是靠一轮轮把边界想清楚挤掉的。
3.4 第四步:让它先写测试,再让它改代码
这是我认为收益最高的一步。让模型先写测试用例,会逼它把"什么算对"想清楚,而你想清楚的东西它写出来的代码自然会贴合。我通常要求覆盖四类:全成功、部分失败、全部失败、参数非法。前三个覆盖主干,第四个覆盖入口校验,这四类能兜住八成的低级错误。
有个细节要注意:测试用例本身也要审。它写的断言有时候会顺着自己实现里的错误逻辑来,形成"测试通过但代码错"的假象。比如它把重试次数实现成总共 3 次而不是额外 3 次,测试里也按总共 3 次去断言,看起来全绿,实际不符合需求。所以断言必须对着需求文档看,不能对着实现看。
3.5 第五步:重构交给它,决策权留给自己
代码能跑、测试通过之后,我会让它做一轮重构,提示词里限定"只做三件事:消除重复、简化嵌套、明确命名,不改变任何外部行为"。限制改动范围很关键,不加这句它会顺手改逻辑,把好不容易验证过的东西又搞乱。
重构完必须重新跑一遍全量测试。这一步我用过的最笨也最有效的办法是:重构前后各跑一次测试,对比结果完全一致才接受。测试覆盖不到的地方,宁可不重构——能跑的丑代码好过不能跑的漂亮代码,这话在赶进度的时候尤其成立。
3.6 一个容易被忽略的环节:让它解释自己的代码
最后一步我固定会做:让它逐段解释刚生成的代码,特别是那些我不太熟的写法。这个过程有两个好处,一是你能立刻发现理解偏差,二是它会暴露出自己"其实没想清楚"的地方——有时候它解释到某一行会开始绕,那一行往往就是问题所在。我自己就靠这招揪出过一个把异常类型判断写反的分支。
4. 踩过的坑:这些问题不提前知道一定翻车
下面这些全是我在实际使用中真实撞上的,按出现频率排。新手看完这一节,能省掉至少一半的调试时间。
4.1 幻觉:不存在的函数和过时的写法
最常见的问题是它给出一个库函数,名字看着特别合理,实际根本不存在,或者参数顺序不对。这类错误在 Java、Python 的第三方库里出现频率最高。判断方法很土但有效:凡是你不认识的函数,先查官方文档再信。它不会主动告诉你"这个方法我不确定",语气永远是那么笃定。
另一个变种是过时写法。它可能给你一个三年前很流行、现在已经废弃的 API,或者用了旧版本的语法。解决办法是在提示词里明确版本号,并且要求"如果某个 API 在当前版本已废弃,请用官方推荐的替代写法并注明"。这句话能过滤掉相当一部分问题。
4.2 边界条件:它默认世界是美好的
它的默认假设是"输入是正常的、网络是通的、文件是存在的"。所以空列表、超长字符串、负数、零、重复元素、缺失字段这些情况,它经常不处理。我现在的做法是在提示词里直接列一张边界清单,让它逐条处理,比事后自己补要省事。
| 问题现象 | 常见根因 | 排查动作 |
|---|---|---|
| 生成的函数不存在 | 模型幻觉 | 查官方文档确认签名,再决定是否替换 |
| 本地跑报语法错 | 用了高版本语法 | 提示词补语言版本,要求兼容写法 |
| 空输入直接崩 | 缺少入口校验 | 补空值、空集合、非法参数三类用例 |
| 异常被静默吞掉 | 过宽的 catch 块 | 检查捕获范围,要求区分异常类型 |
| 并发下数据错乱 | 共享可变状态 | 检查是否有跨线程复用的容器或缓存 |
| 测试全绿但功能不对 | 断言顺着实现写 | 断言对着需求文档重新核一遍 |
| 依赖装不上 | 引用了不存在的包或版本冲突 | 限定只用现有依赖,禁止新增 |
| 性能突然变差 | 循环内重复计算或重复建连 | 检查循环体,把不变的部分提到外面 |
4.3 安全与合规:三类代码必须人工把关
第一类是拼接出来的查询语句。它有时候会为了省事直接用字符串拼接构造查询条件,这在有外部输入的场景下是明显的风险点。规则很简单:只要涉及外部输入,必须是参数化写法,不接受任何形式的字符串拼接。
第二类是凭证与密钥。它倾向于把配置直接写成常量放在代码里,因为这样示例最简洁。这在演示里没问题,进仓库就是事故。我的做法是在提示词里明确要求"所有敏感配置从环境变量读取,代码中不得出现任何硬编码凭证"。
第三类是命令执行与反序列化。涉及执行外部命令、加载外部数据的代码,一律逐行人工确认,不接受"看起来对"。这类代码的错误往往不在语法层,而在输入来源是否可信这一层,模型看不到你的部署环境,它给不出可靠判断。
4.4 三个不出现在文档里的实操心得
第一个:让它把不确定的地方标出来。在提示词末尾加一句"如果你对某个 API 或参数不确定,请在代码注释里标注待确认"。加上这句之后,我审代码的效率明显提高,因为注意力可以直接聚焦到它自己没底的地方。
第二个:不要在一次对话里跨领域切换。前面聊 Python 后端,突然问 CSS 布局,它的上下文会混乱,答案质量下降。分开对话,成本几乎为零,收益很明显。
第三个:保存好用的提示词。我建了一个简单的文本文件,按场景分类存模板,用的时候复制改参数。这件事看起来笨,但半年下来省的时间相当可观,因为好提示词是试出来的,试出来就该留住。
5. 把它变成团队能力:从个人技巧到可复用的规矩
个人用得好是一回事,团队里放开用是另一回事。我参与过一个小团队的试点,最初两个月问题不少:有人直接把它生成的代码合入主干,评审时才发现风格完全不合;有人把整段业务逻辑丢进去,结果拿到一堆似是而非的东西。后来靠几条简单规矩稳住了。
5.1 三条底线规矩
第一条:生成的代码合入前必须能说清每一行在干什么。这条听起来苛刻,实际执行下来并不难,因为大部分生成代码是样板,读起来很快。真正的目的是拦住"复制粘贴但不懂"的情况,这种代码出问题最难排查。
第二条:测试先行。凡是让它生成的功能代码,必须配套测试,且测试要经过人工审核断言是否对着需求写的。这条规矩把"看起来能跑"和"确实正确"区分开了,效果立竿见影。
第三条:敏感区域禁止使用。凭证处理、权限判断、数据迁移这三块,明确划为人工区,不做讨论。有了明确边界,反而没人再有争议,执行阻力比想象中小。
5.2 关于效率的真实体感
关于"到底能省多少时间",我不太喜欢给一个漂亮的百分比,因为不同任务的差异太大。我的真实体感是:样板类任务的耗时大概能压掉六到七成,理解类和设计类任务几乎不省时间,甚至更慢,因为你要花时间写清楚提示词、审代码。所以正确的用法是把省下来的时间投到设计和评审上,而不是用来接更多任务。这一点想明白了,工具带来的才是正向收益。
另外有个现象值得注意:用得越久,提问会越短。因为你会逐渐摸清它的能力边界,知道哪些信息必须给、哪些给了也没用。这跟带新人有点像,前期沟通成本高,磨合好了效率就上来了。我现在的提示词长度大概是刚开始的三分之一,但产出可用率反而更高。
5.3 我个人的一点使用习惯
最后一个习惯分享给大家:每次拿到生成结果,我会先问自己一句"如果这段代码上线后出问题,最可能是哪里"。答案往往就指向需要重点检查的那几行。这个动作花不了二十秒,但拦住过我好几次。工具再好也只是工具,判断力始终在人这边,这句话我在用了两年之后,体会比刚开始更深。