ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

评分对不上先别怪模型:长回答拆短再对账的 3 个口径(附 6 行表)

评分对不上先别怪模型:长回答拆短再对账的 3 个口径(附 6 行表)

我在评分不准的时候干过三件事:换模型、调 temperature、加 few-shot,一件都没用。

真正有用的是把对账口径先定死。

面试吧的评分链路我调了两周,评测集里那批长回答 62 分、线上真实数据 85 分的那个坑,最后不是靠换模型填上的,是靠把「分数到底是什么」「回答按多长切」「什么情况下允许改口径」这三件事写成条文。

这篇只讲这三个口径,按顺序排:先定义分数(不然拆了也没法对账),再定切片长度,最后定什么情况下允许改口径。附的那张 6 行表可以直接存。

一、口径一:分数先定义清楚,不然没法对账

最常见的僵局长这样:一边说分数低了,另一边说模型没变,两边吵半天,谁也拿不出东西。

根子是分数没有定义。

我的定义写死成一句话:分数 = 100 个可检验论点里,检索到支撑证据的论点的个数。

三个约束:

  1. 论点是事先枚举的,不是跑完之后倒推的。评测集建的时候就把每道题的论点列全,列进表里,改一条留一条痕。
  2. 判定是二值的。有证据就是 1,没有就是 0,不存在“部分支撑”这种中间态,模棱两可的单独进人工池。
  3. 总数是固定的。100 就是 100,现在写死在常量里,加了题必须先改这个常量再重跑全量,改之前程序拒绝出分。

这三条定完,“分数低了”这句话才有了可查的落点:是论点枚举漏了,还是检索没召回,还是证据库本身缺料。

二、口径二:切片长度是个参数,不是越细越好

长回答为什么必须拆?因为整篇丢进检索的时候,1840 字只出一个向量,超了模型上限的部分会被直接截掉,压根不进向量。进得去的那部分里,结论、前提、步骤、异常处理、边界全挤在一起,召回回来的证据跟每一段都沾点边、跟每一段都不对准。

拆多长?这个我扫过一遍。同一份 1840 字的回答、同一批 100 个论点、同一个证据库,只改切片粒度:

#!/usr/bin/env python3# 长回答拆短对账:扫描切片粒度,找出证据覆盖率最高的那一档。# 数据来自一次真实跑批(同一份 1840 字的回答,100 个可检验论点)。importmath TOTAL_CHARS=1840TOTAL_CLAIMS=100# (切片粒度 字/片, 有证据支撑的论点数)RAW=[(1840,62),(1600,68),(1200,73),(800,79),(400,85),(200,83),]MIN_SANE=300# 低于这个粒度,切片会把前提和结论切到两片里defaudit(rows):ifnotrows:raiseSystemExit("对账清单为空,拒绝出分")out=[]forsize,hitinrows:ifnot(0<size<=TOTAL_CHARS):raiseSystemExit(f"切片粒度{size}超出回答长度{TOTAL_CHARS}")ifnot(0<=hit<=TOTAL_CLAIMS):raiseSystemExit(f"命中论点{hit}超出论点总数{TOTAL_CLAIMS}")pieces=math.ceil(TOTAL_CHARS/size)out.append((size,pieces,hit,hit*100//TOTAL_CLAIMS))returnoutdefmain():table=audit(RAW)print(f"回答总字数:{TOTAL_CHARS}可检验论点:{TOTAL_CLAIMS}\n")print("粒度(字/片) 片数 末片字数 有证据论点 得分")print("-"*52)forsize,pieces,hit,scoreintable:last=TOTAL_CHARS-size*(pieces-1)print(f"{size:>8}{pieces:>4}{last:>8}{hit:>10}{score:>4}")print("-"*52)best=max(table,key=lambdar:r[3])base=table[0]print(f"\n最优粒度:{best[0]}字/片,{best[1]}片,得分{best[3]}")print(f"整篇不拆:{base[1]}片,得分{base[3]}")print(f"差值:{best[3]-base[3]}分")thin=[rforrintableifr[0]<MIN_SANE]ifthin:s=thin[-1]print(f"\n注意:{s[0]}字/片 已低于{MIN_SANE}字,得分回落至{s[3]},"f"切片过碎会把前提和结论切开")short_tail=[rforrintableifTOTAL_CHARS-r[0]*(r[1]-1)<MIN_SANE]ifshort_tail:print(f"另有{len(short_tail)}档的末片不足{MIN_SANE}字"f"(末片是切剩下的尾巴,长度不由粒度决定),对账时单独看")if__name__=="__main__":main()

跑完的这张表就是结论,建议直接存:

粒度(字/片)片数末片字数有证据论点得分
1840(不拆)118406262
160022406868
120026407373
80032407979
40052408585
20010408383

三个取舍点:

  • 不拆是 62 分,拆到 400 字是 85 分,差 23 分。这 23 分跟模型一点关系没有。
  • 不是越细越好。200 字那一档掉回 83,因为前提和结论被切进了两片,后一片检索时已经不知道前面在说什么。我给这个边界留了个常量MIN_SANE = 300,低于它就打印一行提醒。注意它只是提醒,不拦流水线,粒度是参数,选细了不一定是错,要自己判断。
  • 末片是切剩下的尾巴,长度不由粒度决定。上面 6 档里有 4 档末片不足 300 字(1600 / 800 / 400 / 200 档),这是固定窗口切片的固有现象,对账时单独看,不要把它当成“粒度选小了”。

三、口径二的延伸:两种切法怎么选

按论点切要多一步论点识别,论点表还要落库。我现在评测集上按论点切、线上按固定字数切,两边的差每周对一次,超过 5 分就回去查论点表。

四、口径三:什么时候允许改对账口径

这条最容易被跳过,也最容易埋雷。我给自己定了三条硬规矩:

场景能不能改附加条件
加了新题、论点总数变了能必须同步改对账表,改完重跑全量,不许只跑增量
本轮分数不好看不能分数难看就去查检索和证据库,不许回头改判定标准
换了新的证据库能先跑一遍旧对账表做基线,新旧差超过 5 分要写清楚原因

第二条是重点。分数不好看的时候改口径,等于把尺子改短了再去量身高,短期好看,长期一定漂。

程序上也做了配套:对账清单为空直接拒绝出分(not rows)、切片粒度超出回答长度直接拒绝出分(0 < size <= TOTAL_CHARS)、命中论点数超出论点总数直接拒绝出分(0 <= hit <= TOTAL_CLAIMS)。这三道校验的作用是让“偷偷改口径”这件事在流水线上跑不过去。

五、下次评分对不上,按这个顺序过

  1. 查评测集的长度分布,长回答占比和线上差一倍以上就重采样。
  2. 查长回答有没有拆,整篇检索和拆片检索差 10 分以上就必须拆。
  3. 查论点枚举是不是同一批人做的,换个标注员差 5 分以上就把论点表落库。

前两条能把绝大部分“分数突然掉了”找回来,第三条是防止下个月又漂回去。

这张 6 行对账表可以直接存,下次评分对不上先按它过一遍。

王中阳

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进