
👨💻程序员三明治 :个人主页
🔥 个人专栏 : 《设计模式精解》 《重学数据结构》
《AI探索日志》 《从0带你学深度强化学习》
🤞先做到 再看见!
目录
-
- 从检索到生成:最后一公里的质量问题
-
- [1. 好 chunk + 烂 Prompt = 烂答案](#1. 好 chunk + 烂 Prompt = 烂答案)
- [2. RAG 生成阶段的核心挑战](#2. RAG 生成阶段的核心挑战)
- [RAG Prompt 的三段式结构](#RAG Prompt 的三段式结构)
-
- [1. 系统指令(System Prompt):定义模型的角色和行为边界](#1. 系统指令(System Prompt):定义模型的角色和行为边界)
-
- [1.1 一个基础版 System Prompt](#1.1 一个基础版 System Prompt)
- [1.2 常见的 System Prompt 设计误区](#1.2 常见的 System Prompt 设计误区)
- [2. 检索上下文(Retrieved Context):把 chunk 喂给模型](#2. 检索上下文(Retrieved Context):把 chunk 喂给模型)
-
- [2.1 上下文的组装格式](#2.1 上下文的组装格式)
- [2.2 上下文窗口的限制与应对](#2.2 上下文窗口的限制与应对)
- [3. 用户问题(User Query):原始问题还是改写后的问题](#3. 用户问题(User Query):原始问题还是改写后的问题)
- [4. 完整 Prompt 模板示例](#4. 完整 Prompt 模板示例)
- 幻觉抑制:让模型不知道就说不知道
-
- [1. 什么是幻觉(Hallucination)](#1. 什么是幻觉(Hallucination))
- [2. RAG 场景下幻觉的三种典型表现<!-- 这是一张图片,ocr 内容为:RAG场景下的三类典型幻觉 2 1 3 篡改事实 张冠李戴 凭空捏造 资料1:基础保修约一年 资料: 资料: ; 三 质量异常退货, 拆封后不适用无理由退货 运费由平台承担 资料2:增强计划可延长至约两年 错误回答: X 错误回答: 错误回答: 可以无理由退货 退款将在3-5个工作日到账 默认保修约两年 常用抑制手段 事实句必须带引用 信息不足时拒答 99 降低生成随机性 限定知识来源 -->](#2. RAG 场景下幻觉的三种典型表现)
-
- [2.1 篡改事实](#2.1 篡改事实)
- [2.2 凭空捏造](#2.2 凭空捏造)
- [2.3 张冠李戴](#2.3 张冠李戴)
- [3. 通过 Prompt 抑制幻觉的实用技巧](#3. 通过 Prompt 抑制幻觉的实用技巧)
-
- [3.1 Temperature 和 Top-P 参数的作用](#3.1 Temperature 和 Top-P 参数的作用)
- [3.2 兜底回答的设计](#3.2 兜底回答的设计)
- 引用对齐:让答案可追溯
-
- [1. 为什么需要引用对齐](#1. 为什么需要引用对齐)
- [2. 引用对齐的实现方式](#2. 引用对齐的实现方式)
-
- [2.1 Prompt 中的引用指令设计](#2.1 Prompt 中的引用指令设计)
- [2.2 引用解析与展示](#2.2 引用解析与展示)
- [3. 引用对齐的效果示例](#3. 引用对齐的效果示例)
- 答案约束:控制输出的格式和边界
-
- [1. 格式约束:JSON / 纯文本 / 列表](#1. 格式约束:JSON / 纯文本 / 列表)
- [2. 长度约束:控制回答的详略](#2. 长度约束:控制回答的详略)
- [3. 边界约束:只回答知识库范围内的问题](#3. 边界约束:只回答知识库范围内的问题)
- [Java 实战:完整的 RAG 生成流程](#Java 实战:完整的 RAG 生成流程)
-
- [1. 代码实现:从检索到生成的完整链路](#1. 代码实现:从检索到生成的完整链路)
- [2. 运行效果展示](#2. 运行效果展示)
- [3. Prompt 模板的迭代优化](#3. Prompt 模板的迭代优化)
- [一张图看完整 RAG 链路](#一张图看完整 RAG 链路)
上一篇把混合检索、融合排序和精排链路串了起来。做到这一步,系统通常已经能够从知识库里找到与问题高度相关的内容。但在真实项目里,检索正确并不等于最终答案正确。
这也是 RAG 最容易被低估的一段链路。团队往往会花大量时间调分块策略、Embedding 模型和 Top-K,却把生成阶段理解成"把检索结果交给大模型总结一下"。系统刚开始演示时可能没有问题,一旦进入复杂业务,错误很快就会暴露出来:模型会补充资料中不存在的细节,会把不同文档的规则拼在一起,甚至会直接用预训练知识覆盖知识库中的明确结论。
假设我们维护的是一个商城售后知识库。用户问:
澄光 X9 拆封后还能申请无理由退货吗?
检索返回的内容非常明确:
澄光 X9 采用定制封装工艺,拆封后不适用平台的无理由退货规则。如检测确认存在质量异常,可以进入售后退货流程。
如果没有任何生成约束,模型仍然可能回答:
可以。多数商品在规定期限内都支持无理由退货,建议直接提交退货申请。
检索没有错,错误发生在生成阶段。模型把训练阶段学到的通用规则,放在了知识库中的具体规则之前。
从系统设计的角度看,检索解决的是"证据从哪里来",生成解决的是"模型能不能在证据边界内组织答案"。前者决定系统是否找到了材料,后者决定系统会不会把材料用错。RAG 要真正进入生产环境,必须同时控制这两个环节。
从检索到生成:最后一公里的质量问题
1. 好 chunk + 烂 Prompt = 烂答案
很多生成错误并不是因为模型完全没有看到正确资料,而是模型没有被告知:这些资料究竟是参考信息,还是本轮回答的唯一事实来源。
例如用户问"退货运费由谁承担",知识片段只说明:
商品存在质量异常时,退货配送费用由平台承担;非质量原因退货时,配送费用由用户承担。
模型却可能继续补充:
退款会在约数个工作日内原路返回,退款金额包括商品费用和配送费用。
后半句话听起来符合常见售后流程,但知识库没有提供任何退款时效,也没有说明退款范围。模型只是根据行业语料完成了一个"合理续写"。在内容创作里,这种能力很有价值;在企业问答里,它就是风险。
另一类问题更隐蔽。假设检索返回两条资料:
plain
资料一:声屿 S2 的基础保修期为约一年。
资料二:购买增强保障计划后,服务期可延长至约两年。
模型如果回答"声屿 S2 的保修期为约两年",每个词都能在资料里找到,但适用条件被丢掉了。它把"购买增强保障计划后"改写成了默认规则。这种错误不是凭空编造,而是跨证据错配,人工抽查时也更容易漏掉。
还有一种常见情况是答非所问。用户问"订单什么时候发出",资料里已经写明普通订单通常在付款完成后的一段时间内安排出库,预售订单以商品页约定时间为准。模型却转而介绍合作物流公司、配送范围和查询方式。答案与物流相关,却没有回答用户真正关心的问题。
这几类问题说明,生成阶段不是简单的文本润色。模型要完成的是一项受约束的信息整合任务:识别用户真正的问题,从若干证据中提取直接相关的事实,保留每条规则的适用条件,并拒绝补充未经知识库支持的信息。
2. RAG 生成阶段的核心挑战
从工程上看,生成阶段主要面对三类风险。
| 挑战 | 典型表现 | 系统影响 |
|---|---|---|
| 幻觉 | 改写事实、补充无依据细节、混淆不同证据 | 产生错误结论,严重时引发业务和合规风险 |
| 答非所问 | 回答相关背景,却遗漏用户真正询问的内容 | 用户需要继续追问,系统看似流畅但解决率很低 |
| 缺乏可追溯性 | 答案无法关联到具体知识来源 | 无法审计,也无法判断错误来自知识库、检索还是模型 |
这三个问题不能只靠换一个更大的模型解决。更强的模型通常能够生成更自然的答案,但自然并不等于忠实。真正需要建立的是一套生成约束:明确事实来源、定义信息不足时的处理方式、要求关键结论带引用,并在模型输出后进行必要校验。

RAG Prompt 的三段式结构
RAG 场景下的 Prompt 不应该被当成一段"写给模型看的文案",而应该被当成生成服务的接口契约。
一个稳定的接口会定义输入、行为边界、异常分支和输出格式。RAG Prompt 也一样。它至少要让模型知道三件事:本轮应该遵守什么规则、可以使用哪些证据,以及当前用户究竟问了什么。
因此,一份完整的 RAG Prompt 通常仍然由三部分组成:
plain
System Prompt
Retrieved Context
User Query
这个结构并不复杂,真正困难的是三部分之间的职责必须清楚。System Prompt 负责约束行为,Retrieved Context 负责提供事实,User Query 负责定义本轮任务。不要把业务规则、证据内容和用户问题混成一大段文本,否则模型很难稳定地区分哪些是高优先级指令,哪些只是待分析材料。
1. 系统指令(System Prompt):定义模型的角色和行为边界
System Prompt 是生成链路的总纲。它不需要解释所有业务知识,因为业务知识应该存在于检索上下文中;它真正要定义的是模型如何使用这些知识。
对于企业级 RAG,最关键的一条并不是"回答要专业",而是"知识库证据在本轮回答中的优先级高于模型内部知识"。如果这个关系没有写清楚,模型会把检索结果当作参考材料,而不是事实边界。
1.1 一个基础版 System Prompt
下面是一份适合售后问答场景的基础模板:
plain
你是商城售后知识助手。请根据本轮提供的【参考资料】回答用户问题。
请遵守以下规则:
1. 【参考资料】是本轮回答唯一允许使用的事实来源。
2. 不得使用外部知识补充或修改资料中的主体、条件、数字、时间和结论。
3. 如果资料只能回答部分问题,先回答能够确认的部分,再说明无法确认的部分。
4. 如果资料完全不足,明确说明现有资料无法支持结论,不要猜测。
5. 多条资料存在冲突时,分别说明各自的适用条件;没有明确优先级时,不自行裁决。
6. 每个事实性结论后标注对应资料编号,例如 [1] 或 [1][3]。
7. 先回答用户最关心的结论,再补充必要条件,不扩展无关背景。
这份模板看起来仍然有规则,但它和"机械堆十几条要求"不是一回事。每条规则都对应一个真实的失败模式。
第一条建立知识来源边界,防止模型用通用知识覆盖企业规则。第二条专门限制模型最容易擅自补充的内容,特别是时间、金额、流程和条件。第三、第四条为信息不足提供出口,避免模型为了保持对话流畅而硬编答案。第五条用于处理规则冲突,尤其是通用规则与特殊规则并存的场景。第六条为引用对齐建立基础。最后一条控制回答方向,避免模型围绕相关话题展开,却没有直接解决用户问题。
1.2 常见的 System Prompt 设计误区
最常见的误区是系统指令过短,例如:
plain
你是一名客服助手,请根据资料回答问题。
"根据资料"并没有定义资料的优先级,也没有规定资料不足时如何处理。对模型而言,它完全可以一边参考资料,一边调用自身知识补充答案。
另一个极端是不断向 Prompt 追加规则。每出现一个 Bad Case,就加上一条更具体的限制,最后形成几十条互相交叉的指令。这样的 Prompt 往往不是更安全,而是更难维护。比如一条要求"回答尽可能完整",另一条要求"只保留最简结论";一条要求"冲突时使用最新资料",另一条却没有说明更新时间是否可信。模型最终只能在冲突规则中猜测优先级。
Prompt 的长度不是质量指标。更合理的做法是让每一条规则都能对应一类稳定问题,并在修改后使用历史样本回归。对于复杂业务,规则多一些并不可怕,真正需要避免的是重复、冲突和只针对单个案例的临时补丁。
还有一个经常被忽视的问题是没有兜底分支。产品希望模型"尽量回答",于是没有明确告诉模型无法回答时该怎么办。结果模型自然会选择最像答案的文本继续生成。对于事实型问答,允许模型承认信息不足,不是能力退化,而是可信系统必须具备的边界意识。
2. 检索上下文(Retrieved Context):把 chunk 喂给模型
系统指令定义了模型如何回答,但最终回答是否稳定,还取决于检索上下文如何组织。
很多团队直接把若干 chunk 按顺序拼接起来。这种方式实现简单,却把大量解释成本留给了模型:哪一段是特殊规则,哪一段是通用规则,哪一段已经过期,哪一段只适用于某类用户,都需要模型自己判断。
在生产环境里,上下文不应该只是"几段文本",而应该是带边界的证据集合。
2.1 上下文的组装格式
推荐为每个 chunk 提供独立编号,并附上与规则判断有关的元数据:
plain
【参考资料】
[1]
来源:特殊商品退换规则
版本:v3.x
生效时间:某年第二季度
适用对象:澄光 X9
内容:该商品采用定制封装工艺,拆封后不适用无理由退货规则。
如检测确认存在质量异常,可以进入售后退货流程。
---
[2]
来源:平台通用退换规则
版本:v2.x
生效时间:某年第二季度
适用对象:普通现货商品
内容:商品保持完好且不影响再次销售时,可在规定期限内申请无理由退货。
---
[3]
来源:售后运费规则
版本:v2.x
生效时间:某年第二季度
适用对象:售后退货订单
内容:质量异常导致的退货,配送费用由平台承担;
非质量原因退货,配送费用由用户承担。
编号的作用不仅是让模型输出 [1]、[2]。它还为后端建立了稳定的引用映射。来源、版本、生效时间和适用对象也不是为了让 Prompt 看起来更完整,而是帮助模型区分规则边界。
在真实业务里,语义最相似的资料不一定拥有最高业务优先级。用户问某款特殊商品是否能退,通用退货规则可能与问题非常相关,但真正应该优先采用的是特殊商品规则。因此,进入生成阶段前,不能只依赖相似度排序,还应结合适用范围、版本状态、权限和业务优先级做一次调整。
可以把这个过程理解为:
plain
生成阶段的证据顺序
= 语义相关性
+ 适用范围匹配
+ 规则优先级
+ 时效与版本状态
这不一定需要实现成固定的线性公式,但架构上要明确:Reranker 主要解决语义相关性,业务系统仍然要负责规则优先级。
2.2 上下文窗口的限制与应对
现代模型能够接收很长的上下文,但"能放进去"不等于"应该放进去"。
证据数量增加后,真正关键的内容会被稀释;相似但适用条件不同的规则更容易被混淆;输入成本和响应时间也会同步上升。长上下文还存在典型的中间信息弱关注问题,排在中间位置的关键证据可能反而没有被模型充分利用。
因此,Top-K 不应该由一个固定经验值决定。简单事实问答通常只需要少量高质量证据;需要比较多个对象或综合多个规则时,才有必要扩大上下文。更稳妥的方法是先建立回归集,再观察不同证据数量对准确率、拒答率和延迟的影响。
我的经验是,生成模型不应该替检索系统做第二次"大海捞针"。如果上下文里仍然混入大量弱相关资料,应该优先回到召回、重排序和证据去重环节处理,而不是期待模型自行识别所有噪声。
3. 用户问题(User Query):原始问题还是改写后的问题
大多数情况下,保留用户原始问题是最安全的做法。原始问题包含用户的真实表达和关注点,过度改写可能改变意图。
但多轮对话中,用户经常会使用省略和指代。例如前一轮讨论某款商品,下一轮只问"那拆了还能退吗"。如果直接把这句话交给检索和生成,系统可能无法确定主体。这时需要结合对话历史进行 Query Rewriting,把问题改写成完整、可检索的形式。
需要注意的是,改写后的问题不应该替代原始问题。更稳妥的方式是同时保留两者:
plain
【原始问题】
那拆了还能退吗?
【规范化问题】
澄光 X9 拆封后是否仍适用无理由退货规则?
规范化问题用于检索和消除歧义,原始问题则用于生成阶段判断用户真正的表达方式。与此同时,系统还应保留改写前后的日志,否则一旦答案出现偏差,很难判断问题是检索错误,还是 Query Rewriting 已经改变了用户意图。
4. 完整 Prompt 模板示例
把系统指令、检索上下文和用户问题组合起来,一份完整 Prompt 可以写成下面这样:
plain
【System】
你是商城售后知识助手。请根据本轮提供的【参考资料】回答用户问题。
【事实边界】
- 【参考资料】是本轮回答唯一允许使用的事实来源。
- 不得使用外部知识补充或修改主体、条件、数字、时间和结论。
- 资料没有说明的内容,不得根据经验推测。
【信息不足】
- 如果资料只能回答部分问题,先回答能够确认的部分,再说明无法确认的部分。
- 如果资料完全不足,明确说明现有资料无法支持结论。
【冲突处理】
- 多条资料说法不一致时,保留各自适用条件和来源。
- 只有资料明确给出版本、生效时间或优先级时,才能判断覆盖关系。
【引用要求】
- 每个事实性结论后标注对应资料编号,例如 [1] 或 [1][3]。
- 不得引用无关资料,也不得生成不存在的编号。
【表达要求】
- 先给结论,再说明必要条件。
- 表达简洁自然,不扩展无关背景。
【参考资料】
[1]
来源:特殊商品退换规则
适用对象:澄光 X9
内容:该商品采用定制封装工艺,拆封后不适用无理由退货规则。
如检测确认存在质量异常,可以进入售后退货流程。
[2]
来源:平台通用退换规则
适用对象:普通现货商品
内容:商品保持完好且不影响再次销售时,可在规定期限内申请无理由退货。
[3]
来源:售后运费规则
适用对象:售后退货订单
内容:质量异常导致的退货,配送费用由平台承担;
非质量原因退货,配送费用由用户承担。
【用户问题】
澄光 X9 拆封后还能退吗?如果是质量问题,运费由谁承担?
理想输出不是把所有资料都复述一遍,而是直接回答用户问题:
plain
澄光 X9 拆封后不适用无理由退货规则 [1]。
如果检测确认存在质量异常,仍可以进入售后退货流程 [1],
此时配送费用由平台承担 [3]。
这里没有引用通用规则 [2],因为它并不直接支持最终结论。引用越多不代表回答越可靠,只引用实际使用的资料,反而更便于校验。

幻觉抑制:让模型不知道就说不知道
1. 什么是幻觉(Hallucination)
在 RAG 系统里,幻觉并不只是"模型编了一个完全不存在的事实"。更准确地说,只要模型输出了无法由当前证据支持的事实性结论,就应该被视为生成风险。
大模型的训练目标决定了它倾向于生成连贯且看起来合理的文本。当知识片段不足时,它不会天然停下来判断"这里没有证据",而是会继续使用预训练阶段学到的模式完成答案。模型不是故意欺骗用户,它只是没有把"证据是否充分"当成默认生成条件。
这也是为什么 RAG 场景中的幻觉更危险。用户看到知识库问答系统,会默认答案来自企业内部资料,但模型可能悄悄混入了自身知识。最终文本语气越自信,用户越难意识到其中哪些内容没有依据。
2. RAG 场景下幻觉的三种典型表现

2.1 篡改事实
第一类是事实反转。资料明确写着"不适用无理由退货",模型却按照通用经验回答"可以无理由退货"。
plain
资料:澄光 X9 拆封后不适用无理由退货规则。
错误回答:澄光 X9 拆封后仍可以申请无理由退货。
这种问题最容易识别,但也最严重,因为模型直接改变了业务规则。降低 Temperature 可能让答案更稳定,却不能保证模型不会稳定地输出同一个错误结论。
2.2 凭空捏造
第二类是无依据补充。资料只说明由谁承担运费,模型却继续给出退款到账周期、处理步骤或联系方式。
plain
资料:质量异常退货时,配送费用由平台承担。
错误回答:质量异常退货时配送费用由平台承担,
退款会在约几个工作日内原路到账。
这类答案读起来通常很自然,也可能碰巧符合实际流程。但只要本轮资料没有支持,就不应该被生成。企业系统不能把"模型大概率猜对"当作事实来源。
2.3 张冠李戴
第三类是跨证据错配。模型没有创造新的词,却错误组合了不同证据中的主体、条件或数字。
plain
资料一:声屿 S2 的基础保修期为约一年。
资料二:购买增强保障计划后,服务期可延长至约两年。
错误回答:声屿 S2 的默认保修期为约两年。
这种问题最难处理,因为答案中的信息都能在上下文中找到。解决它的关键不是重复强调"不要幻觉",而是要求模型在组合多条资料时保留前置条件,并尽量让答案事实原子化。
不建议生成这样的长句:
plain
该商品拆封后不能无理由退货,质量问题可以退,
运费由平台承担,退款会很快到账 [1][3]。
一句话混入了多个事实,引用关系也变得模糊。更稳妥的表达是:
plain
该商品拆封后不适用无理由退货规则 [1]。
如果检测确认存在质量异常,可以进入退货流程 [1]。
质量异常退货的配送费用由平台承担 [3]。
现有资料没有说明退款到账时间。
每句话只承载一个可验证结论,后续做引用校验、人工审计和自动评估都会更简单。
3. 通过 Prompt 抑制幻觉的实用技巧
Prompt 无法从根本上消除幻觉,但可以显著降低模型越过证据边界的概率。
第一步仍然是明确限定知识来源。不要只说"参考资料",而要明确说明它是本轮唯一允许使用的事实来源。第二步是给模型提供信息不足时的合法出口,让它知道无法确认时可以部分回答或拒答。第三步是专门限制数字、日期、金额、时效和联系方式等高风险细节。第四步是要求事实性结论带引用,让模型在组织答案时主动考虑每句话的依据。
除此之外,还可以在 Prompt 中加入一条非常实用的要求:
plain
组合多条资料时,必须保留各条资料中的适用对象、
前置条件、例外条件和时间范围,不得省略后改变原意。
这条规则比"不要张冠李戴"更可执行。模型可能不理解抽象的错误标签,但能够理解在生成时需要保留条件。
需要强调的是,Prompt 只是第一层防线。编号合法性、引用覆盖和事实一致性等问题,仍然需要后端校验。把所有可靠性责任都压在 Prompt 上,会让生成服务变成一个无法测试的黑盒。
3.1 Temperature 和 Top-P 参数的作用
Temperature 控制的是采样随机性。值越低,模型越倾向于选择高概率 token,输出通常更稳定、更保守;值越高,表达更开放,也更容易出现不必要的扩展。
对于售后问答、制度查询和内部知识助手,通常应从较低区间开始测试。具体值不必照搬固定配置,因为不同模型对参数的敏感程度并不一致。更重要的是使用回归集观察事实一致性、表达自然度和重复性。
Top-P 控制候选 token 的累计概率范围。它和 Temperature 都属于采样参数,主要影响生成路径,而不是事实判断。多数场景先调整一个参数即可,同时修改两者会增加调试难度。
低 Temperature 不能"关闭幻觉"。如果模型误解了证据,它完全可能在低随机性的情况下持续输出相同错误。参数调优应该放在证据组织、Prompt 约束和后处理之后,而不是把它当成可靠性的核心方案。
3.2 兜底回答的设计
好的兜底不是一句生硬的"我不知道",而是明确说明当前证据覆盖到哪里。
当资料完全无法回答时,可以返回:
plain
现有资料不足以确认该问题,建议通过人工服务或业务页面进一步核实。
当资料只能回答部分问题时,更合理的方式是先回答有证据的部分:
plain
质量异常退货的配送费用由平台承担 [3]。
现有资料没有说明退款到账时间,建议进一步确认。
这种部分回答比整体拒答更有价值,也比补充一个猜测性时效更安全。
从架构上看,兜底最好不是只存在于 System Prompt 中。检索为空时,业务服务可以直接返回统一结果,不必再调用模型。检索有结果但证据覆盖不足时,再由模型完成"已知部分 + 未知部分"的组织。这样既降低成本,也减少无证据生成。

引用对齐:让答案可追溯
1. 为什么需要引用对齐
引用的价值不只是让答案看起来更专业。它承担的是证据追踪和责任定位。
当用户看到一个规则结论时,能够进一步查看它来自哪份文档、哪个版本和哪段原文,系统才真正具备可验证性。出现错误后,团队也可以沿着引用回溯:是知识库本身写错了,检索召回了过期资料,还是模型错误解释了正确证据。
在金融、医疗、法律和企业制度等场景,引用还关系到合规审计。单纯声明"答案来自知识库"没有意义,系统必须能够定位到具体证据。
但需要警惕一种"伪引用":模型在答案后面生成了 [1],并不代表 [1] 真正支持这句话。引用生成只是第一步,后端仍需检查编号是否合法、事实是否有引用,以及引用内容与结论是否一致。
2. 引用对齐的实现方式
引用对齐的基础做法仍然是:上下文中的每个 chunk 使用稳定编号,模型在相关句子后输出编号,后端再把编号映射回证据对象。
在简单系统里,可以先实现编号合法性检查。只有三条证据时,模型输出 [5],系统不能直接忽略,而应该记录为异常。下一步可以检查引用覆盖:答案中出现明确期限、金额、比例和规则结论,却没有任何引用,应当标记为未支持事实。
要求更高的场景还需要语义一致性检查。即使编号存在,也要判断引用文本是否支持结论。这个环节可以从主体、关键数字、否定词和适用条件的规则匹配做起,之后再引入 NLI 模型或独立的事实一致性评估器。
2.1 Prompt 中的引用指令设计
引用规则可以写得更明确一些:
plain
回答时请遵守以下引用规则:
- 每个事实性结论后标注对应资料编号,例如 [1] 或 [1][3]。
- 一句话包含多个独立事实时,应拆分为多句话,并分别标注引用。
- 只引用真正支持当前结论的资料。
- 不得生成上下文中不存在的引用编号。
- 资料没有支持的内容应明确说明未知,不要为其补充引用。
其中"一句话包含多个独立事实时应拆分"非常重要。它能降低引用关系的模糊程度,也方便后端进行句子级校验。
2.2 引用解析与展示
后端可以先通过正则提取引用编号,再关联到对应证据。下面的 Java 示例重新设计了类名和方法名,不依赖原文项目:
java
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class ReferenceIndexReader {
private static final Pattern MARKER =
Pattern.compile("\\[(\\d+)]");
private ReferenceIndexReader() {
}
public static Set<Integer> read(String answerText) {
Set<Integer> indexes = new LinkedHashSet<>();
if (answerText == null || answerText.isBlank()) {
return indexes;
}
Matcher matcher = MARKER.matcher(answerText);
while (matcher.find()) {
indexes.add(Integer.parseInt(matcher.group(1)));
}
return indexes;
}
}
解析后不建议直接在后端拼接 HTML。更稳妥的方式是返回结构化引用对象,由前端决定如何展示:
plain
{
"answer": "澄光 X9 拆封后不适用无理由退货规则 [1]。",
"references": [
{
"id": 1,
"title": "特殊商品退换规则",
"url": "/knowledge/special-return",
"excerpt": "该商品采用定制封装工艺,拆封后不适用无理由退货规则。"
}
]
}
这样可以避免模型或知识库中的文本直接参与 HTML 拼接,也方便前端实现引用卡片、侧边栏和原文定位。
3. 引用对齐的效果示例
假设用户问:
声屿 S2 的保修期是多久?过期后还能维修吗?
检索结果如下:
plain
[1]
来源:产品基础保障说明
内容:声屿 S2 自购买之日起享受约一年的基础保障服务,
覆盖制造缺陷和符合条件的电池问题。
[2]
来源:增强保障计划说明
内容:购买增强保障计划后,服务期限可延长至约两年,
并提供有限次数的意外损坏服务。
[3]
来源:过期维修说明
内容:基础保障期结束后,用户仍可通过授权维修点申请付费维修,
具体费用根据检测结果确定。
经过约束后,模型可以回答:
plain
声屿 S2 的基础保障期约为一年,自购买之日起计算 [1]。
购买增强保障计划后,服务期可延长至约两年 [2]。
基础保障期结束后仍可以申请付费维修,
具体费用需要根据检测结果确定 [3]。
这里最重要的并不是回答带了三个编号,而是模型保留了"基础保障"和"购买增强计划后"的条件差异,没有把两年的期限写成默认规则。每个结论也都能够映射到具体证据。

答案约束:控制输出的格式和边界
生成质量不仅取决于事实是否正确,也取决于答案能否被下游系统稳定消费。客服页面需要自然语言,业务接口可能需要 JSON,知识卡片可能需要列表。不同场景应使用不同约束,不要让一个 Prompt 同时承担所有展示形态。
1. 格式约束:JSON / 纯文本 / 列表
自然语言适合直接面向用户,但仍然要规定回答方向:
plain
请先直接回答用户问题,再补充必要条件。
语言自然、简洁,不重复复述用户问题。
如果结果需要由下游程序消费,建议使用结构化输出:
json
{
"answer": "澄光 X9 拆封后不适用无理由退货规则。",
"citationIds": [1],
"evidenceStatus": "SUPPORTED"
}
不要只依靠 Prompt 保证 JSON 正确。如果模型 API 支持 JSON Schema、结构化响应或严格工具调用,应优先使用平台能力。Prompt 更适合表达业务语义,Schema 更适合保证字段和类型。
列表格式适合规则说明和知识卡片,但也不应为了"清晰"把所有回答都拆成很多点。用户只问一个简单事实时,一两句话通常比五条列表更自然。格式应该服务于问题,而不是成为固定模板。
2. 长度约束:控制回答的详略
"越详细越好"和"越短越好"都不适合作为统一策略。答案长度应该由问题复杂度和业务风险决定。
简单事实问答应直接给出结论和必要条件;多规则组合问题可以分段说明;涉及冲突、例外和部分未知时,需要明确展示不同条件。与用户问题无关的背景知识,即使资料中存在,也不应为了显得全面而全部展开。
比固定字数更有效的约束是:
plain
回答应简洁但完整:
覆盖用户问题中的每个子问题,
保留影响结论的前置条件和例外条件,
不扩展与当前问题无关的背景信息。
对于页面空间、短信或语音播报等确有长度限制的场景,可以再叠加字数要求。但不要让字数限制迫使模型删除关键条件,否则形式合规了,结论却可能失真。
3. 边界约束:只回答知识库范围内的问题
企业知识助手必须有明确服务边界。售后系统不应该回答品牌偏好、价格预测、投资建议或与业务无关的开放问题。否则模型会重新回到通用聊天模式,使用自身知识自由生成。
边界规则可以写成:
plain
你只负责回答商品售后、退换货和物流处理相关问题。
对于商品功能评测、品牌比较、价格预测和个人观点等问题,
请说明当前服务范围,并引导用户查看相应业务渠道。
即使你知道答案,也不要使用外部知识回答超出范围的问题。
最后一句很关键。边界约束不是"不会就不答",而是"即使会也不答"。这是业务系统与通用聊天助手的根本差异。
把角色、事实边界、拒答、引用和表达要求放在一起,一份生产级 System Prompt 可以保持清晰分组,但不需要继续无限增加规则。真正复杂的约束,应该逐步下沉到检索过滤、权限控制、结构化输出和后处理校验中。

Java 实战:完整的 RAG 生成流程
前面的内容分别讨论了 Prompt、上下文、幻觉、引用和输出边界。下面把这些环节串成一条完整的 Java 生成链路。
示例不会绑定真实厂商、模型或开源项目,而是通过模型网关隔离外部服务。这样更符合真实系统的演进方式:业务层负责证据组织和生成约束,适配层负责调用具体模型。后续更换云端模型、企业网关或私有化推理服务时,不需要重写整个生成模块。
1. 代码实现:从检索到生成的完整链路
先定义证据、引用和回答结果:
java
import java.util.List;
public record EvidenceBlock(
String content,
String sourceName,
String sourceUrl,
String effectiveAt,
double rankScore
) {
}
public record CitationRecord(
int citationId,
String sourceName,
String sourceUrl,
String evidenceContent
) {
}
public record AnswerResult(
String answer,
List<CitationRecord> citations,
List<Integer> invalidCitationIds
) {
}
public record DialogueMessage(
String role,
String content
) {
}
public record ModelOptions(
double temperature,
double topP,
int maxTokens
) {
}
模型调用通过接口隔离:
java
import java.util.List;
public interface TextGenerationGateway {
String generate(
List<DialogueMessage> messages,
ModelOptions options
) throws Exception;
}
核心生成服务如下:
java
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class EvidenceBoundAnswerService {
private static final Pattern CITATION_PATTERN =
Pattern.compile("\\[(\\d+)]");
private static final String SYSTEM_POLICY = """
你是商城售后知识助手,请严格使用【参考资料】回答问题。
【事实边界】
- 参考资料是本轮唯一事实来源。
- 不得使用外部知识补充数字、日期、金额、时效和流程。
- 不得改变资料中的主体、条件、适用范围和结论。
【信息不足】
- 资料只能回答部分问题时,回答已知部分并说明未知部分。
- 资料无法支持结论时,不得猜测。
【冲突与引用】
- 资料冲突时分别说明适用条件,没有明确优先级时不自行裁决。
- 每个事实结论后标注资料编号,例如 [1] 或 [1][3]。
- 不得生成不存在的编号。
【表达要求】
- 先给结论,再补充必要条件。
- 表达简洁自然,不扩展无关内容。
""";
private final TextGenerationGateway modelGateway;
public EvidenceBoundAnswerService(
TextGenerationGateway modelGateway
) {
this.modelGateway = modelGateway;
}
public AnswerResult answer(
List<EvidenceBlock> evidenceBlocks,
String userQuestion
) throws Exception {
if (userQuestion == null || userQuestion.isBlank()) {
throw new IllegalArgumentException(
"userQuestion must not be blank"
);
}
if (evidenceBlocks == null || evidenceBlocks.isEmpty()) {
return new AnswerResult(
"现有资料不足以确认该问题,建议进一步核实。",
List.of(),
List.of()
);
}
String userMessage = buildContext(evidenceBlocks)
+ "\n【用户问题】\n"
+ userQuestion.strip();
List<DialogueMessage> messages = List.of(
new DialogueMessage("system", SYSTEM_POLICY),
new DialogueMessage("user", userMessage)
);
ModelOptions options =
new ModelOptions(0.12, 0.91, 800);
String answerText =
modelGateway.generate(messages, options);
CitationInspection inspection =
inspectCitations(answerText, evidenceBlocks.size());
return new AnswerResult(
answerText,
mapCitations(
inspection.validIds(),
evidenceBlocks
),
List.copyOf(inspection.invalidIds())
);
}
private String buildContext(
List<EvidenceBlock> evidenceBlocks
) {
StringBuilder context =
new StringBuilder("【参考资料】\n\n");
for (int index = 0;
index < evidenceBlocks.size();
index++) {
EvidenceBlock block = evidenceBlocks.get(index);
context.append('[')
.append(index + 1)
.append("]\n")
.append("来源:")
.append(normalize(block.sourceName()))
.append('\n')
.append("生效时间:")
.append(normalize(block.effectiveAt()))
.append('\n')
.append("内容:")
.append(normalize(block.content()))
.append("\n\n---\n\n");
}
return context.toString();
}
private CitationInspection inspectCitations(
String answerText,
int evidenceCount
) {
Set<Integer> validIds = new LinkedHashSet<>();
Set<Integer> invalidIds = new LinkedHashSet<>();
Matcher matcher = CITATION_PATTERN.matcher(
answerText == null ? "" : answerText
);
while (matcher.find()) {
int citationId =
Integer.parseInt(matcher.group(1));
if (citationId >= 1
&& citationId <= evidenceCount) {
validIds.add(citationId);
} else {
invalidIds.add(citationId);
}
}
return new CitationInspection(
validIds,
invalidIds
);
}
private List<CitationRecord> mapCitations(
Set<Integer> citationIds,
List<EvidenceBlock> evidenceBlocks
) {
List<CitationRecord> records =
new ArrayList<>();
for (int citationId : citationIds) {
EvidenceBlock block =
evidenceBlocks.get(citationId - 1);
records.add(new CitationRecord(
citationId,
block.sourceName(),
block.sourceUrl(),
block.content()
));
}
return List.copyOf(records);
}
private String normalize(String value) {
return value == null || value.isBlank()
? "未提供"
: value.strip();
}
private record CitationInspection(
Set<Integer> validIds,
Set<Integer> invalidIds
) {
}
}
这段代码有几个关键边界。
System Prompt 与证据内容分开发送。行为规则放在 system 消息,参考资料和用户问题放在 user 消息,避免模型把知识库中的普通文本误判为更高优先级指令。
无证据时不调用模型。检索结果为空已经意味着系统没有事实基础,继续生成只会增加成本和不确定性。
引用编号必须经过边界校验。模型输出不存在的编号时,不会被悄悄映射到其他资料,而是进入 invalidCitationIds,便于日志、告警和质量统计。
代码保留了 rankScore,但没有把它直接拼进 Prompt。不同检索器的分数分布并不统一,模型也很难稳定理解一个相似度分数究竟代表什么。它更适合用于检索调试和服务端排序。
这套实现仍然只完成了引用编号合法性检查。生产系统还应继续增加事实一致性校验,至少覆盖关键数字、主体、否定关系和前置条件。
2. 运行效果展示
下面构造一组经过检索和重排序后的证据:
java
import java.util.List;
public final class RagAnswerDemo {
public static void main(String[] args)
throws Exception {
TextGenerationGateway gateway =
createGatewayAdapter();
EvidenceBoundAnswerService service =
new EvidenceBoundAnswerService(gateway);
List<EvidenceBlock> evidence = List.of(
new EvidenceBlock(
"澄光 X9 采用定制封装工艺,"
+ "拆封后不适用无理由退货规则。"
+ "检测确认存在质量异常时,"
+ "可以进入售后退货流程。",
"特殊商品退换规则",
"/knowledge/special-return",
"某年第二季度",
0.94
),
new EvidenceBlock(
"普通现货商品保持完好且不影响再次销售时,"
+ "可在规定期限内申请无理由退货。",
"平台通用退换规则",
"/knowledge/general-return",
"某年第二季度",
0.83
),
new EvidenceBlock(
"质量异常导致的退货,配送费用由平台承担;"
+ "非质量原因退货,配送费用由用户承担。",
"售后运费规则",
"/knowledge/return-shipping",
"某年第二季度",
0.79
)
);
AnswerResult result = service.answer(
evidence,
"澄光 X9 拆封后还能退吗?"
+ "如果是质量问题,运费由谁承担?"
);
System.out.println(result.answer());
System.out.println(result.citations());
System.out.println(result.invalidCitationIds());
}
private static TextGenerationGateway
createGatewayAdapter() {
throw new UnsupportedOperationException(
"请接入内部模型网关或测试适配器"
);
}
}
期望模型输出:
plain
澄光 X9 拆封后不适用无理由退货规则 [1]。
如果检测确认存在质量异常,仍可以进入售后退货流程 [1],
此时配送费用由平台承担 [3]。
这个结果没有复述与当前问题无关的通用退货规则,也没有编造退款时效、退款方式和联系方式。特殊规则、质量问题例外和运费规则分别绑定到对应证据,后端可以据此生成引用卡片。
需要注意的是,模型可能偶尔遗漏某个引用,或者对可以合理推导出的结论不引用全部资料。因此,引用覆盖率不能只靠肉眼观察,应该纳入自动评估。对于高风险场景,缺少引用的事实句可以进入二次校验或直接降级为保守回答。
3. Prompt 模板的迭代优化
Prompt 上线后一定会继续迭代,但迭代的依据应该是完整 Bad Case,而不是一句"答案不太对"。
一条可用于归因的记录,至少应包含:用户原始问题、规范化问题、召回结果、重排序结果、实际使用的 Prompt 版本、模型参数、模型答案、引用信息和期望结果。只有链路信息完整,才能判断错误到底发生在哪一层。
例如用户问"澄光 X9 拆封后能不能退",最终答案错误,可能存在三种完全不同的原因:
第一种是特殊商品规则没有被召回,这是检索问题。第二种是规则被召回,但通用规则排在前面,这是排序和业务优先级问题。第三种是特殊规则已经排在首位,模型仍然回答可以退,这才是生成约束问题。
如果只看到最后一句错误答案,团队很容易用 Prompt 去修检索问题。Prompt 越改越长,真正的错误源头却没有被处理。
生成侧 Bad Case 可以稳定归入几类:事实反转、无依据补充、跨证据错配、答非所问、错误拒答、引用错误和越界回答。每次修改 Prompt 时,都应该明确它准备修复哪一类问题,并对全部历史样本做回归。
例如模型把增强保障期限写成默认保障期限,应该补充的是一条通用规则:
plain
组合多条资料时,必须保留各条资料中的适用对象、
前置条件和例外条件,不得省略后改变原意。
不应该在 Prompt 里增加某个具体商品的答案。前者修复一类结构性问题,后者只是在记住一个样例。
回归集也不能只包含失败案例。它还应覆盖资料充分的简单问答、部分可回答问题、完全不可回答问题、通用规则与特殊规则冲突、过期资料、提示注入文本和超出业务范围的问题。否则,团队可能修复了"应该拒答却没有拒答",同时让系统走向另一个极端:资料明明足够,模型却频繁返回无法确认。
评估指标也应拆开观察。答案是否被证据支持、是否直接回答问题、引用是否准确、该拒答时是否拒答,以及不该拒答时有没有过度拒答,这些指标对应不同的优化方向。只看一个综合分数,无法判断下一步应该调整知识库、检索、Prompt 还是后处理。
一张图看完整 RAG 链路
生成阶段不是一次孤立的大模型调用,而是整条 RAG 链路中的最后一段。
plain
flowchart LR
A[原始文档] --> B[文本提取]
B --> C[数据分块]
C --> D[元数据与权限管理]
D --> E[向量化与索引构建]
F[用户问题] --> G[问题规范化]
G --> H[混合检索]
E --> H
H --> I[重排序与业务优先级调整]
I --> J[证据去重与上下文组装]
J --> K[三段式 Prompt]
K --> L[受约束生成]
L --> M[引用合法性检查]
M --> N[事实一致性与格式校验]
N --> O[答案与来源展示]
N --> P[Bad Case 日志与回归集]
从原始文档到最终答案,每一层都有自己的职责。
文本提取和数据分块决定知识是否能够被正确索引;元数据管理决定规则的来源、版本、权限和适用范围是否能够被识别;向量与关键词检索负责找到候选证据;重排序和业务优先级负责把真正应该生效的规则放到前面;Prompt 负责定义模型如何使用证据;生成后校验负责拦截引用异常和事实不一致;Bad Case 与回归集则保证同类错误不会反复出现。
一个更强的模型可以暂时掩盖上下文组织问题,但不能替代业务优先级。更大的 Top-K 可以提高召回覆盖,却会增加证据冲突。更严格的拒答规则可以降低幻觉,也可能造成过度拒答。
真正需要设计的不是某一个神奇参数,而是整条链路的责任边界。知识库决定系统掌握什么,检索决定本轮拿到什么,Prompt 决定模型允许如何使用,后处理决定错误能否被拦截,评估体系决定系统能否持续改进。
当系统能够稳定回答三个问题------结论是什么、依据在哪里、哪些内容当前无法确认------生成模块才不再是一个简单的模型调用,而是一套具备证据管理、行为约束、引用校验和持续回归能力的工程系统。
如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!
