Langchain-RAG入门2

文摘   2025-01-07 07:00   四川  
接着上一篇文章,让我们一步一步地分析上面的代码来真正理解发生了什么。
Loading documents
我们首先需要加载博客文章内容。我们可以使用 DocumentLoaders 来实现这一点,它是从源加载数据并返回 Document 对象列表的对象。
我们首先需要加载博客文章内容。我们可以使用 DocumentLoaders 来实现这一点,它是从源加载数据并返回 Document 对象列表的对象。
我们可以通过 bs_kwargs 将参数传递到 BeautifulSoup 解析器来定制 HTML -> 文本解析的规则。这里我嗯根据实际情况去选择对应的校验,我这里选择了保留 class 属性为post-containse的内容,里面是文章的正文。
import bs4from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(    # 这里是一篇苏洋博客中养猫的文章(这里只作为学习示例,请勿违规使用)    web_paths=("https://soulteary.com/2018/07/23/experience-in-breeding-cat.html",),    bs_kwargs=dict(        # 过滤 HTML 内容,只解析具有 class 为 post-container 的内容        parse_only=bs4.SoupStrainer(            class_="post-container"        )    ),)
Splitting documents
我们加载的文档超过 成千上万个字符的时候,就无法一次性放入许多模型的上下文窗口。
退一步,即使对于那些可以在其上下文窗口中容纳完整帖子的模型,模型也很难在很长的输入中找到信息。这是 transformer 架构本身的短板造成的。
为了解决这个问题,我们将文档拆分成块以进行嵌入和向量存储。这应该有助于我们在运行时仅检索博客文章中最相关的部分。
我们使用 RecursiveCharacterTextSplitter,它将使用常见分隔符(如new line)递归地拆分文档,直到每个块都具有合适的大小。
当然这是最简单只管的一种处理方式,效果不一定是最好的,这只是针对通用文本用例推荐的文本分割器。
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(    chunk_size=1000,  # chunk size (characters)    chunk_overlap=200,  # chunk overlap (characters)    add_start_index=True,  # track index in original document)
all_splits = text_splitter.split_documents(docs)print(len(all_splits))
chunk_size表示多少个字符拆一个chunk
chunk_overlap表示每个chunk之间重合多少个字符
Storing documents
到目前为止,现在我们需要索引我们上面split出俩的多个文本块,以便我们可以在运行时搜索它们。
我们的方法是对每个文档分割的内容执行 embedding,并将这些 embedding 插入到向量存储中。给定输入查询,我们可以使用向量搜索来检索相关文档。
# 对切分后的 chunks进行 Index= vector_store.add_documents(documents=all_splits)print("document indexed")
这个从文本 chunk 到 embedding ,然后到 store 的过程我们称之为 Index 。
Retrieval and Generation
也就是我们说的检索和生成。
现在开始看编写实际的应用程序逻辑:我们想要创建一个简单的应用程序,它接受用户问题,搜索与该问题相关的文档,将检索到的文档和初始问题传递给模型,并返回答案。
from langchain import hub
prompt = hub.pull("rlm/rag-prompt")example_messages = prompt.invoke(    {"context""(context goes here)""question""(question goes here)"}).to_messages()
print(example_messages[0].content)
输出:
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.Question: (question goes here) Context: (context goes here) Answer:
这里使用了公共的prompt,通过QuestionContext自动构建对应的QA 场景的prompt。
另外一个重点是:我们使用 LangGraph 将检索和生成步骤整合到一个应用程序中。这将带来许多好处:
  • 我们可以定义一次应用程序逻辑并自动支持多种调用模式,包括stream、async和batch调用。
  • 我们可以通过 LangGraph 平台简化部署。
  • 我们只需进行极少的代码更改,就可以轻松地为我们的应用程序添加关键功能,包括持久化和人机交互循环流程。

要使用 LangGraph,我们需要定义三件事
  1. 定义我们application的state
  2. 定义我们application的nodes
  3. 构建我们application的控制流

state:我们的应用程序的state控制着哪些数据输入到应用程序、在steps之间传输以及应用程序输出。它通常是 TypedDict,但也可以是 Pydantic BaseModel。
对于简单的 RAG 应用程序,我们只需跟踪输入的问题、检索到的上下文和生成的答案:
from langchain_core.documents import Documentfrom typing_extensions import List, TypedDict
class State(TypedDict):    question: str    context: List[Document]    answer: str
Nodes:我们也称为application的steps。我们从两个步骤的简单序列开始:检索和生成。
def retrieve(state: State):    retrieved_docs = vector_store.similarity_search(state["question"])    return {"context": retrieved_docs}
def generate(state: State):    docs_content = "\n\n".join(doc.page_content for doc in state["context"])    messages = prompt.invoke({"question"state["question"], "context": docs_content})    response = llm.invoke(messages)    return {"answer": response.content}
我们的retrieve step只是使用输入问题运行相似性搜索,generate step 将检索到的上下文和原始问题格式化为聊天模型的prompt。
流程控制
最后,我们将应用程序编译为单个图形对象。在本例中,我们只是将检索和生成步骤连接成单个sequence
from langgraph.graph import START, StateGraph
graph_builder = StateGraph(State).add_sequence([retrieve, generate])graph_builder.add_edge(START, "retrieve")graph = graph_builder.compile()
这个代码的意思是:从retrieve开始,然后执行generate
最后执行graph即可:
response = graph.invoke({"question""呼吸过快是什么原因?"})print(response["answer"])
自定义定义我们的Q-A prompt:
from langchain_core.prompts import PromptTemplate
template = """Use the following pieces of context to answer the question at the end.If you don't know the answer, just say that you don't know, don't try to make up an answer.Use three sentences maximum and keep the answer as concise as possible.Always say "thanks for asking!" at the end of the answer.
{context}
Question: {question}
Helpful Answer:"""custom_rag_prompt = PromptTemplate.from_template(template)
Query analysis
到目前为止,我们正在使用原始输入query执行检索。但是,允许模型生成用于检索目的的query有一些优势。例如:
  • 除了语义搜索之外,我们还可以内置结构化过滤器(例如“查找 2020 年以来的文档。”);
  • 该模型可以将用户查询(可能是多方面的或包含不相关的语言)重写为更有效的搜索查询。
Query analysis使用model 从原始用户输入转换或构建优化的搜索查询,当然了,我们可以轻松地将查询分析步骤纳入我们的应用程序中。为了便于说明,我们在向量存储中的文档中添加一些元数据。我们将向文档中添加一些(人为的)东西,以便稍后演示过滤。
total_documents = len(all_splits)third = total_documents // 3
for i, document in enumerate(all_splits):    if i < third:        document.metadata["section"] = "beginning"    elif i < 2 * third:        document.metadata["section"] = "middle"    else:        document.metadata["section"] = "end"
# 对切分后的 all_splits 进行 Index_ = vector_store.add_documents(documents=all_splits)
这段处理逻辑是我们将之前的splits 按照3段进行处理,并且分别在document.metadata添加section属性,表述当前的chunk是整个documents的哪一部分。
接下来让我们为搜索查询定义一个schema。我们将使用结构化输出来实现此目的。在这里,我们将查询定义为包含字符串查询和文档部分(“开头”、“中间”或“结尾”)。
from typing import Literalfrom typing_extensions import Annotated
class Search(TypedDict):    """Search query."""
    query: Annotated[str, ..., "Search query to run."]    section: Annotated[        Literal["beginning""middle""end"],        ...,        "Section to query.",    ]
最后,我们向 LangGraph 应用程序添加一个step,从用户的原始输入生成查询,最终的改造效果如下
# 定义应用的状态class State(TypedDict):    question: str    query: Search    context: List[Document]    answer: str
# 定义应用的执行步骤def analyze_query(state: State):    # 注意这里使用了with_structured_output生成结构化的输出    structured_llm = llm_model.with_structured_output(Search,method="json_schema")    query = structured_llm.invoke(state["question"])    return {"query": query}
def retrieve(state: State):    query = state["query"]    # 使用 vector_store 根据问题检索相关的文档    retrieved_docs = vector_store.similarity_search(        query["query"],        filter={"section": {"$eq": query["section"]}},    )    return {"context": retrieved_docs}
def generate(state: State):    docs_content = "\n\n".join(doc.page_content for doc in state["context"])    messages = prompt.invoke({"question": state["question"], "context": docs_content})    response = llm_model.invoke(messages)    return {"answer": response.content}
# 编译状态图# 定义状态图,将analyze_query, retrieve 和 generate 两个步骤添加到状态图中graph_builder = StateGraph(State).add_sequence([analyze_query,retrieve, generate])# 并且定义状态图的边,开始节点是analyze_querygraph_builder.add_edge(START, "analyze_query")# 编译状态图graph = graph_builder.compile()
最终的流程是这样的:

总结一下,我们介绍了基于数据构建基本问答应用程序的步骤:

  • 使用文档加载器加载数据
  • 使用文本分割器对索引数据进行分块,使模型更容易使用
  • 嵌入数据并将数据存储在矢量存储中
  • 检索先前存储的块以响应传入的问题
  • 使用检索到的块作为上下文来生成答案。

到目前为止,我们只能是一次性的交互,但是在很多场景下,用户的提问和回答是来回交互的,怎么做呢?
下节,我们将继续深入更高级的用法!!!
参考链接:
https://python.langchain.com/docs/tutorials/rag/#detailed-walkthrough

半夏决明
读书,摄影,随笔
 最新文章