第7节 分层模型策略:强模型干活、小模型验收
一句话总结:用便宜快速的小模型做每轮"完成判定";maker/checker/仲裁三层模型选型表(能力×成本×速度);配置化模型路由实现。
本文导航
- 一、不是所有判断都值得用最贵的脑子
- 二、三层模型选型表
- 三、配置化模型路由
- 四、小模型做完成判定
- 五、完整分层循环
- 六、成本核算:我算错过一次账
- 七、小模型的边界:失败模式与踩坑实录
一、不是所有判断都值得用最贵的脑子
这一节的动机来自一张账单。有阵子我跑批量任务,每轮循环里所有环节——写代码、审查、判定"做完了吗"、给日志写摘要——全都打到同一个强模型上。月底一看账单,我盯着"完成判定"那一项的占比发呆了:一个只回答’是/否’的问题,花的钱和写代码差不多。因为判定调用次数最多(每轮至少一次),虽然单次便宜不了多少,架不住它量大。
而"这个任务完成了吗"这种判断,需要顶级推理能力吗?多数时候不需要——对着目标看一眼产出,检查关键点齐不齐,是个"及格线智力"的活。让 GPT-4o 级别的模型天天干这个,等于让总工程师守着门看工牌。
核心思想: 不是所有任务都需要最强模型 强模型(GPT-4o/Claude Sonnet)→ 写代码、复杂推理 小模型(GPT-4o-mini/Haiku)→ 完成判定、分类、摘要 效果:成本降低60-80%,质量几乎不降这个思路在传统工程里有个一模一样的对应物:CPU 调度。重要复杂的任务给大核,琐碎高频的中断给小核处理,大小核协同才有能效比。模型分层就是给 LLM 循环装一套"大小核调度"——按认知负荷分配算力,而不是一刀切全用旗舰。
路由的判据就三个问题:这个环节要生成多少内容(生成量大的给强模型)、要理解多复杂的东西(跨文件推理给强模型)、调用频率多高(高频的优先降档)。写代码三项全占,必须是强模型;完成判定只占"频率高",果断降档。
二、三层模型选型表
把循环里的典型环节过一遍,落到具体选型:
| 角色 | 推荐模型 | 能力 | 成本 | 速度 | 适用 |
|---|---|---|---|---|---|
| Maker | GPT-4o/Claude Sonnet | 最高 | $3/M | 中 | 写代码/复杂推理 |
| Checker | GPT-4o-mini/Haiku | 高 | $0.25/M | 快 | 代码审查/验证 |
| 判定 | GPT-4o-mini | 中 | $0.25/M | 快 | 完成判定/分类 |
| 摘要 | 最便宜档 | 中 | $0.1/M | 最快 | 日志摘要/格式化 |
这里有个和第4节表面上的冲突要澄清:第4节说"Checker 必须换厂商"(防和光同尘),本节又推荐 Checker 用 GPT-4o-mini——如果 Maker 也是 GPT-4o,这不是撞车了吗?两条规则合起来读才完整:换厂商是防偏见共振,降档次是省成本,实操上二选一或叠加都行。我的配置是:Maker 用 GPT-4o,Checker 用别家的小模型(异厂商+低档次双满足);预算紧、任务简单时,Checker 用同厂商小模型也可以接受——小模型和强模型共享的"审美"本来就少,共振风险天然低一档。
成本差到底有多大,直观感受一下同样一百万 token 的处理费:
| 模型档位 | 单价 | 100万token | 相对倍数 |
|---|---|---|---|
| 强模型 | $3/M | $3.00 | 30x |
| 小模型 | $0.25/M | $0.25 | 2.5x |
| 微模型 | $0.1/M | $0.10 | 1x |
判定这种高频环节从 30x 降到 2.5x,就是分层策略的全部经济逻辑。
落到操作层面,我给每个新环节定档用的是三步决策法,几分钟走完:
| 步骤 | 问的问题 | 答案指向 |
|---|---|---|
| 1 | 这个环节的产出错了,下游能兜住吗? | 能兜 → 候选降档;兜不住 → 强模型 |
| 2 | 单次任务的输入/输出结构固定吗? | 固定(如是/否、分类标签)→ 候选降档 |
| 3 | 调用频率多高? | 每轮都跑 → 降档收益放大;偶尔跑 → 无所谓 |
三步全绿才降档,任何一步存疑就先强模型跑着,等日志攒够了证据再降。这套流程的精髓是第一步:兜底能力决定降档空间。判定可以降档是因为 pytest 和黄金样例在它前面站着;maker 不能随便降档,是因为它错了整个循环白跑——兜底的位置不同,能承受的模型质量下限就不同。
三、配置化模型路由
选型不该散落在代码里,集中成一个 pydantic 配置模型,改档位只动配置不动逻辑:
# src/ai_loop_course/model_router.py"""分层模型路由:角色→模型的映射全部配置化 运行:uv run python -m ai_loop_course.model_router 环境:Python 3.12 + uv """importosimporttimefromdatetimeimportdatetimefromopenaiimportOpenAIfrompydanticimportBaseModelfrom.logging_configimportget_logger logger=get_logger("model_router")classModelTier(BaseModel):"""模型分层配置:改档位只改这里,循环代码零改动"""maker:str="gpt-4o"# 写代码:要生成质量checker:str="gpt-4o-mini"# 审查:要快和便宜judge:str="gpt-4o-mini"# 完成判定:是/否问题summarizer:str="gpt-4o-mini"# 摘要:格式转换classCallStat(BaseModel):"""单次调用统计:与全系列LLM日志字段同口径"""role:strmodel:strbaseurl:strkey_tail6:strinput_len:intoutput_len:intstart:strend:strcost_sec:floatclassModelRouter:"""模型路由器:按角色分发client,顺手记全字段日志"""def__init__(self,tier:ModelTier,clients:dict[str,OpenAI]):self.tier=tier self.clients=clients# {"gpt-4o": client1, "gpt-4o-mini": client2}self.stats:list[CallStat]=[]defget_client(self,role:str)->tuple[OpenAI,str]:"""根据角色获取(client, 模型名)"""model_name=getattr(self.tier,role)returnself.clients[model_name],model_namedefcall(self,role:str,prompt:str)->str:"""按角色路由调用:baseurl/端口、key尾6位、长度、起止、耗时全记录"""client,model=self.get_client(role)baseurl=os.getenv("OPENAI_BASE_URL","https://api.openai.com:443")key=os.getenv("OPENAI_API_KEY","sk-none")t0,start=time.time(),datetime.now().strftime("%Y-%m-%d %H:%M:%S")resp=client.chat.completions.create(model=model,messages=[{"role":"user","content":prompt}])content=resp.choices[0].message.contentor""stat=CallStat(role=role,model=model,baseurl=baseurl,key_tail6=key[-6:],input_len=len(prompt),output_len=len(content),start=start,end=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),cost_sec=round(time.time()-t0,2))self.stats.append(stat)logger.info("LLM调用 角色=%s baseurl=%s key尾6位=%s 模型=%s 输入长度=%d 输出长度=%d 开始=%s 结束=%s 耗时=%.2fs",stat.role,stat.baseurl,stat.key_tail6,stat.model,stat.input_len,stat.output_len,stat.start,stat.end,stat.cost_sec)returncontentdefsummary(self)->None:"""退出前打印量化总结:各角色分开的成本画像"""forrolein("maker","checker","judge","summarizer"):rs=[sforsinself.statsifs.role==role]ifnotrs:continuecosts=[s.cost_secforsinrs]logger.info("[总结] 角色=%s 次数=%d 输入长度 平均=%.0f 最大=%d 耗时 最大=%.2fs 平均=%.2fs",role,len(rs),sum(s.input_lenforsinrs)/len(rs),max(s.input_lenforsinrs),max(costs),sum(costs)/len(costs))这套结构的好处是审计免费:因为每次调用都带 role 和 model 落进日志,月底拉出来一统计,每个角色花了多少 token、平均耗时多少,清清楚楚。哪一档该升该降,不再拍脑袋,看数据说话。
四、小模型做完成判定
判定环节的实现出奇地短——问题本身就简单,提示词要做的只是把"简单"固定下来,别让模型发挥:
# src/ai_loop_course/judge.py"""小模型完成判定:高频环节降档省钱的主战场"""frompydanticimportBaseModel,ValidationErrorfrom.logging_configimportget_loggerfrom.model_routerimportModelRouter logger=get_logger("judge")classJudgeVerdict(BaseModel):"""判定输出:结构化,不收自由文本"""done:boolreason:strdefjudge_completion(goal:str,code:str,result:str,router:ModelRouter)->bool:"""用小模型判定任务是否完成:结构化JSON输出,pydantic校验+文本兜底"""prompt=f"""任务目标:{goal}代码:{code[:500]}运行结果:{result[:500]}这个任务完成了吗?只输出JSON,不要任何其他内容: {{"done": true或false, "reason": "一句话理由"}}"""response=router.call("judge",prompt)# 第一层:JSON过pydantic校验;第二层:格式不守约时按文本前缀兜底try:verdict=JudgeVerdict.model_validate_json(response)exceptValidationError:verdict=JudgeVerdict(done=response.strip().startswith(("是","yes","Yes")),reason="格式兜底:按文本前缀判定")logger.info("判定结果:%s(理由:%s)","完成"ifverdict.doneelse"未完成",verdict.reason[:60])returnverdict.done真实运行的控制台输出(注意 model 字段是 mini 档):
$ uv run python-mai_loop_course.judge2026-09-18 09:41:03[model_router]INFO LLM调用 角色=judgebaseurl=https://api.openai.com:443 key尾6位=a1b2c3 模型=gpt-4o-mini 输入长度=1047输出长度=42开始=2026-09-18 09:41:03 结束=2026-09-18 09:41:04 耗时=0.86s2026-09-18 09:41:04[judge]INFO 判定结果:完成(理由:除零保护已实现且测试通过)2026-09-18 09:41:04[model_router]INFO LLM调用 角色=judgebaseurl=https://api.openai.com:443 key尾6位=a1b2c3 模型=gpt-4o-mini 输入长度=1123输出长度=38开始=2026-09-18 09:41:04 结束=2026-09-18 09:41:05 耗时=0.79s2026-09-18 09:41:05[judge]INFO 判定结果:未完成(理由:未处理BOM头,UTF-8-sig场景会挂)耗时不到一秒——这在强模型上经常要三五秒,判定是每轮都跑的环节,速度差距乘以轮数就是体感差距。
成本对比:
| 方案 | 判定成本/次 | 100次成本 |
|---|---|---|
| GPT-4o判定 | $0.03 | $3.00 |
| GPT-4o-mini判定 | $0.002 | $0.20 |
节省93%,判定质量差异极小。
“判定质量差异极小"这句话我得拿数据背书,不然就是空口白话。我抽了 50 个已人工标注完成状态的任务,让两档模型分别判,跟人工标签对:强模型判定准确率 88%,小模型 84%。四个点的差距,换来 93% 的成本节省——而且这四个点里的多数错误是"小模型过早判完成”,用第5节的黄金样例一票否决就能兜住大半。分层的前提是每层都有兜底,不是裸奔。
顺带交代判定提示词的三条设计原则,都是拿失败案例换来的:
| 原则 | 反例(我犯过) | 正解 |
|---|---|---|
| 输出形态唯一 | “请评估完成情况” → 回一篇小作文 | 强制"是/否+一句话" |
| 证据摆在判断前 | 只给代码不给运行结果 → 凭想象判 | 代码+测试结果一起给 |
| 判定标准可核对 | “判断质量是否合格” → 无锚点 | 提示词里引用任务原始目标的验收条件 |
第二条最容易被忽视:小模型的"想象力"比强模型更收不住,不给运行结果它就脑补一个。证据齐了它才老实做比对,这也是判定准确率从飘忽到稳定的关键一步。
五、完整分层循环
把路由和判定拼进循环,就是完整的分层版:
# src/ai_loop_course/layered_loop.py"""分层模型循环:强模型干活,小模型判定"""fromai_loop_course.logging_configimportget_loggerfromai_loop_course.model_routerimportModelRouter logger=get_logger("layered_loop")deflayered_loop(goal:str,router:ModelRouter,max_steps:int=15)->str|None:"""分层模型循环:maker每轮写,judge每轮判,小模型当门卫"""feedback=""try:forstepinrange(1,max_steps+1):# 强模型写代码(带上轮判定反馈)code=router.call("maker",f"任务:{goal}\n上一轮反馈:{feedbackor'无'}\n请给出完整实现。")# 小模型判定完成(配合pytest结果一起判,不裸判)# run_tests:跑pytest的确定性验证器,即第2节tdd_loop里那套subprocess封装test_ok,result=run_tests(code)# 确定性验证器先跑iftest_okandjudge_completion(goal,code,result,router):logger.info("✅ 小模型判定完成(%d轮)",step)returncode feedback=result[:300]# 未完成:反馈截断后回传logger.warning("达到最大轮数:%d",max_steps)returnNonefinally:router.summary()# 退出必打量化总结注意if test_ok and judge_completion(...)这个顺序——确定性验证器在前,模型判定在后,短路求值天然实现了"pytest 挂了就不浪费判定的钱"。这是本章哲学(确定性验证 > 模型自评)在成本维度的直接应用:免费的裁判先开口,付费的裁判只处理免费裁判判不了的部分。
跑一个真实任务的完整轨迹,感受一下大小核协同的节奏:
=== 分层循环:任务"给CSV解析器加上编码自动探测" === [Step 1] maker(gpt-4o) 生成代码(1024字符) 耗时6.8s [Step 1] pytest: 3 failed 2 passed → 跳过判定(省钱) [Step 2] maker(gpt-4o) 修正代码(1180字符) 耗时7.1s [Step 2] pytest: 5 passed → judge(gpt-4o-mini) 判定:未完成 耗时0.8s 理由:未处理BOM头,UTF-8-sig场景会挂 [Step 3] maker(gpt-4o) 修正代码(1355字符) 耗时6.9s [Step 3] pytest: 6 passed → judge(gpt-4o-mini) 判定:完成 耗时0.7s ✅ 收敛(3轮) maker总耗时20.8s judge总耗时1.5s [总结] 角色=maker 次数=3 输入长度 平均=1420 最大=1850 耗时 最大=7.10s 平均=6.93s [总结] 角色=judge 次数=2 输入长度 平均=1085 最大=1123 耗时 最大=0.80s 平均=0.75s这条轨迹里藏着分层的全部要义:Step 1 的 pytest 失败让 judge 一次都没被调用(短路);judge 两次调用总共 1.5 秒、几厘钱,却把住了 Step 2 那个"测试全绿但 BOM 没处理"的质量关——这种"测试过了但需求没吃透"的缝隙,恰恰是确定性验证器的盲区、小模型够得着的判断。大小核各干各的活,账单和速度两头赚。
六、成本核算:我算错过一次账
下面这段是我真实的算账翻车现场,保留思考过程比直接给正确答案更有价值。
100个任务的总成本对比: 全用强模型: 100任务 × 5轮 × $0.03 = $15.00 分层模型: 100任务 × 5轮 × $0.03(maker)+ 100 × 5 × $0.002(judge) = $15.00 + $1.00 = $16.00?不对 实际:maker只在每轮用,judge每轮判定用小模型 maker: 100 × 5 × $0.03 = $15.00 judge: 100 × 5 × $0.002 = $1.00 总计:$16.00 vs 全强模型 $15.00 等等,这里反而贵了? 因为judge是额外的调用。 真正的节省在于: - 判定轮用小模型(省93%) - 摘要用小模型(省90%) - 分类用小模型(省95%) 实际节省:30-60%我当时盯着 $16 > $15 愣了半天:说好的省钱呢?想明白之后发现是对照对象搞错了。上面那个"全用强模型 $15"的算法里,根本没算判定环节的钱——它假设循环靠别的机制终止(比如固定轮数跑满)。但真实情况是:没有判定环节的循环要么提前停(质量差),要么跑满轮数(更贵)。把账算全:
| 方案 | 写代码 | 完成判定 | 总成本 | 效果 |
|---|---|---|---|---|
| 强模型+固定轮数跑满 | $15.00 | $0 | $15.00 | 每5轮才交卷,质量靠运气 |
| 强模型+强模型判定 | $15.00 | $15.00 | $30.00 | 每轮判定,及时收敛 |
| 强模型+小模型判定 | $15.00 | $1.00 | $16.00 | 每轮判定,及时收敛 |
正确的对照是后两行:判定环节从强模型降到小模型,$15 → $1,这才叫省93%。和第一行比成本确实多了 $1,但换来的是"该停就停"——跑满轮数的方案平均浪费 2 轮无效迭代,实际 token 消耗反而更高,这个隐性成本第一行的账根本没体现。
教训提炼成一句话:算分层的账,必须拿"同等功能的全强模型方案"当基线,拿残缺功能方案当基线,怎么算都是分层的错。
七、小模型的边界:失败模式与踩坑实录
7.1 什么时候不够格
分层不是无脑降档,有些环节降了会出事故。我把小模型的判定失败模式归了类,配合一个能力校准流程用:
| 失败模式 | 现场 | 后果 | 对策 |
|---|---|---|---|
| 过早放行 | 代码能跑但边界缺失就判"是" | 假完成 | pytest前置+黄金样例否决 |
| 过度保守 | 吹毛求疵永远判"否" | 循环空转烧maker的钱 | 判定提示词放宽+escalate兜底 |
| 理解不了长上下文 | 产出超2000行后判定质量骤降 | 判定失真 | 长产出改判"分块摘要后判" |
第三种最隐蔽,我也是在日志里发现的:判定准确率随输入长度上升而下滑,超过某个阈值断崖式下跌——小模型的上下文理解能力先于生成能力枯竭。所以我的路由配置里有条硬规则:产出超过1500行时,判定环节自动升回强模型。省钱省在该省的地方,该花的时候不含糊。
踩坑提示:别用"感觉小模型还行"当降档依据,每个环节降档前先跑一次上面那个校准(50个标注样本的一致率)。模型版本一升级,小模型的能力边界就会挪动,半年前的校准结论可能已经过期。
7.2 踩坑实录
**坑1:路由配置写死在代码里。**第一版我把模型名硬编码在每个调用点,想做个"checker 降档实验"得改五个文件。改成 ModelTier 配置化之后,实验变成改一行、跑一批、看日志的循环,第8节的调优效率直接翻倍。配置和逻辑分离这种老生常谈,在实验密集的场景里价值会放大十倍。
**坑2:judge 的提示词让模型"自由发挥"。**早期判定提示词写的是"请评估任务完成情况",小模型经常回一大段分析,还喜欢答"基本完成,但建议……"——是/否的判断被稀释成了议论文。改成强制"只回答是或否+一句话"之后,输出长度从 300 字符掉到 40,解析稳定,判定速度也快了(后来为了配合 pydantic 校验又升级成了固定 JSON 输出,思路不变:把输出形态钉死)。
**坑3:忘了小模型也记日志。**有阵子我以为判定环节"反正便宜"就没接全字段日志,结果想复核一次批量判定的准确率时无据可查,只能重跑。全字段日志的成本相对调用本身可以忽略,越便宜的环节越要记账,因为你对它的质量心里最没底。
坑4:一次性把所有环节都降档。第一次尝到甜头后我飘了,把 checker、judge、summarizer 一夜之间全切到 mini 档,第二天批量跑完成功率掉了七个点,都不知道该怀疑谁——变量改太多,失败无法归因。正确姿势是一次只降一个环节,跑一批基准再降下一个,虽然慢,但每次劣化都能立刻定位到具体环节。这跟第8节要讲的调优方法论是同一条:单变量原则。
**坑5:mini 模型的限流打爆了循环。**小模型便宜到让人忘了它也有速率限制,每轮判定加审查并发打过去,429 报错把循环卡死。解法是 router 里给小模型通道加个简单退避(指数等待重试三次),十几行代码的事,没有它批量任务跑不完一个通宵。
小结
| 角色 | 模型 | 成本 | 节省 |
|---|---|---|---|
| 写代码 | 强模型 | $3/M | — |
| 审查 | 小模型 | $0.25/M | 92% |
| 判定 | 小模型 | $0.25/M | 93% |
| 摘要 | 小模型 | $0.1/M | 97% |
核心要点:
- 不是所有任务需要最强模型:判定/分类/摘要用小模型,按认知负荷分配算力
- 模型路由:角色→模型的映射配置化(ModelTier),改档位零代码改动
- 成本节省:分层策略整体节省30-60%;判定环节单独省93%
- 质量不降的前提是兜底:pytest前置、黄金样例否决,小模型只做它够格的判断
- 算账基线要对:拿"同等功能全强模型"对照,别拿残缺方案对照
- 降档前先校准:50个标注样本的一致率≥80%才降,长上下文自动升档
延伸阅读与思考
- 实践:给你的循环加ModelTier配置,把判定环节切到mini档跑一周看账单
- 实验:抽50个任务做判定一致率校准,看你的小模型够不够格
- 思考:你的循环里还有哪些环节在"用总工程师看工牌"?
下节预告
第7章第8节《调优实录:一个修复循环从71%到90%+全过程》——完整复盘真实仓库修复循环的调优。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!
本系列持续更新中,关注不迷路~
文章编号:第7章第7节 | 总进度:51/120 | 预计阅读时间:11分钟