技术

Python实践 下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论及实践 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Python一些比较有意思的库 LLM部分技术源码学习 LangChain源码学习 通用分布式计算引擎Ray Python并发 go依赖注入 go collection gc的基本原理 golang性能分析及优化 数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器以及defer实现 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go堆内存分配 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 递归、回溯、动态规划 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

大模型推理服务框架 模型服务化(未完成) 大模型RHLF 大模型训练 大模型推理 从Attention到Transformer k8s设备管理 LLM工具栈 ddd从理念到代码 如何应用LLM 小鼠如何驾驭大象(LLM)? 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 AutoML和AutoDL 特征平台 实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练和推理 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从混部到统一调度 从RNN到Attention pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Admission Controller 与 Admission Webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 概率论 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 用户认证问题 资源的分配与回收——池 消息/任务队列

标签

k8s设备管理 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 helm 从混部到统一调度 volcano特性源码分析 kubebuilder 学习 client-go学习 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 openkruise学习 基于Kubernetes选主及应用 Admission Controller 与 Admission Webhook k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? Kubernetes events学习及应用 CRI 资源调度泛谈 如何学习Kubernetes 以应用为中心 kubernetes operator kubernetes扩缩容 serverless 泛谈 什么是云原生 自定义CNI IPAM docker和k8s安全访问机制 Kubernetes监控 Kubernetes 控制器模型 Kubernetes资源调度——scheduler Kubernetes类型系统 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 Kubernetes源码分析——从kubectl开始 kubernetes yaml配置 CNI——容器网络是如何打通的 当我在说PaaS时,我在说什么 《深入剖析kubernetes》笔记 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件

LLM外挂知识库

2023年09月25日

简介

RAG(Retrieval-Augmented Generation),LLM相比传统算法最重要的就是zero shot能力,最重要的是从海量的文档中找到和问题相关的片段。找到后使用LLM生成回答其实很鸡肋,尤其是针对技术问题的回答,不需要LLM 去造答案,LLM 就帮我找到相关文档就行了。RAG 的主要目标是通过利用检索的优势来增强生成过程,使 NLG 模型能够生成更合理且适合上下文的响应。通过将检索中的相关信息纳入生成过程,RAG 旨在提高生成内容的准确性、连贯性和信息量。With RAG, we augment the knowledge base of an LLM by inserting relevant context into the prompt and relying upon the in context learning abilities of LLMs to produce better output by using this context.

引入外部知识 的几个示例

大语言模型的原理,就是利用训练样本里面出现的文本的前后关系,通过前面的文本对接下来出现的文本进行概率预测。如果类似的前后文本出现得越多,那么这个概率在训练过程里会收敛到少数正确答案上,回答就准确。如果这样的文本很少,那么训练过程里就会有一定的随机性,对应的答案就容易似是而非。

LLM 擅长于一般的语言理解与推理,而不是某个具体的知识点。如何为ChatGPT/LLM大语言模型添加额外知识?

  1. 通过fine-tuning来和新知识及私有数据进行对话,OpenAI 模型微调的过程,并不复杂。你只需要把数据提供给 OpenAI 就好了,对应的整个微调的过程是在云端的“黑盒子”里进行的。需要提供的数据格式是一个文本文件,每一行都是一个 Prompt,以及对应这个 Prompt 的 Completion 接口会生成的内容。
     {"prompt": "<prompt text>", "completion": "<ideal generated text>"}
     {"prompt": "<prompt text>", "completion": "<ideal generated text>"}
     {"prompt": "<prompt text>", "completion": "<ideal generated text>"}
     ...
    

    有了准备好的数据,我们只要再通过 subprocess 调用 OpenAI 的命令行工具,来提交微调的指令就可以了。

     subprocess.run('openai api fine_tunes.create --training_file data/prepared_data_prepared.jsonl --model curie --suffix "ultraman"'.split())
    

    微调模型还有一个能力,不断收集新的数据,不断在前一个微调模型的基础之上继续微调我们的模型。

  2. 通过word embeddings + pinecone数据库来搭建自己私有知识库。 chatgpt预训练完成后,会生成一个embeddings向量字典,比如我们可以将我们的私有知识库各个章节通过openai的相关api获取到对应的embeddings,然后将这些embeddings保存到向量数据库(比如 Facebook 开源的 Faiss库、Pinecone 和 Weaviate),当用户要对某个领域后者问题进行语义查询时,则将用户的输入同样通过openai的相关api来获取相应的embeddings向量,然后再和向量数据库pinecone中的我们的私有知识库类型做语义相似度查询,然后返回给用户。PS: 内容向量化
    1. 比如判断某一段文本 是积极还是消极,向chatgpt 查询目标文本的向量,然后计算其与“积极” “消极” 两个词 embedding 向量的“距离”,谁更近,说明这段文本更偏向于积极或消极。
    2. 过几天openAI的模型版本升级了,这些保存的embedding会失效吗?特定模型也有带日期的快照版本,选取那些快照版本就好了。
    3. 向量是基于大模型生成的,因此对两段文本向量相似度计算必须基于同一个模型,不同的模型算出来的向量之间是没有任何关系的,甚至连维数都不一样。不过你可以把基于A 模型来算向量相似度进行检索把文本找出来,然后把找到的文本喂给B模型来回答问题。
  3. 通过langchain这个chatgpt编程框架来给chatgpt赋能。 langchain可以将不同的工具模块和chatgpt给链接(chain)起来。
  4. chatgpt 插件,比如有一个提供酒旅租车信息的插件

