跳到主要内容

快速开始

LangChain设计了许多组件,旨在帮助构建问答应用,以及更广泛的RAG应用。为了熟悉这些组件,我们将构建一个简单的问答应用,覆盖文本数据源。在此过程中,我们将了解典型的问答架构,讨论相关的LangChain组件,并强调更多高级问答技术的额外资源。我们还将看到LangSmith如何帮助我们追踪和理解我们的应用。随着应用复杂性的增长,LangSmith将越来越有用。

Architecture

我们将创建一个典型RAG应用,如问答介绍中所述,它有两个主要组件:

索引:一个用于从源摄取数据并进行索引的管道。这通常在离线时发生。

检索和生成:实际的RAG链,它在运行时接收用户查询,从索引中检索相关数据,然后将数据传递给模型。

从原始数据到答案的完整序列将如下所示:

索引

  1. 加载:首先,我们需要加载我们的数据。我们将为此使用DocumentLoaders
  2. 分割文本分割器将大的Documents分割成较小的块。这对于索引数据和将其传递给模型都很有用,因为大块更难搜索,而且不会适应模型的有限上下文窗口。
  3. 存储:我们需要一个地方来存储和索引我们的分割,以便以后可以搜索。这通常使用VectorStoreEmbeddings模型来完成。

检索和生成

  1. 检索:给定用户输入,使用Retriever从存储中检索相关分割。
  2. 生成ChatModel / LLM使用包括问题和检索到的数据的提示生成答案。

Setup

Dependencies

在本教程中,我们将使用OpenAI的聊天模型、嵌入和Chroma向量存储,但所展示的所有内容都适用于任何ChatModelLLMEmbeddingsVectorStoreRetriever

我们将使用以下包:

!pip install -U langchain openai chromadb langchainhub bs4

我们需要设置环境变量OPENAI_API_KEY,可以直接设置或从.env文件加载,示例如下:

import getpass
import os

os.environ["OPENAI_API_KEY"] = getpass.getpass()

LangSmith

使用LangChain构建的许多应用程序包含多个步骤,其中多次调用LLM。随着这些应用程序变得越来越复杂,检查您的链或代理内部发生的事情变得至关重要。最好的方法是使用LangSmith

请注意,LangSmith不是必需的,但它很有帮助。如果您想使用LangSmith,在上面的链接注册后,请确保设置环境变量以开始记录跟踪:

os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = getpass.getpass()

Preview

假设我们想在Lilian Weng的LLM Powered Autonomous Agents博客文章上构建一个问答应用程序。我们可以在大约20行代码中创建一个简单的管道:

import bs4
from langchain import hub
from langchain_community.chat_models import ChatOpenAI
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.embeddings import OpenAIEmbeddings
from langchain.schema import StrOutputParser
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_core.runnables import RunnablePassthrough

# 使用WebBaseLoader加载博客文章内容
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content","post-title","post-header")
)
),
)
docs = loader.load()

# 使用RecursiveCharacterTextSplitter拆分文档
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)

# 创建Chroma向量存储
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()

# 设置问题和ChatModel
prompt = hub.pull("rlm/rag-prompt")
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

# 创建RAG链
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)

# 回答问题
question = "What is Task Decomposition?"
answer = rag_chain.invoke(question)
print(answer)

# 清理
vectorstore.delete_collection()

LangSmith trace上查看。

Detailed walkthrough

让我们逐步详细解释上述代码中的每个步骤。

Step 1. Indexing: Load

首先,我们需要加载博客文章内容。我们可以使用DocumentLoader来完成这一步,它是从源加载数据并将其作为Documents返回的对象。一个Document是一个具有page_content(字符串)和metadata(字典)属性的对象。

在这种情况下,我们将使用WebBaseLoader,它使用urllibBeautifulSoup来加载和解析传递的网络URL,每个URL返回一个Document。我们可以通过向bs_kwargs传递参数来自定义HTML到文本解析(参见BeautifulSoup文档)。在这种情况下,只有带有类“post-content”、“post-title”或“post-header”的HTML标签是相关的,所以我们将删除所有其他标签。

from langchain_community.document_loaders import WebBaseLoader

loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs={
"parse_only": bs4.SoupStrainer(
class_=("post-content","post-title","post-header")
)
},
)
docs = loader.load()

len(docs[0].page_content)
42824
print(docs[0].page_content[:500])
LLM Powered Autonomous Agents

Date: June 23, 2023 | Estimated Reading Time: 31 min | Author: Lilian Weng

