RAG [Retrieval-Augmented Generation]
原文
《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》
预训练的自然语言模型能够从数据学习到大量深度知识,使其仅需访问参数化的隐性数据库而无需访问外部数据;但无法简单的扩展和修改记忆 ,同时可能有幻觉现象 的产生;将参数记忆和非参数记忆相结合构建混合模型,利用其直接修改和拓展记忆;REALM 和 ORQA 将语言模型和可微分的检索器(Retriever) 相结合,取得不错的结果;

将预训练的检索器(Retriever) 与预训练的seq2seq模型结合,并进行微调;检索器(Retriever) 对于输入的Query使用最大内积查找(MIPS)找到前K个文档切片(z);对于最终预测结果y,将文档切片(z)作为潜在变量,根据不同文档切片对seq2seq预测边缘化;
这种通⽤微调⽅法将参数记忆(parametric-memory)⽣成模型⾮参数记忆(non-parametric memory),我们称之为检索增强⽣成(RAG);
总结
-
核心思想 是将自然语言模型的记忆分为参数化记忆 和非参数化记忆,解决记忆动态更新和幻像问题;
- 参数化记忆: 存储了通过预训练学习到的语言模式和通用知识。
- 非参数化记忆: 使用一个可检索的动态知识库。
-
检索器(Retriever) 承载定制化索引知识库(Embedding) 和输入内容的检索能力,对知识库进行拆分和向量化存储称之为索引数据库;用户输入的提示词(User Prompt),检索器会对内容也进行一次向量化处理,再到向量化数据库检索出k片相关性高的切片;
-
生成器(Generator) 承载大语言模型,对每一片切片、用户提示词和系统提示词(System Prompt)作为上下文,返回结果;
RAG 实践
实践结果
以倪海夏针灸专题为数据源,设计包含下针方法、对应视频节点的元数据结构作为索引知识库,实现症状到关键知识的精准检索
cmd
问题1: 病人中风怎么办
回答: 如果病人中风,出现舌强不语、手握拳的症状,可找到廉泉穴进行治疗。
找穴方法为:用大拇指指尖顶到下巴处,指尖到处即为廉泉穴。下针时,针要对
着舌根,而不是直针下,下针深度一寸到一寸半即可。该穴位大部分用针治疗,一般不用灸。
问题2: 对应视频时间节点是多少
回答: 文档中提到病人中风相关内容的视频时间是 2-00:43:13 。(第二集的43分13秒)
系统架构


-
模型:
- 向量模型:Doubao-embedding-large
- 大语言模型:Doubao-pro-32k
-
数据库: Redis
-
框架: eino(File Loader、Transform、Embedder、Generator、Retriver基础库提供)
数据准备
视频字幕提取

火山引擎申请音视频字幕生成能力,对本地视频进行批量字幕提取;
markdown文件生成
使用AI归纳总结内容并生成markdown文件,效果如下:
markdown
# 任脉与督脉
##time (1-02:18:45)
我们开始介绍任脉。
**任**,女子妊也,女人会怀孕是靠任脉。十二经络开始介绍之前,要从任脉开始介绍。找穴道要从任、督二脉为基准,找到标准,就可以很快速的找到穴道。我最怕就是鸡同鸭讲,心里知道答案,结果穴道找错了。
- **任脉**:是所有阴汇积的地方
- **督脉**:是诸阳之会!全身的动能,能量,都在督脉上面
> 我最常跟病人讲一句话就是,无论如何不要让别人碰你的脊椎骨,督脉不能碰,脊椎骨像龙骨一样,有人椎间盘凸出,有人去开刀,开完反而更坏。MAS 根本就是疫苗引起的。造成一开始脊椎就弯的,你想想,疫苗可以把所有阳气所在的督脉都打烂,这种人就不长寿。比如说我们叫天柱倾,脖子都歪过去,这样的人命在旦夕,一两天就走了。
女人怀孕全靠任脉。督脉走在后面,诸阳之会,脊椎骨上面,任督二脉交会在鼻子人中这边。刚好嘴巴讲话,讲话时任督二脉是开的,你在听课的时候,舌头是顶着上颚。这是你的牙龈,这牙齿,侧面看哦,然后这是下牙,这是嘴唇。简单的概念,舌头顶到上颚的时候,任督二脉是通的。注意看乌龟,乌龟就是这样。所以,乌龟很长寿。我在讲课的时候,嘴巴会动,而你们在听课时,是把舌头顶上去的,脑筋会很清醒,阴电阳电相通。
## 任脉穴位
任脉有三八二十四个穴道,任脉三八起会阴。
......
KnowledgeIndexing

