真实检索、生成与引用验证链路
前置为本章上下文选择、真实文本embedding与本地模型服务。本例贯穿文本编码、候选排序、带来源上下文、模型调用及输出验证。每个阶段都打印可检查对象。
先使用抽取式回答:answer必须等于quote,quote必须是引用来源的原文子串。这样可以确定性证明“这句话确实出现在这个来源中”,但仍不能证明模型选中了回答问题的正确句子。因此结构支持与相关性/完整性分开评价,不能把引用存在当作全答案正确。
首次完整实验
复用检索课Python3.12环境与固定embed目录,启动本地8087服务后将代码保存为rag_pipeline.py,运行 OMP_NUM_THREADS=1 TOKENIZERS_PARALLELISM=false EMBED_DIR=./embed python rag_pipeline.py。无需另建数据库;固定小语料先用精确检索隔离生成问题。
import os,json
import numpy as np
from urllib.request import Request,urlopen
from sentence_transformers import SentenceTransformer
embed=SentenceTransformer(os.environ['EMBED_DIR'],device='cpu',local_files_only=True)
docs={'s1':'Returns within seven days.', 's2':'Opened items cannot be returned.', 's3':'Shipping takes two days.'}
question='Can an opened item be returned?'
ids=list(docs);vectors=embed.encode(list(docs.values()),normalize_embeddings=True)
q=embed.encode([question],normalize_embeddings=True)
rank=np.argsort(-(q@vectors.T)[0])[:2]
evidence={ids[i]:docs[ids[i]] for i in rank}
def validate(raw,context):
try:value=json.loads(raw)
except (ValueError,TypeError):return {'status':'invalid_json'}
if not isinstance(value,dict) or set(value)!={'answer','source','quote'}:return {'status':'invalid_schema'}
if all(value[k] is None for k in value):return {'status':'abstain'}
source=value['source'];quote=value['quote']
if not isinstance(source,str) or source not in context:return {'status':'invalid_source'}
if not isinstance(quote,str) or not quote or quote not in context[source]:return {'status':'unsupported_quote'}
# This lesson uses extractive answers so support can be checked without an LLM judge.
if value['answer']!=quote:return {'status':'unsupported_answer'}
return {'status':'supported',**value}
prompt='Use evidence only. Return JSON with answer,source,quote. Answer must equal an exact quote; use all null if unknown.\n'+json.dumps(evidence)+'\nQuestion: '+question
if os.environ.get('GENERATOR_MODE','local')=='openai':
body={'model':os.environ['OPENAI_MODEL'],'messages':[{'role':'user','content':prompt}],'response_format':{'type':'json_object'}}
req=Request('https://api.openai.com/v1/chat/completions',data=json.dumps(body).encode(),headers={'Authorization':'Bearer '+os.environ['OPENAI_API_KEY'],'Content-Type':'application/json'})
raw=json.load(urlopen(req,timeout=60))['choices'][0]['message']['content']
else:
req=Request('http://127.0.0.1:8087/generate',data=json.dumps({'prompt':prompt,'max_new_tokens':16}).encode(),headers={'Content-Type':'application/json'})
raw=json.load(urlopen(req,timeout=60))['text']
print('retrieved',evidence,'raw',raw,'validation',validate(raw,evidence))
correct=json.dumps({'answer':docs['s2'],'source':'s2','quote':docs['s2']})
assert validate(correct,docs)['status']=='supported'
assert validate(correct,{'s1':docs['s1']})['status']=='invalid_source' # lost exception at retrieval/context stage
bad=json.dumps({'answer':docs['s2'],'source':'s1','quote':docs['s2']})
assert validate(bad,docs)['status']=='unsupported_quote' # wrong citation during generation
assert validate('{"answer":"yes","source":"s1","quote":"Returns within seven days."}',docs)['status']=='unsupported_answer'
print('three pipeline failures distinguished')实际检索顺序是s2开封例外、s1七天条件;tiny-gpt2生成重复stairs,被明确判invalid_json。真实模型链路已执行,但没有产生合格答案,不虚报质量。若用已授权云模型,设置GENERATOR_MODE=openai、OPENAI_MODEL和OPENAI_API_KEY,再跑相同输入;该分支会收费,本文未执行。保留s2句子的确定性positive fixture只测试校验器,不算模型生成结果。
三个变体分别区分:检索/上下文漏掉s2导致source不在context;生成引用s1却引用s2原句导致unsupported_quote;引用真实句子但answer私自改成yes导致unsupported_answer。下一步若放开成自然语言综合回答,需要拆成claim并检查蕴含、时间和例外,单凭字符串包含不够。
查询改写如何改变失败位置
用户问“拆了还能退?”可先改写成“opened item refund policy”再编码;原问题必须继续进入生成提示,防止改写把“不想退”误变“要退款”。多查询可并行得到不同候选,再按来源ID去重/RRF合并,不能简单相加不同模型的原始分数。
HyDE先生成假想文档,例如“Opened purchases may be returned during a trial period”,再用其embedding检索。它可能跨越短问与长文档的表达差异,也可能把不存在的试用期引入检索;假想文档只能作查询,不能进入证据上下文。对本例实际“开封不可退”的语料,必须仍检查真实s2是否被召回。self-query把“2026年A租户”分成过滤与语义部分;租户来自认证主体,不能由改写模型任意指定。
组合策略的收益要在同一保留集分别记录原始召回、改写召回、合并后召回与最终支持率。只有生成失败时换更强模型未必解决漏证据;只有召回命中也未必解决上下文删掉例外。
练习
用只有s1的context重放支持s2的结果,再用完整context重放;添加s4为过期的相反政策,先按版本过滤再编码,确认旧政策不能进入evidence。
完成后查看本章答案。
下一章:定位质量损失与生产演进。