Building agents with LLM (large language model) as its core controller is a cool concept. Several proof-of-concepts demos, such as AutoGPT, GPT-Engineer and BabyAGI, serve as inspiring examples. The potentiality of LLM extends beyond generating well-written copies, stories, essays and programs; it can be framed as a powerful general problem solver.
Agent System Overview#
In

Go Deeper

DocumentLoader:是从源加载数据并将其转换为Documents对象的工具。 - 文档:关于如何使用DocumentLoaders的详细文档。 - 集成:有160多个集成可供选择。 - 接口:基础接口的API参考。

Step 2. Indexing: Split

我们加载的文档超过42,000个字符,这对于许多模型的上下文窗口来说太长了。即使对于那些可以容纳完整文章的模型,根据经验,模型在非常长的提示中很难找到相关的上下文。

因此,我们将文档拆分成小块,以进行嵌入和向量存储。这应该有助于我们在运行时只检索博客文章的最相关部分。

在这种情况下,我们将文档拆分为每个块1000个字符,块之间有200个字符的重叠。重叠有助于减少将语句与与之相关的重要上下文分开的可能性。我们使用RecursiveCharacterTextSplitter,该工具将递归地使用常见分隔符(如换行符)拆分文档,直到每个块达到适当的大小。它也是推荐用于通用文本用例的文本分割器。

我们设置add_start_index=True,以便每个分割的Documents在初始文档中开始的字符索引作为元数据属性“start_index”被保留。

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(docs)

len(all_splits)

# 66

len(all_splits[0].page_content)

# 969

all_splits[10].metadata

{'source': 'https://lilianweng.github.io/posts/2023-06-23-agent/','start_index': 7056}

Go Deeper

TextSplitter:一个对象,它将Document列表分割成更小的块。是DocumentTransformers的子类。 - 探索Context-aware splitters,它们保持每个分割在原始Document中的位置(“上下文”): - Markdown文件 - 代码(Python或JavaScript) - 科学论文 - 接口:基础接口的API参考。

DocumentTransformer:一个对象,它对Document列表执行转换。 - 文档:关于如何使用DocumentTransformers的详细文档 - 集成 - 接口:基础接口的API参考。

Step 3. Indexing: Store

现在我们在内存中有66个文本块,我们需要将它们存储和索引,以便稍后在我们的RAG应用程序中搜索它们。最常见的方法是嵌入每个文档分割的内容并将这些嵌入上传到矢量存储器。

然后,当我们想要搜索我们的分割时,我们也嵌入搜索查询,并执行某种“相似性”搜索以识别具有与我们查询嵌入最相似的嵌入的存储分割。最简单的相似性度量是余弦相似度 - 我们度量每对嵌入之间的角度的余弦值(它们只是非常高维的向量)。

我们可以使用Chroma矢量存储器和OpenAIEmbeddings模型一次嵌入和存储所有文档分割。

from langchain_community.embeddings import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

vectorstore = Chroma.from_documents(documents=all_splits, embedding=OpenAIEmbeddings())

Go Deeper

Embeddings:文本嵌入模型的包装器,用于将文本转换为嵌入。 - 文档:关于如何使用嵌入的详细文档。 集成:有30多个集成可供选择。 接口:基础接口的API参考。

VectorStore:向量数据库的包装器,用于存储和查询嵌入。 文档:关于如何使用向量存储的详细文档。 集成:有40多个集成可供选择。 接口:基础接口的API参考。

这完成了管道的索引部分。此时,我们拥有一个可查询的向量存储,其中包含我们博客文章的分块内容。给定用户问题,理想情况下,我们应该能够返回回答该问题的博客文章片段。

Step 4. Retrieve

现在让我们编写实际的应用程序逻辑。我们希望创建一个简单的应用程序,让用户提问问题,搜索与该问题相关的文档,将检索到的文档和初始问题传递给模型,最后返回答案。

LangChain定义了一个Retriever接口,它包装了一个可以返回与字符串查询相关的文档的索引。所有检索器都实现了一个共同的方法get_relevant_documents()(以及其异步变体aget_relevant_documents())。

最常见的Retriever类型是VectorStoreRetriever,它使用矢量存储器的相似性搜索功能来进行检索。任何VectorStore都可以用VectorStore.as_retriever()轻松转换为Retriever

retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k":6})

retrieved_docs = retriever.get_relevant_documents(
"任务分解的方法是什么?"
)

