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.mzph.cn/web/67760.shtml

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

相关文章

DeepSeek-R1大模型本地化部署

前言 Ollama作为一个轻量级、易上手的工具,可以帮助你在自己的电脑上快速部署和运行大型语言模型,无需依赖云端服务。通过加载各种开源模型,比如LLaMA、GPT-J等,并通过简单的命令行操作进行模型推理和测试。 此小结主要介绍使用…

【高级篇 / IPv6】(7.6) ❀ 03. 宽带IPv6 - ADSL拨号宽带上网配置 ❀ FortiGate 防火墙

【简介】大部分ADSL拨号宽带都支持IPv6,这里以ADSL拨号宽带为例,演示在FortiGate防火墙上的配置方法。 准备工作 同上篇文章一样,为了兼顾不熟悉FortiGate防火墙的朋友,我们从基础操作进行演示,熟练的朋友可以跳过这一…

Linux第104步_基于AP3216C之I2C实验

Linux之I2C实验是在AP3216C的基础上实现的,进一步熟悉修改设备树和编译设备树,以及学习如何编写I2C驱动和APP测试程序。 1、AP3216C的原理图 AP3216C集成了一个光强传感器ALS,一个接近传感器PS和一个红外LED,为三合一的环境传感…

基于单片机的盲人智能水杯系统(论文+源码)

1 总体方案设计 本次基于单片机的盲人智能水杯设计,采用的是DS18B20实现杯中水温的检测,采用HX711及应力片实现杯中水里的检测,采用DS1302实现时钟计时功能,采用TTS语音模块实现语音播报的功能,并结合STC89C52单片机作…

高清种子资源获取指南 | ✈️@seedlinkbot

在如今的数字时代,高清影视、音乐、游戏等资源的获取方式不断丰富。对于追求高质量资源的用户而言,一个高效的资源分享平台至关重要。而 ✈️seedlinkbot 正是这样一个便捷的资源获取工具,为用户提供高质量的种子资源索引和下载信息。 1. ✈️…

Spring Boot项目如何使用MyBatis实现分页查询

写在前面:大家好!我是晴空๓。如果博客中有不足或者的错误的地方欢迎在评论区或者私信我指正,感谢大家的不吝赐教。我的唯一博客更新地址是:https://ac-fun.blog.csdn.net/。非常感谢大家的支持。一起加油,冲鸭&#x…

【论文笔记】Fast3R:前向并行muti-view重建方法

众所周知,DUSt3R只适合做稀疏视角重建,与sapnn3r的目的类似,这篇文章以并行的方法,扩展了DUSt3R在多视图重建中的能力。 abstract 多视角三维重建仍然是计算机视觉领域的核心挑战,尤其是在需要跨不同视角实现精确且可…

本地部署DeepSeek教程(Mac版本)

第一步、下载 Ollama 官网地址:Ollama 点击 Download 下载 我这里是 macOS 环境 以 macOS 环境为主 下载完成后是一个压缩包,双击解压之后移到应用程序: 打开后会提示你到命令行中运行一下命令,附上截图: 若遇…

微机原理与接口技术期末大作业——4位抢答器仿真

在微机原理与接口技术的学习旅程中,期末大作业成为了检验知识掌握程度与实践能力的关键环节。本次我选择设计并仿真一个 4 位抢答器系统,通过这个项目,深入探索 8086CPU 及其接口技术的实际应用。附完整压缩包下载。 一、系统设计思路 &…

【Redis】Redis 经典面试题解析:深入理解 Redis 的核心概念与应用

Redis 是一个高性能的键值存储系统,广泛应用于缓存、消息队列、排行榜等场景。在面试中,Redis 是一个高频话题,尤其是其核心概念、数据结构、持久化机制和高可用性方案。 1. Redis 是什么?它的主要特点是什么? 答案&a…

昆仑万维Java开发面试题及参考答案

进程和线程的区别是什么? 进程和线程都是操作系统中非常重要的概念,它们在多个方面存在显著的区别。 从定义上看,进程是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间,包括代码段、数据段、堆栈段等。例如,当你在电脑上同时打开浏览器和音乐播放…

Visual Studio Code应用本地部署的deepseek

1.打开Visual Studio Code,在插件中搜索continue,安装插件。 2.添加新的大语言模型,我们选择ollama. 3.直接点connect,会链接本地下载好的deepseek模型。 参看上篇文章:deepseek本地部署-CSDN博客 4.输入需求生成可用…

SpringBoot 整合 Mybatis:注解版

第一章&#xff1a;注解版 导入配置&#xff1a; <groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.1</version> </dependency> 步骤&#xff1a; 配置数据源见 Druid…

2025年最新在线模型转换工具优化模型ncnn,mnn,tengine,onnx

文章目录 引言最新网址地点一、模型转换1. 框架转换全景图2. 安全的模型转换3. 网站全景图 二、转换说明三、模型转换流程图四、感谢 引言 在yolov5&#xff0c;yolov8&#xff0c;yolov11等等模型转换的领域中&#xff0c;时间成本常常是开发者头疼的问题。最近发现一个超棒的…

理解知识蒸馏中的散度损失函数(KLDivergence/kldivloss )-以DeepSeek为例

1. 知识蒸馏简介 什么是知识蒸馏&#xff1f; 知识蒸馏&#xff08;Knowledge Distillation&#xff09;是一种模型压缩技术&#xff0c;目标是让一个较小的模型&#xff08;学生模型&#xff0c;Student Model&#xff09;学习一个较大、性能更优的模型&#xff08;教师模型…

Electron使用WebAassembly实现CRC-8 MAXIM校验

Electron使用WebAssembly实现CRC-8 MAXIM校验 将C/C语言代码&#xff0c;经由WebAssembly编译为库函数&#xff0c;可以在JS语言环境进行调用。这里介绍在Electron工具环境使用WebAssembly调用CRC-8 MAXIM格式校验的方式。 CRC-8 MAXIM校验函数WebAssebly源文件 C语言实现CR…

Vue3.0实战:大数据平台可视化

文章目录 创建vue3.0项目项目初始化项目分辨率响应式设置项目顶部信息条创建页面主体创建全局引入echarts和axios后台接口创建express销售总量图实现完整项目下载项目任何问题都可在评论区,或者直接私信即可。 创建vue3.0项目 创建项目: vue create vueecharts选择第三项:…

vector容器(详解)

本文最后是模拟实现全部讲解&#xff0c;文章穿插有彩色字体&#xff0c;是我总结的技巧和关键 1.vector的介绍及使用 1.1 vector的介绍 https://cplusplus.com/reference/vector/vector/&#xff08;vector的介绍&#xff09; 了解 1. vector是表示可变大小数组的序列容器。…

Airflow:深入理解Apache Airflow Task

Apache Airflow是一个开源工作流管理平台&#xff0c;支持以编程方式编写、调度和监控工作流。由于其灵活性、可扩展性和强大的社区支持&#xff0c;它已迅速成为编排复杂数据管道的首选工具。在这篇博文中&#xff0c;我们将深入研究Apache Airflow 中的任务概念&#xff0c;探…

开发环境搭建-4:WSL 配置 docker 运行环境

在 WSL 环境中构建&#xff1a;WSL2 (2.3.26.0) Oracle Linux 8.7 官方镜像 基本概念说明 容器技术 利用 Linux 系统的 文件系统&#xff08;UnionFS&#xff09;、命名空间&#xff08;namespace&#xff09;、权限管理&#xff08;cgroup&#xff09;&#xff0c;虚拟出一…