CoVe:逐条验证事实断言
旅游助手可能写出一句很流畅的话:
明天下午西湖会放晴,茶叶博物馆晚上 9 点关门。
这句话包含两个事实断言。让同一个模型"再检查一遍"没什么用——它大概率读自己的答案然后继续相信。
CoVe 更严格:先写草稿,抽取可验证的断言,逐条验证,再根据验证结果修订。
一句话
CoVe 把"事后自信"变成"断言抽取 → 独立验证 → 基于证据修订",让事实错误有机会被捕获。
它修什么失败
| 问题 | 表面看起来 | 实际风险 |
|---|---|---|
| 流畅的回答 | 听着是真的 | 事实可能是错的 |
| 模糊的审查 | "检查过了" | 没有证据留痕 |
| 引用漂移 | 有引用 | 引用可能不支持断言 |
它引入什么复杂度
| 谁 | 负责什么 |
|---|---|
| 草稿模型 | 写初始回答 |
| 断言抽取器 | 把回答拆成可验证的断言 |
| 验证器 | 对每条断言返回 ok + 证据 |
| 修订模型 | 删除或改写失败的断言 |
验证要产出证据,不是产出感觉。
和其他模式的关系
flowchart LR
COVE["CoVe"] -.->|"需要整体质量反馈"| MC["Maker-Checker"]
COVE -.->|"证据要多轮搜索"| ARAG["Agentic RAG"]
COVE -.->|"短答案方差"| VOT["Voting"]
- Maker-Checker:CoVe 逐条检查事实;Maker-Checker 检查整体质量(结构、完整性、风格)。两者可以叠加。
- Agentic RAG:如果断言的验证需要先搜索证据,那是 Agentic RAG 的活。CoVe 假设验证函数已经存在。
- Voting:Voting 处理随机性;CoVe 处理事实错误。如果多数答案都包含同一个错误断言,Voting 救不了。
完整实现:旅游信息事实检查
"""cove_travel.py — 旅游信息的逐条验证"""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any
# ── 极简运行时 ──────────────────────────────────────────
@dataclass(frozen=True)
class Message:
role: str
content: str
class MockLLM:
def __init__(self, responses: list[str]):
self._responses = list(responses)
self._idx = 0
def complete(self, messages: list[Message]) -> str:
if self._idx >= len(self._responses):
raise RuntimeError("MockLLM 回复用完了")
out = self._responses[self._idx]
self._idx += 1
return out
@dataclass(frozen=True)
class ClaimVerification:
claim: str
ok: bool
evidence: str
# ── CoVe 核心逻辑 ──────────────────────────────────────
def chain_of_verification(
model: MockLLM,
*,
question: str,
verify_claim,
max_claims: int = 6,
) -> str:
"""
1) 写草稿
2) 抽取可验证断言
3) 逐条验证
4) 根据验证结果修订
"""
# 第一步:草稿
draft = model.complete([
Message(role="system", content="尽你所能回答问题。"),
Message(role="user", content=question),
])
print(f"[草稿] {draft}")
# 第二步:抽取断言
raw_claims = model.complete([
Message(role="system", content="抽取关键事实断言。只返回 JSON。"),
Message(role="user", content=f"问题:\n{question}\n\n草稿:\n{draft}"),
])
claims_obj = json.loads(raw_claims)
claims: list[str] = claims_obj["claims"][:max_claims]
print(f"[断言] 抽取到 {len(claims)} 条:")
for c in claims:
print(f" - {c}")
# 第三步:逐条验证
verifications: list[ClaimVerification] = []
for claim in claims:
v = verify_claim(claim)
verifications.append(v)
status = "OK" if v.ok else "FAIL"
print(f"[验证] [{status}] {v.claim} — 证据:{v.evidence}")
# 第四步:修订
if not verifications:
return draft
verification_text = "\n".join(
f"- [{'OK' if v.ok else 'FAIL'}] {v.claim}\n 证据:{v.evidence}"
for v in verifications
)
revised = model.complete([
Message(
role="system",
content="根据验证结果修订草稿。如果断言不被支持,删除或修正。",
),
Message(
role="user",
content=f"问题:\n{question}\n\n草稿:\n{draft}\n\n验证结果:\n{verification_text}",
),
])
return revised
# ── 运行 ────────────────────────────────────────────────
def main() -> None:
question = "杭州茶叶博物馆的开放时间和门票?"
model = MockLLM([
# 草稿(包含一条错误断言)
"中国茶叶博物馆每天 9:00-17:00 开放,门票 50 元,周一闭馆。",
# 抽取断言
json.dumps({
"claims": [
"中国茶叶博物馆每天 9:00-17:00 开放",
"门票 50 元",
"周一闭馆",
]
}, ensure_ascii=False),
# 修订(删除错误断言)
"中国茶叶博物馆每天 9:00-17:00 开放,免费参观,周一闭馆。",
])
def verify_claim(claim: str) -> ClaimVerification:
"""确定性验证器,模拟工具或数据库查询。"""
if "9:00-17:00" in claim:
return ClaimVerification(
claim=claim, ok=True,
evidence="官网确认:开放时间 9:00-17:00"
)
if "50 元" in claim:
return ClaimVerification(
claim=claim, ok=False,
evidence="官网确认:免费参观(凭身份证入馆)"
)
if "周一闭馆" in claim:
return ClaimVerification(
claim=claim, ok=True,
evidence="官网确认:每周一闭馆(法定节假日除外)"
)
return ClaimVerification(claim=claim, ok=False, evidence="未找到依据")
result = chain_of_verification(
model,
question=question,
verify_claim=verify_claim,
)
print(f"\n最终回答:\n{result}")
if __name__ == "__main__":
main()
运行日志
[草稿] 中国茶叶博物馆每天 9:00-17:00 开放,门票 50 元,周一闭馆。
[断言] 抽取到 3 条:
- 中国茶叶博物馆每天 9:00-17:00 开放
- 门票 50 元
- 周一闭馆
[验证] [OK] 中国茶叶博物馆每天 9:00-17:00 开放 — 证据:官网确认:开放时间 9:00-17:00
[验证] [FAIL] 门票 50 元 — 证据:官网确认:免费参观(凭身份证入馆)
[验证] [OK] 周一闭馆 — 证据:官网确认:每周一闭馆(法定节假日除外)
最终回答:
中国茶叶博物馆每天 9:00-17:00 开放,免费参观,周一闭馆。
"门票 50 元"被验证器标为 FAIL,修订时被替换为"免费参观"。
调试:trace 里看什么
| 要看的 | 怎么看 | 说明什么 |
|---|---|---|
| 断言数量 | len(claims) |
太少说明抽取不充分;太多增加验证成本 |
| 漏抽的断言 | 读草稿,找没被抽出的事实 | 需要强制原子化断言抽取 |
| 验证结果 | 每条的 ok + evidence |
"widely known" 这种证据太弱 |
| 修订是否删干净 | 对比草稿和最终回答 | FAIL 断言如果还留着,修订 prompt 有问题 |
| 高风险断言被跳过 | 如 "过敏安全" 没被抽取 | 先验证高风险断言 |
工程笔记
成本公式
总调用数 = 1(草稿) + 1(抽取) + n_verify(验证器调用) + 1(修订)
= 3 + n_verify
如果验证器是模型调用:
总调用数 = 3 + n_claims
验证器如果是工具/规则/数据库查询,n_verify 的成本很低。 验证器如果是模型调用,成本和断言数线性增长。
常见坑
| 坑 | 现象 | 修法 |
|---|---|---|
| 断言漏抽 | 错误断言从不被检查 | 强制原子化断言抽取 |
| 证据太弱 | 到处是"众所周知" | 要求存文档 ID、片段、计算过程 |
| 只查简单的 | 高风险断言被跳过 | 优先验证高风险断言 |
| 高成本 | 长文章有很多断言 | 按段落批量验证,或只查关键断言 |
| 修订不充分 | FAIL 断言还在最终输出里 | 显式要求"删除或标注不确定" |
什么时候用
- 输出包含多条事实断言。
- 你有验证手段:工具、检索、规则、人工。
- 事实错误代价高。
- 你需要断言到证据的映射。
什么时候别用
- 没有验证来源。
- 任务是创意写作。
- 输出短且低风险。
- 需要先搜集证据才能回答;那更接近 Agentic RAG。
读完以后
CoVe 适用于答案已经写出来、事实断言需要逐条核实的场景。
如果需要先搜索直到证据充足,读 Agentic RAG。 如果需要整体质量反馈而不是逐条事实,读 Maker-Checker。