len(retrieved_docs)
6
print(retrieved_docs[0].page_content)
“Tree of Thoughts”(Yao等人,2023年)通过探索每个步骤的多种推理可能性来扩展了CoT。它首先将问题分解为多个思考步骤,并在每个步骤中生成多种思考,从而创建了一个树状结构。搜索过程可以是BFS(广度优先搜索)或DFS(深度优先搜索),每个状态都由分类器(通过提示)或多数投票来评估。
任务分解可以通过以下方式进行:(1)使用简单提示的LLM,例如“XYZ的步骤。\n1。”,“实现XYZ的子目标是什么?”;(2)使用任务特定的说明,例如为了写小说而写“写故事大纲。”,或(3)使用人工输入。

Go Deeper

向量存储通常用于检索,但还有其他检索方法。

Retriever:一个对象,它根据文本查询返回Documents - 文档:关于接口和内置检索技术的进一步文档。其中一些包括:- MultiQueryRetriever生成输入问题的变体以提高检索命中率。- MultiVectorRetriever(下图)生成嵌入的变体,也是为了提高检索命中率。- Max marginal relevance在检索到的文档中选择相关性和多样性以避免传递重复的上下文。- 在向量存储检索期间,可以使用metadata过滤器过滤文档。- 集成:与检索服务的集成。- 接口:基础接口的API参考。

Step 5. Retrieval and Generation: Generate

让我们将所有内容整合成一个链条,接受一个问题,检索相关文档,构建一个提示,将其传递给模型,并解析输出。

我们将使用gpt-3.5-turbo的OpenAI聊天模型,但可以替换为任何LangChain的LLMChatModel

from langchain_community.chat_models import ChatOpenAI

llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

我们将使用RAG的提示,该提示已经存储在LangChain提示中心(这里)。

from langchain import hub

prompt = hub.pull("rlm/rag-prompt")

example_messages = prompt.invoke(
{"context": "filler context", "question": "filler question"}
).to_messages()

example_messages
[HumanMessage(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.\nQuestion: filler question \nContext: filler context \nAnswer:")]
print(example_messages[0].content)
Human: 您是用于问题回答任务的助手。使用下面的检索到的上下文来回答问题。如果您不知道答案,只需说您不知道。最多使用三个句子,并保持答案简明扼要。
问题:填充的问题
上下文:填充的上下文
答案:

我们将使用LCEL Runnable协议来定义链条,这允许我们以透明的方式连接组件和函数,自动跟踪LangSmith中的链条,并获得流式、异步和批处理调用的支持。

from langchain.schema import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

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

rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
for chunk in rag_chain.stream("什么是任务分解?"):
print(chunk, end="", flush=True)
任务分解是一种将复杂任务拆分为较小且更简单步骤的技术。它可以通过Chain of Thought (CoT)或Tree of Thoughts等方法来实现,这些方法涉及将任务划分为可管理的子任务,并在每个步骤中探索多种推理可能性。任务分解可以通过提示、任务特定说明或人类输入来执行。

请查看LangSmith跟踪

Go Deeper

选择模型

ChatModel:一个由LLM支持的聊天模型。接收一系列消息并返回一条消息。 - 文档:详细文档 - 集成:有25多个集成可供选择。 - 接口:基础接口的API参考。

LLM:一个文本输入文本输出的LLM。接收一个字符串并返回一个字符串。 - 文档 - 集成:有75多个集成可供选择。 - 接口:基础接口的API参考。

查看关于本地运行模型的RAG指南这里

自定义提示

如上所示,我们可以从提示中心加载提示(例如,这个RAG提示)。提示也可以很容易地进行自定义:

from langchain.prompts import PromptTemplate

template ="""使用以下上下文片段来回答末尾的问题。
如果你不知道答案,就说你不知道,不要试图编造答案。
最多使用三句话,尽量让答案简洁。
在答案末尾总是说“谢谢你的提问!”

{context}

问题:{question}

有帮助的答案:"""
custom_rag_prompt = PromptTemplate.from_template(template)

rag_chain =(
{"context": retriever | format_docs,"question": RunnablePassthrough()}
| custom_rag_prompt
| llm
| StrOutputParser()
)

rag_chain.invoke("What is Task Decomposition?")

'Task decomposition is a technique used to break down complex tasks into smaller and simpler steps. It involves transforming big tasks into multiple manageable tasks, allowing for a more systematic and organized approach to problem-solving. Thanks for asking!'

查看LangSmith追踪

Next Step

我们在很短的时间内涵盖了很多内容。在上述每个部分中,都有许多功能、集成和扩展可以探索。除了上面提到的深入学习资源外,好的下一步包括: