客户问:“商品签收十天后发现质量问题,退货运费谁承担?”模型可能给出一段听起来合理的常识,但店铺可以有自己的适用条款,模型未必知道这份政策,也未必知道它是否更新。要回答这个具体问题,先需要具体依据。
下面全部政策、订单和回答都是教学示例,不是真实商家的规则。假设店铺文档写着:
普通退货:签收七天内,商品未使用且包装完好,可申请退货,运费由买家承担。
质量问题退货:签收三十天内,经核实属于商品质量问题的退货,运费由商家承担。
发票:订单完成后,可申请电子发票。
读者很容易看出,十天不符合普通退货的七天条件,却仍落在质量问题条款的三十天范围内。但“客户说有质量问题”和“已经核实属于质量问题”还不是同一件事。因此更准确的回答应该保留核实条件,而不是直接承诺商家一定支付。
只有一页资料时,先把它完整交给模型
最简单的做法,就是把问题和这三段资料一同放进上下文,并要求只依据资料回答、保留适用条件、引用条款编号。模型没有在这个过程中更新参数,它只是利用本次输入里的信息生成回答。
这已经包含了检索增强生成的核心想法:回答时借助外部资料,而不只依赖参数里学到的知识。资料很少时,应用甚至不需要单独的向量库;直接提供完整文档就能先检验“给了依据后是否回答正确”。RAG 的研究背景可参考 原始论文,本文采用的是便于应用理解的检索后生成流程。
一份合适的教学回答是:“按质量问题退货条款,签收十天仍在三十天期限内;经核实属于质量问题时,退货运费由商家承担。目前还需核实质量问题。[policy-quality]”这里的最后一句来自证据缺口,而不是为了礼貌随意加的免责声明。
现在把店铺资料扩大到几千份:售后政策、商品说明、历史通知、发票指南。每次全部发送不仅浪费上下文,还把很多无关或过期内容一起交给模型。应用就需要在回答前,选出本次相关的少量材料。这一步叫检索,随后把检索结果装配进上下文,再生成答案。
先画出资料与问题各走哪条路
把资料放进系统与回答问题,是两条相关但不同的路径。资料侧先读取文件,保留结构,切成适合检索的片段,再编码与存储。问题侧收到当前问题,确定查询范围,检索已有片段,组织模型输入,最后生成回答。前一条通常不需要每次提问都重做。
| 阶段 | 输入 | 输出 | 本例要保留的东西 |
|---|---|---|---|
| 解析与切分 | 政策文件 | 可单独阅读的条款 | 三十天、核实条件、承担方 |
| 编码与入库 | 条款及来源 | 文本向量、ID、元数据 | 版本和原文定位 |
| 检索 | 问题与范围 | 排序后的候选 | 候选分数与原文 |
| 装配 | 问题、候选、规则 | 完整模型输入 | 条件、来源和资料边界 |
| 生成与核验 | 模型响应 | 有依据的回答或缺口 | 引用与结论的对应关系 |
“离线建库”里的离线指与一次用户查询解耦,不一定指断网。编码器可能来自本机模型,也可能调用服务。对于三条条款,每次重新编码便于观察;对于十万份资料,应复用已有表示,并在文档变化时更新。
问题向量与文档向量必须处于兼容的空间。维度相同不意味着能随意混用模型;索引里的编码模型、前缀和归一化配置都属于建库版本。第三篇会从公式解释这些要求。
先用真实编码器找两个候选
下面采用一个多语言句向量模型,将三个完整条款编码为矩阵,再计算当前问题与每条的分数。它运行时会加载真实权重,并非关键词计数;本次文档改写没有执行它。准备依赖见配套指南下载。
from sentence_transformers import SentenceTransformer
# 为每个完整条款分配稳定 ID,后续把 ID 和原文一起交给生成模型。
documents = [
{"id": "policy-normal", "text": "普通退货:签收七天内,商品未使用且包装完好,可申请退货,运费由买家承担。"},
{"id": "policy-quality", "text": "质量问题退货:签收三十天内,经核实属于商品质量问题的退货,运费由商家承担。"},
{"id": "policy-invoice", "text": "发票:订单完成后,可申请电子发票。"},
]
# CPU 方便在普通电脑运行;第一次加载会下载权重,后续可复用本地缓存。
model = SentenceTransformer(
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", device="cpu"
)
texts = [doc["text"] for doc in documents]
question = "商品签收十天后发现质量问题,退货运费谁承担?"
# 用同一个模型和分词器检查长度;禁止静默截断,把缺条件的问题藏起来。
# 正式知识库应重新切分超长片段,并继续保留条款条件和来源。
for text in [*texts, question]:
token_ids = model.tokenizer(text, truncation=False)["input_ids"]
if len(token_ids) > model.max_seq_length:
raise ValueError("文本超过编码器长度限制,请重新切分")
# 三条文档得到 [3, 384] 矩阵;归一化后每行向量长度为 1。
document_vectors = model.encode(texts, normalize_embeddings=True, convert_to_numpy=True)
# 用列表编码问题,先得到 [1, 384],再取第一行成为 [384]。
query_vector = model.encode([question], normalize_embeddings=True, convert_to_numpy=True)[0]
# 矩阵与向量相乘,为每条文档得到一个余弦分数,结果形状为 [3]。
scores = document_vectors @ query_vector
indices = scores.argsort()[::-1][:2] # 降序取两个候选;入选不表示事实已经核实。
for index in indices:
print(documents[index]["id"], round(float(scores[index]), 4), documents[index]["text"])
先别把分数当成答案。质量政策和普通退货政策都可能排得很近,因为它们都讨论运费。即使把“十天”改成“四十天”,质量政策仍然可能是最相关的材料:它恰好是判断不满足期限所需的依据。
Top-k 只是截取排序的前 k 条。即使问“海外维修费能报销吗”,库里没有对应资料,程序仍能排出第一名。因此“检索有结果”和“证据足以回答”必须分开处理。
把候选变成模型可以使用的依据
候选不能只剩一组向量或 ID。生成模型需要原文和可定位的来源,还需要知道外部资料是待使用的数据,其中的命令不改变任务。一个简化的输入可以包含当前问题,以及 policy-quality 的完整条款与版本。
我们要求回答保留期限与核实条件,并给关键结论附上本次资料 ID。接入生成的完整入口已经放在 semantic_search.py下载:默认打印待发送输入,显式添加 —live 后才调用配置的模型。输出供人工核验,不直接触发退款。
教学期望回答应表达三层信息:找到三十天质量条款,十天在期限内,但质量问题仍需核实。它既不一口答应免费退回,也不因为超过普通退货七天就拒绝。此处的期望是作者依据教学条款写出的,不冒充模型实际响应。
引用核验至少问两件事:ID 是否来自本次候选,该片段是否支持相应结论。回答写“商家承担”却引用普通退货条款,ID 可以真实存在,结论仍然错。程序能检查引用集合,语义支持还需进一步判断,第六篇会展开。
什么时候需要 RAG,什么时候先别增加检索
如果资料只有一页,完整提供可以避免检索漏证据,是很有价值的基线。资料很多、经常变化、需要追溯原文或按权限选择时,再引入检索来组织信息。RAG 不是所有模型应用都必须经过的一层。
长上下文回答解决的是能读多少输入,不自动选择当前适用资料。微调改变模型参数,适合塑造能力和任务行为,但不应被当成频繁更新订单事实的数据库。三者可以组合:模型经过任务训练,应用通过检索选择资料,再利用较长窗口读取完整证据。
例如“怎样礼貌解释退款条件”可能主要是表达任务;“今天这笔订单能否退款”需要当前事实;“比较十份政策差异”则需要足够覆盖范围和跨文档组织。先判断缺的是能力、信息还是信息选择,再决定要增加哪个组件。
一个最小系统怎样暴露错误
把同一问题放进三种输入:无资料、完整条款、只剩“商家承担运费”的残缺条款。对照结果时,观察条件是否保留,而不是只看哪一段文字更自然。残缺片段如果导致肯定答复,首先应修复切分,换生成模型无法保证恢复已经丢失的事实。
再准备一个库里没有答案的问题。正确系统应允许给出“现有资料未覆盖”的结果;它不是检索程序崩溃,也不等于政策明确禁止。这样的样本从第一天就应保留,后面评价才不会只奖励总能说出一段话的系统。
这篇建立了完整路径。下一篇先回到原文件,解释解析、切分与来源保留。检索器读到的世界由入库过程决定,源头错误会沿着整个流程传播。