RAG 的最后一公里:流式生成、来源引用和历史记录应该怎么做

0 阅读10分钟

自动检索上下文、Prompt 组装、SSE 流式输出、来源引用、历史记录、取消生成、失败重试、Markdown 安全渲染。我想解决的不是“把检索结果丢给 LLM”,而是“怎么把 RAG 最终交付成一个真正可用的产品体验”。

前面几篇我依次写了文档加载、分块、向量化、索引和检索。
如果把这整条链路看成一个系统工程,那这一篇终于来到了最后一个模块:文本生成

很多 RAG demo 到这里就收尾了:

  • 把问题和上下文拼到 Prompt
  • 调一下 LLM
  • 返回一段文本

这个流程当然能证明系统“能回答问题”。
但如果你真的想把它变成用户可用的产品,很快就会发现,生成层其实有很多事情要做:

  • 怎么把检索结果组织成合理上下文
  • 上下文太长时怎么截断
  • 回答怎么实时输出,而不是让用户一直等
  • 结果怎么带引用来源
  • 失败后怎么重试
  • 生成过程怎么取消
  • Markdown 怎么安全渲染
  • 历史怎么保存和回看

所以在 RAG Pipeline Hub 里,我把文本生成模块理解成:

RAG 的最后一公里,也是用户真正感知整个系统质量的地方。

项目地址:

  • GitHub: https://github.com/qingni/rag-pipeline-hub

为什么生成层是 RAG 的最后一公里

前面的所有模块都很重要,但说到底,用户最终看到的还是生成结果。

这意味着无论你前面做了多少工程优化,用户最后的感受通常都会落在几个问题上:

  • 回答快不快
  • 回答顺不顺
  • 回答能不能对上问题
  • 回答有没有引用来源
  • 回答是不是像一个可用产品,而不是一段原始模型输出

所以生成层在我看来不是“最后调一下模型”,而是:

把检索能力、上下文组织、交互体验和结果可信度统一交付出来的出口层。

很多 RAG 项目做到后面,真正拉开差距的不是“模型参数”,而是这些最终交互细节。

为什么生成模块和普通聊天接口完全不是一回事

很多人第一次做 RAG 生成时,会不自觉把它当成一个聊天接口。

但这两者其实有本质区别。

普通聊天接口主要关注:

  • 用户问题
  • 系统提示词
  • 模型输出

而 RAG 生成模块还要多处理一大层上下文问题:

  • 上下文从哪里来
  • 上下文是不是检索出来的高质量结果
  • 上下文有多少条
  • 上下文总长度是否超预算
  • 回答中的结论是否能对应到来源

也就是说,它本质上是:

检索结果 + Prompt 编排 + 生成交付

三件事叠加在一起,而不是单纯的大模型调用。

这个模块当前在整条链路里负责什么

生成模块位于搜索查询模块之后,是整条 RAG 链的最终输出层。

它的典型流程大致是:

用户输入问题
  -> 自动执行检索
  -> 获取上下文片段
  -> 按 token 预算截断和组织上下文
  -> 构建 Prompt
  -> 调用 LLM
  -> 流式返回内容
  -> 提取和展示引用来源
  -> 保存历史记录

可以看到,这里真正难的不是“调用模型”,而是:

  • 怎么把前面检索层产出的结果,组织成一个对生成友好的输入
  • 怎么把模型输出,组织成一个对用户友好的结果

前者偏系统编排,后者偏产品交付。
这也是为什么我觉得生成模块特别值得单独讲一篇。

Prompt 组装为什么不能只靠简单拼接

很多 demo 的 Prompt 组装方式都很直接:

上下文 + 问题

这当然是最小实现,但在真实场景里往往不够。

因为你很快会遇到这些问题:

  • 检索结果太多,放不下
  • 上下文顺序不合理
  • 不同来源内容混在一起
  • 引用关系不清楚
  • 用户问题和上下文之间缺少明确约束

所以在这个项目里,生成模块会显式做几件事:

1. 接收检索结果作为结构化上下文

不是把一堆字符串生拼,而是把:

  • 内容
  • 来源文件
  • chunk 标识
  • 相似度
  • 元数据

一起作为上下文输入基础。

2. 按 token 预算做截断

这一步非常重要。

因为上下文不是越多越好。
如果一股脑全塞进去,常见后果通常是:

  • Prompt 变得臃肿
  • 真正重要的信息被淹没
  • 输出不稳定
  • 成本上升

所以我更愿意把生成模块里的上下文组织理解成一种“预算分配问题”,而不是“能塞多少塞多少”。

3. 构建更明确的提示结构

问题、上下文、系统要求之间需要有清晰边界。
否则模型很容易对引用、回答范围和输出格式理解不稳定。

为什么流式输出几乎是必须项

如果你真的用过 RAG 类产品,就会很快发现:

用户对“等待”的感知非常敏感。

尤其是 RAG 场景里,前面已经经历过:

  • 检索
  • 上下文整理
  • LLM 生成

整个链路天然比普通接口更长。

如果最后还让用户一直盯着加载状态,体验通常会很差。

