以编程方式使用文档

概述

在本教程中,我们将构建一个 检索 智能体。

LangChain 提供内置 智能体 实现,采用 LangGraph 原语构建。如果需要更深入的定制,可以直接在 LangGraph 中实现智能体。本指南演示了检索智能体的示例实现。 检索 智能体在您希望 LLM 决定是从向量存储中检索上下文还是直接回复用户时很有用。

在本教程结束时,我们将完成以下操作:

  1. 获取并预处理将用于检索的文档。
  2. 为语义搜索索引这些文档,并为智能体创建检索器工具。
  3. 构建一个可以决定何时使用检索器工具的智能体 RAG 系统。

!混合 RAG

概念

我们将涵盖以下概念:

设置

让我们下载所需的包并设置 API 密钥:

npm install @langchain/langgraph @langchain/openai @langchain/textsplitters cheerio
pnpm install @langchain/langgraph @langchain/openai @langchain/textsplitters cheerio
yarn add @langchain/langgraph @langchain/openai @langchain/textsplitters cheerio
bun add @langchain/langgraph @langchain/openai @langchain/textsplitters cheerio

1. 预处理文档

1. 获取要在 RAG 系统中使用的文档。我们将使用 Lilian Weng 优秀的博客中最近的三篇文章。我们将首先使用基于 fetch 构建的最小辅助函数获取页面的内容 cheerio: 2. 将获取的文档分割成更小的块,以便索引到向量存储中:

2. 创建检索器工具

现在我们已经有了分割好的文档,可以将它们索引到用于语义搜索的向量存储中。

1. 使用内存向量存储和 OpenAI 嵌入: 2. 使用 LangChain 的预构建 createRetrieverTool:

  const tool = createRetrieverTool(
    retriever,
    {
      name: "retrieve_blog_posts",
      description:
        "Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.",
    },
  );
  const tools = [tool];
  

3. 生成查询

现在我们开始构建组件(节点)用于我们的代理式 RAG 图。

1. 构建一个 generateQueryOrRespond 节点。它将调用 LLM 根据当前图状态(消息列表)生成响应。根据输入消息,它将决定使用检索工具进行检索,或直接响应用户。请注意,我们通过 tools 向聊天模型提供我们之前创建的 .bindTools: 2. 用随机输入测试:

  const input = { messages: [new HumanMessage("hello!")] };
  const result = await generateQueryOrRespond(input);
  console.log(result.messages[0]);
  

Output:

  AIMessage {
    content: "Hello! How can I help you today?",
    tool_calls: []
  }
  

3. 提出一个需要语义搜索的问题:

  const input = {
    messages: [
      new HumanMessage("What does Lilian Weng say about types of reward hacking?")
    ]
  };
  const result = await generateQueryOrRespond(input);
  console.log(result.messages[0]);
  

Output:

  AIMessage {
    content: "",
    tool_calls: [
      {
        name: "retrieve_blog_posts",
        args: { query: "types of reward hacking" },
        id: "call_...",
        type: "tool_call"
      }
    ]
  }
  

4. 给文档评分

1. 添加一个节点——gradeDocuments——用于确定检索到的文档是否与问题相关。该节点首先使用带有 Zod 结构化输出的模型进行文档评分,如果结构化解析失败则回退到简单的 yes 或 no 响应。然后我们添加一个 条件边 根据 gradeDocuments 结果进行路由(generate or rewrite): 2. 使用工具响应中无关的文档运行此操作:

  const input = {
    messages: [
      new HumanMessage("What does Lilian Weng say about types of reward hacking?"),
      new AIMessage({
        tool_calls: [
          {
            type: "tool_call",
            name: "retrieve_blog_posts",
            args: { query: "types of reward hacking" },
            id: "1",
          }
        ]
      }),
      new ToolMessage({
        content: "meow",
        tool_call_id: "1",
      })
    ]
  }
  const result = await gradeDocuments(input);
  

3. 确认相关文档被正确分类:

  const input = {
    messages: [
      new HumanMessage("What does Lilian Weng say about types of reward hacking?"),
      new AIMessage({
        tool_calls: [
          {
            type: "tool_call",
            name: "retrieve_blog_posts",
            args: { query: "types of reward hacking" },
            id: "1",
          }
        ]
      }),
      new ToolMessage({
        content: "reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering",
        tool_call_id: "1",
      })
    ]
  }
  const result = await gradeDocuments(input);
  

5. 重写问题

1. 构建 rewrite 节点。检索工具可能返回潜在不相关的文档,这表明需要改进原始用户问题。为此,我们将调用 rewrite node: 2. 试试看:

  const input = {
    messages: [
      new HumanMessage("What does Lilian Weng say about types of reward hacking?"),
      new AIMessage({
        content: "",
        tool_calls: [
          {
            id: "1",
            name: "retrieve_blog_posts",
            args: { query: "types of reward hacking" },
            type: "tool_call"
          }
        ]
      }),
      new ToolMessage({ content: "meow", tool_call_id: "1" })
    ]
  };

  const response = await rewrite(input);
  console.log(response.messages[0].content);
  

Output:

  What are the different types of reward hacking described by Lilian Weng, and how does she explain them?
  

6. 生成答案

1. 构建 generate 节点:如果我们通过了评估器检查,我们可以根据原始问题和检索到的上下文生成最终答案: 2. 试一试:

  const input = {
    messages: [
      new HumanMessage("What does Lilian Weng say about types of reward hacking?"),
      new AIMessage({
        content: "",
        tool_calls: [
          {
            id: "1",
            name: "retrieve_blog_posts",
            args: { query: "types of reward hacking" },
            type: "tool_call"
          }
        ]
      }),
      new ToolMessage({
        content: "reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering",
        tool_call_id: "1"
      })
    ]
  };

  const response = await generate(input);
  console.log(response.messages[0].content);
  

Output:

  Lilian Weng categorizes reward hacking into two types: environment or goal misspecification, and reward tampering. She considers reward hacking as a broad concept that includes both of these categories. Reward hacking occurs when an agent exploits flaws or ambiguities in the reward function to achieve high rewards without performing the intended behaviors.
  

7. 组装图

现在我们将把所有节点和边组装成一个完整的图:

  • * 从一个 generateQueryOrRespond 开始,确定是否需要调用检索工具
  • * 使用条件边路由到下一步:
  • * If generateQueryOrRespond 返回 tool_calls,调用检索工具来检索上下文
  • * 否则,直接响应用户
  • * 评估检索到的文档内容与问题的相关性(gradeDocuments)并路由到下一步:
  • * 如果不相关,使用 rewrite 重写问题,然后调用 generateQueryOrRespond 再次
  • * 如果相关,继续进行到 generate 并使用 ToolMessage 结合检索到的文档上下文生成最终响应

8. 运行智能 RAG

现在让我们通过一个问题来测试完整的图: