《上下文工程》系列的前 14 篇都在做同一件事:设计上下文——预算怎么分配、历史怎么压缩、检索结果怎么注入、工具输出怎么治理。每篇结尾其实都悬着同一个问题:改完之后,怎么知道真的有效?多数团队的答案是"看模型回答好不好"。本篇要论证这个答案不可靠:输出好不等于上下文对,上下文坏了输出也不会立刻坏。上下文需要一套自己的专项评测。
这是《上下文工程》系列第 15 篇,也是通往下一个系列 EDD(Eval-Driven Development,评测驱动开发)的桥梁。本文先讲上下文质量为什么必须单独评测,再给出充分性、噪音度、时效性三个可量化的维度与配套实现,然后落到 A/B 上下文实验与回归测试,最后给出 LLM 审计提示词的设计纪律。
1、上下文质量的特殊性
先看两个失效模式,它们解释了为什么只评输出不够用。
**输出好不等于上下文对。**模型答对了,可能是上下文送对了事实,也可能是参数知识碰巧覆盖、甚至纯靠运气。输出评测度量的是"上下文加模型"的整体效果,拆不出上下文本身的贡献——一条没把关键事实送进窗口的链路照样能通过输出评测,直到撞上一个模型不认识的冷门问题才暴露。
**上下文坏了,输出未必立刻坏。**第 5 篇的压缩丢掉一条事实,之后几轮模型照样答对:信息还残留在近期对话原文里。再过三轮残留耗尽,模型开始编造,事故离原因隔了整整一段会话。这种潜伏期让"出问题查最近改动"的调试直觉失效——查到的改动都是无辜的,真凶早在几十轮前就离开了现场。
上下文是确定性的中间产物,输出只是它的概率投影。评测应该量投影的源头,而不是只盯着投影本身。
这句话也是工程上的可行性判断:上下文在送入模型之前就是普通的字符串与结构化数据,不调用模型也能检查——上下文评测因此比输出评测更便宜、更稳定、可进 CI。Anthropic 在工程博客里反复强调维持高信噪比的最小 token 集合,本篇回答的是:怎么验证自己真的做到了。
2、三个评测维度
把"上下文质量"拆成三个维度,每个维度一句话定义、一个可计算的口径:
| 维度 | 定义 | 量化口径 | 典型病灶 |
|---|---|---|---|
| 充分性 | 任务所需关键信息是否在上下文中 | 事实包含率 = 命中事实数 / 清单总数 | 压缩丢事实、检索漏召回 |
| 噪音度 | 与当前任务无关的内容占比 | 无关 token 占比、配额偏离度 | top-k 过大、工具输出未治理 |
| 时效性 | 过时信息是否被及时清出 | 过时条目留存数、信息平均年龄 | 旧版本文档、已推翻的结论残留 |
充分性问"该有的在不在",噪音度问"不该有的占多少",时效性问"过时的走没走"。前两者是 RAG 评测里 Context Recall 与 Context Precision 的推广——RAGAS 文档对这两个指标有完整定义。但检索只是上下文的来源之一,同样的口径应推广到系统提示词、压缩摘要、工具结果等全部信息源。
时效性值得单列,因为过时信息比无关信息更危险:它往往自带具体数值与版本细节,显得比模型的参数知识更权威,模型会优先采信上下文里的旧值。量化用两个数:过时条目留存数,即已知失效的信息仍留在上下文里的条数;信息平均年龄,即各信息源时间戳距当前的加权平均。工程对策是给信息源设 TTL(Time To Live,存活期)并做冲突检测——同名字段出现两个值时直接报警。
评测设计本身也有纪律——样本量、判官校准、防评测集过拟合——Anthropic 的评测方法拆解值得先通读一遍,本文后面各节只展开上下文特有的部分。
3、黄金事实清单评测法
充分性评测的核心工具是黄金事实清单:为每类高频任务列出"没有它就答不对"的关键事实,再检查实际上下文覆盖了多少。
3.1、事实清单构建
第一步不是写代码,是整理清单。以代码库问答 Agent 为例,评测集里的一条样例如下:
{
"task_id": "t-041",
"question": "连接池默认大小是多少?在哪里配置?",
"golden_facts": [
"连接池默认大小为 20",
"配置项名为 pool.max_size",
"配置文件位于 config/database.yaml",
"修改后需重启进程生效"
]
}
清单的来源有三个:历史任务的标准答案、领域文档、线上事故复盘。每类任务 10~20 条起步,随版本演进显式维护。写法上每条事实要独立可判定——一条只含一个断言,"默认 20 且改完要重启"这种复合句必须拆开,否则判定结果没法归因。
3.2、包含率计算
第二步跑真实管线,截取注入完成、即将送入模型的最终上下文,逐条判定事实能否仅凭上下文确认。实现如下:
def fact_coverage(golden, context, judge):
"""逐条判定黄金事实能否仅凭上下文确认"""
hits = []
for fact in golden["golden_facts"]:
verdict = judge(
f"<context>\n{context}\n</context>\n"
f"事实:{fact}\n"
"仅凭上下文即可确认该事实吗?回答 yes 或 no。"
)
hits.append(verdict.strip().lower() == "yes")
return sum(hits) / len(hits) # 事实包含率
判定口径是"仅凭上下文"——把模型的参数知识隔离在判定之外,这是上下文评测与输出评测的本质区别:输出评测里模型可以自己兜底,这里不行。judge 初期用人工,每条事实半分钟;样本量上来后换 LLM 助评,第 7 节的审计提示词就是它的完整版。经验阈值:事实包含率低于 90% 的链路,不值得进入输出层评测——上下文缺着事实,输出层调提示词是白费劲。
4、噪音度量化评测
噪音度用两个互补的指标:一个对账总量,一个检查内容。
**配额偏离度。**第 4 篇给过 token 预算表:系统提示词、工具定义、检索结果、对话历史各有配额。评测的第一步是拿实测占比对账。实现如下:
def quota_report(actual, budget):
"""actual/budget:各信息源的 token 实测数与配额"""
rows = []
for source, limit in budget.items():
used = actual.get(source, 0)
rows.append({
"source": source,
"utilization": round(used / limit, 2),
"overspend": round(max(0, used - limit) / limit, 2),
})
return rows
两个数各有含义:超支率是噪音的直接证据——某源挤占配额,其他源就被压缩;利用率长期低于 50% 则是反向信号——要么配额虚高,要么该源供给不足,后者要回连第 3 节的包含率一起看,两者合起来才是一个信息源的完整体检。
**无关检索占比。**配额对账只看总量,看不出配额里装了什么。对每条检索结果按与问题的相关度打分——2 相关、1 弱相关、0 无关——再按 token 加权汇总:
def noise_ratio(question, chunks, judge):
"""chunks:[(文本, token 数)],逐条评与问题的相关度"""
irrelevant, total = 0, sum(t for _, t in chunks)
for text, tokens in chunks:
score = judge(question, text) # 0 无关 / 1 弱相关 / 2 相关
if score == 0:
irrelevant += tokens
return irrelevant / total # 无关 token 占比
经验阈值:占比超过 30%,先降 top-k 再谈重排——第 10 篇的 RRF 融合是第二步。打 1 分的弱相关不算噪音但值得单独观察,它是检索器逼近上限的信号。这套口径与 RAGAS 的 Context Precision 同源,把概率化判定简化成三档,代价是粒度变粗,收益是人工抽检能快速校准。
5、A/B 上下文实验
单指标定位问题,比较才能做决策。A/B 上下文实验的形态:同一评测集、同一被测模型,只换上下文方案——比如压缩对清理、top-k 取 5 对 10、自然语言摘要对第 7 篇的结构化笔记。实验配置模板如下:
EXPERIMENT = {
"name": "compaction_vs_pruning",
"dataset": "evals/sets/ops-qa-v3.jsonl", # 版本化的固定评测集
"arms": {
"A": {"compact": True, "prune": False},
"B": {"compact": False, "prune": True},
},
"metrics": ["fact_coverage", "noise_ratio", "token_total"],
"judge_model": "与被测模型分离的强模型",
}
跑分之外有三条纪律:
- 单变量:一次只改一个上下文策略,其余配置全部冻结
- 评测集版本化:两次跑分必须是同一份数据,否则数字不可比
- 判官分离:judge 与被测模型不同源,避免同源模型的共性盲区互相掩护
结果解读要联动输出层。上下文指标改善但输出评测没动,说明该维度不是当前瓶颈,力气该花到别处;反过来,上下文指标变差而输出反而变好,多半是噪音里藏着信息——黄金事实清单漏收了事实,先修清单再下结论。
6、上下文回归测试
一次性评测回答"现在好不好",回归测试回答"这次改动有没有弄坏"。上下文策略的日常改动——调 top-k、改压缩阈值、换摘要提示词——个个都预期无伤,个个都可能悄悄丢事实。把第 3、4 节的指标装进门禁,挂进 CI:
BASELINE = {"fact_coverage": 0.95, "noise_ratio": 0.12}
def regression_gate(metrics):
"""上下文指标 CI 门禁,阈值留容差抗评测噪声"""
if metrics["fact_coverage"] < BASELINE["fact_coverage"] - 0.02:
return "FAIL:事实包含率回退超过 2 个百分点"
if metrics["noise_ratio"] > BASELINE["noise_ratio"] + 0.05:
return "FAIL:噪音占比上升超过 5 个百分点"
return "PASS"
门禁逻辑借自测试驱动开发,但对象是概率性管线:LLM 判定有抖动,阈值必须留容差(包含率 ±2 个百分点),否则 CI 会在噪声里频繁误报,很快被人改成永远跳过。baseline 不是永恒标准,随版本显式更新并留档。评测跑批不必自研,promptfoo 这类开源工具原生支持配置化评测与 CI 集成。
这一节也是本系列与 EDD 系列的交接点:改动之前先补评测,评测红了再动代码——评测从质量保障手段升级为开发流程本身。EDD 系列将展开评测集构建、判官校准与评测节奏的工程化,本文的三个维度与回归门禁就是它的入口。
7、审计提示词设计
前面几节的 judge 都指向同一个组件:让强模型审计"给定上下文能否回答给定问题、还缺什么"。这个提示词的质量决定整套评测的可信度。模板如下:
你是上下文审计员,只依据给定上下文工作,禁止用自身知识补全判断。
<question>待回答的问题</question>
<context>待审计的上下文</context>
按步骤执行:
1. 列出回答该问题所需的全部关键事实
2. 逐条在上下文中定位支持原文,找不到则标记 missing
3. 只输出 JSON:
{"answerable": true/false,
"coverage": 0 到 1 之间的包含率,
"missing": ["缺失事实", ...],
"evidence": {"事实": "支持原文摘录"}}
注意:上下文中与问题无关的内容不计入缺失,也不得用作支持证据。
**第一条纪律是"只依据上下文"。**没有这句话,判官会用参数知识替被测系统兜底,包含率虚高——这恰恰是被评测的失败模式,必须在第一行封死。
第二条是"定位支持原文"。要求判官对每条事实给出上下文原文摘录,程序再用子串匹配校验摘录是否真实存在,对不上的判定直接作废——这一步把 LLM 判定从"可信度存疑"变成"可机器抽查",是防判官幻觉的关键一招。
第三条是结构化输出。JSON 落地后,包含率、缺失清单、证据摘录直接进指标管线,缺失清单还能反向喂给黄金事实清单的维护——评一次测,清单也跟着变厚。
第四条是校准。上线前抽 20% 样本人工双评,一致率达标才允许 LLM 判定进 CI;不达标先修提示词,而不是调大容差把噪声放进来。
8、总结
前 14 篇设计上下文,本篇补上度量。评测对象要从输出下移到上下文本身——它是确定性中间产物,检查它比检查输出更便宜也更稳定。三个维度各自有量化口径:充分性用黄金事实清单算事实包含率,噪音度用配额偏离度与无关检索占比对账,时效性盯过时条目留存与信息年龄。决策靠 A/B 上下文实验的三个纪律,守住改动靠带容差的回归门禁,LLM 助评靠审计提示词的四条纪律——只依据上下文、定位支持原文、结构化输出、人工校准。
对于已维护长会话 Agent 的团队,建议本周就为最高频的一类任务整理 10 条黄金事实,把事实包含率纳入发布检查——两小时投入,先拦住"压缩丢事实"这类最贵、最难定位的事故。
练习:给第 5 篇的 compaction 实现建一套"压缩前后事实保留率"评测。要求:构造 5 个多轮会话,各附黄金事实清单;在压缩触发前后分别截取上下文,跑事实包含率;保留率 = 压缩后包含率 / 压缩前包含率。验收两条:平均保留率不低于 0.9;对低于阈值的会话归纳出 3 类最易丢失的事实,针对性修改 SUMMARY_PROMPT 后复测,保留率有可复现的提升。

