你改了一版 prompt,跑了几条样例,结果"感觉比之前好了"。
队友问你:好在哪?好多少?下次再改会不会退步?
你答不上来。我也答不上来——直到我在自己的项目里被这个问题卡了好几天。
LLM 的输出天然带随机性,质量判断又带主观性。你觉得"总结得够到位了",换个人可能觉得不够。没有固定的验证方式,每次改动都会变成一场谁也说服不了谁的辩论。
这篇文章讲的就是怎样用最低成本建起一套"改了之后能说清是进步还是回退"的最小回归基线。不需要评测框架,不需要标注数据集——三样东西就够了。
这个项目长什么样
我在做一个文本处理 API:FastAPI 做框架,对接 OpenAI 兼容的上游模型,对外提供三个端点——总结(/summarize)、要点提取(/key-points)和改写(/rewrite)。第一周把接口跑通了,第二周想把"能用"往"稳得住"推一推。
单元测试全过,本地调用也正常。听起来没什么问题,对吧?
真实文本一跑就露馅了。改写接口偶尔扩写 30%,要点提取动不动写成长解释句,总结会压掉关键信息。全都不报错,日志里看不出异常,只有人肉盯着输出才能发现。
"感觉好多了"——然后呢?
第二周前半段我开始收紧 prompt。给 rewrite 加了"不要扩写、不要加标题",给 key-points 加了"每条尽量短、保留术语",给 summarize 加了"保留主结论和限制条件"。
跑了几条,嗯,感觉好多了。
但"感觉"经不起追问。好了多少?有没有引入新问题——比如 summarize 变好了但 key-points 偷偷退步了?下次再改,拿什么判断是前进还是后退?
举个真实的例子。/key-points 接口收到这段输入:
做 AI 接口不能只看状态码是否为 200。除了接口可调通,还要确保模型输出结构稳定、错误路径可观测、异常能被测试覆盖,并且文档能让两天后的自己快速接手。
prompt 收紧之前,模型返回的是这样的"要点":
- 不能仅依赖状态码 200 判断接口成功,需综合评估响应内容
- 确保模型输出结构稳定,避免因格式变化导致解析失败
- 错误路径必须可观测,具备完整的日志和错误追踪能力
- 异常场景需有充分的测试覆盖,保障系统健壮性
- 文档应清晰易懂,让后续接手者能快速理解和维护
134 个字,每一条都在"解释"而不是"提炼"。你让它提要点,它给你写了五句说明文。接口没报错,格式也算合规——但这不是要点提取,这是换了个排版的原文复述。
如果我只凭感觉看一眼就放过了,下次 prompt 一改,这种退化可能更严重,而我根本不知道什么时候开始滑坡的。
三样东西就够
没有对照基准,就回答不了"好了还是退了"。解法不复杂,三样东西。
一批固定下来的真实样例
不需要几百条标注数据。我从前几天的真实调用里挑了 18 条,存成 JSON 锁住:summarize × 5,key-points × 5,rewrite × 8,涵盖短句、长文、口语和技术描述。
关键不在"多",在"不变"。后续每次回归都跑同一批输入,不要现编——现编的最大问题是没法做前后对比。
写下来"什么算达标"
这一步最容易被跳过,也最关键。
我给三个接口各写了几条规则,比如 /key-points 的口径是:每行以 - 开头;每条尽量短,不展开成解释句;保留具体术语和命令名。/rewrite 的口径是:单段文本,不加标题或前缀;长度控制在原文 0.8~1.3 倍;口语句不强行书面化。
这些规则完美吗?当然不完美。但写下来之后,"是进步还是回退"突然就有了对照基准——争论的焦点从"我觉得还行"变成了"它过没过第三条规则"。
一个能自动跑的脚本
把验收口径翻译成判定函数,大概长这样:
def judge_rewrite(input_text: str, result: str) -> dict:
issues = []
if result.strip().startswith("#"):
issues.append("输出包含标题")
ratio = len(result) / max(len(input_text), 1)
if ratio > 1.5:
issues.append(f"明显扩写 (ratio={ratio:.2f})")
paragraphs = [p for p in result.strip().split("\n\n") if p.strip()]
if len(paragraphs) > 1:
issues.append(f"输出有 {len(paragraphs)} 个段落")
return {"pass": len(issues) == 0, "issues": issues}
每个接口一个判定函数,脚本跑完吐一张表:
| 接口 | 样例数 | 通过数 | 判定 |
|---|---|---|---|
/summarize | 5 | 5 | PASS |
/key-points | 5 | 5 | PASS |
/rewrite | 8 | 8 | PASS |
以后改了 prompt,跑一遍就知道有没有引入回退。通过率下降了,要么改动有问题,要么口径该更新——不管哪种,至少你知道发生了什么。
回到那个翻车的例子
还记得前面那个 134 字的"要点提取"吗?同一条输入,prompt 收紧之后:
- 不能只看状态码200
- 模型输出结构稳定
- 错误路径可观测
- 异常测试覆盖
- 文档便于快速接手
53 个字。每条都是硬邦邦的短要点,没有多余的解释。
从 134 到 53,压缩了 60%。这不是"感觉变短了",是跑完脚本以后白纸黑字摆在那里的数据。如果这次收紧反而让另一条样例从 PASS 变成 FAIL,我也能立刻看到,而不是过了三天才在某次人肉检查里偶然发现。
整套基线的建设成本大概两三个小时:选样例半小时,写口径半小时,写脚本一小时。换来的是以后每次改动,5 分钟跑完一轮回归,拿到一个不需要争论的结论。
你可以现在就做的几件事
如果你也在做 LLM API 项目,不用等到"有空了再说"。下面这几件事,挑一件今天就能动手:
-
从你最近的真实调用里挑 5 条输入存成 JSON。不用追求覆盖率,先有一批固定样例比什么都重要——没有固定输入,所有对比都是空谈。
-
给你最头疼的那个接口写 3 条"什么算不及格"的规则。注意,不是写"什么算好",而是写"什么算不及格"——后者更容易想清楚,也更容易翻译成代码。比如:"输出超过原文 1.5 倍算不及格""返回了标题算不及格"。
-
试着写一个
judge_xxx()函数。如果你发现自己连判定条件都写不出来,说明你的"达标标准"还停留在直觉层面。写不出来本身就是最有价值的信号——它逼着你把模糊的感觉拆成具体的规则。 -
改 prompt 之前先跑一遍回归,改完再跑一遍。哪怕脚本很粗糙,哪怕只覆盖 5 条样例——有对比就比没对比强。这是避免"改着改着退步了但三天后才发现"的最低成本防线。
下期预告
这篇讲的是"怎么知道改了之后没退步"。但回归基线只是质量防线的第一层——现在的判定函数还很粗糙,样例数量也不够多,而且每次回归都要手动跑脚本。下一步要做的事包括:把回归脚本升级成可复用的评测模块、把请求记录落进数据库以便事后分析、给日志加上 JSON 结构化输出让排查不再靠 grep。这些会在接下来几周逐步推进,到时候再聊。
本文是「从零开始的 AI 开发」系列的第二篇技术文章。项目源码在 GitHub,欢迎 Star 和 Issue。