检索增强生成(RAG)是一种通过为大型语言模型(LLM)提供相关外部知识来增强它们的技术。它已成为构建 LLM 应用程序最广泛使用的方法之一。 本教程将向您展示如何使用 LangSmith 评估您的 RAG 应用程序。您将学习:
  1. 如何创建测试数据集
  2. 如何在这些数据集上运行您的 RAG 应用程序
  3. 如何使用不同的评估指标衡量应用程序的性能

概述

典型的 RAG 评估工作流程包括三个主要步骤:
  1. 创建包含问题及其预期答案的数据集
  2. 在这些问题上运行您的 RAG 应用程序
  3. 使用评估器衡量应用程序的表现,查看以下因素:
    • 答案相关性
    • 答案准确性
    • 检索质量
对于本教程,我们将创建并评估一个机器人,该机器人回答有关 Lilian Weng’s 几篇富有洞察力的博客文章的问题。

设置

环境

首先,让我们设置环境变量:
import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "YOUR LANGSMITH API KEY"
os.environ["OPENAI_API_KEY"] = "YOUR OPENAI API KEY"
并安装我们需要的依赖项:
pip install -U langsmith langchain[openai] langchain-community

应用程序

虽然本教程使用 LangChain,但这里演示的评估技术和 LangSmith 功能适用于任何框架。请随意使用您喜欢的工具和库。
在本节中,我们将构建一个基本的检索增强生成(RAG)应用程序。 我们将坚持一个简单的实现:
  • 索引:将 Lilian Weng 的几篇博客分块并索引到向量存储中
  • 检索:根据用户问题检索这些块
  • 生成:将问题和检索的文档传递给 LLM。

索引和检索

首先,让我们加载我们想要为其构建聊天机器人的博客文章并对其进行索引。
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

# List of URLs to load documents from
urls = [
    "https://lilianweng.github.io/posts/2023-06-23-agent/",
    "https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/",
    "https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/",
]

# Load documents from the URLs
docs = [WebBaseLoader(url).load() for url in urls]
docs_list = [item for sublist in docs for item in sublist]

# Initialize a text splitter with specified chunk size and overlap
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=250, chunk_overlap=0
)

# Split the documents into chunks
doc_splits = text_splitter.split_documents(docs_list)

# Add the document chunks to the "vector store" using OpenAIEmbeddings
vectorstore = InMemoryVectorStore.from_documents(
    documents=doc_splits,
    embedding=OpenAIEmbeddings(),
)

# With langchain we can easily turn any vector store into a retrieval component:
retriever = vectorstore.as_retriever(k=6)

生成

现在我们可以定义生成管道。
from langchain_openai import ChatOpenAI
from langsmith import traceable

llm = ChatOpenAI(model="gpt-4o", temperature=1)

# 添加装饰器以便在 LangSmith 中跟踪此函数
@traceable()
def rag_bot(question: str) -> dict:
    # LangChain 检索器会自动跟踪
    docs = retriever.invoke(question)
    docs_string = "".join(doc.page_content for doc in docs)
    instructions = f"""你是一个擅长分析参考资料并回答问题的智能助理。
       使用以下源文档回答用户的问题。
       如果你不知道答案,就直接说明不知道。
       最多使用三句话,并保持回答简洁。

文档:
{docs_string}"""
    # LangChain ChatModel 会自动被 traced
    ai_msg = llm.invoke([
            {"role": "system", "content": instructions},
            {"role": "user", "content": question},
        ],
    )
    return {"answer": ai_msg.content, "documents": docs}

数据集

现在我们有了应用程序,让我们构建一个数据集来评估它。在这种情况下,我们的数据集将非常简单:我们将有示例问题和参考答案。
from langsmith import Client

client = Client()

