自动检索上下文、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 个模块拆解”的系列文章就算完整串起来了。
整个链路依次覆盖了:
- 文档加载
- 文档分块
- 文档向量化
- 向量索引
- 搜索检索
- 文本生成
如果后面继续写,我比较想展开的方向包括:
- 各模块之间的数据流怎么设计
- RAG 工程里的调试与观测体系
- 不同模块的推荐引擎怎么做
- 如何把这套工作台进一步产品化
项目地址:
- GitHub:
https://github.com/qingni/rag-pipeline-hub
如果这篇文章或这个系列对你有帮助,欢迎:
- 点个
star - 提个
issue - 留言说说你最关心哪一个后续方向
如果你也做过 RAG 产品化、流式输出、引用标注或生成体验优化,欢迎交流你的经验。
我越来越觉得,真正让用户记住一个 RAG 系统的,往往不是链路图,而是最后这一步的真实使用感受。