614 分钟

RAG 生成侧 —— 拼 Prompt + 引用绑定 + Verifier

掌握检索后如何拼装 context、让 LLM 标注引用、用 Verifier 验证答案无幻觉,以及修正循环的完整链路。

RAGPromptVerifier引用防幻觉Python
进度保存在本机浏览器;验收通过后再点更稳妥

第 6 课:RAG 生成侧 —— 拼 Prompt + 引用绑定 + Verifier

本节目标:掌握检索后如何拼装 context、如何让 LLM 标注引用、如何用 Verifier 验证答案没有幻觉,以及修正循环的完整链路。

学完本课后,推荐阅读 附06:Prompt 工程深度 —— 七大核心技巧、多 Prompt 拆分设计、防注入、黄金模板。

第 4-5 课解决了"怎么找到相关 chunks"。这一课解决找到之后怎么办。


1. 全局视角:RAG 的前半场和后半场

Code
┌── 前半场(检索侧,第4-5课)──────┐
│ query rewrite → embed → hybrid   │
│ → RRF → rerank → top-K chunks    │
└──────────────┬───────────────────┘
               ↓ chunks
┌── 后半场(生成侧,本课)──────────┐
│ 1. 过滤低分 chunk                 │
│ 2. 拼 context(带 [1][2] 编号)   │
│ 3. 渲染 prompt 模板               │
│ 4. LLM 生成(带 [N] 引用)        │
│ 5. 解析引用 → Citation 对象       │
│ 6. Verifier 验证 + 修正循环       │
└──────────────────────────────────┘

2. 过滤 + 构建 Context

过滤低分 chunk

Code
filtered = [h for h in hits if h["score"] >= min_score]  # 默认 0.3

if not hits:
    return _fallback("no_hit")       # 向量库零命中 → "无法回答"
if not filtered:
    return _fallback("low_score")    # 全低于阈值 → "无法回答"

两种兜底分开的原因:运营需要区分——no_hit 要补文档,low_score 可能是 query 表述或 chunk 切分问题。

构建 Context 文本

python
def _build_context(chunks, *, limit=4000):
    for c in chunks:
        block = f"[{c.index}] (source: {c.source}, title: {c.doc_title})\n{c.text}\n"
        if total + len(block) > limit:
            break        # 超限截断,不是跳过整块
        parts.append(block)

最终送进 LLM 的 context 长这样:

text
[1] (source: hr_handbook.md, title: 请假制度)
员工请假须提前3个工作日在OA系统提交申请,由直属主管审批...

[2] (source: policy_2024.md, title: 年假规定)
正式员工入职满一年享有5天带薪年假...

三个设计要点:① [N] 编号是 LLM 和解析层的共同语言;② 4000 字符截断(超出停止,字符数估算成本为零);③ context 带元数据让 LLM 能利用来源信息。


3. Prompt 模板:驯服 LLM 的 5 条规则

yaml
name: rag_qa
---

[SYSTEM]
你是一个严谨的知识库问答助手:

- 如果 Context 里有答案:回答并在相关语句末尾用 [N] 标注引用
- 如果 Context 信息不足:直接说"根据现有资料无法回答",**绝对不要编造**
- 一句话可以引用多个编号:[1][2]
- 不要在答案开头或结尾加"根据资料..."这种套话,直接给结论
- 回答用简洁中文,不超过 300 

Context: ${context}

[USER] ${question}
规则为什么需要
严格基于 Context防幻觉核心——不让 LLM 用自己的"知识"
用 [N] 标引用让引用绑定可自动化解析
不足就说不知道不答优于乱答
不要套话省字数,不提供信息
不超 300 字控 token + 强迫精炼

最重要的一条:"绝对不要编造"。但这不是万能的——LLM 可能在"总结 context"时偷偷加入 context 里没有的信息。这就是为什么后面需要 Verifier。


4. 调 LLM + 解析引用

temperature=0.1(不是 0):0 会导致重复退化,0.1 极低随机性但偶尔可跳出重复。RAG 最佳实践区间 0.0-0.2。

解析引用

Code
def _parse_citations(answer, chunks):
    cited_indices = sorted({int(m) for m in _CITATION_RE.findall(answer)})
    for idx in cited_indices:
        c = chunk_by_idx.get(idx)
        if c is None:         # 越界编号 → 跳过
            continue
        citations.append(Citation(index=c.index, source=c.source,
                                   doc_title=c.doc_title, snippet=c.text[:120]))
    return citations
Code
LLM 输出: "员工请假须提前3天申请[1],年假5天[3]。"
   ↓ 正则 \[(\d+)\]
提取: {1, 3} → 逐个绑定 chunk → Citation 列表

三个防御:越界编号过滤(LLM 标了 [7] 但你只给了 5 条→跳过)、去重(多次 [1] 只生成一个 Citation)、snippet 截断(前 120 字供前端预览)。

finish_reasonanswered(正常)、no_hit(零命中)、low_score(全低于阈值)、no_context_used(最危险——LLM 回答了但一个 [N] 都没标,可能是幻觉)


5. Verifier:用第二个"法官 LLM"验证答案

核心概念:Claim + Evidence + Verification

python
class Claim(BaseModel):
    text: str              # 一句可独立验证的话
    evidence_ids: list[str]  # 绑定的证据 ID
    verification: Verification  # supported / partial / unsupported

class Evidence(BaseModel):
    text: str
    source: Literal["vector", "keyword", "hybrid"]

例子

Code
答案: "员工请假须提前3天申请,年假最多5天。"
  c1: "请假须提前3天"  → evidence [1] → supported c2: "年假最多5天"     → evidence []    → unsupported ↑ 知识库里写的是"至少5天",LLM 编的"最多"

三种验证策略

策略特点成本
RuleVerifier规则快筛(同步)0 LLM
LLMJudgeVerifierLLM 裁判(能看语义)1 LLM 调用
HybridVerifier先规则,不确定上 LLM30-40% 走 LLM

RuleVerifier 决策树

Code
claims 为空? → INSUFFICIENT
有冲突? → CONFLICT
全部 unsupported? → INSUFFICIENT
unsupported 或 partial 太多? → REVISE
默认 → PASS

打分:(supported×1 + partial×0.5 + unsupported×0) / total

LLMJudgeVerifier

把 question、answer、claims、evidences 全部打包 → LLM 做 JSON 结构化输出(temperature=0.0,验证必须确定性):

json
{
  "verdict": "revise",
  "score": 0.6,
  "issues": ["未回答副作用部分", "claim 2 证据相似度偏低"],
  "suggestions": ["补充副作用段落", "查找更直接的 evidence"]
}

suggestions 是关键——给下一轮生成看的修正方向。

HybridVerifier(主线)

Code
rule_res = self.rule.verify(...)

# 高置信负面 → 直接返回(省 LLM)
if rule_res.verdict in (CONFLICT, INSUFFICIENT):
    return rule_res

# 规则判 PASS → 直接返回(省 LLM)
if not self.use_llm_always and rule_res.verdict == PASS:
    return rule_res

# REVISE 才调 LLM 细化
llm_res = await self.llm.verify(...)
return self._merge(rule_res, llm_res)    # 以 LLM 为主,CONFLICT 例外

成本:60-70% 被规则直接判定,只有 30-40% 需要 LLM。成本降一半以上。


6. Verdict 四种判定 + 修正循环

Verdict系统行为
PASS合格,直接返回
REVISE有小问题 → 带 suggestions 重新生成(最多 2 次)
INSUFFICIENT证据不足 → 告诉用户"无法回答"
CONFLICT证据矛盾 → 需人工介入

修正循环

Code
round 1: generate → verify → REVISE
            issues: "未回答副作用部分"
            suggestions: "补充副作用段落"
              ↓
round 2: generate(with hints) → verify → PASS ↓
         返回最终答案

max_revisions=2:超过 2 轮还 REVISE → 问题本身证据有问题,强制返回。Verifier 失败兜底为 PASS(不让验证器自己把流程搞崩)。