使用Eino-dev插件构建向量知识库Workflow,提供常用的 AI 组件 以及集成组件编排能力; 编排后改自己申请的模型配置即可;

编排后可以看到生成了代码文件,如图上串联;入口文件是orchestration.go
;输入文件路径返回ids;
Transformer
主要对输入知识库进行分割,构建向量数据的元数据;主要对标题和时间进行存储和索引;
go
func newDocumentTransformer(ctx context.Context) (tfr document.Transformer, err error) {
config := &markdown.HeaderConfig{
Headers: map[string]string{
"#": "title",
"##time": "time",
},
TrimHeaders: false}
tfr, err = markdown.NewHeaderSplitter(ctx, config)
if err != nil {
return nil, err
}
return tfr, nil
}
元数据(metadata)存储title和time,content为段落内容;
Main
main函数批量读本地知识库文件数据,并调用orchestration.go
内的函数,传入需要处理的文件路径即可;
go
func main() {
ctx := context.Background()
err := indexMarkdownFiles(ctx, "./docs")
if err != nil {
panic(err)
}
}
func indexMarkdownFiles(ctx context.Context, dir string) error {
runner, err := knowledgeindexing.BuildKnowledgeIndexing(ctx)
if err != nil {
return fmt.Errorf("build index graph failed: %w", err)
}
err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("walk dir failed: %w", err)
}
if d.IsDir() {
return nil
}
ids, err := runner.Invoke(ctx, document.Source{URI: path})
if err != nil {
return fmt.Errorf("invoke index graph failed: %w", err)
}
return nil
})
return err
}
执行main函数,我们可以看到Redis数据库已经写入向量数据,如图:

RAG System

-
lambda: 将开发者任意的函数转换成可被编排的节点,在 RAG 中,有两个转换场景
- 将 User Prompt 消息转换成 ChatTemplate 节点的 map[string]any
- 将 User Prompt 转换成 RedisRetriever 的输入 query
-
retriever/redis: 根据用户 Query 从 Redis Vector Database 根据语义相关性,召回和 Query 相关的上下文,以 schema.Document List 的形式返回。
-
chatTemplate: 通过字符串字面量构建 Prompt 模板,支持 文本替换符 和 消息替换符,将输入的任意map[string]any,转换成可直接输入给模型的 Message List。
-
reAct: 自动决策下一步的 Action,直至能够产生最终的回答。
-
model/ark: Ark 平台提供的能够进行对话文本补全的大模型。作为 ReAct Agent 的依赖注入。
-
DuckDuckGo: 互联网搜索工具。
Main
简单实现一个交互式问答系统,启动RAG系统并将用户输入内容传入;
go
func main() {
// 定义命令行参数
useRedis := flag.Bool("redis", true, "是否使用Redis进行检索增强")
topK := flag.Int("topk", 3, "检索的文档数量")
flag.Parse()
// 构建RAG系统
ctx := context.Background()
ragSystem, err := rag.BuildRAG(ctx, *useRedis, *topK)
if err != nil {
fmt.Fprintf(os.Stderr, "构建RAG系统失败: %v\n", err)
os.Exit(1)
}
// 显示启动信息
if *useRedis {
fmt.Println("启动RAG系统 (使用Redis检索)")
} else {
fmt.Println("启动RAG系统 (不使用检索)")
}
fmt.Println("输入问题或输入'exit'退出")
// 创建输入扫描器
scanner := bufio.NewScanner(os.Stdin)
// 主循环
for {
fmt.Print("\n问题> ")
// 读取用户输入
if !scanner.Scan() {
break
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
// 检查退出命令
if strings.ToLower(input) == "exit" {
break
}
// 处理问题
answer, err := ragSystem.Answer(ctx, input)
if err != nil {
fmt.Fprintf(os.Stderr, "处理问题时出错: %v\n", err)
continue
}
// 显示回答
fmt.Println("\n回答:")
fmt.Println(answer)
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "读取输入时出错: %v\n", err)
}
}
实践总结
开发效率
基于Eino框架可快速构建知识处理流水线,显著降低实现复杂度。
效果验证
• 成功实现知识精准定位(内容+时间节点)
• 有效解决中医知识结构化检索需求
• 待优化:上下文对话能力(后续版本迭代)