跳转至

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