3、从langchain到rag

文章目录

  • 本文介绍
  • 向量和向量数据库
    • 向量
    • 向量数据库
  • 索引
  • 开始动手实现rag
    • 加载文档数据并建立索引
    • 将向量存放到向量数据库中
    • 检索生成
    • 构成一条链

本文介绍

从本节开始,有了上一节的langchain基础学习,接下来使用langchain实现一个rag应用,并稍微深入的讲解一下流程

向量和向量数据库

向量

在rag中,不得不提向量和向量数据库,在ai检索中多数采用特征,在文本中多数采用向量来表示文本。采用向量后,“语义”的匹配程度就转换成了向量之间的相似程度。计算向量相似度的算法有很多,比如,余弦相似度、内积、欧氏距离等等。

有了向量,当用户提出问题时,处理过程就变成了将问题转换为向量,然后计算向量之间的距离,找到与问题向量最接近的文档向量,从而实现“语义”的匹配。

OpenAI 提供了一个专门负责将文本转换成向量的 API——Embeddings。我们可以根据需要,选择自己部署模型,或是选择别人提供的服务。不同的 Embedding 模型之间的差异主要取决于训练样本,比如有的模型会在中文处理上表现得比较好。

向量数据库

在 RAG 系统中,我们要把数据存放到哪里呢?我们需要一个数据库,只不过,我们需要的既不是 Oracle、MySQL 这样的关系数据库,也不是 MongoDB、Redis 这样的 NoSQL 数据库。因为我们后续处理的都是向量,所以,我们需要的是向量数据库。

向量数据库与传统数据库有很大的差别,在使用方式上,传统数据库搜索信息倾向于精确匹配,而向量数据库的匹配则是语义上的接近。

在实现上二者也存在不小的差别,比如,由于向量本身通常维度会很多,如果按照传统数据库的方式直接进行存储,将会带来很多问题。向量数据库需要把向量数据作为一个完整的单元处理,底层存储结构也需要根据这个特点进行规划。另外,向量数据格式也相对单一,每个维度的数据往往都是固定的数据格式(浮点数、二进制整数等)。

索引

下面是一个常见的索引过程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个过程里面,我们会先对信息源做一次信息提取。信息源可能是各种文档,比如 Word 文档、PDF 文件,Web 页面,甚至是一些图片。从这些信息源中,我们把内容提取出来,也就是其中的文本。

接下来,我们会把这些文本进行拆分,将其拆分成更小的文本块。之所以要拆分,主要是原始的文本可能会比较大,这并不利于检索,还有一点重要原因是,我们前面说过,要把检索到的信息拼装到提示词里,过大的文本可能会造成提示词超过模型有限的上下文窗口。

再来,就是把文本块转换成向量,也就是得到 Embedding 的过程。前面我们说过,这个过程往往是通过训练好的模型来完成的。到这里,我们就把信息转换成了向量。最后一步,就是把得到的向量存储到向量数据库中,供后续的检索使用。

至此,我们对常见的 RAG 流程已经有了基本了解。但实际上,RAG 领域正处于一个快速发展的过程中,有很多相关技术也在不断地涌现:

  • 虽然采用向量搜索对于语义理解很有帮助,但一些人名、缩写、特定 ID 之类的信息,却是传统搜索的强项,有人提出混合搜索的概念,将二者结合起来;
  • 通过各种搜索方式,我们会得到很多的候选内容,但到底哪个与我们的问题更相关,有人引入了重排序(Rerank)模型,以此决定候选内容与查询问题的相关程度;
  • 除了在已有方向的努力,甚至还有人提出了 RAG 的新方向。我们前面讨论的流程前提条件是把原始信息转换成了向量,但这本质上还是基于文本的,更适合回答一些事实性问题。它无法理解更复杂的关系,比如,我的朋友里谁在 AI 领域里工作。所以,有人提出了基于知识图谱的 RAG,知识图谱是一种结构化的语义知识库,特别适合找出信息之间的关联。

开始动手实现rag

加载文档数据并建立索引

建索引,使用TextLoader从txt中加载数据

from langchain_community.document_loaders import TextLoaderloader = TextLoader("introduction.txt")
docs = loader.load()text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)
vectorstore.add_documents(splits)

