judge 只看草稿不看证据,新查的资料又被截在上下文之外:B 线循环的两个漏洞。
B 线先生成一份草稿,再由 judge 判断要不要继续查资料。这段循环想解决报告依据不够的问题。但九月重新整理 backend/dual_graph.py,我觉得有两个地方必须展开讲,否则一张带回环箭头的图很容易把效果说大。
judge 看的是草稿,不是证据
_judge_report 的参数只有 query、report 和模型后端,prompt 放的是问题与草稿,没有直接传 recall_bundle。草稿生成时读过证据,不代表 judge 又独立对照了一遍。它返回 sufficient 和 reason,程序把原问题与 reason 拼起来,作为下一次检索的 query。
问题 + 初始证据 → 草稿问题 + 草稿 → judge → sufficient / reason不充分且未到上限 → 用原问题与 reason 补查所以这份判断更接近对草稿的检查。要验证某句话是否忠实于来源,得把对应证据明确交给检查过程,当前函数没做这一步。judge 决定循环继续还是停,自始至终只看到问题与草稿;新查回的材料只能通过新草稿间接影响下一轮判断。
公开做法里,这一步的输入有三样
拿 Self-RAG 对照最清楚。它的论文表 1给几种批评 token 分别列了输入:判“草稿是否被材料支持”的 IsSup,输入是问题、材料、生成内容三样(x,d,y);只看问题和生成内容(x,y)的 IsUse,判的是有没有用。按这个分法,我们的 judge 其实只做了 IsUse 那一档的事,却被拿来决定“依据够不够”。
别处的口径也一致:
- Ragas 的 Faithfulness 指标把回答拆成逐条陈述,逐条对照检索到的上下文,对照对象是 retrieved context,不是参考答案;
- Anthropic 的 Citations 把文档切成句子,回答按“断言 + 引用”分块返回,官方说引用一定指向提供的文档。
两种都是看着证据做检查,比 MediAC 现在一份草稿级别的 sufficient 细得多。
sufficient 是一个带偏差的信号
judge 也不是不可用。MT-Bench 那篇论文测出 GPT-4 级裁判和人类偏好的一致率超过 80%,和人与人之间相当,但同时列出位置偏差、冗长偏差、自我增强偏差。另一篇进一步证实 GPT-4 有明显的自我偏好,作者认为根子在困惑度:模型偏爱自己熟悉的文本。judge 和草稿用同一个后端时,“判成充分”本身就带着偏向。
“够不够”也未必要二值。Self-RAG 的检索决策有 yes / no / continue 三档,可以按概率设阈值;CRAG 用上下两个阈值分出 Correct、Incorrect、Ambiguous,中间档两边知识都用。还有一篇专门研究上下文是否充分的论文发现,强模型在上下文不充分时常常答错而不是弃答;作者单独训了一个充分性分类器来指导弃答,弃答后回答的正确率提升 2–10%。依据够不够,被当成一个要单独度量的问题。
追加在后面,截取还在前面
查回新材料以后,代码用 bundle.extend(extra) 加到末尾。下一份草稿却只读 bundle[:graph_snippet_limit]。如果首轮已经填满上限,新加的片段可能一条也进不了生成上下文。
用上限为 3 的自拟例子:
初始:[A, B, C]追加:[D, E]合并:[A, B, C, D, E]草稿实际读取前 3 项:[A, B, C]日志里有追加检索,bundle 也确实变长,报告读到的仍是旧三条。这时再要求模型认真参考新增资料没有用,该先查选择上下文的代码。这是对现存实现的静态观察,还没在这里修掉,也没拿它推算过去演示的效果。
“用原问题加 reason 再查一次”这个方向本身有出处。IRCoT 的动机就是一次检索不够,检索什么取决于前面已经推出了什么。末尾这个位置也不吃亏:Lost in the Middle 发现相关材料放在上下文开头或结尾时效果最好,落在中间才明显变差。病根只在截取那一行。
先排再截,或者按条精炼
要修,有两种现成写法:
| 做法 | 怎么选进上下文 | 代价 |
|---|---|---|
| 合并后重排再截 | Anthropic Contextual Retrieval 初检 150 条,重排后取 20 条;试过 5/10/20 条,20 最好,top-20 检索失败率从 5.7% 降到 1.9% | 多一次重排调用;官方也提醒信息多了可能分散注意力 |
| 按条精炼 | CRAG 第 4.4 节把文档切成细粒度 knowledge strips,逐条打分,滤掉不相关的再按顺序拼回 | 每条都要打分 |
两者的共同点是按相关性决定谁进上下文,不是按位置。
两个上限,管的不是同一件事
循环用 B_MAX_EXTRA_RETRIEVAL 限制追加次数,默认五次。judge 表示充分会停止,查不到新增材料也会提前退出。代码里空结果退出后有一条状态文案仍写“首轮依据已够用”,与实际的不足判断不一致,读日志时要特别区分。
到上限只代表停止追加,不能反过来证明报告已经充分。追加次数和进入生成上下文的片段数是两个上限:前者到顶就不再取新材料,后者满了以后取回来也进不了上下文。两种情况下,报告的依据都不会再变多。
框架层的递归上限帮不上这个忙。LangGraph 的 recursion limit 计的是图的 super-step 数,1.0.6 起默认 1000,超了抛 GraphRecursionError。B 循环跑在 run_tracks 节点内部,对图来说只是一步。框架的递归上限管的是图的步数,节点内部的循环得自己设上限。真正起作用的是这两个:
- 循环次数上限,控成本;
- 上下文片段数上限,决定取回来的东西有没有用。
次数定多少,官方也不替你决定。LangGraph 的 Agentic RAG 教程用二值评分判断检索到的文档是否相关,不相关就改写问题重查,说这条重试回路是从弱首检里恢复的办法;整页没给重试次数上限。注意它判的对象是文档,不是草稿。片段数上限一旦满了,循环次数再宽也只是白花调用。
下一步该补的测试
test_llm_judge.py 目前验证合法 JSON 与解析失败回退,没覆盖这整条循环。可以补的检查很明确:让初始 bundle 填满上限,再返回一条带独特标记的新证据,看下一次草稿的实际 prompt 里有没有它。这个问题和医学判断无关,用普通示例文本就能测清楚。
这类“检查到底有没有看到证据”的行为,公开实现里也常被成本压掉。LangChain 的自反思 RAG 文章写明,他们没照论文逐块生成、逐块打分,而是对所有相关文档做一次生成,为的是少一次 LLM 调用、降低延迟。所以这个被简化掉的环节更需要专门测。
问诊场景里还有一类缺口只能继续问用户。资料库能查一般知识,不能告诉系统这次不适持续了多久。A 线仍要收集这些信息,B 线追加再多检索,也不能把它们补成用户已经说过的事实。CRAG 的摘要也承认,从静态、有限的语料里检索,可能只拿到次优文档,所以它另接了网页搜索;而“症状持续多久”这种事,网页也给不了,只能问人。本文只讨论教学 demo 的执行路径,不作为诊断依据。
留下几句话 读者来信
读到这里,有什么想法?欢迎留下你的看法。