比如针对问题:鲁迅先生去日本学习医学的老师是谁。因为 LLM(大语言模型)对上下文长度的限制,你不能将《藤野先生》整体作为提示语然后问“鲁迅在日本的医学老师是谁?”。 先通过搜索的方式,找到和询问的问题最相关的语料。可以用传统的基于关键词搜索的技术。也可以先分块存到向量数据库中(向量和文本块之间的关系),使用 Embedding 的相似度进行语义搜索的技术。然后,我们将和问题语义最接近的前几条内容,作为提示语的一部分给到 AI(使用检索结果作为 LLM 的 Prompt)。然后请 AI 参考这些内容,再来回答这个问题。

这也是利用大语言模型的一个常见模式(这个模式实在太过常用了,所以有人为它写了一个开源 Python 包,叫做 llama-index)。因为大语言模型其实内含了两种能力。PS:有点像推荐的粗排和精排,纯向量化的召回在一些Benchmark上表现还不如关键字搜索。

  1. 海量的语料中,本身已经包含了的知识信息。比如,我们前面问 AI 鱼香肉丝的做法,它能回答上来就是因为语料里已经有了充足的相关知识。我们一般称之为“世界知识”。
  2. 根据你输入的内容,理解和推理的能力。这个能力,不需要训练语料里有一样的内容。而是大语言模型本身有“思维能力”,能够进行阅读理解。这个过程里,“知识”不是模型本身提供的,而是我们找出来,临时提供给模型的。如果不提供这个上下文,再问一次模型相同的问题,它还是答不上来的。

Embedding 生成向量使用的模型 跟最后prompt 调用的模型可以不是同一个,因此有的向量数据库也包含Segment 和 Embedding,通过自然语言就能直接和向量数据库交互。也就是说,我们可以直接把文档扔给数据库,大段文本的切分,以及文本向量化,向量数据库 会帮我们处理。我们也可以直接把问题扔给数据库,请他来查询相似度较高的文本块,问题向量化以及检索的细节,向量数据库会帮我们处理。

基于大语言模型构建知识问答系统

  1. 传统搜索系统基于关键字匹配,在面向:游戏攻略、技术图谱、知识库等业务场景时,缺少对用户问题理解和答案二次处理能力。
  2. 领域知识不在预训练的数据集中,比如:
    1. 较新的内容。同一个知识点不断变更:修改、删除、添加。如何反馈当前最新的最全面的知识。比如对于 ChatGpt 而言,训练数据全部来自于 2021.09 之前。
    2. 未公开的、未联网的内容。
  3. 基于 LLM 搭建问答系统的解决方案有以下几种:
    1. Fine-Tuning
    2. 基于 Prompt Engineering,比如 Few-Shot方式。将特定领域的知识作为输入消息提供给模型。类似于短期记忆,容量有限但是清晰。举个例子给 ChatGPT 发送请求,将特定的知识放在请求中,让 ChatGPT 对消息中蕴含的知识进行分析,并返回处理结果。
    3. 与普通搜索结合,使用基础模型对搜索结果加工。在做问答时的方式就是把 query 转换成向量,然后在文档向量库中做相似度搜索。
    4. 用户输入query之后,首先先从知识库搜索到结果,然后基于搜索到的结果进行解析构造,生成新的prompt,然后调用LLM,LLM根据输入的prompt自行进行知识库的检索与plugins的调用