# 为数据集定义示例
examples = [
    {
        "inputs": {"question": "ReAct 智能体如何利用自我反思?"},
        "outputs": {"answer": "ReAct 将推理与行动融合在一起:先调用诸如 Wikipedia 搜索 API 等工具执行动作,再对工具输出进行观察与推理。"},
    },
    {
        "inputs": {"question": "少样本提示会产生哪些偏差类型?"},
        "outputs": {"answer": "少样本提示可能出现的偏差包括: (1) 多数标签偏差、(2) 近期偏差、(3) 常见 token 偏差。"},
    },
    {
        "inputs": {"question": "常见的五种对抗攻击类型是什么?"},
        "outputs": {"answer": "常见的五种对抗攻击包括: (1) Token 篡改、(2) 基于梯度的攻击、(3) Jailbreak 提示、(4) 人工红队测试、(5) 模型红队测试。"},
    },
]

# 在 LangSmith 中创建数据集和示例
dataset_name = "Lilian Weng 博客问答"
dataset = client.create_dataset(dataset_name=dataset_name)
client.create_examples(
    dataset_id=dataset.id,
    examples=examples
)

评估器

思考不同类型的 RAG 评估器的一种方法是作为正在评估的内容 X 它所针对评估的内容的元组:
  1. 正确性:响应 vs 参考答案
  • 目标:测量”相对于真实答案,RAG 链答案有多相似/正确
  • 模式:需要通过数据集提供的真实(参考)答案
  • 评估器:使用 LLM 作为评判者来评估答案正确性。
  1. 相关性:响应 vs 输入
  • 目标:测量”生成的响应在多大程度上解决了初始用户输入
  • 模式:不需要参考答案,因为它将答案与输入问题进行比较
  • 评估器:使用 LLM 作为评判者来评估答案相关性、帮助性等。
  1. 基础性:响应 vs 检索的文档
  • 目标:测量”生成的响应在多大程度上与检索的上下文一致
  • 模式:不需要参考答案,因为它将答案与检索的上下文进行比较
  • 评估器:使用 LLM 作为评判者来评估忠实性、幻觉等。
  1. 检索相关性:检索的文档 vs 输入
  • 目标:测量”我检索的结果对于此查询的相关性如何
  • 模式:不需要参考答案,因为它将问题与检索的上下文进行比较
  • 评估器:使用 LLM 作为评判者来评估相关性

正确性:响应 vs 参考答案

from typing_extensions import Annotated, TypedDict

# 评分输出模式
class CorrectnessGrade(TypedDict):
    # 字段定义顺序决定了模型生成字段的顺序。
    # 先让模型给出解释,再输出最终判断,有助于它先思考再作答:
    explanation: Annotated[str, ..., "请说明你给出该分数的理由"]
    correct: Annotated[bool, ..., "如果答案正确则为 True,否则为 False。"]

# 评分提示词
correctness_instructions = """你是一名正在批改测验的老师。你会获得 QUESTION、GROUND TRUTH(正确答案)和 STUDENT ANSWER。请遵循以下评分标准:
(1) 仅根据学生答案相对于正确答案的事实准确性评分。
(2) 确保学生答案中不存在与正确答案冲突的陈述。
(3) 如果学生答案包含比正确答案更多的信息,只要这些信息与正确答案一致且准确,也算正确。

Correctness(正确性):
当正确性为 True 时,表示学生答案满足全部评分标准。
当正确性为 False 时,表示学生答案未满足全部评分标准。

请按步骤解释你的推理,以确保结论准确。避免在开头直接给出正确答案。"""

# 评分器 LLM
grader_llm = ChatOpenAI(model="gpt-4o", temperature=0).with_structured_output(
    CorrectnessGrade, method="json_schema", strict=True
)

def correctness(inputs: dict, outputs: dict, reference_outputs: dict) -> bool:
    """RAG 答案准确性的评估器"""
    answers = f"""\
QUESTION: {inputs['question']}
GROUND TRUTH ANSWER: {reference_outputs['answer']}
STUDENT ANSWER: {outputs['answer']}"""
    # 运行评估器
    grade = grader_llm.invoke([
        {"role": "system", "content": correctness_instructions},
        {"role": "user", "content": answers}
    ])
    return grade["correct"]

