:::tips
此文档是LangChain官方教程的实践总结:
https://python.langchain.com/v0.2/docs/tutorials/rag/
实践前你需要准备:
OPENAI_API_KEY Generator:根据检索到的信息和用户的查询生成自然语言的回答。
LANGCHAIN_API_KEY 密切监控和评估您的应用程序,以便您可以快速、自信地开发。
py 环境准备(conda)。
:::
简介
我们将在网站上构建一个 QA 应用程序。我们将使用的具体网站是 Lilian Weng 的 LLM Powered Autonomous Agents 博客文章,该网站允许我们提出有关帖子内容的问题。
一步一步成功
数据收集
我们需要首先加载博客文章内容。为此,我们可以使用 DocumentLoaders,它们是从源加载数据并返回文档列表的对象。 Document 是一个带有一些 page_content (str) 和 metadata (dict) 的对象。
在本例中,我们将使用 WebBaseLoader,它使用 urllib 从 Web URL 加载 HTML,并使用 BeautifulSoup 将其解析为文本。我们可以通过 bs_kwargs 将参数传递给 BeautifulSoup 解析器来自定义 HTML -> 文本解析(请参阅 BeautifulSoup 文档)。在这种情况下,只有类为“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)
43131
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
数据分块
我们加载的文档长度超过 42k 个字符。这太长了,无法适应许多模型的上下文窗口。即使对于那些可以在其上下文窗口中容纳完整帖子的模型,模型也可能很难在很长的输入中找到信息。
为了解决这个问题,我们将把 Document 分割成块以进行嵌入和向量存储。这应该可以帮助我们在运行时仅检索博客文章中最相关的部分。
在本例中,我们将把文档分成 1000 个字符的块,块之间有 200 个字符的重叠。重叠有助于降低将语句与与其相关的重要上下文分开的可能性。我们使用 RecursiveCharacterTextSplitter,它将使用常见的分隔符(如换行符)递归地分割文档,直到每个块的大小合适。这是针对一般文本用例推荐的文本分割器。
我们设置 add_start_index=True ,以便将初始文档中每个分割文档开始的字符索引保留为元数据属性“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)
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}
文本嵌入+存储
现在我们需要为 66 个文本块建立索引,以便我们可以在运行时搜索它们。最常见的方法是嵌入每个文档分割的内容并将这些嵌入插入向量数据库(或向量存储)中。当我们想要搜索分割时,我们采用文本搜索查询,将其嵌入,并执行某种“相似性”搜索,以识别与查询嵌入最相似的嵌入的存储分割。最简单的相似性度量是余弦相似性——我们测量每对嵌入(高维向量)之间角度的余弦。
我们可以使用 Chroma 矢量存储和 OpenAIEmbeddings 模型将所有文档分割嵌入并存储在单个命令中。
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
vectorstore = Chroma.from_documents(documents=all_splits, embedding=OpenAIEmbeddings())
检索
现在让我们编写实际的应用逻辑。我们想要创建一个简单的应用程序,它接受用户问题,搜索与该问题相关的文档,将检索到的文档和初始问题传递给模型,然后返回答案。
首先,我们需要定义搜索文档的逻辑。 LangChain 定义了一个 Retriever 接口,它包装了一个索引,该索引可以在给定字符串查询的情况下返回相关的 Documents 。
最常见的 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)
6
print(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.
生成
我们使用 gpt-3.5-turbo OpenAI 聊天模型,但任何 LangChain LLM 或 ChatModel 都可以替换。
import os
os.environ["OPENAI_API_KEY"] = "your openai api key"
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo-0125")
我们使用已签入 LangChain 提示中心的 RAG 提示。rlm/rag-prompt
from langchain import hub
prompt = hub.pull("rlm/rag-prompt")
组装链
我们将使用 LCEL Runnable 协议来定义链,使我们能够
- 以透明的方式将组件和功能连接在一起
- 在 LangSmith 中自动追踪我们的链条
- 开箱即用地进行流式、异步和批量呼叫。
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your langchain api key"
os.environ["OPENAI_API_KEY"] = "your openai api key"
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo-0125")
import bs4
from langchain import hub
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 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()
)
response = rag_chain.invoke("What is Agent System Overview?")
print(response)
调用
我询问了两个问题
- What is Task Decomposition?
- What is Agent System Overview?
剖析 LCEL(链) 以了解发生了什么
首先:这些组件中的每一个( retriever 、 prompt 、 llm 等)都是 Runnable 的实例。这意味着它们实现相同的方法 - 例如同步和异步 .invoke 、 .stream 或 .batch - 这使它们更容易连接在一起。它们可以通过 | 运算符连接到 RunnableSequence(另一个 Runnable)。
当遇到 | 运算符时,LangChain 会自动将某些对象转换为可运行对象。此处, format_docs 被转换为 RunnableLambda,带有 “context” 和 “question” 的字典被转换为 RunnableParallel。细节并不重要,重要的是每个对象都是一个 Runnable。
让我们跟踪输入问题如何流经上述可运行对象:
正如我们在上面看到的, prompt 的输入预计是一个带有键 “context” 和 “question” 的字典。因此,该链的第一个元素构建了可运行对象,它将根据输入问题计算这两个值:
- retriever | format_docs 将问题传递给检索器,生成Document对象,然后传递给 format_docs 生成字符串;
- RunnablePassthrough() 不变地传递输入问题。
然后 chain.invoke(question) 将构建一个格式化的提示,准备进行推理。
最后一步是 llm ,它运行推理,以及 StrOutputParser() ,它只是从 LLM 的输出消息中提取字符串内容。
遇到的问题
USER_AGENT
不阻塞运行,解决方案:
添加下面代码
user_agent = "MyPythonApp/1.0 (Language=Python/3.9; Platform=Linux/Ubuntu20.04)"
os.environ["USER_AGENT"] = user_agent
SSLError
阻塞运行,解决方案:
换个代理。