多文档问答难点

上下文注入,即不修改LLM,专注于提示本身,并将相关上下文注入到提示中,让模型参考这个提示进行作答,但是其问题在于如何为提示提供正确的信息。目前我们所能看到的就是相关性召回,其有个假设,即问题的答案在召回的最相似的文档里。

单文档问答,即类似ChatPDF的需求,ChatPDF的目标是尽可能全面地总结出整篇文档的信息。这个需求以目前ChatGPT可接受的16K上下文而言,不少文档可以直接丢进去问答,不需要使用召回工具先做知识检索。PS:慢慢随着技术进步不成问题。

如果我们看一下RAG系统中的流程链:

  1. 将文本分块(chunking)并生成块(chunk)的Embedding
  2. 通过语义相似度搜索检索数据块
  3. 根据top-k块的文本生成响应 我们会看到所有的过程都是有信息损失的,这意味着不能保证所有的信息都能保存在结果中。如果我们把所有的限制放在一起,重新考虑一些公司即将推出的基于RAG的企业搜索,我真的很好奇它们能比传统的全文搜索引擎好多少。

RAG探索之路的血泪史及曙光 值得细读。

深入剖析开源大模型+Langchain框架智能问答系统性能下降原因常规方案中对智能问答系统准确率影响最大的几个因素如下:

  1. embedding 在 Retrieval 任务中 TopK 的准确率 (受 embedding 模型自身能力、Retrieval 算法、K等三个因素影响)
  2. K 的大小,K 原则越大越好,但是 LLM 的 tokens 限制导致 K 由不能太大。(上述 1、2 的都是为了从数据库海量 chunks 中选择出包含正确答案的 chunks)
  3. LLM 自身阅读理解与总结推理能力。

分级

如何让LLM简要、准确回答细粒度知识?如何让LLM回答出全面的粗粒度(跨段落)知识?QA 的难度主要在于回答这个问题所依赖的信息在长文档中的分布情况,具体可以大致分为下面三种情况:

  1. 相关信息可以出现在不超过一个固定上下文长度(512)限制的 block 以内
  2. 相关信息出现在多个 block 里,出现的总数不超过 5-10 个,最好是能在大模型支持的有效长度内。
  3. 需要考虑多个片段,并综合结合上下文信息甚至全部内容才能得到结果。 PS: 可能要知识图谱帮一点忙。

受限于数据集的大小和规模以及问题的难度,目前主要研究偏向于 L1 和 L2 。能比较好的整合和回答 L1 类问题,L2 类问题也有比较不错的结果,但对于 L3 类问题,如果所涉及到的片段长度过长,还是无法做到有效的回答。了解RAG能做什么和不能做什么,可以让我们为RAG寻找最适合的领域,避免强行进入错误的地方

文档处理

如何将文档拆分为文本片段。主要有两种,一种就是基于策略规则,另外一种是基于算法模型。

  1. 如何保证文档切片不会造成相关内容的丢失?一般而言,文本分割如果按照字符长度进行分割,这是最简单的方式,但会带来很多问题。例如,如果文本是一段代码,一个函数被分割到两段之后就成了没有意义的字符。因此,我们也通常会使用特定的分隔符进行切分,如句号,换行符,问号等。可以使用专门的模型去切分文本,尽量保证一个chunk的语义是完整的,且长度不会超过限制。
  2. 文档切片的大小如何控制? 太小则 容易造成信息丢失,太大则不利于向量检索命中。此外还要考虑LLM context 长度的限制,GPT-3.5-turbo 支持的上下文窗口为 4096 个令牌,这意味着输入令牌和生成的输出令牌的总和不能超过 4096,否则会出错。为了保证不超过这个限制,我们可以预留约 2000 个令牌作为输入提示,留下约 2000 个令牌作为返回的消息。这样,如果你提取出了五个相关信息块,那么每个片的大小不应超过 400 个令牌。最详细的文本分块(Chunking)方法——可以直接影响基于LLM应用效果
  3. 基于算法模型,主要是使用类似 BERT 结构的语义段落分割模型,能够较好的对段落进行切割,并获取尽量完整的上下文语义。需要微调,上手难度高,而且切分出的段落有可能大于向量模型所支持的长度,这样就还需要进行切分。