相关性:响应 vs 输入

流程与上面类似,但我们只查看 inputsoutputs,而不需要 reference_outputs。没有参考答案,我们无法评分准确性,但仍然可以评分相关性——即模型是否解决了用户的问题。
# 评分输出模式
class RelevanceGrade(TypedDict):
    explanation: Annotated[str, ..., "请说明你给出该分数的理由"]
    relevant: Annotated[
        bool, ..., "判断该答案是否真正回应了问题的评分结果"
    ]

# 评分提示
relevance_instructions = """你是一名正在批改测验的老师。你会获得 QUESTION 和 STUDENT ANSWER。请遵循以下评分标准:
(1) 确保学生答案简洁且与问题相关。
(2) 确保学生答案确实有助于回答该问题。

Relevance(相关性):
当相关性为 True 时,表示学生答案满足全部评分标准。
当相关性为 False 时,表示学生答案未满足全部评分标准。

请按步骤解释你的推理,以确保推理过程与结论正确。避免在开头直接给出正确答案。"""

# 评分器 LLM
relevance_llm = ChatOpenAI(model="gpt-4o", temperature=0).with_structured_output(
    RelevanceGrade, method="json_schema", strict=True
)

# 评估函数
def relevance(inputs: dict, outputs: dict) -> bool:
    """用于评估 RAG 答案是否有帮助的简单评估器。"""
    answer = f"QUESTION: {inputs['question']}\nSTUDENT ANSWER: {outputs['answer']}"
    grade = relevance_llm.invoke([
        {"role": "system", "content": relevance_instructions},
        {"role": "user", "content": answer}
    ])
    return grade["relevant"]

基础性:响应 vs 检索的文档

评估响应而不需要参考答案的另一种有用方法是检查响应是否由检索的文档证明(或”基于”检索的文档)。
# 评分输出模式
class GroundedGrade(TypedDict):
    explanation: Annotated[str, ..., "请说明你给出该分数的理由"]
    grounded: Annotated[
        bool, ..., "判断答案是否脱离文档产生幻觉的评分结果"
    ]

# 评分提示
grounded_instructions = """你是一名正在批改测验的老师。你会获得 FACTS 和 STUDENT ANSWER。请遵循以下评分标准:
(1) 确保学生答案基于 FACTS。
(2) 确保学生答案不包含超出 FACTS 范围的“幻觉”信息。

Grounded(基于文档):
当 grounded 为 True 时,表示学生答案满足全部评分标准。
当 grounded 为 False 时,表示学生答案未满足全部评分标准。

请按步骤解释你的推理,以确保推理过程与结论正确。避免在开头直接给出正确答案。"""

# 评分器 LLM
grounded_llm = ChatOpenAI(model="gpt-4o", temperature=0).with_structured_output(
    GroundedGrade, method="json_schema", strict=True
)

# 评估函数
def groundedness(inputs: dict, outputs: dict) -> bool:
    """用于检查 RAG 答案是否基于检索文档的简单评估器。"""
    doc_string = "\n\n".join(doc.page_content for doc in outputs["documents"])
    answer = f"FACTS: {doc_string}\nSTUDENT ANSWER: {outputs['answer']}"
    grade = grounded_llm.invoke([
        {"role": "system", "content": grounded_instructions},
        {"role": "user", "content": answer}
    ])
    return grade["grounded"]

检索相关性:检索的文档 vs 输入

# 评分输出模式
class RetrievalRelevanceGrade(TypedDict):
    explanation: Annotated[str, ..., "请说明你给出该分数的理由"]
    relevant: Annotated[
        bool,
        ...,
        "如果检索到的文档与问题相关则为 True,否则为 False",
    ]

# Grade prompt
retrieval_relevance_instructions = """你是一名正在批改测验的老师。你会获得一个 QUESTION 和一组由学生提供的 FACTS。请遵循以下评分标准:
(1) 你的目标是识别与 QUESTION 完全无关的 FACTS。
(2) 如果 FACTS 包含任何与问题相关的关键词或语义,请考虑它们相关。
(3) 即使 FACTS 包含一些与问题无关的信息,只要 (2) 满足,也可以接受。

Relevance(相关性):
当相关性为 True 时,表示 FACTS 包含与 QUESTION 相关的任何关键词或语义,因此相关。
当相关性为 False 时,表示 FACTS 与 QUESTION 完全无关。

请按步骤解释你的推理,以确保推理过程与结论正确。避免在开头直接给出正确答案。"""

# Grader LLM
retrieval_relevance_llm = ChatOpenAI(
    model="gpt-4o", temperature=0
).with_structured_output(RetrievalRelevanceGrade, method="json_schema", strict=True)

def retrieval_relevance(inputs: dict, outputs: dict) -> bool:
    """用于评估检索文档相关性的评估器"""
    doc_string = "\n\n".join(doc.page_content for doc in outputs["documents"])
    answer = f"FACTS: {doc_string}\nQUESTION: {inputs['question']}"
    # Run evaluator
    grade = retrieval_relevance_llm.invoke([
        {"role": "system", "content": retrieval_relevance_instructions},
        {"role": "user", "content": answer}
    ])
    return grade["relevant"]

运行评估

现在我们可以使用所有不同的评估器启动评估作业。
def target(inputs: dict) -> dict:
    return rag_bot(inputs["question"])

experiment_results = client.evaluate(
    target,
    data=dataset_name,
    evaluators=[correctness, groundedness, relevance, retrieval_relevance],
    experiment_prefix="rag-doc-relevance",
    metadata={"version": "LCEL context, gpt-4-0125-preview"},
)

# Explore results locally as a dataframe if you have pandas installed
# experiment_results.to_pandas()
你可以在此处查看示例结果展示:LangSmith 链接

参考代码

from langchain_community.document_loaders import WebBaseLoader
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langsmith import Client, traceable
from typing_extensions import Annotated, TypedDict

# List of URLs to load documents from
urls = [
    "https://lilianweng.github.io/posts/2023-06-23-agent/",
    "https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/",
    "https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/",
]

# Load documents from the URLs
docs = [WebBaseLoader(url).load() for url in urls]
docs_list = [item for sublist in docs for item in sublist]

# Initialize a text splitter with specified chunk size and overlap
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=250, chunk_overlap=0
)

# Split the documents into chunks
doc_splits = text_splitter.split_documents(docs_list)

# Add the document chunks to the "vector store" using OpenAIEmbeddings
vectorstore = InMemoryVectorStore.from_documents(
    documents=doc_splits,
    embedding=OpenAIEmbeddings(),
)

# With langchain we can easily turn any vector store into a retrieval component:
retriever = vectorstore.as_retriever(k=6)

llm = ChatOpenAI(model="gpt-4o", temperature=1)

# 添加装饰器以便在 LangSmith 中跟踪此函数
@traceable()
def rag_bot(question: str) -> dict:
    # LangChain 检索器会自动跟踪
    docs = retriever.invoke(question)
    docs_string = "".join(doc.page_content for doc in docs)
    instructions = f"""你是一个擅长分析参考资料并回答问题的智能助理。
       使用以下源文档回答用户的问题。
       如果你不知道答案,就直接说明不知道。
       最多使用三句话,并保持回答简洁。

文档:
{docs_string}"""
    # LangChain ChatModel 会自动被 traced
    ai_msg = llm.invoke([
            {"role": "system", "content": instructions},
            {"role": "user", "content": question},
        ],
    )
    return {"answer": ai_msg.content, "documents": docs}

client = Client()