7. 工程教训

  1. 引用绑定是 RAG 可信度的基石——没有引用 = 和 ChatGPT 没区别。用户能点击 [1] 查证
  2. Verifier 失败兜底为 PASS——增强功能不阻塞主流程,和第5课 HyDE 兜底一脉相承
  3. 规则 + LLM 混合是成本最优解——70% 规则判定免费,30% LLM 精判
  4. no_context_used 是最重要的监控指标——突然升高说明 prompt 被改坏或 context 质量下降,必须告警
  5. 修正循环不超过 3 次——再多说明证据本身有问题,改也改不好

8. 自测

问题 1:chunk 本身 5000 字符超了 MAX_CONTEXT_CHARS=4000,会发生什么?暴露了 ingestion 的什么问题?

问题 2:LLM 输出 [1,2](逗号分隔)而不是 [1][2],当前正则 \[(\d+)\] 能匹配吗?怎么改?

问题 3:HybridVerifier 规则判 PASS 太松(1 个 supported 就 PASS)会导致什么后果?怎么收紧?

问题 4:为什么 Verifier 用 temperature=0.0 而不是生成用的 0.1?

问题 5(最重要):LLM 从 context [1] 和 [2] 自行总结"A 比 B 便宜 30%",引用都标了,Verifier 也判 PASS。但 context 里根本没有价格对比原文——为什么 Verifier 没拦住?怎么改进?


答案与解析

问题 1:超限 chunk

第一个 chunk 5000 字符 → total(0) + len(block)(5000+) > 4000 → break → parts 为空 → context 空 → LLM 要么"无法回答"要么幻觉。

根本原因:ingestion 阶段 chunk_size 设太大或根本没切。两层防御:ingestion 切 chunk(500-1000 字符)+ _build_context 截断单个 chunk 而非跳过。

上游不做好切分,下游怎么补都是打补丁。

问题 2:逗号分隔引用

\[(\d+)\] 匹配 [1] 但不匹配 [1,2]。兼容改法:先宽松匹配 [...] 块,再从里面提取所有数字。

更深层:正则解析 LLM 输出永远脆弱。生产用 structured output(JSON mode)让 LLM 直接输出 citations 数组,不从自然语言里"猜"。

问题 3:规则太松

1 个 supported + 4 个 unsupported → 判 PASS → 4 个幻觉放行 → 用户看到"验证通过"但答案 80% 是编的。

收紧方案:score 阈值(≥0.7)+ unsupported 零容忍 + 上线后监控 PASS/REVISE 比例调参。

问题 4:temperature=0.0

验证是判断题不是创作题。0.0 = 每次相同判断可复现。0.3 → 同一份答案第一次 PASS 第二次 REVISE → 用户同样的问题两次结果不同 → 信任崩塌。

场景temperature原因
RAG 生成0.0-0.2基于证据,低随机
Verifier0.0判断必须确定性
HyDE0.3需要创造力编假设答案

问题 5:引用正确但内容是推理幻觉

为什么没拦住:Verifier 验证的是"evidence 是否提及了 claim 的内容",不是"evidence 是否蕴含了 claim 的结论"。"便宜30%"是一个推理结论,需要从两个证据中计算推导——LLM 自行做了这个推导,Verifier 看不出来。

4 层递进防御

方案成本
1Prompt 约束:"不要做数学计算或推理,只转述原文结论"免费
2Claim 级验证加强:标记 "inferred" 类 claim → 降级为 partial中等
3NLI 模型(如 deberta-v3):判 entailment/contradiction/neutral推理 50ms
4Fact-checking pipeline:NER 提取数字 + 验证数学关系最重

引用 ≠ 正确。LLM 可以在"看起来有引用"的外壳下做任意推理。防御按业务风险等级逐层叠加。


本节要点

  1. Context 拼装核心是 [N] 编号——LLM 和解析层的共同语言,串联生成、引用、验证三个环节
  2. Verifier 精髓是"规则快筛 + LLM 精判"混合——70% 免费判定,30% 花钱精判
  3. 4 种 verdict 覆盖所有答案质量状态:PASS / REVISE / INSUFFICIENT / CONFLICT
  4. 修正循环最多 2-3 次——超出说明证据本身有问题
  5. 引用 ≠ 正确——LLM 可标着 [1][2] 但答案是自行推理的。防御需从 prompt 约束到 NLI 到事实核查多层递进