经常遇到一些复杂文档的情况,这些文档中可能有表格,有图片,有单双栏等情况。尤其是对于一些扫描版本的文档时候,则需要将文档转换成可以编辑的文档,这就变成了版面还原的问题。具体的,可以利用ppstructrue进行文档版面分析,在具体实现路线上,图像首先经过版面分析模型,将图像划分为文本、表格、图像等不同区域,随后对这些区域分别进行识别。

  1. pdf pdf解析关键问题
  2. 表格 Langchain下利用MutiVector Retriever更好支持RAG架构下表格文字混合内容问答 完全指南——使用python提取PDF中的文本信息(包括表格和图片OCR)

利用 LLM 对知识语料进行增强和扩充。对一篇文档/chunk生成知识点、问题、短摘要,当根据query 进行匹配时,可能先匹配到知识点、问题、短摘要,再找到原始chunk。MultiVectorRetriever/ParentDocumentRetriever 。

文档切片最好是按照语义切割。

  1. 将包含主从关系的段落合并,保证每一段在说同一件事情。
    1. 可以利用NLP的篇章分析(discourse parsing)工具提取出段落之间的主要关系,或利用BERT等模型来实现语义分割。BERT等模型在预训练的时候采用了NSP(next sentence prediction)的训练任务,因此BERT完全可以判断两个句子(段落)是否具有语义衔接关系。这里我们可以设置相似度阈值t,从前往后依次判断相邻两个段落的相似度分数是否大于t,如果大于则合并,否则断开。
  2. 合并后的段落提取关键信息。
    1. 利用NLP中的成分句法分析(constituency parsing)工具和命名实体识别(NER)工具提取,前者可以提取核心部分(名词短语、动词短语……),后者可以提取重要实体(货币名、人名、企业名……)。
    2. 可以用语义角色标注(Semantic Role Labeling)来分析句子的谓词论元结构,提取“谁对谁做了什么”的信息作为关键信息。
    3. NLP的研究中本来就有关键词提取工作(Keyphrase Extraction)。一个工具是 HanLP ,中文效果好,但是付费,免费版调用次数有限。还有一个开源工具是KeyBERT,英文效果好,但是中文效果差。
    4. 垂直领域建议的方法。以上两个方法在垂直领域都有准确度低的缺陷,垂直领域可以仿照ChatLaw的做法,即:训练一个生成关键词的模型。ChatLaw就是训练了一个KeyLLM。
  3. 对关键信息做embedding
  4. 问题查询时,先query 向量检索到关键信息,再由关键信息找到段落文本

LangChain+LLM大模型问答能力搭建与思考一般通用分段方式,是在固定max_length的基础上,对出现。/;/?/....../\n等等地方进行切割。但这种方式显然比较武断,面对特殊情况需要进一步优化。比如1.xxx, 2.xxx, ..., 10.xxx超长内容的情况,直接按这种方法切割就会导致潜在的内容遗漏。对于这种候选语料”内聚性“很强的情况,容易想到,我们要么在切割语料时动手脚(不把连续数字符号所引领的多段文本切开);要么在切割时照常切割、但在召回时动手脚(若命中了带连续数字符号的这种长文本的开头,那么就一并把后面连续数字符号引领的多段文本一起召回)。笔者目前只想到了这两种方法且还没具体做实验,只是凭空想来,前者方案有较明显瑕疵,因为这样会

  1. 相对于更短文段而言,长文段的语义更丰富,每个单独的语义点更容易被淹没,所以在有明确语义query的召回下这种长文段可能会吃亏;
  2. 长文段一旦被召回,只要不是针对整段文本的提问,那么也是引入了更多的噪声(不过鉴于LLM的能力,这可能也无伤大雅,就是费点显存or接口费用了) 但后者就显得更灵活些,不过确实也不够聪明。暂时没想到其他办法,有好想法的人可以来交流一下~此外,有研究表明,长文本作为输入LLM输入时,LLM倾向于更关注长文本的开头、结尾处,然而中间部分的语义可能会被忽略。