这里的 TextLoader 属于 DocumentLoader。在 LangChain 中,有一个很重要的概念叫文档(Document),它包括文档的内容(page_content)以及相关的元数据(metadata)。所有原始信息都是文档,索引信息的第一步就是把这些文档加载进来,这就是 DocumentLoader 的作用。

除了这里用到的 TextLoader,LangChain 社区里已经实现了大量的 DocumentLoader,比如,从数据库里加载数据的 SQLDatabaseLoader,从亚马逊 S3 加载文件的 S3FileLoader。基本上,大部分我们需要的文档加载器都可以找到直接的实现。

拆分加载进来的文档是 TextSplitter 的主要职责。

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)

虽然都是文本,但怎样拆分还是有讲究的,拆分源代码和拆分普通文本,处理方法就是不一样的。LangChain 社区里同样实现了大量的 TextSplitter,我们可以根据自己的业务特点进行选择。我们这里使用了 RecursiveCharacterTextSplitter,它会根据常见的分隔符(比如换行符)递归地分割文档,直到把每个块拆分成适当的大小。

将向量存放到向量数据库中

做好基础的准备之后,就要把拆分的文档存放到向量数据库里了:

vectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)
vectorstore.add_documents(splits)

LangChain 支持了很多的向量数据库,它们都有一个统一的接口:VectorStore,在这个接口中包含了向量数据库的统一操作,比如添加、查询之类的。这个接口屏蔽了向量数据库的差异,在向量数据库并不为所有程序员熟知的情况下,给尝试不同的向量数据库留下了空间。各个具体实现负责实现这些接口,我们这里采用的实现是 Chroma。

在 Chroma 初始化的过程中,我们指定了 Embedding 函数,它负责把文本变成向量。这里我们采用了 OpenAI 的 Embeddings 实现,你完全可以根据自己的需要选择相应的实现,LangChain 社区同样提供了大量的实现,比如,你可以指定 Hugging Face 这个模型社区中的特定模型来做 Embedding。

到这里,我们就完成了索引的过程,看上去还是比较简单的。为了验证我们索引的结果,我们可以调用 similarity_search 检索向量数据库的数据:

vectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)
documents = vectorstore.similarity_search("专栏的作者是谁?")
print(documents)

这里用的 similarity_search 表示的是根据相似度进行搜索,还可以使用 max_marginal_relevance_search,它会采用 MMR(Maximal Marginal Relevance,最大边际相关性)算法。这个算法可以在保持结果相关性的同时,尽量选择与已选结果不相似的内容,以增加结果的多样性。

检索生成

现在,我们已经为我们 RAG 应用准备好了数据。接下来,就该正式地构建我们的 RAG 应用了。我在之前的聊天机器上做了一些修改,让它能够支持 RAG,代码如下:

from operator import itemgetter
from typing import List
import tiktoken
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage, SystemMessage, trim_messages
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import OpenAIEmbeddings
from langchain_openai.chat_models import ChatOpenAI
from langchain_chroma import Chromavectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)retriever = vectorstore.as_retriever(search_type="similarity")# Token 计算
def str_token_counter(text: str) -> int:enc = tiktoken.get_encoding("o200k_base")return len(enc.encode(text))def tiktoken_counter(messages: List[BaseMessage]) -> int:num_tokens = 3tokens_per_message = 3tokens_per_name = 1for msg in messages:if isinstance(msg, HumanMessage):role = "user"elif isinstance(msg, AIMessage):role = "assistant"elif isinstance(msg, ToolMessage):role = "tool"elif isinstance(msg, SystemMessage):role = "system"else:raise ValueError(f"Unsupported messages type {msg.__class__}")# 确保 msg.content 不是 Nonecontent = msg.content or ""  num_tokens += (tokens_per_message+ str_token_counter(role)+ str_token_counter(content))if msg.name:num_tokens += tokens_per_name + str_token_counter(msg.name)return num_tokens# 限制 token 长度
trimmer = trim_messages(max_tokens=4096,strategy="last",token_counter=tiktoken_counter,include_system=True,
)store = {}def get_session_history(session_id: str) -> BaseChatMessageHistory:if session_id not in store:store[session_id] = InMemoryChatMessageHistory()return store[session_id]model = ChatOpenAI()prompt = ChatPromptTemplate.from_messages([("system","""You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.Context: {context}""",),MessagesPlaceholder(variable_name="history"),("human", "{question}"),]
)def format_docs(docs):return "\n\n".join(doc.page_content for doc in docs)context = itemgetter("question") | retriever | format_docs
# first_step的内容有可能是这样的:
# {
#     "question": "什么是LangChain?",
#     "context": "LangChain 是一个用于构建基于 LLM 的应用的框架。\n它支持多种数据检索方式。"
# }
first_step = RunnablePassthrough.assign(context=context)from langchain_core.prompt_values import ChatPromptValue
def format_prompt_output(input):"""确保 `prompt` 生成的内容是 `List[BaseMessage]`"""# print("\n[DEBUG] Prompt 输出类型:", type(input))# print("[DEBUG] Prompt 输出内容:", input)# 如果 input 是 `ChatPromptValue`,提取 `.messages`if isinstance(input, ChatPromptValue):return input.messages  # 直接返回 `List[BaseMessage]`# 如果 input 已经是 `List[BaseMessage]`,直接返回if isinstance(input, list) and all(isinstance(msg, BaseMessage) for msg in input):return input# 其他情况报错raise TypeError(f"❌ format_prompt_output 传入了不正确的格式: {type(input)}")chain = first_step | prompt | format_prompt_output | trimmer  | modelwith_message_history = RunnableWithMessageHistory(chain,get_session_history=get_session_history,input_messages_key="question",history_messages_key="history",
)config = {"configurable": {"session_id": "dreamhead"}}while True:user_input = input("You:> ")if user_input.lower() == 'exit':breakif user_input.strip() == "":continue# 修正 stream 传参stream = with_message_history.stream({"question": user_input},  # 直接传入字符串config=config)# print(prompt)for chunk in stream:print(chunk.content, end='', flush=True)print()

示例代码运行如下:

这里是引用

为了进行检索,我们需要指定数据源,这里就是我们的向量数据库,其中存放着我们前面已经索引过的数据:

vectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)retriever = vectorstore.as_retriever(search_type="similarity")

为什么不直接使用向量数据库呢?因为 Retriever 并不只有向量数据库一种实现,比如,WikipediaRetriever 可以从 Wikipedia 上进行搜索。所以,一个 Retriever 接口就把具体的实现隔离开来。

回到向量数据库上,当我们调用 as_retriever 创建 Retriever 时,还传入了搜索类型(search_type),这里的搜索类型和前面讲到向量数据库的检索方式是一致的,这里我们传入的是 similarity,当然也可以传入 mmr。

文档检索出来,并不能直接就和我们的问题拼装到一起。这时,就轮到提示词登场了。下面是我们在代码里用到的提示词

You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
Context: {context}

在这段提示词里,我们告诉大模型,根据提供的上下文回答问题,不知道就说不知道。这是一个提示词模板,在提示词的最后是我们给出的上下文(Context)。这里上下文是根据问题检索出来的内容。

有了这个提示词,再加上聊天历史和我们的问题,就构成了一个完整的提示词模板:

prompt = ChatPromptTemplate.from_messages([("system","""You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.Context: {context}""",),MessagesPlaceholder(variable_name="history"),("human", "{question}"),]
)

构成一条链

接下来,就是把各个组件组装到一起,构成一条完整的链:

context = itemgetter("question") | retriever | format_docs
first_step = RunnablePassthrough.assign(context=context)
chain = first_step | prompt | trimmer | modelwith_message_history = RunnableWithMessageHistory(chain,get_session_history=get_session_history,input_messages_key="question",history_messages_key="history",
)

在这段代码里,我们首先构建了一个 context 变量,它也一条链。第一步是从传入参数中获取到 question 属性,也就是我们的问题,然后把它传给 retriever。retriever 会根据问题去做检索,对应到我们这里的实现,就是到向量数据库中检索,检索的结果是一个文档列表。

文档是 LangChain 应用内部的表示,要传给大模型,我们需要把它转成文本,这就是 format_docs 做的事情,它主要是把文档内容取出来拼接到一起:

def format_docs(docs):return "\n\n".join(doc.page_content for doc in docs)

这里补充几句实现细节。在 LangChain 代码里, | 运算符被用作不同组件之间的连接,其实现的关键就是大部分组件都实现了 Runnable 接口,在这个接口里实现了 orroror 表示这个对象出现在| 左边时的处理,相应的 ror 表示这个对象出现在右边时的处理。

Python 在处理 a | b 这个表达式时,它会先尝试找 a 的 or,如果找不到,它会尝试找 b 的 ror。所以,在 context 的处理中, 来自标准库的 itemgetter 虽然没有实现__or__,但 retriever 因为实现了 Runnable 接口,所以,它也实现了 ror。所以,这段代码才能组装出我们所需的链。

有了 context 变量,我们可以用它构建了另一个变量 first_step:

first_step = RunnablePassthrough.assign(context=context)

RunnablePassthrough.assign 这个函数就是在不改变链当前状态值的前提下,添加新的状态值。前面我们说了,这里赋给 context 变量的值是一个链,我们可以把它理解成一个函数,它会在运行期执行,其参数就是我们当前的状态值。现在你可以理解 itemgetter(“question”) 的参数是从哪来的了。这个函数的返回值会用来在当前的状态里添加一个叫 context 的变量,以便在后续使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/11262.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【自然语言处理(NLP)】基于Transformer架构的预训练语言模型:BERT 训练之数据集处理、训练代码实现

文章目录 介绍BERT 训练之数据集处理BERT 原理及模型代码实现数据集处理导包加载数据生成下一句预测任务的数据从段落中获取nsp数据生成遮蔽语言模型任务的数据从token中获取mlm数据将文本转换为预训练数据集创建Dataset加载WikiText-2数据集 BERT 训练代码实现导包加载数据构建…

41【文件名的编码规则】

我们在学习的过程中,写出数据或读取数据时需要考虑编码类型 火山采用:UTF-16 易语言采用:GBK php采用:UTF-8 那么我们写出的文件名应该是何种编码的?比如火山程序向本地写出一个“测试.txt”,理论上这个“测…

NLP深度学习 DAY4:Word2Vec详解:两种模式(CBOW与Skip-gram)

用稀疏向量表示文本,即所谓的词袋模型在 NLP 有着悠久的历史。正如上文中介绍的,早在 2001年就开始使用密集向量表示词或词嵌入。Mikolov等人在2013年提出的创新技术是通过去除隐藏层,逼近目标,进而使这些单词嵌入的训练更加高效。…

HarmonyOS简介:应用开发的机遇、挑战和趋势

问题 更多的智能设备并没有带来更好的全场景体验 连接步骤复杂数据难以互通生态无法共享能力难以协同 主要挑战 针对不同设备上的不同操作系统,重复开发,维护多套版本 多种语言栈,对人员技能要求高 多种开发框架,不同的编程…

Windows11 不依赖docker搭建 deepseek-R1 1.5B版本(附 Open WebUi搭建方式)

零、前言 过年这几天发现 DeepSeek 非常火,试用了一下发现确实不错。与豆包、kimi、perplexity 这些相比完全不是一个次元的存在,特别是用ta写文章的时候体验非常好。所以试着自己搭一个环境。 一、安装 Ollama和DeepSeek-R1 我的安装方式很简单&#xf…

解决whisper 本地运行时GPU 利用率不高的问题

我在windows 环境下本地运行whisper 模型,使用的是nivdia RTX4070 显卡,结果发现GPU 的利用率只有2% 。使用 import torch print(torch.cuda.is_available()) 返回TRUE。表示我的cuda 是可用的。 最后在github 的下列网页上找到了问题 极低的 GPU 利…

springCload快速入门

原作者:3. SpringCloud - 快速通关 前置知识: Java17及以上、MavenSpringBoot、SpringMVC、MyBatisLinux、Docker 1. 分布式基础 1.1. 微服务 微服务架构风格,就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自…

Gradle配置指南:深入解析settings.gradle.kts(Kotlin DSL版)

文章目录 Gradle配置指南:深入解析settings.gradle.kts(Kotlin DSL版)settings.gradle.kts 基础配置选项单项目配置多项目配置 高级配置选项插件管理(Plugin Management)基础配置模板案例:Android项目标准配…

C++ Primer 标准库类型string

欢迎阅读我的 【CPrimer】专栏 专栏简介:本专栏主要面向C初学者,解释C的一些基本概念和基础语言特性,涉及C标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级…

