快速开始
LangChain 有许多旨在帮助构建问答应用以及更广泛的 RAG 应用的组件。为了熟悉这些组件,我们将在一个文本数据源上构建一个简单的问答应用。在此过程中,我们将介绍一个典型的问答架构,讨论相关的 LangChain 组件,并重点介绍更多进阶问答技术的附加资源。我们还将了解 LangSmith 如何帮助我们追踪和理解应用。随着应用复杂度的增长,LangSmith 将变得越来越有帮助。
架构
我们将按照 问答简介 中所述创建一个典型的 RAG 应用,它有两个主要组件:
索引(Indexing):从源摄取数据并对其建立索引的流水线。这通常离线进行。
检索与生成(Retrieval and generation):实际的 RAG 链,它在运行时接收用户查询,从索引中检索相关数据,然后将其传递给模型。
从原始数据到答案的完整流程如下:
索引
- 加载(Load):首先我们需要加载数据。为此我们将使用 文档加载器。
- 拆分(Split):文本拆分器 将大型
Document拆分为较小的块。这对于建立索引和将数据传入模型都很有用,因为大块更难搜索,并且无法放入模型有限的上下文窗口。 - 存储(Store):我们需要一个地方来存储和索引我们的拆分,以便之后可以对其搜索。这通常使用 向量存储 和 嵌入 模型来完成。
检索与生成
设置
依赖
在本演练中,我们将使用 OpenAI 聊天模型和嵌入,以及 Chroma 向量存储,但这里展示的一切都适用于任何 聊天模型 或 LLM、嵌入 和 向量存储 或 检索器。
我们将使用以下包:
%pip install --upgrade --quiet langchain langchain-community langchainhub langchain-openai langchain-chroma bs4我们需要为嵌入模型设置环境变量 OPENAI_API_KEY,可以直接设置或像这样从 .env 文件加载:
import getpass
import os
os.environ["OPENAI_API_KEY"] = getpass.getpass()
# import dotenv
# dotenv.load_dotenv()LangSmith
你使用 LangChain 构建的许多应用都会包含多个步骤和多次 LLM 调用。随着这些应用变得越来越复杂,能够检查链或代理内部究竟发生了什么就变得至关重要。做到这一点最好的方式是使用 LangSmith (opens in a new tab)。
请注意,LangSmith 不是必需的,但它很有帮助。如果你确实想使用 LangSmith,在上面链接注册后,请确保设置你的环境变量以开始记录追踪:
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = getpass.getpass()预览
在本指南中,我们将基于 Lilian Weng 的 LLM Powered Autonomous Agents (opens in a new tab) 博客文章构建一个问答应用,它允许我们就文章内容提问。
我们可以用大约 20 行代码创建一个简单的索引流水线和 RAG 链:
import bs4
from langchain import hub
from langchain_community.document_loaders import WebBaseLoader
from langchain_chroma import Chroma
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitterfrom langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)# Load, chunk and index the contents of the blog.
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()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
# Retrieve and generate using the relevant snippets of the blog.
retriever = vectorstore.as_retriever()
prompt = hub.pull("rlm/rag-prompt")
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()
)rag_chain.invoke("What is Task Decomposition?")'Task decomposition is a technique used to break down complex tasks into smaller and simpler steps. It can be done through prompting techniques like Chain of Thought or Tree of Thoughts, or by using task-specific instructions or human inputs. Task decomposition helps agents plan ahead and manage complicated tasks more effectively.'# cleanup
vectorstore.delete_collection()查看 LangSmith 追踪 (opens in a new tab)
详细演练
让我们逐步了解上面的代码,以真正理解发生了什么。
1. 索引:加载 {#indexing-load}
我们首先需要加载博客文章的内容。为此我们可以使用 文档加载器,它是从源加载数据并返回一列 Document (opens in a new tab) 的对象。Document 是一个带有一些 page_content(str)和 metadata(dict)的对象。
在本例中,我们将使用 WebBaseLoader,它使用 urllib 从 Web URL 加载 HTML,并使用 BeautifulSoup 将其解析为文本。我们可以通过 bs_kwargs 向 BeautifulSoup 解析器传入参数来自定义 HTML -> 文本的解析(参见 BeautifulSoup 文档 (opens in a new tab))。在本例中,只有类为 "post-content"、"post-title" 或 "post-header" 的 HTML 标签是相关的,因此我们将移除所有其他标签。
import bs4
from langchain_community.document_loaders import WebBaseLoader
# Only keep post title, headers, and content from the full HTML.
bs4_strainer = bs4.SoupStrainer(class_=("post-title", "post-header", "post-content"))
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs={"parse_only": bs4_strainer},
)
docs = loader.load()len(docs[0].page_content)42824print(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深入了解
DocumentLoader:从源加载数据为一列 Document 的对象。
- 文档:关于如何使用
DocumentLoaders的详细文档。 - 集成:160+ 个集成可供选择。
- 接口 (opens in a new tab):基础接口的 API 参考。
2. 索引:拆分 {#indexing-split}
我们加载的文档长度超过 42k 字符。这对于许多模型的上下文窗口来说太长了。即使对于那些能够将整篇文章放入其上下文窗口的模型,它们也可能难以在非常长的输入中找到信息。
为了处理这个问题,我们将 Document 拆分为块,以便进行嵌入和向量存储。这应该有助于我们在运行时只检索博客文章中最相关的部分。
在本例中,我们将文档拆分为 1000 个字符的块,块之间重叠 200 个字符。重叠有助于降低将某个陈述与其相关的重要上下文分开的可能性。我们使用 RecursiveCharacterTextSplitter,它会使用换行符等常见分隔符递归地拆分文档,直到每个块达到合适的大小。这是通用文本用例推荐的文本拆分器。
我们设置 add_start_index=True,这样每个拆分 Document 在初始 Document 中开始的字符索引会作为元数据属性 "start_index" 保留下来。
from langchain_text_splitters 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)66len(all_splits[0].page_content)969all_splits[10].metadata{'source': 'https://lilianweng.github.io/posts/2023-06-23-agent/',
'start_index': 7056}深入了解
TextSplitter:将一列 Document 拆分为较小块的对象。是 DocumentTransformer 的子类。
- 探索
Context-aware splitters,它们会保留每个拆分在原始Document中的位置("上下文"): - 接口 (opens in a new tab):基础接口的 API 参考。
DocumentTransformer:对一列 Document 执行转换的对象。
- 文档:关于如何使用
DocumentTransformers的详细文档 - 集成
- 接口 (opens in a new tab):基础接口的 API 参考。
3. 索引:存储 {#indexing-store}
现在我们需要为 66 个文本块建立索引,以便在运行时可以对其搜索。最常见的方法是嵌入每个文档拆分的内容,并将这些嵌入插入到向量数据库(或向量存储)中。当我们要搜索我们的拆分时,我们取一个文本搜索查询,对其进行嵌入,并执行某种 "相似度" 搜索,以识别出与我们查询的嵌入最相似的已存储拆分。最简单的相似度度量是余弦相似度——我们测量每一对嵌入(它们是高维向量)之间夹角的余弦值。
我们可以使用 Chroma 向量存储和 OpenAIEmbeddings 模型,通过一条命令嵌入并存储我们所有的文档拆分。
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
vectorstore = Chroma.from_documents(documents=all_splits, embedding=OpenAIEmbeddings())深入了解
Embeddings:文本嵌入模型的封装,用于将文本转换为嵌入。
- 文档:关于如何使用嵌入的详细文档。
- 集成:30+ 个集成可供选择。
- 接口 (opens in a new tab):基础接口的 API 参考。
VectorStore:向量数据库的封装,用于存储和查询嵌入。
- 文档:关于如何使用向量存储的详细文档。
- 集成:40+ 个集成可供选择。
- 接口 (opens in a new tab):基础接口的 API 参考。
至此完成了流水线的 索引 部分。此时我们有了一个可查询的向量存储,其中包含我们博客文章的分块内容。给定一个用户问题,我们理想情况下应该能够返回博客文章中回答该问题的片段。
4. 检索与生成:检索 {#retrieval-and-generation-retrieve}
现在让我们编写实际的应用逻辑。我们想创建一个简单的应用,它接收一个用户问题,搜索与该问题相关的文档,将检索到的文档和初始问题传递给模型,并返回答案。
首先我们需要定义搜索文档的逻辑。LangChain 定义了一个 检索器 接口,它封装了一个索引,可以在给定字符串查询的情况下返回相关的 Document。
最常见的 Retriever 类型是 VectorStoreRetriever,它使用向量存储的相似度搜索能力来促进检索。任何 VectorStore 都可以通过 VectorStore.as_retriever() 轻松转换为 Retriever:
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6})retrieved_docs = retriever.invoke("What are the approaches to Task Decomposition?")len(retrieved_docs)6print(retrieved_docs[0].page_content)Tree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.
Task decomposition can be done (1) by LLM with simple prompting like "Steps for XYZ.\n1.", "What are the subgoals for achieving XYZ?", (2) by using task-specific instructions; e.g. "Write a story outline." for writing a novel, or (3) with human inputs.深入了解
向量存储通常用于检索,但也有一些其他检索方式。
Retriever:在给定文本查询的情况下返回 Document 的对象
- 文档:关于接口和内置检索技术的进一步文档。其中一些包括:
MultiQueryRetriever生成输入问题的变体 以提高检索命中率。MultiVectorRetriever(见下图)则生成 嵌入的变体,同样是为了提高检索命中率。Max marginal relevance在检索到的文档中选取 相关性和多样性 (opens in a new tab),以避免传入重复的上下文。- 可以使用元数据过滤器在向量存储检索期间过滤文档,例如使用 自查询检索器。
- 集成:与检索服务的集成。
- 接口 (opens in a new tab):基础接口的 API 参考。
5. 检索与生成:生成 {#retrieval-and-generation-generate}
让我们把所有内容整合成一个链:它接收一个问题,检索相关文档,构造提示,将其传递给模型,并解析输出。
我们将使用 gpt-3.5-turbo OpenAI 聊天模型,但任何 LangChain LLM 或 ChatModel 都可以替换进来。
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2, max_tokens=1024)我们将使用一个已签入 LangChain 提示中心(此处 (opens in a new tab))的 RAG 提示。
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)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: filler question
Context: filler context
Answer:我们将使用 LCEL Runnable 协议来定义链,使我们能够 - 以透明的方式将组件和函数管道连接在一起 - 自动在 LangSmith 中追踪我们的链 - 开箱即用地获得流式、异步和批处理调用
from langchain_core.output_parsers 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("What is Task Decomposition?"):
print(chunk, end="", flush=True)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 easier interpretation and execution by autonomous agents or models. Task decomposition can be done through various methods, such as using prompting techniques, task-specific instructions, or human inputs.查看 LangSmith 追踪 (opens in a new tab)
深入了解
选择模型
ChatModel:由 LLM 驱动的聊天模型。接收一列消息并返回一条消息。
- 文档
- 集成:25+ 个集成可供选择。
- 接口 (opens in a new tab):基础接口的 API 参考。
LLM:文本输入文本输出的 LLM。接收一个字符串并返回一个字符串。
- 文档
- 集成:75+ 个集成可供选择。
- 接口 (opens in a new tab):基础接口的 API 参考。
查看关于使用本地运行模型进行 RAG 的指南 此处。
自定义提示
如上所示,我们可以从提示中心加载提示(例如,这个 RAG 提示 (opens in a new tab))。提示也可以轻松自定义:
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)
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 追踪 (opens in a new tab)
下一步
我们在短时间内涵盖了大量内容。上述每个部分都有大量特性、集成和扩展可供探索。除了上面提到的 深入了解 资源外,不错的下一步包括: