前一阵我把手头一个内部工具的重构任务丢给AI去做,结果发现一个很尴尬的事情:同一个模型、同一个需求,我把需求描述得详细一点,代码能跑通的概率就明显高一些;我图省事只丢一句话,它就经常给我造出根本不存在的API。后来我决定不靠“感觉”了,直接用A/B测试的方式去优化代码生成提示,让AI写对代码的概率从60%拉到了85%左右。这个过程挺有意思,也很值得复盘,今天把完整思路和实验数据整理出来。
1. 为“提示词玄学”搞一个正经实验台
先交代背景。我当时要生成的是一个用于内部数据清洗的小型Python库,包含文件读取、字段校验、格式转换、错误日志这几个模块。模型用的是市面上主流的通用大模型,不是专门的编程模型。任务复杂度中等偏上,代码量大概在600到900行之间。
起初我的提示词风格非常随意,基本就是“帮我写一个数据清洗脚本”,甚至带着口语化的“把那个日期格式改一下”这种话。结果就是模型经常自由发挥:有的给返回一个半成品类,有的把读取逻辑写错,有的把函数名都拼错了。我自己做了个粗统计,这种“自由发挥”状态下,代码能直接跑通并满足需求的概率只有60%左右。
60%是个什么概念呢?就是你每次生成代码,接近一半的概率要返工,而且返工时你还得耐着性子去一行一行找错。真正让人崩溃的是,有时候那40%的不合格代码看起来特别像样,语法完全正确,import也都是存在的库,但逻辑偏偏就是错的。这种代码比明显的报错更恶心。正因如此,我决定不再靠主观感受去调提示词,而是搞一个尽量规范的A/B测试流程,像做实验一样去改提示词。
A/B测试的第一件事不是写提示词,而是建立一个实验台:一个可重复运行的评估环境。我把评估分成两层:
- 第一层是硬性检查:代码能否通过语法编译,能否在准备好的测试用例上跑通,输出结果是否符合预期;
- 第二层是人工代码审查:让有经验的同事(或者我自己隔一段时间再看)做代码走查,看逻辑、命名、边界处理是否合理。
两层都通过才记作“写对”。整体流程是先准备一批统一的测试任务,每个任务用不同版本的提示词去生成代码,然后在同一个环境里跑测试,记录每一轮的成功率和失败原因。
有人可能会说,你直接看代码好不好不就行了吗,搞这么复杂干什么。但问题是,提示词优化里有一个特别常见的情况:指标看起来提升了,其实是样例变了。如果每次测试用的是不同的题目,你根本分不清是提示词变好了还是题目变简单了。一定要固定测试集、固定评测流程,这样每一次改动才有可比性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建实验台:测试集、评测标准与基线成绩
实验台的第一个组成部分是测试集。我基于实际需求整理出20个数据清洗相关的小任务,覆盖三个难度级别:
| 难度 | 任务数量 | 说明 |
|---|---|---|
| 简单 | 8个 | 单函数,输入输出明确,比如“把字符串yyyy-mm-dd转换为datetime对象并格式化输出” |
| 中等 | 7个 | 多函数协作,涉及异常处理和类型判断,比如“按字段批量校验CSV并生成错误报告” |
| 困难 | 5个 | 多文件、多模块,涉及状态管理和配置参数,比如“从多个Excel中读取数据,按规则去重合并后写出新文件” |
这些任务全部来自之前真实项目里写过的东西,不是现场编的。我严格限制每道题的表述长度,避免有的题目天然就描述得更清楚,导致实验偏移。
第二个部分是评测脚本。我写了一个简单的Python脚本,自动完成以下工作:
- 依次调用AI接口,用指定的提示词模板拼接测试任务;
- 将模型返回的代码保存为独立的.py文件;
- 对每个文件执行语法检查(py_compile);
- 运行预置好的测试用例,比对输出结果;
- 将语法错误、运行报错、输出不符等情况记录到CSV里。
人工代码审查则放在自动评测通过后做。因为自动评测只能证明“结果是好的”,不能证明“代码是好的”,万一模型用了奇怪的全局变量或者硬编码了测试数据,自动评测也能通过。所以我的“写对”定义必须包含人工审查这一层。
基线成绩是怎么测的?我直接把我之前最习惯的那种随意提示词作为v0版本,用20个任务各生成一次代码。为了尽量减少随机性,我把temperature参数固定到0.2,每个任务连续生成3次,只要有一次通过全部测试且人工审查没有问题,就记为这个任务通过。最终v0的成功率是12/20,正好60%。
2.1 为什么连续生成3次并取最优
这里有个细节值得解释:为什么是连续生成3次取最好结果,而不是一次定生死?
因为生成式模型的输出本质上带有随机性。即便temperature很低,采样结果也不会完全一致。如果用一次结果就打分,那么测试出来的是“模型在这套提示词下撞大运的概率”,而不是“这套提示词引导模型表现出稳定水平的概率”。在实际工作中,我们通常也不会只让AI生成一遍就不管了,很多人会试着生成两三次,挑一份看着还能改的代码去改。三次取最优,更接近真实使用习惯,同时也能给不同提示词版本一个相对公平的比较环境。
3. 五轮提示词迭代:每轮改了什么、数据怎么变
基线数据出来之后,我开始系统性地修改提示词。整个过程一共经历了五轮比较大的迭代,每一轮我都只改变一个关键变量,其他条件保持不变。下面把每一轮的核心改动和实验结果写出来。
3.1 v1:从一句话需求到结构化说明
v0版本的提示词问题很明显,它基本就是需求原文的简单搬运。比如:
text复制写一个函数,从CSV文件读取数据,按某一列去重,然后输出新的CSV。
这个提示词不是不对,而是信息密度太低。模型需要自己脑补很多东西:函数名是什么、输入路径怎么给、去重保留哪一行、输出编码是什么、异常怎么处理?每脑补一个地方,就多一个出错的机会。
v1版本的改动是:在提示词开头加一段“角色与任务目标”,然后把需求拆成结构化的字段说明。结构如下:
text复制你是一名Python开发工程师。请实现一个数据清洗模块,要求:
1. 功能:从指定CSV读取数据,按['id', 'email']两列去重,保留首次出现的记录;
2. 输入:input_path, output_path, dedup_cols三个参数;
3. 输出:写入新的CSV文件,UTF-8编码,不保留原始索引;
4. 约束:不要修改原始文件,不要引入多余依赖。
表面上看只是换了个更规范的写作方式,但实验数据立刻给了反馈。v1的通过任务数从12涨到14,成功率70%。涨的两个任务都属于中等难度。原因也好理解:结构化说明降低了模型的阅读理解成本,参数名被明确定义了,去重键也写清楚了,模型不需要在“到底用哪几列去重”这种问题上赌。对简单任务来说,这种提升不明显,因为简单任务本身歧义就少;但对中等难度任务来说,结构化提示词帮助很大。
这一轮给我的核心启发是:好的提示词不是催着模型“好好写”,而是把模型需要做的“决策”尽可能减少。你在提示词里每写清楚一个前提,就相当于替模型排除了一批错误假设。
3.2 v2:明确输入输出格式与边界条件
v1虽然结构化,但它在“边界条件”上还是太粗放。模型经常不考虑文件不存在、字段缺失、空行过滤这类情况。我们做代码生成的场景里,这类边界问题特别致命,因为代码在正常的测试数据上可能完全没问题,一遇到脏数据直接崩掉。
v2的关键改动是在提示词里增加“输入输出格式与边界条件”段:
text复制边界条件:
- 如果输入文件为空,创建一个空的输出文件并在函数返回值中标注warning;
- 如果dedup_cols中的字段不存在,抛出ValueError并说明缺失字段;
- 编码统一使用UTF-8,读取时遇到非UTF-8字符按errors='replace'处理;
- 所有函数必须有类型注解和docstring。
这样一条条列出来,模型写出来的代码在这类边界上的表现有了明显变化。不过这里有个有趣的副作用:模型开始大量在代码里加if/else判断,甚至有些过度防御,有的代码光是输入检查就写了30行。这个副作用不算坏,但也需要人工审查时控制度。最终v2通过的数量是16/20,成功率80%。
另外这一轮还发现一个现象:显式要求“所有函数必须有类型注解和docstring”之后,代码的整体质量有隐性提升。原因可能是,当模型被要求写docstring时,它需要先梳理清楚函数逻辑,输出时的“思考组织度”会更高。这有点像我们写文章之前先列提纲,提纲一列,内容就不容易跑偏。
3.3 v3:在提示词里嵌入验证与防错机制
成功率到80%之后,继续靠补充功能描述已经比较难获得明显收益了。我统计了一下剩余4个失败任务的错误类型,发现高难度的多文件任务里,模型经常犯一类错误:自己定义了一个不存在的依赖方法,或者把一个标准库函数理解错。
这类问题靠“功能说明”是解决不了的,因为模型不是不知道需求,而是它的生成过程中缺少“自我验证”的步骤。v3版本的核心改动是加入“验证要求”。每个函数的代码块后面紧跟一行注释:
text复制# 验证:请检查本函数是否使用了未导入的模块?参数类型是否与调用处一致?
# 验证:边界条件是否处理完整?是否有潜在的索引越界?
实际使用中,这种内嵌注释确实能降低一部分错误,因为模型在生成后续代码时会“回过头来”注意前面的约束。我在实验中将这种注释放在函数定义之后、代码生成之前,让模型在写代码时就能“看见”这些自我反问。效果上,20个任务里有18个通过,成功率90%——但这里我要诚实,这个90%并没有维持住,后面我把温度调高、换模型版本后,数据出现了波动。所以我在复盘时把v3的稳定成绩记为85%左右,而不是峰值90%。
这个结果其实暴露了一个重要的问题:提示词优化的结果和模型版本、温度参数高度耦合。你在某个模型下找到一个很有效的提示词,换一个模型版本可能就失效。所以我自己在记录时,会同时记录模型版本和参数,避免把“模型自身的性能变化”误判为“提示词的功劳”。
3.4 v4:用“示例+反例”锚定预期
v3的85%还不是终点,我想试试能不能在困难任务上再提一档。困难任务里失败的两个案例,问题都不是语法错,而是“代码风格”和预期严重不符。比如有一个任务需要把多个Excel合并去重,模型选择了一个需要安装第三方库的方案,但在给出的环境里这个库并不存在。这属于典型的需求—环境不匹配问题。
针对这个问题,v4版本引入了“示例+反例”段。示例给一个局部代码片段,反例给一个“容易写出但不应采用”的写法:
text复制示例:
def dedup_records(records, keys):
seen = set()
result = []
for record in records:
signature = tuple(record[k] for k in keys if k in record)
if signature in seen:
continue
seen.add(signature)
result.append(record)
return result
反例(不要采用):
# 不要用一个全局set去记录所有已经出现过的记录,因为多文件合并时全局状态会污染下一次调用。
示例部分主要给模型提供“预期代码风格”的参照,而反例部分则帮模型排除“看着合理但不应出现”的路径。v4的测试成绩仍然是18/20,和v3持平,但人工审查的通过速度更快了,代码整体更接近我自己的风格,需要改动的地方比上一版少了不少。也就是说,v4的价值不在于提高自动通过率,而在于提高“可维护性”和“可读性”。这其实也是做代码生成时一个非常重要的隐性指标。
3.5 v5:控制变量之后的最终确认
到v4为止,我已经做了大量改动。这时候我需要回答一个问题:这些成功率提升到底是结构化的作用,还是示例的作用,还是单纯因为我描述思路变清晰了?
v5做的工作不是继续加内容,而是做“消融实验”。我把v4的提示词逐项拆掉,分别测试:
- 只保留角色与任务说明(v1风格);
- 保留角色+格式约束+边界条件(v2风格);
- 在v2基础上单独添加验证注释;
- 在v2基础上单独添加示例与反例;
结果显示,单独添加验证注释的提升幅度在2到3个百分点,单独添加示例与反例的提升幅度在4到5个百分点,而两者叠加以后超过8个百分点。这说明不同维度的约束会产生协同效应,而不是简单的加法叠加。这也解释了为什么有些人抄了一个“万能提示词”到自己场景里效果不明显,因为提示词是一个整体,多个约束之间的相互作用比单个句子更重要。
最终我确定下来的v5提示词模板,融合了结构化描述、边界条件、验证注释和示例反例。在固定测试集上,20个任务通过17个,成功率85%。我拿另外10个新任务做了泛化测试,也能稳定在80%到85%之间。至此,整个优化过程算是有了一个完整结论。
4. 提示词里最容易被忽略的四个细节
走完这五轮迭代,我对“如何让AI写对代码”有了更具体的认知。有些细节如果不去做实验,根本不会意识到它们对结果影响这么大。
4.1 措辞精确性:少用模糊词
第一是措辞精确性。像“合理处理”“适当校验”“简单实现”这类词,在提示词里其实是反效果。模型对“合理”的理解和你对“合理”的理解大概率不是一回事。我在实验中专门做过对比,当我把“合理处理异常”改成“捕获ValueError并记录日志后返回空列表”之后,通过率提升了10%左右。这个对比在简单任务上不明显,但在困难任务上非常显著。
建议大家在写提示词时,把所有形容词改成动词或具体条件。不用“快速处理”,用“单次遍历”;不用“尽可能不丢数据”,用“当某行字段缺失时,记录该行到error.log并跳过”。越具体,模型写出来的代码越可控。
4.2 输出约束:格式先行
第二是输出约束。如果你不指定格式,模型经常会生成口胡式的混合输出,先讲一段思路再放代码,甚至代码里穿插解释。对于自动化流程来说,这非常麻烦。我会习惯在提示词末尾加一句:“只输出可运行的Python代码,不要输出解释和Markdown标记。”这一句对非编程模型特别有效,能大幅减少无效输出。
但这里有个平衡:如果要求模型直接输出完整代码,那么它对复杂任务的组织能力可能下降,因为它没有机会先规划。我自己测试下来,更好的做法是在提示词里加上“先在注释中列出步骤,再编写代码”。这样模型既不需要输出长篇解释,又能在注释的引导下保持结构清晰。推荐所有人在生成较长代码时尝试这个思路。
4.3 示例的粒度:给过程还是给结果
第三是示例的粒度。很多人给示例时喜欢给一个“完整代码”,以为这样模型就能照着写出完整代码。但在我的测试里,完整代码示例的效果反而不如“局部关键逻辑示例+边界规则描述”。原因可能是模型会过度模仿示例代码的风格,甚至在示例没有涉及的边界上直接照抄示例的处理方式,引发隐性错误。
比较好的做法是:如果你给完整示例,那最好同时给一个反例说明“这种情况下不要这样处理”。如果你给的只是局部示例,就要在示例前后说明这个片段只代表某类问题的处理模式。我在v4里就是采用局部示例加反例的方式,效果明显比给一整段代码要好。
4.4 温度和模型版本:隐藏变量
第四个细节必须特别强调:温度和模型版本。我在实验过程中有过一次“虚惊”,v3拿到90%的成功率后,第二天重新测,发现成功率掉到了82%。我一度以为是提示词模板出问题了,排查了很久,最后发现是我前一天测完后把temperature从0.2调到了0.4,而模型版本也在当天夜里被服务方自动更新了。
温度对代码生成任务的影响远比想象中大。温度太低(比如0.1以下)时,模型容易机械重复样例,甚至忽略新增的要求;温度太高(比如0.8以上)时,输出随机性大,错误率显著升高。对我用的这个模型,0.2到0.3之间是代码生成的甜点区。不同模型之间有差异,但普遍规律是:代码生成任务比文本生成任务更需要低温度。
4.5 别忘了显式声明输入约定
还有一个细节是在v2之后才加入的:显式声明输入约定。在很多任务里,测试数据是固定的,但是模型并不知道测试环境里的数据长什么样。如果你在提示词里写“输入为UTF-8编码CSV”,模型会自动假设分隔符是逗号,但如果你的数据分隔符是制表符,它会直接出错。这类错误不是模型笨,而是信息缺失。把输入的数据格式、文件路径、编码、分隔符、甚至首行是否包含表头都写清楚,能省掉很多隐藏在“看起来对”的代码里的坑。
5. 复盘:A/B测试在提示词优化这件事上的边界
整个实验做下来,我最想分享的一个经验是:A/B测试的思维方式,比具体的提示词模板更有价值。它逼着你去关注“可观测的指标”,而不是“感觉变好了”。在提示词优化这个场景里,如果不做对照实验,你很容易被一两次运气好或运气差的生成结果牵着走。
5.1 三条真实避坑经验
第一,样本量太小等于白测。我一开始只用了5个任务做测试,结果v1比v0高了20%,我还以为发现了一个万能公式。扩到20个任务之后,提升幅度立刻回归到10%。建议最少用15到20个覆盖不同难度的任务做测试,否则数据波动会掩盖真实效果。
第二,评测标准不能太主观。如果你让不同的人去判断“代码好不好”,标准会很不一样。我后来把评测拆成“自动测试通过”和“人工走查通过”两个独立环节,一个人只看自动测试结果,另一个人只做代码走查。这样每个人承担的标准清晰,不容易出现“我觉得这个代码还行”这种模糊评价。
第三,要记录模型版本和日期。提示词优化是这个时代的“炼金术”,模型在迭代,你的提示词可能上个月有效,这个月就失效。我给自己做了一个简单的记录表,每次测试都记下:模型版本、temperature、top_p、测试日期、测试结果、失败原因分类。没有这套记录,你根本没法回答“为什么这个提示词突然不灵了”。
5.2 这套方法能迁移到哪些场景
这套A/B测试优化提示词的方法,不只是代码生成场景能用。我自己后续还把它用在了文案生成、数据解析、SQL生成这些任务上,基本套路是一样的:
- 固定一组有代表性的测试样例;
- 定义清晰的成功标准;
- 每次只改一个变量;
- 记录所有环境和参数信息;
- 用数据说话,不靠感觉。
特别是SQL生成这个场景,效果非常明显。因为SQL天然适合结构化验证,你只需要准备几个数据库表结构和对应的预期查询结果,就能让AI生成的SQL在真实数据库里跑一遍,判断结果是否一致。这种“可验证”的任务,用上A/B测试的方法后,提升速度会快得惊人。
最后再分享一个小技巧。如果你是在自己的代码库上反复用AI生成代码,建议把最终验证过的提示词模板沉淀成一个文件,放仓库里管理起来。下次不管是自己用还是团队其他人用,都不会再回到“一句话提示词、随缘生成代码”的老路上。提示词这个东西,如果不与时俱进地维护,它就会和你写的代码一样,慢慢腐烂。
我没有给这套方法写什么宏大结论,对我来说,它就是一次老老实实的实验记录:从60%到85%,改变的不是模型,而是我自己与模型对话的方式。