# Define the examples for the dataset
examples = [
    {
        "inputs": {"question": "ReAct 智能体如何利用自我反思?"},
        "outputs": {"answer": "ReAct 将推理与行动融合在一起:先调用诸如 Wikipedia 搜索 API 等工具执行动作,再对工具输出进行观察与推理。"},
    },
    {
        "inputs": {"question": "少样本提示会产生哪些偏差类型?"},
        "outputs": {"answer": "少样本提示可能出现的偏差包括: (1) 多数标签偏差、(2) 近期偏差、(3) 常见 token 偏差。"},
    },
    {
        "inputs": {"question": "常见的五种对抗攻击类型是什么?"},
        "outputs": {"answer": "常见的五种对抗攻击包括: (1) Token 篡改、(2) 基于梯度的攻击、(3) Jailbreak 提示、(4) 人工红队测试、(5) 模型红队测试。"},
    },
]

# Create the dataset and examples in LangSmith
dataset_name = "Lilian Weng 博客问答"
if not client.has_dataset(dataset_name=dataset_name):
    dataset = client.create_dataset(dataset_name=dataset_name)
    client.create_examples(
        dataset_id=dataset.id,
        examples=examples
    )

# Grade output schema
class CorrectnessGrade(TypedDict):
    # 字段定义顺序决定了模型生成字段的顺序。
    # 先让模型给出解释,再输出最终判断,有助于它先思考再作答:
    explanation: Annotated[str, ..., "请说明你给出该分数的理由"]
    correct: Annotated[bool, ..., "如果答案正确则为 True,否则为 False。"]

# 评分提示词
correctness_instructions = """你是一名正在批改测验的老师。你会获得 QUESTION、GROUND TRUTH(正确答案)和 STUDENT ANSWER。请遵循以下评分标准:
(1) 仅根据学生答案相对于正确答案的事实准确性评分。
(2) 确保学生答案中不存在与正确答案冲突的陈述。
(3) 如果学生答案包含比正确答案更多的信息,只要这些信息与正确答案一致且准确,也算正确。

Correctness(正确性):
当正确性为 True 时,表示学生答案满足全部评分标准。
当正确性为 False 时,表示学生答案未满足全部评分标准。

请按步骤解释你的推理,以确保结论准确。避免在开头直接给出正确答案。"""

# 评分器 LLM
grader_llm = ChatOpenAI(model="gpt-4o", temperature=0).with_structured_output(
    CorrectnessGrade, method="json_schema", strict=True
)

def correctness(inputs: dict, outputs: dict, reference_outputs: dict) -> bool:
    """An evaluator for RAG answer accuracy"""
    answers = f"""\
QUESTION: {inputs['question']}
GROUND TRUTH ANSWER: {reference_outputs['answer']}
STUDENT ANSWER: {outputs['answer']}"""
    # Run evaluator
    grade = grader_llm.invoke([
            {"role": "system", "content": correctness_instructions},
            {"role": "user", "content": answers},
        ]
    )
    return grade["correct"]

# Grade output schema
class RelevanceGrade(TypedDict):
    explanation: Annotated[str, ..., "请说明你给出该分数的理由"]
    relevant: Annotated[
        bool, ..., "判断该答案是否真正回应了问题的评分结果"
    ]

# 评分提示
relevance_instructions = """你是一名正在批改测验的老师。你会获得 QUESTION 和 STUDENT ANSWER。请遵循以下评分标准:
(1) 确保学生答案简洁且与问题相关。
(2) 确保学生答案确实有助于回答该问题。

Relevance(相关性):
当相关性为 True 时,表示学生答案满足全部评分标准。
当相关性为 False 时,表示学生答案未满足全部评分标准。

请按步骤解释你的推理,以确保推理过程与结论正确。避免在开头直接给出正确答案。"""

# 评分器 LLM
relevance_llm = ChatOpenAI(model="gpt-4o", temperature=0).with_structured_output(
    RelevanceGrade, method="json_schema", strict=True
)

# 评估函数
def relevance(inputs: dict, outputs: dict) -> bool:
    """用于评估 RAG 答案是否有帮助的简单评估器。"""
    answer = f"QUESTION: {inputs['question']}\nSTUDENT ANSWER: {outputs['answer']}"
    grade = relevance_llm.invoke([
        {"role": "system", "content": relevance_instructions},
        {"role": "user", "content": answer}
    ])
    return grade["relevant"]

