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 实践
实践结果
以倪海夏针灸专题为数据源,设计包含下针方法、对应视频节点的元数据结构作为索引知识库,实现症状到关键知识的精准检索
问题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文件,效果如下:
# 任脉与督脉##time (1-02:18:45)我们开始介绍任脉。**任**,女子妊也,女人会怀孕是靠任脉。十二经络开始介绍之前,要从任脉开始介绍。找穴道要从任、督二脉为基准,找到标准,就可以很快速的找到穴道。我最怕就是鸡同鸭讲,心里知道答案,结果穴道找错了。- **任脉**:是所有阴汇积的地方- **督脉**:是诸阳之会!全身的动能,能量,都在督脉上面> 我最常跟病人讲一句话就是,无论如何不要让别人碰你的脊椎骨,督脉不能碰,脊椎骨像龙骨一样,有人椎间盘凸出,有人去开刀,开完反而更坏。MAS 根本就是疫苗引起的。造成一开始脊椎就弯的,你想想,疫苗可以把所有阳气所在的督脉都打烂,这种人就不长寿。比如说我们叫天柱倾,脖子都歪过去,这样的人命在旦夕,一两天就走了。女人怀孕全靠任脉。督脉走在后面,诸阳之会,脊椎骨上面,任督二脉交会在鼻子人中这边。刚好嘴巴讲话,讲话时任督二脉是开的,你在听课的时候,舌头是顶着上颚。这是你的牙龈,这牙齿,侧面看哦,然后这是下牙,这是嘴唇。简单的概念,舌头顶到上颚的时候,任督二脉是通的。注意看乌龟,乌龟就是这样。所以,乌龟很长寿。我在讲课的时候,嘴巴会动,而你们在听课时,是把舌头顶上去的,脑筋会很清醒,阴电阳电相通。## 任脉穴位任脉有三八二十四个穴道,任脉三八起会阴。……
KnowledgeIndexing
使用Eino-dev插件构建向量知识库Workflow,提供常用的 AI 组件以及集成组件编排能力; 编排后改自己申请的模型配置即可;
编排后可以看到生成了代码文件,如图上串联;入口文件是orchestration.go
;输入文件路径返回ids;
Transformer
主要对输入知识库进行分割,构建向量数据的元数据;主要对标题和时间进行存储和索引;
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
内的函数,传入需要处理的文件路径即可;
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系统并将用户输入内容传入;
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框架可快速构建知识处理流水线,显著降低实现复杂度。
效果验证
• 成功实现知识精准定位(内容+时间节点)
• 有效解决中医知识结构化检索需求
• 待优化:上下文对话能力(后续版本迭代)