对于大模型RAG技术的一些思考针对各种类型的文档,分别进行了很多定制化的措施,用于完整的提取文档内容。

  1. Doc类文档还是比较好处理的,直接解析其实就能得到文本到底是什么元素,比如标题、表格、段落等等。这部分直接将文本段及其对应的属性存储下来,用于后续切分的依据。
  2. PDF类文档的难点在于,如何完整恢复图片、表格、标题、段落等内容,形成一个文字版的文档。这里使用了多个开源模型进行协同分析,例如版面分析使用了百度的PP-StructureV2,能够对Text、Title、Figure、Figure caption、Table、Table caption、Header、Footer、Reference、Equation10类区域进行检测,统一了OCR和文本属性分类两个任务。
  3. PPT的难点在于,如何对PPT中大量的流程图,架构图进行提取。因为这些图多以形状元素在PPT中呈现,如果光提取文字,大量潜藏的信息就完全丢失了。于是这里只能先将PPT转换成PDF形式,然后用上述处理PDF的方式来进行解析。
  4. 当然,这里还没有解决出图片信息如何还原的问题。大量的文档使用了图文混排的形式,例如上述的PPT文件,转换成PDF后,仅仅是能够识别出这一块是一幅图片,对于图片,直接转换成向量,不利于后续的检索。所以我们只能通过一个较为昂贵的方案,即部署了一个多模态模型,通过prompt来对文档中的图片进行关键信息提取,形成一段摘要描述,作为文档图片的索引。

对于每个文档,实际上元素的组织形式是树状形式。例如一个文档包含多个标题,每个标题又包括多个小标题,每个小标题包括一段文本等等。我们只需要根据元素之间的关系,通过遍历这颗文档树,就能取到各个较为完整的语义段落,以及其对应的标题。有些完整语义段落可能较长,于是我们对每一个语义段落,再通过大模型进行摘要。这样文档就形成了一个结构化的表达形式: |id| text| summary| source| type| image_source| |–|–|–|–|–|–| |1| 文本原始段落| 文本摘要| 来源文件| 文本元素类别(主要用于区分图片和文本)| 图片存储位置(在回答中返回这个位置,前端进行渲染)|

文档召回

文档召回过程中如何保证召回内容跟问题是相关的? 或者说,如何尽可能减少无关信息? 召回数据相关性的影响方面很多,既包括文档的切分,也包括文档query输入的清晰度,因此现在也出现了从多query、多召回策略以及排序修正等多个方案。

召回的输入(不一定是用户原输入)?召回的输出(不一定是Vectordb原输出)?召回的方式(不一定直接查vectordb,微调embedding)?

