
第7节 分层模型策略:强模型干活、小模型验收
一句话总结:用便宜快速的小模型做每轮"完成判定";maker/checker/仲裁三层模型选型表(能力×成本×速度);配置化模型路由实现。
本文导航
- 一、不是所有判断都值得用最贵的脑子
- 二、三层模型选型表
- 三、配置化模型路由
- 四、小模型做完成判定
- 五、完整分层循环
- 六、成本核算:我算错过一次账
- 七、小模型的边界:失败模式与踩坑实录
一、不是所有判断都值得用最贵的脑子
这一节的动机来自一张账单。有阵子我跑批量任务,每轮循环里所有环节——写代码、审查、判定"做完了吗"、给日志写摘要——全都打到同一个强模型上。月底一看账单,我盯着"完成判定"那一项的占比发呆了:一个只回答’是/否’的问题,花的钱和写代码差不多。因为判定调用次数最多(每轮至少一次),虽然单次便宜不了多少,架不住它量大。
而"这个任务完成了吗"这种判断,需要顶级推理能力吗?多数时候不需要——对着目标看一眼产出,检查关键点齐不齐,是个"及格线智力"的活。让 GPT-4o 级别的模型天天干这个,等于让总工程师守着门看工牌。
核心思想:
不是所有任务都需要最强模型
强模型(GPT-4o/Claude Sonnet)→ 写代码、复杂推理
小模型(GPT-4o-mini/Haiku)→ 完成判定、分类、摘要
效果:成本降低60-80%,质量几乎不降
这个思路在传统工程里有个一模一样的对应物:CPU 调度。重要复杂的任务给大核,琐碎高频的中断给小核处理,大小核协同才有能效比。模型分层就是给 LLM 循环装一套"大小核调度"——按认知负荷分配算力,而不是一刀切全用旗舰。
#mermaid-svg-yFjvSgnQP27hfLTv{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yFjvSgnQP27hfLTv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yFjvSgnQP27hfLTv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yFjvSgnQP27hfLTv .error-icon{fill:#552222;}#mermaid-svg-yFjvSgnQP27hfLTv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yFjvSgnQP27hfLTv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yFjvSgnQP27hfLTv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yFjvSgnQP27hfLTv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yFjvSgnQP27hfLTv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yFjvSgnQP27hfLTv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yFjvSgnQP27hfLTv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yFjvSgnQP27hfLTv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yFjvSgnQP27hfLTv .marker.cross{stroke:#333333;}#mermaid-svg-yFjvSgnQP27hfLTv svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yFjvSgnQP27hfLTv p{margin:0;}#mermaid-svg-yFjvSgnQP27hfLTv .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-yFjvSgnQP27hfLTv .cluster-label text{fill:#333;}#mermaid-svg-yFjvSgnQP27hfLTv .cluster-label span{color:#333;}#mermaid-svg-yFjvSgnQP27hfLTv .cluster-label span p{background-color:transparent;}#mermaid-svg-yFjvSgnQP27hfLTv .label text,#mermaid-svg-yFjvSgnQP27hfLTv span{fill:#333;color:#333;}#mermaid-svg-yFjvSgnQP27hfLTv .node rect,#mermaid-svg-yFjvSgnQP27hfLTv .node circle,#mermaid-svg-yFjvSgnQP27hfLTv .node ellipse,#mermaid-svg-yFjvSgnQP27hfLTv .node polygon,#mermaid-svg-yFjvSgnQP27hfLTv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yFjvSgnQP27hfLTv .rough-node .label text,#mermaid-svg-yFjvSgnQP27hfLTv .node .label text,#mermaid-svg-yFjvSgnQP27hfLTv .image-shape .label,#mermaid-svg-yFjvSgnQP27hfLTv .icon-shape .label{text-anchor:middle;}#mermaid-svg-yFjvSgnQP27hfLTv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yFjvSgnQP27hfLTv .rough-node .label,#mermaid-svg-yFjvSgnQP27hfLTv .node .label,#mermaid-svg-yFjvSgnQP27hfLTv .image-shape .label,#mermaid-svg-yFjvSgnQP27hfLTv .icon-shape .label{text-align:center;}#mermaid-svg-yFjvSgnQP27hfLTv .node.clickable{cursor:pointer;}#mermaid-svg-yFjvSgnQP27hfLTv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yFjvSgnQP27hfLTv .arrowheadPath{fill:#333333;}#mermaid-svg-yFjvSgnQP27hfLTv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yFjvSgnQP27hfLTv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yFjvSgnQP27hfLTv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yFjvSgnQP27hfLTv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yFjvSgnQP27hfLTv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yFjvSgnQP27hfLTv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yFjvSgnQP27hfLTv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yFjvSgnQP27hfLTv .cluster text{fill:#333;}#mermaid-svg-yFjvSgnQP27hfLTv .cluster span{color:#333;}#mermaid-svg-yFjvSgnQP27hfLTv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yFjvSgnQP27hfLTv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yFjvSgnQP27hfLTv rect.text{fill:none;stroke-width:0;}#mermaid-svg-yFjvSgnQP27hfLTv .icon-shape,#mermaid-svg-yFjvSgnQP27hfLTv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yFjvSgnQP27hfLTv .icon-shape p,#mermaid-svg-yFjvSgnQP27hfLTv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yFjvSgnQP27hfLTv .icon-shape .label rect,#mermaid-svg-yFjvSgnQP27hfLTv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yFjvSgnQP27hfLTv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yFjvSgnQP27hfLTv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yFjvSgnQP27hfLTv :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
写代码/推理
判定/分类
摘要/格式化
任务
任务类型
强模型$3/M tokens
小模型$0.25/M tokens
微模型$0.1/M tokens
路由的判据就三个问题:这个环节要生成多少内容(生成量大的给强模型)、要理解多复杂的东西(跨文件推理给强模型)、调用频率多高(高频的优先降档)。写代码三项全占,必须是强模型;完成判定只占"频率高",果断降档。
二、三层模型选型表
把循环里的典型环节过一遍,落到具体选型:
| 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 的处理费:
| 强模型 | $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
"""
import os
import time
from datetime import datetime
from openai import OpenAI
from pydantic import BaseModel
from .logging_config import get_logger
logger = get_logger("model_router")
class ModelTier(BaseModel):
"""模型分层配置:改档位只改这里,循环代码零改动"""
maker: str = "gpt-4o" # 写代码:要生成质量
checker: str = "gpt-4o-mini" # 审查:要快和便宜
judge: str = "gpt-4o-mini" # 完成判定:是/否问题
summarizer: str = "gpt-4o-mini" # 摘要:格式转换
class CallStat(BaseModel):
"""单次调用统计:与全系列LLM日志字段同口径"""
role: str
model: str
baseurl: str
key_tail6: str
input_len: int
output_len: int
start: str
end: str
cost_sec: float
class ModelRouter:
"""模型路由器:按角色分发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] = []
def get_client(self, role: str) –> tuple[OpenAI, str]:
"""根据角色获取(client, 模型名)"""
model_name = getattr(self.tier, role)
return self.clients[model_name], model_name
def call(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.content or ""
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)
return content
def summary(self) –> None:
"""退出前打印量化总结:各角色分开的成本画像"""
for role in ("maker", "checker", "judge", "summarizer"):
rs = [s for s in self.stats if s.role == role]
if not rs:
continue
costs = [s.cost_sec for s in rs]
logger.info("[总结] 角色=%s 次数=%d 输入长度 平均=%.0f 最大=%d 耗时 最大=%.2fs 平均=%.2fs",
role, len(rs), sum(s.input_len for s in rs) / len(rs),
max(s.input_len for s in rs), max(costs), sum(costs) / len(costs))
这套结构的好处是审计免费:因为每次调用都带 role 和 model 落进日志,月底拉出来一统计,每个角色花了多少 token、平均耗时多少,清清楚楚。哪一档该升该降,不再拍脑袋,看数据说话。
四、小模型做完成判定
判定环节的实现出奇地短——问题本身就简单,提示词要做的只是把"简单"固定下来,别让模型发挥:
# src/ai_loop_course/judge.py
"""小模型完成判定:高频环节降档省钱的主战场"""
from pydantic import BaseModel, ValidationError
from .logging_config import get_logger
from .model_router import ModelRouter
logger = get_logger("judge")
class JudgeVerdict(BaseModel):
"""判定输出:结构化,不收自由文本"""
done: bool
reason: str
def judge_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)
except ValidationError:
verdict = JudgeVerdict(done=response.strip().startswith(("是", "yes", "Yes")),
reason="格式兜底:按文本前缀判定")
logger.info("判定结果:%s(理由:%s)",
"完成" if verdict.done else "未完成", verdict.reason[:60])
return verdict.done
真实运行的控制台输出(注意 model 字段是 mini 档):
$ uv run python -m ai_loop_course.judge
2026-09-18 09:41:03 [model_router] INFO LLM调用 角色=judge baseurl=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.86s
2026-09-18 09:41:04 [judge] INFO 判定结果:完成(理由:除零保护已实现且测试通过)
2026-09-18 09:41:04 [model_router] INFO LLM调用 角色=judge baseurl=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.79s
2026-09-18 09:41:05 [judge] INFO 判定结果:未完成(理由:未处理BOM头,UTF-8-sig场景会挂)
耗时不到一秒——这在强模型上经常要三五秒,判定是每轮都跑的环节,速度差距乘以轮数就是体感差距。
成本对比:
| 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
"""分层模型循环:强模型干活,小模型判定"""
from ai_loop_course.logging_config import get_logger
from ai_loop_course.model_router import ModelRouter
logger = get_logger("layered_loop")
def layered_loop(goal: str, router: ModelRouter, max_steps: int = 15) –> str | None:
"""分层模型循环:maker每轮写,judge每轮判,小模型当门卫"""
feedback = ""
try:
for step in range(1, max_steps + 1):
# 强模型写代码(带上轮判定反馈)
code = router.call("maker", f"任务:{goal}\\n上一轮反馈:{feedback or '无'}\\n请给出完整实现。")
# 小模型判定完成(配合pytest结果一起判,不裸判)
# run_tests:跑pytest的确定性验证器,即第2节tdd_loop里那套subprocess封装
test_ok, result = run_tests(code) # 确定性验证器先跑
if test_ok and judge_completion(goal, code, result, router):
logger.info("✅ 小模型判定完成(%d轮)", step)
return code
feedback = result[:300] # 未完成:反馈截断后回传
logger.warning("达到最大轮数:%d", max_steps)
return None
finally:
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 什么时候不够格
分层不是无脑降档,有些环节降了会出事故。我把小模型的判定失败模式归了类,配合一个能力校准流程用:
#mermaid-svg-G5XO7xi09wSdZqQW{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-G5XO7xi09wSdZqQW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G5XO7xi09wSdZqQW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G5XO7xi09wSdZqQW .error-icon{fill:#552222;}#mermaid-svg-G5XO7xi09wSdZqQW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G5XO7xi09wSdZqQW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G5XO7xi09wSdZqQW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G5XO7xi09wSdZqQW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G5XO7xi09wSdZqQW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G5XO7xi09wSdZqQW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G5XO7xi09wSdZqQW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G5XO7xi09wSdZqQW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G5XO7xi09wSdZqQW .marker.cross{stroke:#333333;}#mermaid-svg-G5XO7xi09wSdZqQW svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G5XO7xi09wSdZqQW p{margin:0;}#mermaid-svg-G5XO7xi09wSdZqQW .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-G5XO7xi09wSdZqQW .cluster-label text{fill:#333;}#mermaid-svg-G5XO7xi09wSdZqQW .cluster-label span{color:#333;}#mermaid-svg-G5XO7xi09wSdZqQW .cluster-label span p{background-color:transparent;}#mermaid-svg-G5XO7xi09wSdZqQW .label text,#mermaid-svg-G5XO7xi09wSdZqQW span{fill:#333;color:#333;}#mermaid-svg-G5XO7xi09wSdZqQW .node rect,#mermaid-svg-G5XO7xi09wSdZqQW .node circle,#mermaid-svg-G5XO7xi09wSdZqQW .node ellipse,#mermaid-svg-G5XO7xi09wSdZqQW .node polygon,#mermaid-svg-G5XO7xi09wSdZqQW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-G5XO7xi09wSdZqQW .rough-node .label text,#mermaid-svg-G5XO7xi09wSdZqQW .node .label text,#mermaid-svg-G5XO7xi09wSdZqQW .image-shape .label,#mermaid-svg-G5XO7xi09wSdZqQW .icon-shape .label{text-anchor:middle;}#mermaid-svg-G5XO7xi09wSdZqQW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-G5XO7xi09wSdZqQW .rough-node .label,#mermaid-svg-G5XO7xi09wSdZqQW .node .label,#mermaid-svg-G5XO7xi09wSdZqQW .image-shape .label,#mermaid-svg-G5XO7xi09wSdZqQW .icon-shape .label{text-align:center;}#mermaid-svg-G5XO7xi09wSdZqQW .node.clickable{cursor:pointer;}#mermaid-svg-G5XO7xi09wSdZqQW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-G5XO7xi09wSdZqQW .arrowheadPath{fill:#333333;}#mermaid-svg-G5XO7xi09wSdZqQW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-G5XO7xi09wSdZqQW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-G5XO7xi09wSdZqQW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G5XO7xi09wSdZqQW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-G5XO7xi09wSdZqQW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G5XO7xi09wSdZqQW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-G5XO7xi09wSdZqQW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-G5XO7xi09wSdZqQW .cluster text{fill:#333;}#mermaid-svg-G5XO7xi09wSdZqQW .cluster span{color:#333;}#mermaid-svg-G5XO7xi09wSdZqQW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-G5XO7xi09wSdZqQW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-G5XO7xi09wSdZqQW rect.text{fill:none;stroke-width:0;}#mermaid-svg-G5XO7xi09wSdZqQW .icon-shape,#mermaid-svg-G5XO7xi09wSdZqQW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G5XO7xi09wSdZqQW .icon-shape p,#mermaid-svg-G5XO7xi09wSdZqQW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-G5XO7xi09wSdZqQW .icon-shape .label rect,#mermaid-svg-G5XO7xi09wSdZqQW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G5XO7xi09wSdZqQW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-G5XO7xi09wSdZqQW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-G5XO7xi09wSdZqQW :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
≥80%
70%~80%
<70%
抽样50个已标注任务
小模型判定
与人工标签一致率
该环节可降档
降档+加兜底黄金样例/双判投票
保留强模型此环节别省
| 过早放行 | 代码能跑但边界缺失就判"是" | 假完成 | 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% |
核心要点:
延伸阅读与思考
下节预告
第7章第8节《调优实录:一个修复循环从71%到90%+全过程》——完整复盘真实仓库修复循环的调优。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连! 本系列持续更新中,关注不迷路~
文章编号:第7章第7节 | 总进度:51/120 | 预计阅读时间:11分钟