[EAI-028] Diffusion-VLA,能够进行多模态推理和机器人动作预测的VLA模型

Paper Card 论文标题:Diffusion-VLA: Scaling Robot Foundation Models via Unified Diffusion and Autoregression 论文作者:Junjie Wen, Minjie Zhu, Yichen Zhu, Zhibin Tang, Jinming Li, Zhongyi Zhou, Chengmeng Li, Xiaoyu Liu, Yaxin Peng, Chao…

使用MATLAB进行雷达数据采集可视化

本文使用轮趣科技N10雷达,需要源码可在后台私信或者资源自取 1. 项目概述 本项目旨在通过 MATLAB 读取 N10 激光雷达 的数据,并进行 实时 3D 点云可视化。数据通过 串口 传输,并经过解析后转换为 三维坐标点,最终使用 pcplayer 进…

UE求职Demo开发日志#19 给物品找图标,实现装备增加属性,背包栏UI显示装备

1 将用到的图标找好,放一起 DataTable里对应好图标 测试一下能正确获取: 2 装备增强属性思路 给FMyItemInfo添加一个枚举变量记录类型(物品,道具,装备,饰品,武器)--> 扩展DataT…

Docker 部署 Starrocks 教程

Docker 部署 Starrocks 教程 StarRocks 是一款高性能的分布式分析型数据库,主要用于 OLAP(在线分析处理)场景。它最初是由百度的开源团队开发的,旨在为大数据分析提供一个高效、低延迟的解决方案。StarRocks 支持实时数据分析&am…

(9) 上:学习与验证 linux 里的 epoll 对象里的 EPOLLIN、 EPOLLHUP 与 EPOLLRDHUP 的不同

(1)经过之前的学习。俺认为结论是这样的,因为三次握手到四次挥手,到 RST 报文,都是 tcp 连接上收到了报文,这都属于读事件。所以: EPOLLIN : 包含了读事件, FIN 报文的正常四次挥手、…

python学opencv|读取图像(五十二)使用cv.matchTemplate()函数实现最佳图像匹配

【1】引言 前序学习了图像的常规读取和基本按位操作技巧,相关文章包括且不限于: python学opencv|读取图像-CSDN博客 python学opencv|读取图像(四十九)原理探究:使用cv2.bitwise()系列函数实现图像按位运算-CSDN博客…

数据分析系列--⑦RapidMiner模型评价(基于泰坦尼克号案例含数据集)

一、前提 二、模型评估 1.改造⑥ 2.Cross Validation算子说明 2.1Cross Validation 的作用 2.1.1 模型评估 2.1.2 减少过拟合 2.1.3 数据利用 2.2 Cross Validation 的工作原理 2.2.1 数据分割 2.2.2 迭代训练与测试 ​​​​​​​ 2.2.3 结果汇总 ​​​​​​​ …

DeepSeek r1本地安装全指南

环境基本要求 硬件配置 需要本地跑模型,兼顾质量、性能、速度以及满足日常开发需要,我们需要准备以下硬件: CPU:I9内存:128GB硬盘:3-4TB 最新SSD,C盘确保有400GB,其它都可划成D盘…

AI开发学习之——PyTorch框架

PyTorch 简介 PyTorch (Python torch)是由 Facebook AI 研究团队开发的开源机器学习库,广泛应用于深度学习研究和生产。它以动态计算图和易用性著称,支持 GPU 加速计算,并提供丰富的工具和模块。 PyTorch的主要特点 …

纯后训练做出benchmark超过DeepseekV3的模型?

论文地址 https://arxiv.org/pdf/2411.15124 模型是AI2的,他们家也是玩开源的 先看benchmark,几乎是纯用llama3 405B后训练去硬刚出一个gpt4o等级的LLamA405 我们先看之前的机遇Lllama3.1 405B进行全量微调的模型 Hermes 3,看着还没缘模型…

像接口契约文档 这种工件,在需求 分析 设计 工作流里面 属于哪一个工作流

οゞ浪漫心情ゞο(20***328) 2016/2/18 10:26:47 请教一下,像接口契约文档 这种工件,在需求 分析 设计 工作流里面 属于哪一个工作流? 潘加宇(35***47) 17:17:28 你这相当于问用例图、序列图属于哪个工作流,看内容。 如果你的&quo…