优化召回的输入/如何使检索对用户输入的变异性稳健/优化发送给检索器的搜索查询?

  1. 查询转换
    1. 将用户问题采用多个不同的视角去提问,借助子问题检索,然后 LLM 会得出最终结果。大多数人在问问题的过程中,如果不懂 prompt 工程,往往不专业,要么问题过于简单化,要么有歧义,意图不明显。那么向量搜索也是不准确的,导致LLM回答的效果不好。所以需要 LLM 进行问题的修正和多方位解读。MultiQueryRetriever 。使用RAG-Fusion和RRF让RAG在意图搜索方面更进一步
    2. 结合历史对话的重新表述,在进行多轮对话时,用户的提问中的某个词可能会指代上文中的部分信息,因此可以将历史信息和用户提问一并交给LLM重新表述。
    3. 假设文档嵌入(HyDE),核心思想是接收用户提问后,先让LLM在没有外部知识的情况下生成一个假设性的回复。然后,将这个假设性回复和原始查询一起用于向量检索。假设回复可能包含虚假信息,但蕴含着LLM认为相关的信息和文档模式,有助于在知识库中寻找类似的文档。
    4. 退后提示(Step Back Prompting):如果原始查询太复杂或返回的信息太广泛,我们可以选择生成一个抽象层次更高的“退后”问题,与原始问题一起用于检索,以增加返回结果的数量。例如,原问题是“桌子君在特定时期去了哪所学校”,而退后问题可能是关于他的“教育历史”。这种更高层次的问题可能更容易找到答案。
    5. 关键信息抽取:关键信息抽取是通过自然语言处理技术从用户提问中提取关键词或短语,以便更准确地检索信息。基于LLM的关键词抽取 再看RAG在真实金融文档问答场景的实践方案:SMP2023 金融大模型挑战赛的两种代表实现思路
      1. 采用In-Context Learning的关键词抽取方案。通过构造history,模拟多轮对话的方式进行,让模型能稳定输出json,对于异常json通过调整temperature=1加上retry多次,使其更稳定输出。PS:有点意思,带有历史记录的 icl
      2. 有监督方案
  2. Finetune 向量模型。embedding 模型 可能从未见过你文档的内容,也许你的文档的相似词也没有经过训练。在一些专业领域,通用的向量模型可能无法很好的理解一些专有词汇,所以不能保证召回的内容就非常准确,不准确则导致LLM回答容易产生幻觉(简而言之就是胡说八道)。可以通过 Prompt 暗示 LLM 可能没有相关信息,则会大大减少 LLM 幻觉的问题,实现更好的拒答。
    1. 大模型应用中大部分人真正需要去关心的核心——Embedding
    2. 分享Embedding 模型微调的实现 ,此外,原则上:embedding 所得向量长度越长越好,过长的向量也会造成 embedding 模型在训练中越难收敛。
    3. 手工微调embedding模型,让RAG应用检索能力更强 未细读
    4. 如何提高LLMs的文本表征(Text Embedding)能力?
    5. 大模型落地技术总结:大模型幻觉的起因、评估及落地场景下基于知识图谱的缓解策略探索
  3. 许多向量存储支持了对元数据的操作。LangChain 的 Document 对象中有个 2 个属性,分别是page_content和metadata,metadata就是元数据,我们可以使用metadata属性来过滤掉不符合条件的Document。元数据过滤的方法虽然有用,但需要我们手动来指定过滤条件,我们更希望让 LLM 帮我们自动过滤掉不符合条件的文档。SelfQueryRetriever
  4. 增加追问机制。这里是通过Prompt就可以实现的功能,只要在Prompt中加入“如果无法从背景知识回答用户的问题,则根据背景知识内容,对用户进行追问,问题限制在3个以内”。这个机制并没有什么技术含量,主要依靠大模型的能力。不过大大改善了用户体验,用户在多轮引导中逐步明确了自己的问题,从而能够得到合适的答案。

     def ask_questions(model, context, query, max_follow_ups=3):
     follow_ups = 0
     while follow_ups < max_follow_ups:
         if model.can_answer(query, context):
             return model.answer(query, context)
         else:
             follow_up_query = generate_follow_up_query(query, context)
             query = follow_up_query
             follow_ups += 1
     return "I'm sorry, I couldn't find an answer to your question."
     def generate_follow_up_query(current_query, context):
         # 这里可以根据上下文和当前查询生成一个追问
         # 例如,询问用户是否需要更具体的信息
         return "Can you please provide more details or clarify your question?"
     # 示例
     context = "The capital of France is Paris."
     query = "What is the capital?"
     answer = ask_questions(model, context, query)
    
     print(answer)
    

在专业的垂直领域,待检索的文档往往都是非常专业的表述,而用户的问题往往是非常不专业的白话表达。所以直接拿用户的query去检索,召回的效果就会比较差。Keyword LLM就是解决这其中GAP的。例如在ChatDoctor中会先让大模型基于用户的query生成一系列的关键词,然后再用关键词去知识库中做检索。ChatDoctor是直接用In-Context Learning的方式进行关键词的生成。我们也可以对大模型在这个任务上进行微调,训练一个专门根据用户问题生成关键词的大模型。这就是ChatLaw中的方案。

