在基准任务上评估 RAG 架构#
介绍#
如果您想比较不同方法在文档上的 Q&A,那么您会发现此笔记本有助于开始评估不同配置和常见 RAG 架构在基准任务上的表现。目标是让您轻松地尝试不同的技术,了解它们的权衡,并为您的特定用例做出明智的决策。
什么是 RAG?#
LLM 具有知识截止日期。为了让它们准确地响应用户查询,它们需要访问相关信息。检索增强生成 (RAG)(又称“为 LLM 提供搜索引擎”)是解决这个问题的常见设计模式。关键组件是
检索器:从知识库中获取信息,知识库可以是向量搜索引擎、数据库或任何搜索引擎。
生成器:使用学习的知识和检索到的信息的结合来合成响应。
系统的整体质量取决于这两个组件。
基准任务和数据集(截至 2023 年 11 月 21 日)#
目前有以下数据集可用
LangChain 文档 Q&A - 基于 LangChain python 文档的技术问题
半结构化收益 - 关于包含表格和图表财务 PDF 的财务问题和答案
每个任务都附带一个标记数据集,包含问题和答案。它们还提供可配置的工厂函数,以便轻松自定义相关源文档的分块和索引。
有了这些,让我们开始吧!
先决条件#
由于我们将比较许多技术和模型,因此我们将为此示例安装相当多的先决条件。
我们将使用 LangSmith 来捕获评估跟踪。您可以在 smith.langchain.com 上创建一个免费帐户。完成此操作后,您可以创建一个 API 密钥并在下面设置它。
由于我们将在本笔记本中比较许多方法,因此我们将安装的依赖项列表很长。
%pip install -U --quiet langchain langsmith langchainhub langchain_benchmarks
%pip install --quiet chromadb openai huggingface pandas langchain_experimental sentence_transformers pyarrow anthropic tiktoken
import os
os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
os.environ["LANGCHAIN_API_KEY"] = "sk-..." # Your API key
os.environ["OPENAI_API_KEY"] = "sk-..." # Your OpenAI API key
os.environ["ANTHROPIC_API_KEY"] = "sk-..." # Your Anthropic API key
# Silence warnings from HuggingFace
os.environ["TOKENIZERS_PARALLELISM"] = "false"
import uuid
# Generate a unique run ID for these experiments
run_uid = uuid.uuid4().hex[:6]
回顾 Q&A 任务#
注册表提供配置以在经过整理的数据集上测试常见架构。以下是在撰写本文时可用的任务列表。
from langchain_benchmarks import clone_public_dataset, registry
registry.filter(Type="RetrievalTask")
名称 | 类型 | 数据集 ID | 描述 |
---|---|---|---|
LangChain 文档 Q&A | RetrievalTask | 452ccafc-18e1-4314-885b-edd735f17b9d | 基于 LangChain python 文档快照的问题和答案。环境提供文档和检索器信息。每个示例都包含一个问题和参考答案。成功与答案相对于参考答案的准确性有关。我们还衡量模型响应相对于检索到的文档(如果有)的忠实度。 |
半结构化报告 | RetrievalTask | c47d9617-ab99-4d6e-a6e6-92b8daf85a7d | 基于包含表格和图表的 PDF 的问题和答案。该任务提供原始文档以及工厂方法,以便轻松地对它们进行索引并创建一个检索器。每个示例都包含一个问题和参考答案。成功与答案相对于参考答案的准确性有关。我们还衡量模型响应相对于检索到的文档(如果有)的忠实度。 |
langchain_docs = registry["LangChain Docs Q&A"]
langchain_docs
clone_public_dataset(langchain_docs.dataset_id, dataset_name=langchain_docs.name)
基本向量检索#
对于我们的第一个示例,我们将为数据集中的每个文档生成单个嵌入,不进行分块或索引,然后将该检索器提供给 LLM 进行推理。
from langchain.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="thenlper/gte-base",
model_kwargs={"device": 0}, # Comment out to use CPU
)
retriever_factory = langchain_docs.retriever_factories["basic"]
# Indexes the documents with the specified embeddings
# Note that this does not apply any chunking to the docs,
# which means the documents can be of arbitrary length
retriever = retriever_factory(embeddings)
# Factory for creating a conversational retrieval QA chain
chain_factory = langchain_docs.architecture_factories["conversational-retrieval-qa"]
from langchain.chat_models import ChatAnthropic
# Example
llm = ChatAnthropic(model="claude-2", temperature=1)
chain_factory(retriever, llm=llm).invoke({"question": "what's lcel?"})
from functools import partial
from langsmith.client import Client
from langchain_benchmarks.rag import get_eval_config
client = Client()
RAG_EVALUATION = get_eval_config()
test_run = client.run_on_dataset(
dataset_name=langchain_docs.name,
llm_or_chain_factory=partial(chain_factory, retriever, llm=llm),
evaluation=RAG_EVALUATION,
project_name=f"claude-2 qa-chain simple-index {run_uid}",
project_metadata={
"index_method": "basic",
"embedding_model": "thenlper/gte-base",
"llm": "claude-2",
},
verbose=True,
)
test_run.get_aggregate_feedback()
与其他索引策略进行比较#
上面使用的索引基于每个文档的单个向量来检索原始文档。它不会执行任何其他分块操作。您可以在生成索引时尝试更改分块参数。
自定义分块#
您可以对索引进行的最简单的更改是配置您如何拆分文档。
from langchain.text_splitter import RecursiveCharacterTextSplitter
def transform_docs(docs):
splitter = RecursiveCharacterTextSplitter(chunk_size=4000, chunk_overlap=200)
yield from splitter.split_documents(docs)
# Used for the cache
transformation_name = "recursive-text-cs4k-ol200"
retriever_factory = langchain_docs.retriever_factories["basic"]
chunked_retriever = retriever_factory(
embeddings,
transform_docs=transform_docs,
transformation_name=transformation_name,
search_kwargs={"k": 4},
)
chunked_results = client.run_on_dataset(
dataset_name=langchain_docs.name,
llm_or_chain_factory=partial(chain_factory, chunked_retriever, llm=llm),
evaluation=RAG_EVALUATION,
project_name=f"claude-2 qa-chain chunked {run_uid}",
project_metadata={
"index_method": "basic",
"chunk_size": 4000,
"chunk_overlap": 200,
"embedding_model": "thenlper/gte-base",
"llm": "claude-2",
},
verbose=True,
)
chunked_results.get_aggregate_feedback()
父文档检索器#
此索引技术对文档进行分块并为每个块生成 1 个向量。在检索时,将获取 K 个“最相似”的块,然后返回完整的父文档供 LLM 推理。
这确保了该块以其完整的自然上下文呈现。它还可以潜在地提高初始检索质量,因为相似度得分仅限于单个块。
让我们看看这种技术在我们的例子中是否有效。
retriever_factory = langchain_docs.retriever_factories["parent-doc"]
# Indexes the documents with the specified embeddings
parent_doc_retriever = retriever_factory(embeddings)
parent_doc_test_run = client.run_on_dataset(
dataset_name=langchain_docs.name,
llm_or_chain_factory=partial(chain_factory, parent_doc_retriever, llm=llm),
evaluation=RAG_EVALUATION,
project_name=f"claude-2 qa-chain parent-doc {run_uid}",
project_metadata={
"index_method": "parent-doc",
"embedding_model": "thenlper/gte-base",
"llm": "claude-2",
},
verbose=True,
)
parent_doc_test_run.get_aggregate_feedback()
HyDE#
HyDE(假设文档嵌入)是指使用 LLM 生成示例查询的技术,这些查询可用于检索文档。通过这样做,生成的嵌入将自动“更符合”从查询生成的嵌入。这会带来额外的索引成本,因为每个文档在索引时都需要对 LLM 进行额外调用。
retriever_factory = langchain_docs.retriever_factories["hyde"]
retriever = retriever_factory(embeddings)
hyde_test_run = client.run_on_dataset(
dataset_name=langchain_docs.name,
llm_or_chain_factory=partial(chain_factory, retriever=retriever, llm=llm),
evaluation=RAG_EVALUATION,
verbose=True,
project_name=f"claude-2 qa-chain HyDE {run_uid}",
project_metadata={
"index_method": "HyDE",
"embedding_model": "thenlper/gte-base",
"llm": "claude-2",
},
)
hyde_test_run.get_aggregate_feedback()
比较嵌入#
到目前为止,我们一直在使用现成的 GTE-Base 嵌入来检索文档,但您可能会在其他嵌入中获得更好的结果。您甚至可以尝试在您自己的文档上微调嵌入并在此处进行评估。
让我们将到目前为止的结果与 OpenAI 的嵌入进行比较。
from langchain.embeddings.openai import OpenAIEmbeddings
openai_embeddings = OpenAIEmbeddings()
openai_retriever = langchain_docs.retriever_factories["basic"](openai_embeddings)
openai_embeddings_test_run = client.run_on_dataset(
dataset_name=langchain_docs.name,
llm_or_chain_factory=partial(chain_factory, openai_retriever),
evaluation=RAG_EVALUATION,
project_name=f"claude-2 qa-chain oai-emb basic {run_uid}",
project_metadata={
"index_method": "basic",
"embedding_model": "openai/text-embedding-ada-002",
"llm": "claude-2",
},
verbose=True,
)
openai_embeddings_test_run.get_aggregate_feedback()
比较模型#
我们在之前的测试中使用了 Anthropic 的 Claude-2 模型,但让我们尝试其他模型。
您可以在下面的响应生成器中替换任何 LangChain LLM。我们将首先尝试一个长上下文 llama 2 模型(使用 Ollama)。
from langchain.chat_models import ChatOllama
# A llama2-based model with 128k context
# (in theory) In practice, we will see how well
# it actually leverages that context.
ollama = ChatOllama(model="yarn-llama2:7b-128k")
# We'll go back to the GTE embeddings for now
retriever_factory = langchain_docs.retriever_factories["basic"]
retriever = retriever_factory(embeddings)
ollama_test_run = client.run_on_dataset(
dataset_name=langchain_docs.name,
llm_or_chain_factory=partial(chain_factory, llm=ollama, retriever=retriever),
evaluation=RAG_EVALUATION,
project_name=f"yarn-llama2:7b-128k qa-chain basic {run_uid}",
project_metadata={
"index_method": "basic",
"embedding_model": "thenlper/gte-base",
"llm": "ollama/yarn-llama2:7b-128k",
},
verbose=True,
)
更改响应生成器中的提示#
默认提示主要针对 OpenAI 的 gpt-3.5 模型进行了测试。在切换模型时,如果您修改提示,可能会获得更好的结果。让我们尝试一个简单的提示。
from langchain import hub
from langchain.schema.output_parser import StrOutputParser
prompt = hub.pull("wfh/rag-simple")
generator = prompt | ChatAnthropic(model="claude-2", temperature=1) | StrOutputParser()
new_chain = chain_factory(response_generator=generator, retriever=openai_retriever)
claude_simple_prompt_test_run = client.run_on_dataset(
dataset_name=langchain_docs.name,
llm_or_chain_factory=partial(
chain_factory, response_generator=generator, retriever=retriever, llm=llm
),
evaluation=RAG_EVALUATION,
project_name=f"claude-2 qa-chain basic rag-simple {run_uid}",
project_metadata={
"index_method": "basic",
"embedding_model": "thenlper/gte-base",
"prompt": "wfh/rag-simple",
"llm": "claude-2",
},
verbose=True,
)
测试代理#
代理使用 LLM 来决定行动和生成响应。有两种明显的方法可以让它们在上面方法失败的地方成功
上面的链不会“改写”用户查询。可能是改写后的问题将导致更多相关的文档。
上面的链必须基于单个检索步骤进行响应。代理可以迭代地查询检索器或将查询细分为不同的部分,以便在最后进行合成。我们的数据集包含许多需要来自不同文档的信息的问题 - 如果
让我们评估一下上面“合理”的陈述是否值得权衡。我们将使用基本检索器作为它们的工具。
from typing import List, Tuple
from langchain.agents import AgentExecutor
from langchain.agents.format_scratchpad import format_to_openai_functions
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.pydantic_v1 import BaseModel, Field
from langchain.schema.messages import AIMessage, HumanMessage
from langchain.tools import tool
from langchain.tools.render import format_tool_to_openai_function
# This is used to tell the model how to best use the retriever.
@tool
def search(query, callbacks=None):
"""Search the LangChain docs with the retriever."""
return retriever.get_relevant_documents(query, callbacks=callbacks)
tools = [search]
llm = ChatOpenAI(model="gpt-4-1106-preview", temperature=0)
assistant_system_message = """You are a helpful assistant tasked with answering technical questions about LangChain. \
Use tools (only if necessary) to best answer the users questions. Do not make up information if you cannot find the answer using your tools."""
prompt = ChatPromptTemplate.from_messages(
[
("system", assistant_system_message),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in tools])
def _format_chat_history(chat_history: List[Tuple[str, str]]):
buffer = []
for human, ai in chat_history:
buffer.append(HumanMessage(content=human))
buffer.append(AIMessage(content=ai))
return buffer
agent = (
{
"input": lambda x: x["input"],
"chat_history": lambda x: _format_chat_history(x["chat_history"]),
"agent_scratchpad": lambda x: format_to_openai_functions(
x["intermediate_steps"]
),
}
| prompt
| llm_with_tools
| OpenAIFunctionsAgentOutputParser()
)
class AgentInput(BaseModel):
input: str
chat_history: List[Tuple[str, str]] = Field(..., extra={"widget": {"type": "chat"}})
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=False).with_types(
input_type=AgentInput
)
class ChainInput(BaseModel):
question: str
def mapper(input: dict):
return {"input": input["question"], "chat_history": []}
agent_executor = (mapper | agent_executor | (lambda x: x["output"])).with_types(
input_type=ChainInput
)
oai_functions_test_run = client.run_on_dataset(
dataset_name=langchain_docs.name,
llm_or_chain_factory=agent_executor,
evaluation=RAG_EVALUATION,
project_name=f"oai-functions basic rag-simple {run_uid}",
project_metadata={
"index_method": "basic",
"embedding_model": "thenlper/gte-base",
"llm": "gpt-4-1106-preview",
"architecture": "oai-functions-agent",
},
verbose=True,
)
助手#
OpenAI 通过其助手 API 提供了一个托管代理服务。
您可以将您的 LangChain 检索器连接到 OpenAI 的助手 API 并评估其性能。让我们在下面进行测试
import json
from langchain.agents import AgentExecutor
from langchain.tools import tool
from langchain_experimental.openai_assistant import OpenAIAssistantRunnable
@tool
def search(query, callbacks=None) -> str:
"""Search the LangChain docs with the retriever."""
docs = retriever.get_relevant_documents(query, callbacks=callbacks)
return json.dumps([doc.dict() for doc in docs])
tools = [search]
agent = OpenAIAssistantRunnable.create_assistant(
name="langchain docs assistant",
instructions="You are a helpful assistant tasked with answering technical questions about LangChain.",
tools=tools,
model="gpt-4-1106-preview",
as_agent=True,
)
assistant_exector = (
(lambda x: {"content": x["question"]})
| AgentExecutor(agent=agent, tools=tools)
| (lambda x: x["output"])
)
assistant_test_run = client.run_on_dataset(
dataset_name=langchain_docs.name,
llm_or_chain_factory=assistant_exector,
evaluation=RAG_EVALUATION,
project_name=f"oai-assistant basic rag-simple {run_uid}",
project_metadata={
"index_method": "basic",
"embedding_model": "thenlper/gte-base",
"llm": "gpt-4-1106-preview",
"architecture": "oai-assistant",
},
verbose=True,
)
assistant_test_run.get_aggregate_feedback()