【AI】RAG 生成阶段的最后一公里:Prompt 设计、幻觉抑制与引用对齐

👨‍💻程序员三明治个人主页
🔥 个人专栏 : 《设计模式精解》 《重学数据结构》
《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 决定模型允许如何使用,后处理决定错误能否被拦截,评估体系决定系统能否持续改进。

当系统能够稳定回答三个问题------结论是什么、依据在哪里、哪些内容当前无法确认------生成模块才不再是一个简单的模型调用,而是一套具备证据管理、行为约束、引用校验和持续回归能力的工程系统。

如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!

相关推荐
DLite1 小时前
开源一个轻量化本地知识库:Bishon V2
人工智能·开源
gongzhxu1 小时前
JetBrains IDEA开发环境搭建
java·ide·intellij-idea
CTA终结者1 小时前
近期AI量化学习,把规则改写接到策略开发
人工智能·python
有Li1 小时前
使用整合电子健康记录的大语言模型智能体实现前列腺癌患者教育个性化文献速递/医学智能体前沿
人工智能·python·机器学习·语言模型·医学生
EIConferenceEmma1 小时前
9月份海口站,第二届人工智能、人机交互与自然语言处理国际学术会议(ICAHN 2026)
人工智能·自然语言处理·人机交互
码农学院1 小时前
GEO团队SOP、绩效考核与知识沉淀:技术团队管理体系化工程实践
运维·人工智能·windows
小保CPP1 小时前
OpenCV C++基于极值区域滤波算法的场景文本检测(OCR)
c++·人工智能·opencv·算法·计算机视觉·ocr
Bug收容所1 小时前
12305项目学习day5
java·spring boot·redis·mysql·spring·rocketmq