多种召回方式

  1. 同时使用了es 和向量召回。 EnsembleRetriever。 |倒排召回|向量召回|知识图谱召回| |—|—|—| |检索速度更快||| |精确匹配能力强,比如一些专有名词、人名、产品名、缩写、id、低频词等|考虑语义相似性,更加智能|实体-关系检索| |没有语义信息,对”一词多义”现象解决的不好|不理解专有词汇,容易出现语义相似但主题不相似的情况|
  2. 对于两个不同检索方式(关键词检索、矢量检索)得到的召回数据,分数范围不一致,一个比较直接的想法就是对分数做归一化,然后把归一化后的数据做权重加和,得到最终分数。
    1. RRF,比单独的lexical search和单独的semantic search的效果要好,RRF存在两个问题:
      1. RRF只是对召回的topk数据的顺序进行近似排序计算,并有真正的对数据顺序计算。
      2. RRF只关注topk数据的位置,忽略了真实分数以及分布信息。
    2. 用rerank 对query + chunk 相关性进行打分。
  3. 合并检索时首先需要将文档按特定的层次结构进行切割,比如按两层结构进行切割即首先将文档按块大小(chunk_size)为1024进行切割,切割成若干个大文档块,然后每个大文档块(chunk_size=1024)再被切分成4个块大小为512的子文档块,那么这些子文档块就是所谓的叶子节点,而子文档块所属的大文档块就是所谓的父节点,而在检索时只拿叶子节点和问题进行匹配,当某个父节点下的多数叶子节点都与问题匹配上则将该父节点作为context返回给LLM。
    1. 在知识问答系统中,检索成本是一个不容忽视的问题。随着知识库的增长,可能需要遍历大量文档,导致检索速度缓慢,在检索大量信息时,如何确保检索结果的相关性和准确性也是一个挑战。层次检索是一种优化策略,它通过构建知识库的层次结构来减少检索范围,从而提高检索效率。实践方法:分析知识库的结构,建立层次索引;从顶层开始检索,逐步向下深入到具体的文档或信息片段;在每个层次上应用剪枝策略,只保留最相关的部分进行进一步检索。
  4. 多向量检索同样会给一个知识文档转化成多个向量存入数据库,不同的是,这些向量不仅包括文档在不同大小下的分块,还可以包括该文档的摘要,用户可能提出的问题等等有助于检索的信息。在使用多向量查询的情况下,每个向量可能代表了文档的不同方面,使得系统能够更全面地考虑文档内容,并在回答复杂或多方面的查询时提供更精确的结果。例如,如果查询与文档的某个具体部分或摘要更相关,那么相应的向量就可以帮助提高这部分内容的检索排名。
    1. 多向量检索也适用于包含文本和表格混合的半结构化文档。在这些情况下,可以提取每个表,生成适合检索的表摘要,但将原始表返回到LLM进行答案合成。

基于ElasticSearch的混合检索实战&原理分析

优化召回输出/Post-Processing,如何合并我检索到的文件?因为Content Windows的大小有限。

  1. 如果一篇文档与查询非常相关,但与已经呈现给用户的文档非常相似,那么这篇文档的边际收益可能就不大。MMR 是一种广泛应用于信息检索和自然语言处理领域的算法。MMR 的主要目标是在文档排序和摘要生成等任务中平衡相关性和新颖性。换句话说,MMR 旨在为用户提供既相关又包含新信息的结果。
  2. 训练一个排序模型的方式对Topk 进行进一步相关性打分。建议以上游打分 Topk 作为训练数据,特别是结合真实的用户反馈数据。
  3. 文档重排:LLM 对位置是相对比较敏感的,得分好的放在首或尾,LLM会重点关注。LongContextReorder 。Rerank——RAG中百尺竿头更进一步的神器,从原理到解决方案

上述一些方法是以时间换效果,并且query改写成多个,多个容易漂移,而且选项太多对于排序也有影响,这个提分不明显,是优化阶段要做的事,重点要放在文本切割上。因此,如何增强大模型自身的知识,或许才是正道?但这明显十分漫长。

对LLM的要求

对于 RAG 来说,LLM 最基础也是最重要的能力其实包含:

  1. 摘要能力;
  2. 可控性:既 LLM 是否听话,是否会不按照提示要求的内容自由发挥产生幻觉;
  3. 翻译能力,这对于跨语言 RAG 是必备的。

优化

  1. 缓存
    1. LangChain 提供了 CacheBackedEmbeddings , 可以提高 embedings 的二次加载和解析的效率,首次正常速度,后续有一个 3倍效率的提升。
  2. 多模态。如何从非文本内容中提取有用的信息,并将其转化为可检索的格式?如何将提取的多模态信息与文本信息融合,以便进行统一的检索和回答?如何构建一个能够索引多模态内容的知识库。
  3. RAG与Long-Context之争—没必要争
  4. RAG 当前最大的问题是什么?笔者觉得是R 和 G 的割离。如果我们能在检索之前对知识点进行学习理解,去指导检索器应该从哪方面去查找,是不是能解决当下的很多问题?比如:花里胡哨的各路检索策略?、花里胡哨的各路query扩展和排序策略?、lost middle(因为查询到的都是相关的)?比如:法律场景笔者瞎编一个案例,张三走在路上,突然大叫一声,把旁边的老大爷吓的晕倒在地,请问这种情况张三犯法嘛,犯了什么法?当前的检索逻辑能召回什么法条?那如果要改进,该怎么办?笔者思考的是,在检索前让模型先分析案例,提出可能的法条方向,然后告诉检索器进行检索,是不是比瞎蒙的情况要更好?那么这个检索前的模型(暂且称之为检索指导模型)应该做到哪些点?应该对知识点有充分的理解能力。