# Grade output schema
class GroundedGrade(TypedDict):
    explanation: Annotated[str, ..., "请说明你给出该分数的理由"]
    grounded: Annotated[
        bool, ..., "判断答案是否脱离文档产生幻觉的评分结果"
    ]

# 评分提示
grounded_instructions = """你是一名正在批改测验的老师。你会获得 FACTS 和 STUDENT ANSWER。请遵循以下评分标准:
(1) 确保学生答案基于 FACTS。
(2) 确保学生答案不包含超出 FACTS 范围的“幻觉”信息。

Grounded(基于文档):
当 grounded 为 True 时,表示学生答案满足全部评分标准。
当 grounded 为 False 时,表示学生答案未满足全部评分标准。

请按步骤解释你的推理,以确保推理过程与结论正确。避免在开头直接给出正确答案。"""

# 评分器 LLM
grounded_llm = ChatOpenAI(model="gpt-4o", temperature=0).with_structured_output(
    GroundedGrade, method="json_schema", strict=True
)

# 评估函数
def groundedness(inputs: dict, outputs: dict) -> bool:
    """用于检查 RAG 答案是否基于检索文档的简单评估器。"""
    doc_string = "\n\n".join(doc.page_content for doc in outputs["documents"])
    answer = f"FACTS: {doc_string}\nSTUDENT ANSWER: {outputs['answer']}"
    grade = grounded_llm.invoke([
        {"role": "system", "content": grounded_instructions},
        {"role": "user", "content": answer}
    ])
    return grade["grounded"]

# Grade output schema
class RetrievalRelevanceGrade(TypedDict):
    explanation: Annotated[str, ..., "请说明你给出该分数的理由"]
    relevant: Annotated[
        bool,
        ...,
        "如果检索到的文档与问题相关则为 True,否则为 False",
    ]

# Grade prompt
retrieval_relevance_instructions = """你是一名正在批改测验的老师。你会获得一个 QUESTION 和一组由学生提供的 FACTS。请遵循以下评分标准:
(1) 你的目标是识别与 QUESTION 完全无关的 FACTS。
(2) 如果 FACTS 包含任何与问题相关的关键词或语义,请考虑它们相关。
(3) 即使 FACTS 包含一些与问题无关的信息,只要 (2) 满足,也可以接受。

Relevance(相关性):
当相关性为 True 时,表示 FACTS 包含与 QUESTION 相关的任何关键词或语义,因此相关。
当相关性为 False 时,表示 FACTS 与 QUESTION 完全无关。

请按步骤解释你的推理,以确保推理过程与结论正确。避免在开头直接给出正确答案。"""

# Grader LLM
retrieval_relevance_llm = ChatOpenAI(
    model="gpt-4o", temperature=0
).with_structured_output(RetrievalRelevanceGrade, method="json_schema", strict=True)

def retrieval_relevance(inputs: dict, outputs: dict) -> bool:
    """用于评估检索文档相关性的评估器"""
    doc_string = "\n\n".join(doc.page_content for doc in outputs["documents"])
    answer = f"FACTS: {doc_string}\nQUESTION: {inputs['question']}"
    # Run evaluator
    grade = retrieval_relevance_llm.invoke([
        {"role": "system", "content": retrieval_relevance_instructions},
        {"role": "user", "content": answer}
    ])
    return grade["relevant"]

def target(inputs: dict) -> dict:
    return rag_bot(inputs["question"])

experiment_results = client.evaluate(
    target,
    data=dataset_name,
    evaluators=[correctness, groundedness, relevance, retrieval_relevance],
    experiment_prefix="rag-doc-relevance",
    metadata={"version": "LCEL context, gpt-4-0125-preview"},
)

# Explore results locally as a dataframe if you have pandas installed
# experiment_results.to_pandas()

Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.