所以我在这个项目里把流式输出作为重点能力来做。
后端通过 SSE 输出流式数据,前端用 fetch + ReadableStream 实时展示内容。

这件事带来的价值非常直接:

  • 用户更快感知到系统在工作
  • 回答过程更自然
  • 长回答的等待体验明显更好
  • 更接近真实产品而不是离线任务

有时候,流式输出并不会让模型“更聪明”,但它会让系统“更可用”。
而在产品层面,这一点非常重要。

来源引用为什么关键

如果说流式输出解决的是“体验问题”,那来源引用解决的就是“可信度问题”。

因为 RAG 的核心承诺之一就是:

回答不是凭空生成的,而是建立在检索上下文之上。

如果回答出来了,但用户看不到依据,那它和普通聊天模型相比,差异就会被削弱很多。

所以在这个项目里,我非常看重引用能力。

这部分的价值主要体现在:

1. 提高可验证性

用户可以快速确认答案来自哪里。

2. 提高可信度

尤其在知识库、制度文档、技术文档场景下,来源展示本身就是产品价值的一部分。

3. 帮助排查问题

如果回答不理想,至少可以回头看:

  • 是检索结果有问题
  • 还是生成阶段理解错了

所以我一直觉得:

没有来源引用的 RAG,很容易只剩下“像 RAG”,而不是“真正可验证的 RAG”。

为什么历史记录、取消生成和重试能力不能少

这部分是很多 demo 最容易省掉的,但在真实使用里非常重要。

历史记录

用户不仅要拿到一次回答,还需要:

  • 回看之前问过什么
  • 看当时的回答是什么
  • 对比不同问题的输出

所以我在项目里保留了生成历史记录,并支持查看、删除和清空。

取消生成

如果回答已经明显跑偏,或者用户不想继续等了,就应该允许中断。
这也是让交互更接近产品体验的重要细节。

失败重试

有时候失败并不一定是用户问题本身,而可能是:

  • 模型服务波动
  • 上下文过长
  • 某次网络不稳定

这时候如果让用户从头来一遍,体验会很差。
所以生成失败后能基于当前问题和配置快速重试,是非常必要的能力。

这些点单看都不算“核心算法”,但它们对真实可用性影响非常大。

Markdown 渲染为什么不是小问题

生成结果在大多数场景里都不会只是纯文本。
尤其技术类问答里,经常包含:

  • 标题
  • 列表
  • 代码块
  • 表格
  • 引用标记

如果前端只是简单把文本原样显示,阅读体验会明显下降。

所以这个项目里对生成结果做了 Markdown 渲染支持。
但这里还有另一个不能忽略的问题:

安全。

因为模型输出本质上是外部不可信内容。
如果直接渲染 HTML,风险会很大。

所以在前端上,我同时考虑了:

  • Markdown 展示能力
  • 安全清洗能力

这也是我一直觉得“生成层产品化”里一个很重要、但又很容易被忽视的点。

这一层和“把检索结果丢给 LLM”最大的区别是什么

如果只做最小实现,生成层可以非常简单:

问题 + 检索结果 -> LLM -> 输出

但在这个项目里,我更看重的是下面这些更完整的交付能力:

  • 自动从 Collection 执行检索
  • 按 token 预算组织上下文
  • 构建更稳定的 Prompt
  • 支持流式输出
  • 支持引用来源
  • 支持历史记录
  • 支持取消生成
  • 支持失败重试
  • 支持 Markdown 安全渲染

所以它和普通 demo 的区别,不是“多几个接口”,而是:

它把生成模块从一个模型调用动作,升级成一个真正面向用户交付的产品层。

我对这个模块的一个核心判断

如果只让我总结一句话,我会说:

RAG 的最后一公里,不是让模型回答出来,而是让回答在速度、可信度、可追溯性和使用体验上都更接近一个真正可用的产品。

前面的加载、分块、向量化、索引和检索都很重要。
但最终用户记住的,往往还是这一步的体验。

所以在 RAG Pipeline Hub 里,我才会把生成模块做成一个完整的交付层,而不是只保留一个 generate() 接口。

这一轮系列先到这里

到这篇为止,这一组“整体介绍 + 6 个模块拆解”的系列文章就算完整串起来了。

整个链路依次覆盖了:

  1. 文档加载
  2. 文档分块
  3. 文档向量化
  4. 向量索引
  5. 搜索检索
  6. 文本生成

如果后面继续写,我比较想展开的方向包括:

  • 各模块之间的数据流怎么设计
  • RAG 工程里的调试与观测体系
  • 不同模块的推荐引擎怎么做
  • 如何把这套工作台进一步产品化

项目地址:

  • GitHub: https://github.com/qingni/rag-pipeline-hub

如果这篇文章或这个系列对你有帮助,欢迎:

  • 点个 star
  • 提个 issue
  • 留言说说你最关心哪一个后续方向

如果你也做过 RAG 产品化、流式输出、引用标注或生成体验优化,欢迎交流你的经验。
我越来越觉得,真正让用户记住一个 RAG 系统的,往往不是链路图,而是最后这一步的真实使用感受。