Self-RAG

Self-RAG 框架:更精准的信息检索与生成 未读 也看引入自我反思的大模型RAG检索增强生成框架:SELF-RAG的数据构造及基本实现思路 未读

Self-RAG 及其实现 Self-RAG 主要步骤概括如下:

  1. 判断是否需要额外检索事实性信息(retrieve on demand),仅当有需要时才召回
  2. 平行处理每个片段:生产prompt+一个片段的生成结果。PS: query + chunk ==> 带有反思标记(relevant/supported/partital/inrelevant)的chunk
  3. 使用反思字段,检查输出是否相关,选择最符合需要的片段;
  4. 再重复检索
  5. 生成结果会引用相关片段,以及输出结果是否符合该片段,便于查证事实。

Self-RAG 的一个重要创新是 Reflection tokens (反思字符):通过生成反思字符这一特殊标记来检查输出。这些字符会分为 Retrieve 和 Critique 两种类型,会标示:检查是否有检索的必要,完成检索后检查输出的相关性、完整性、检索片段是否支持输出的观点。模型会基于原有词库和反思字段来生成下一个 token。

要不要微调

如何权衡对需求prompt、continue pretrain 、sft?实时性强的知识以外挂知识库为主,专业的较为稳定的领域知识去微调模型:continue pretain 更多微调领域知识(比如知道感冒是什么),sft 更多是微调领域指令(比如能听懂“给我开个药方”,“下游任务”)。

一般指令数据集都是手工构造的,

效果评价

如何评估 RAG 应用的质量?最典型的方法论和评估工具都在这里了

  1. 评估指标。标准的 RAG 流程就是用户提出 Query 问题,RAG 应用去召回 Context,然后 LLM 将 Context 组装,生成满足 Query 的 Response 回答。那么在这里出现的三元组:—— Query、Context 和 Response 就是 RAG 整个过程中最重要的三元组,它们之间两两相互牵制。我们可以通过检测三元组之间两两元素的相关度,来评估这个 RAG 应用的效果

无需 ground-truth 也能做评估 1. Context Relevance: 衡量召回的 Context 能够支持 Query 的程度。如果该得分低,反应出了召回了太多与Query 问题无关的内容,这些错误的召回知识会对 LLM 的最终回答造成一定影响。三元指标其中的某个可能还有具体的一些细分,比如 Ragas中就将 Context Relevance 这一步又分为Context Precision、Context Relevancy、Context Recall。 2. Groundedness: 衡量 LLM 的 Response 遵从召回的 Context 的程度。如果该得分低,反应出了 LLM 的回答不遵从召回的知识,那么回答出现幻觉的可能就越大。 3. Answer Relevance: 衡量最终的 Response 回答对 Query 提问的相关度。如果该得分低,反应出了可能答不对题。

  1. 基于 Ground-truth 的指标,当一个数据集已经标注好了ground-truth 回答,那就可以直接比较 RAG 应用的回答和 ground-truth 之间的相关性,来端到端地进行衡量。
  2. LLM 回答本身的指标,比如评估回答本身是否友好,是否有害,是否简洁等,它们参考来源的是 LLM 本身的一些评估指标。

具体怎么衡量这三个得分,也有不同的方式。最常见的就是基于目前最好的 LLM(如 GPT-4)做为一个裁判,给输入的这一对元组打分,判断它们的相似度。根据我们目前的观察,GPT-4 这在方面做得已经很好了。人类都有可能打错分,GPT-4 的表现和人类类似,误判的比例保持在很低就可以保证这种方法的有效性。因此,如何设计 prompt 同样重要,这就要用到一些高级的 prompt 工程技巧,比如 multi-shot,或 CoT(Chain-of-Thought)思维链技巧。在设计这些 prompt 时,有时还要考虑 LLM 的一些偏见,比如 LLM 常见的位置偏见:当 prompt 比较长时,LLM 容易注意到 prompt 里前面的一些内容,而忽略一些中间位置的内容。好在这些 prompt 的设计已经被设计和集成在 RAG 应用的评估工具中。

工程架构

值得一看的大模型RAG问答总括性梳理:模块化(Modular)RAG范式的定义、构成及机遇RAG的快速发展早已经突破传统的链式RAG范式,展示出了模块化的特点。