LangChain——代理架构

23 阅读17分钟

在第五章描述的架构基础上,本章将介绍当前所有 LLM 架构中或许最重要的——代理架构。首先,我们介绍使 LLM 代理独特的特点,然后展示如何构建这些代理,以及如何将它们扩展以适应常见的使用案例。

在人工智能领域,创建(智能)代理有着悠久的历史,最简单的定义是“能够采取行动的东西”,正如 Stuart Russell 和 Peter Norvig 在他们的《人工智能》教科书(Pearson, 2020)中所说。事实上,“采取行动”这个词比字面意思更具含义:

  • 采取行动需要某种能力来决定该做什么。
  • 决定该做什么意味着有多种可能的行动方式可供选择。毕竟,没有选项的决定根本不是决定。
  • 为了做出决定,代理还需要获取有关外部环境的信息(即任何在代理本身之外的事物)。

因此,一个代理型的 LLM 应用必须是这样的应用:它使用 LLM 根据关于当前世界状态或某个期望的下一状态的上下文,从一个或多个可能的行动方案中进行选择。这些属性通常通过混合两种我们在前言中首先遇到的提示技术来实现:

工具调用

在你的提示中包括一个外部函数列表,LLM 可以利用这些函数(即它可以决定采取的行动),并提供有关如何在生成的输出中格式化其选择的说明。稍后你将看到这在提示中的具体样子。

思维链

研究人员发现,当 LLM 被指示通过将复杂问题分解为逐步执行的任务来进行推理时,它们“做出更好的决策”。这通常是通过添加“逐步思考”之类的指令,或者包括问题的示例并将其分解成多个步骤/行动来实现的。

以下是一个使用工具调用和思维链的示例提示:

工具:

  • search: 这个工具接受一个网页搜索查询并返回最相关的结果。
  • calculator: 这个工具接受数学表达式并返回其结果。

如果你想使用工具得出答案,请以 CSV 格式输出工具和输入的列表,第一行是标题:tool,input

逐步思考;如果你需要调用多个工具来得出答案,返回的只有第一个。

问题: 美国第30任总统去世时多大?

tool,input

search,30th president of the United States

当在 gpt-3.5-turbo 上以温度 0(确保 LLM 按所需的输出格式,即 CSV)和换行符作为停止序列运行时(指示 LLM 当达到此字符时停止生成输出),它会生成如下输出。这样,LLM 会产生一个单一的行动(如预期,因为提示要求如此):

search,30th president of the United States

最新的 LLM 和聊天模型已经经过微调,以提高它们在工具调用和思维链应用中的表现,消除了在提示中添加特定指令的需要: (添加工具调用模型的示例提示和输出)

计划-执行循环

代理架构与第五章讨论的架构不同之处在于我们尚未涉及的一个概念:LLM 驱动的循环。

每个程序员在编写代码时都遇到过循环。这里的“循环”指的是反复执行相同的代码,直到满足停止条件。代理架构的关键在于让 LLM 控制停止条件——也就是说,决定何时停止循环。

在这个循环中,我们将运行以下某种变体:

  • 规划一个或多个行动
  • 执行上述行动

以之前部分的示例为基础,我们接下来将使用搜索工具,输入“美国第30任总统”,它将生成以下输出:

Calvin Coolidge (born John Calvin Coolidge Jr.; /ˈkuːlɪdʒ/; July 4, 1872January 
5, 1933) was an American attorney and politician who served as the 30th president 
of the United States from 1923 to 1929. John Calvin Coolidge Jr.

然后,我们将重新运行提示,添加一个小的变化: 工具:

  • search:该工具接受一个网页搜索查询并返回最相关的结果。
  • calculator:该工具接受数学表达式并返回其结果。
  • output:该工具结束交互。用它来表示你已经得到了最终答案。

如果你想使用工具来得出答案,请以 CSV 格式输出工具和输入的列表,第一行是标题:tool,input

逐步思考;如果你需要多次调用工具才能得到答案,只返回第一个调用。

问题: 美国第30任总统去世时多大?

tool,input

search,30th president of the United States

search: Calvin Coolidge (born John Calvin Coolidge Jr.; /ˈkuːlɪdʒ/; July 4, 1872 – January 5, 1933) was an American attorney and politician who served as the 30th president of the United States from 1923 to 1929. John Calvin Coolidge Jr.

tool,input

calculator,1933 - 1872

输出:

output,61

我们添加了两件事:

  1. 一个“output”工具——当 LLM 找到最终答案时应使用它,我们也将用它作为停止循环的信号。
  2. 上一步工具调用的结果,简单地包含工具的名称及其(文本)输出。这是为了让 LLM 在交互的下一步中继续进行。换句话说,我们在告诉 LLM:“嘿,我们得到了你请求的结果,接下来你想做什么?”

接下来继续进行第三次迭代:

工具:

  • search:该工具接受一个网页搜索查询并返回最相关的结果。
  • calculator:该工具接受数学表达式并返回其结果。
  • output:该工具结束交互。用它来表示你已经得到了最终答案。

如果你想使用工具来得出答案,请以 CSV 格式输出工具和输入的列表,第一行是标题:tool,input

逐步思考;如果你需要多次调用工具才能得到答案,只返回第一个调用。

问题: 美国第30任总统去世时多大?

tool,input

search,30th president of the United States

search: Calvin Coolidge (born John Calvin Coolidge Jr.; /ˈkuːlɪdʒ/; July 4, 1872 – January 5, 1933) was an American attorney and politician who served as the 30th president of the United States from 1923 to 1929. John Calvin Coolidge Jr.

tool,input

calculator,1933-1872

calculator: 61

tool,input

output, 61

通过 calculator 工具的结果,LLM 现在有足够的信息提供最终答案,所以它选择了 output 工具,并选择“61”作为最终答案。

这就是代理架构如此有用的原因——LLM 被赋予了决策权。下一步是得出答案,并决定要采取多少步骤——即何时停止。

这个架构叫做 ReAct,最早由 Shunyu Yao 等人提出。本章的其余部分将探讨如何提高代理架构的性能,灵感来自第五章中的电子邮件助手示例。

但首先,让我们看看如何使用聊天模型和 LangGraph 实现基本的代理架构。

构建 LangGraph 代理

在这个示例中,我们需要为我们选择使用的搜索工具 DuckDuckGo 安装额外的依赖。对于 Python,可以使用以下命令安装:

Python:

pip install duckduckgo-search

对于 JavaScript,我们还需要为计算器工具安装依赖:

JavaScript:

npm i duck-duck-scrape expr-eval

完成这些安装后,让我们进入实现代理架构的实际代码:

Python:

import ast
from typing import Annotated, TypedDict

from langchain_community.tools import DuckDuckGoSearchRun
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

from langgraph.graph import START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition

@tool
def calculator(query: str) -> str:
    """A simple calculator tool. Input should be a mathematical expression."""
    return ast.literal_eval(query)

search = DuckDuckGoSearchRun()
tools = [search, calculator]
model = ChatOpenAI(temperature=0.1).bind_tools(tools)

class State(TypedDict):
    messages: Annotated[list, add_messages]

def model_node(state: State) -> State:
    res = model.invoke(state["messages"])
    return {"messages": res}

builder = StateGraph(State)
builder.add_node("model", model_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "model")
builder.add_conditional_edges("model", tools_condition)
builder.add_edge("tools", "model")

graph = builder.compile()

JavaScript:

import {
  DuckDuckGoSearch
} from "@langchain/community/tools/duckduckgo_search";
import {
  Calculator
} from "@langchain/community/tools/calculator";
import {
  StateGraph,
  Annotation,
  messagesStateReducer,
  START,
} from "@langchain/langgraph";
import {
  ToolNode,
  toolsCondition
} from "@langchain/langgraph/prebuilt";

const search = new DuckDuckGoSearch();
const calculator = new Calculator();
const tools = [search, calculator];
const model = new ChatOpenAI({
  temperature: 0.1
}).bindTools(tools);

const annotation = Annotation.Root({
  messages: Annotation({
    reducer: messagesStateReducer,
    default: () => []
  }),
});

async function modelNode(state) {
  const res = await model.invoke(state.messages);
  return { messages: res };
}

const builder = new StateGraph(annotation)
  .addNode("model", modelNode)
  .addNode("tools", new ToolNode(tools))
  .addEdge(START, "model")
  .addConditionalEdges("model", toolsCondition)
  .addEdge("tools", "model");

const graph = builder.compile();

图形的可视化表示如图 6-1 所示。

image.png

一些需要注意的事项:

在这个示例中,我们使用了两个工具:一个是搜索工具,另一个是计算器工具,但你可以很容易地添加更多工具或替换我们使用的工具。在 Python 示例中,你还可以看到如何创建一个自定义工具的示例。

我们使用了 LangGraph 提供的两个便捷函数。ToolNode 作为我们图中的节点,它执行状态中最新的 AI 消息请求的工具调用,并返回一个包含每个工具结果的 ToolMessageToolNode 还会处理由工具引发的异常——使用错误信息构建一个 ToolMessage,然后传递给 LLM,LLM 可以决定如何处理这个错误。

tools_condition 作为一个条件边函数,查看状态中的最新 AI 消息,并在有工具需要执行时将流程路由到 tools 节点。如果没有工具需要执行,则结束图的执行。

最后,注意到这个图在模型节点和工具节点之间形成了一个循环。也就是说,模型本身负责决定何时结束计算,这是代理架构的一个关键特性。每当我们在 LangGraph 中编写循环时,我们可能希望使用条件边,因为它允许你定义何时退出循环并停止执行的停止条件。

接下来,我们来看一下如何在之前的示例中运行它:

Python:

input = {
    "messages": [
        HumanMessage("""How old was the 30th president of the United States 
            when he died?""")
    ]
}
for c in graph.stream(input):
    print(c)

JavaScript:

const input = {
  messages: [
    HumanMessage(`How old was the 30th president of the United States when he 
      died?`)
  ]
}
for await (const c of await graph.stream(input)) {
  console.log(c)
}

输出:

{
    "model": {
        "messages": AIMessage(
            content="",
            tool_calls=[
                {
                    "name": "duckduckgo_search",
                    "args": {
                        "query": "30th president of the United States age at 
                            death"
                    },
                    "id": "call_ZWRbPmjvo0fYkwyo4HCYUsar",
                    "type": "tool_call",
                }
            ],
        )
    }
}
{
    "tools": {
        "messages": [
            ToolMessage(
                content="Calvin Coolidge (born July 4, 1872, Plymouth, Vermont, 
                    U.S.—died January 5, 1933, Northampton, Massachusetts) was 
                    the 30th president of the United States (1923-29). Coolidge 
                    acceded to the presidency after the death in office of 
                    Warren G. Harding, just as the Harding scandals were coming 
                    to light....",
                name="duckduckgo_search",
                tool_call_id="call_ZWRbPmjvo0fYkwyo4HCYUsar",
            )
        ]
    }
}
{
    "model": {
        "messages": AIMessage(
            content="Calvin Coolidge, the 30th president of the United States, 
                died on January 5, 1933, at the age of 60.",
        )
    }
}

解析这个输出:

  1. 首先,model 节点执行并决定调用 duckduckgo_search 工具,这导致条件边将我们路由到 tools 节点。
  2. ToolNode 执行了搜索工具并返回了上面打印的搜索结果,其中包含了答案:“年龄和死亡年份。1933年1月5日(享年60岁)”。
  3. 模型工具再次被调用,这次使用搜索结果作为最新的消息,并生成了最终答案(没有更多的工具调用);因此,条件边结束了图的执行。

接下来,我们将看看如何扩展这个基本的代理架构,定制规划和工具调用的过程。

始终先调用工具

在标准的代理架构中,LLM 总是被调用来决定接下来应该调用哪个工具。这种安排有一个明显的优点:它为 LLM 提供了极大的灵活性,可以根据每个用户查询来调整应用程序的行为。但是,这种灵活性也带来了代价:不可预测性。例如,如果你作为应用的开发者知道搜索工具应该始终第一个被调用,这实际上对你的应用有益:

  • 它将减少总体延迟,因为它跳过了第一个 LLM 调用,该调用会生成请求以调用搜索工具。
  • 它将防止 LLM 错误地决定在某些用户查询中不需要调用搜索工具。

另一方面,如果你的应用没有类似“你应该始终先调用这个工具”的明确规则,强制引入这样的约束反而会使应用变得更差。

让我们来看一下如何实现这一点:

Python:

import ast
from typing import Annotated, TypedDict
from uuid import uuid4

from langchain_community.tools import DuckDuckGoSearchRun
from langchain_core.messages import AIMessage, HumanMessage, ToolCall
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

from langgraph.graph import START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition

@tool
def calculator(query: str) -> str:
    """一个简单的计算器工具。输入应该是一个数学表达式。"""
    return ast.literal_eval(query)

search = DuckDuckGoSearchRun()
tools = [search, calculator]
model = ChatOpenAI(temperature=0.1).bind_tools(tools)

class State(TypedDict):
    messages: Annotated[list, add_messages]

def model_node(state: State) -> State:
    res = model.invoke(state["messages"])
    return {"messages": res}

def first_model(state: State) -> State:
    query = state["messages"][-1].content
    search_tool_call = ToolCall(
        name="duckduckgo_search", args={"query": query}, id=uuid4().hex
    )
    return {"messages": AIMessage(content="", tool_calls=[search_tool_call])}

builder = StateGraph(State)
builder.add_node("first_model", first_model)
builder.add_node("model", model_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "first_model")
builder.add_edge("first_model", "tools")
builder.add_conditional_edges("model", tools_condition)
builder.add_edge("tools", "model")

graph = builder.compile()

JavaScript:

import {
  DuckDuckGoSearch
} from "@langchain/community/tools/duckduckgo_search";
import {
  Calculator
} from "@langchain/community/tools/calculator";
import {
  AIMessage,
} from "@langchain/core/messages";
import {
  StateGraph,
  Annotation,
  messagesStateReducer,
  START,
} from "@langchain/langgraph";
import {
  ToolNode,
  toolsCondition
} from "@langchain/langgraph/prebuilt";

const search = new DuckDuckGoSearch();
const calculator = new Calculator();
const tools = [search, calculator];
const model = new ChatOpenAI({ temperature: 0.1 }).bindTools(tools);

const annotation = Annotation.Root({
  messages: Annotation({ reducer: messagesStateReducer, default: () => [] }),
});

async function firstModelNode(state) {
  const query = state.messages[state.messages.length - 1].content;
  const searchToolCall = {
    name: "duckduckgo_search",
    args: { query },
    id: Math.random().toString(),
  };
  return {
    messages: [new AIMessage({ content: "", tool_calls: [searchToolCall] })],
  };
}

async function modelNode(state) {
  const res = await model.invoke(state.messages);
  return { messages: res };
}

const builder = new StateGraph(annotation)
  .addNode("first_model", firstModelNode)
  .addNode("model", modelNode)
  .addNode("tools", new ToolNode(tools))
  .addEdge(START, "first_model")
  .addEdge("first_model", "tools")
  .addEdge("tools", "model")
  .addConditionalEdges("model", toolsCondition);

const graph = builder.compile();

图形的可视化表示如图 6-2 所示。

image.png

注意与上一节相比的不同之处: 现在,我们通过调用 first_model 开始所有调用,该节点根本不调用 LLM。它只是为搜索工具创建一个工具调用,使用用户的消息原样作为搜索查询。之前的架构会让 LLM 生成这个工具调用(或者它认为更合适的其他响应)。 之后,我们继续执行 tools 节点,它与之前的示例相同,然后继续执行代理节点,就像之前一样。

接下来,让我们来看一下相同查询的示例输出:

Python:

input = {
    "messages": [
        HumanMessage("""How old was the 30th president of the United States 
            when he died?""")
    ]
}
for c in graph.stream(input):
    print(c)

JavaScript:

const input = {
  messages: [
    HumanMessage(`How old was the 30th president of the United States when he 
        died?`)
  ]
}
for await (const c of await graph.stream(input)) {
  console.log(c)
}

输出:

{
    "first_model": {
        "messages": AIMessage(
            content="",
            tool_calls=[
                {
                    "name": "duckduckgo_search",
                    "args": {
                        "query": "How old was the 30th president of the United 
                            States when he died?"
                    },
                    "id": "9ed4328dcdea4904b1b54487e343a373",
                    "type": "tool_call",
                }
            ],
        )
    }
}
{
    "tools": {
        "messages": [
            ToolMessage(
                content="Calvin Coolidge (born July 4, 1872, Plymouth, Vermont, 
                    U.S.—died January 5, 1933, Northampton, Massachusetts) was 
                    the 30th president of the United States (1923-29). Coolidge 
                    acceded to the presidency after the death in office of 
                    Warren G. Harding, just as the Harding scandals were coming 
                    to light....",
                name="duckduckgo_search",
                tool_call_id="9ed4328dcdea4904b1b54487e343a373",
            )
        ]
    }
}
{
    "model": {
        "messages": AIMessage(
            content="Calvin Coolidge, the 30th president of the United States, 
                was born on July 4, 1872, and died on January 5, 1933. To 
                calculate his age at the time of his death, we can subtract his 
                birth year from his death year. \n\nAge at death = Death year - 
                Birth year\nAge at death = 1933 - 1872\nAge at death = 61 
                years\n\nCalvin Coolidge was 61 years old when he died.",
        )
    }
}

这次,我们跳过了初始的 LLM 调用。我们首先进入了 first_model 节点,它直接返回了一个搜索工具的工具调用。接着,我们进入了之前的流程——即我们执行了搜索工具,最后回到模型节点生成了最终答案。

接下来,让我们了解当你有许多工具希望提供给 LLM 时可以做些什么。

处理多个工具

LLM 远非完美,它们在面对多个选择或过多信息时表现得更差。这些局限性也扩展到了规划下一步行动。当给出许多工具(例如,超过 10 个工具)时,规划性能(即 LLM 选择正确工具的能力)开始下降。解决这个问题的方法是减少 LLM 可选择的工具数量。但如果你确实有许多工具,并希望它们用于不同的用户查询,该怎么办呢?

一种优雅的解决方案是使用 RAG 步骤预先选择当前查询最相关的工具,然后仅将这些工具的子集传递给 LLM,而不是整个工具库。这也有助于减少调用 LLM 的成本(商业 LLM 通常根据提示和输出的长度收费)。另一方面,这个 RAG 步骤会给你的应用增加额外的延迟,因此只有在添加更多工具后看到性能下降时,才应该采用这种方式。

让我们来看一下如何实现这一点:

Python:

import ast
from typing import Annotated, TypedDict

from langchain_community.tools import DuckDuckGoSearchRun
from langchain_core.documents import Document
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_core.vectorstores.in_memory import InMemoryVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

from langgraph.graph import START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition

@tool
def calculator(query: str) -> str:
    """一个简单的计算器工具。输入应该是一个数学表达式。"""
    return ast.literal_eval(query)

search = DuckDuckGoSearchRun()
tools = [search, calculator]

embeddings = OpenAIEmbeddings()
model = ChatOpenAI(temperature=0.1)

tools_retriever = InMemoryVectorStore.from_documents(
    [Document(tool.description, metadata={"name": tool.name}) for tool in tools],
    embeddings,
).as_retriever()

class State(TypedDict):
    messages: Annotated[list, add_messages]
    selected_tools: list[str]

def model_node(state: State) -> State:
    selected_tools = [
        tool for tool in tools if tool.name in state["selected_tools"]
    ]
    res = model.bind_tools(selected_tools).invoke(state["messages"])
    return {"messages": res}

def select_tools(state: State) -> State:
    query = state["messages"][-1].content
    tool_docs = tools_retriever.invoke(query)
    return {"selected_tools": [doc.metadata["name"] for doc in tool_docs]}

builder = StateGraph(State)
builder.add_node("select_tools", select_tools)
builder.add_node("model", model_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "select_tools")
builder.add_edge("select_tools", "model")
builder.add_conditional_edges("model", tools_condition)
builder.add_edge("tools", "model")

graph = builder.compile()

JavaScript:

import { DuckDuckGoSearch } from "@langchain/community/tools/duckduckgo_search";
import { Calculator } from "@langchain/community/tools/calculator";
import { ChatOpenAI } from "@langchain/openai";
import { OpenAIEmbeddings } from "@langchain/openai";
import { Document } from "@langchain/core/documents";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import {
  StateGraph,
  Annotation,
  messagesStateReducer,
  START,
} from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { HumanMessage } from "@langchain/core/messages";

const search = new DuckDuckGoSearch();
const calculator = new Calculator();
const tools = [search, calculator];

const embeddings = new OpenAIEmbeddings();
const model = new ChatOpenAI({ temperature: 0.1 });

const toolsStore = await MemoryVectorStore.fromDocuments(
  tools.map(
    (tool) =>
      new Document({
        pageContent: tool.description,
        metadata: { name: tool.constructor.name },
      })
  ),
  embeddings
);
const toolsRetriever = toolsStore.asRetriever();

const annotation = Annotation.Root({
  messages: Annotation({ reducer: messagesStateReducer, default: () => [] }),
  selected_tools: Annotation(),
});

async function modelNode(state) {
  const selectedTools = tools.filter((tool) =>
    state.selected_tools.includes(tool.constructor.name)
  );
  const res = await model.bindTools(selectedTools).invoke(state.messages);
  return { messages: res };
}

async function selectTools(state) {
  const query = state.messages[state.messages.length - 1].content;
  const toolDocs = await toolsRetriever.invoke(query as string);
  return {
    selected_tools: toolDocs.map((doc) => doc.metadata.name),
  };
}

const builder = new StateGraph(annotation)
  .addNode("select_tools", selectTools)
  .addNode("model", modelNode)
  .addNode("tools", new ToolNode(tools))
  .addEdge(START, "select_tools")
  .addEdge("select_tools", "model")
  .addEdge("tools", "model")
  .addConditionalEdges("model", toolsCondition);

const graph = builder.compile();

图形的可视化表示如图 6-3 所示。

image.png

现在让我们看一下相同查询的示例输出:

Python:

input = {
  "messages": [
    HumanMessage("""How old was the 30th president of the United States when 
        he died?""")
  ]
}
for c in graph.stream(input):
    print(c)

JavaScript:

const input = {
  messages: [
    HumanMessage(`How old was the 30th president of the United States when he 
      died?`)
  ]
}
for await (const c of await graph.stream(input)) {
  console.log(c)
}

输出:

{
    "select_tools": {
        "selected_tools": ["duckduckgo_search", "calculator"]
    }
}
{
    "model": {
        "messages": AIMessage(
            content="",
            tool_calls=[
                {
                    "name": "duckduckgo_search",
                    "args": {
                        "query": "30th president of the United States"
                    },
                    "id": "9ed4328dcdea4904b1b54487e343a373",
                    "type": "tool_call",
                }
            ],
        )
    }
}
{
    "tools": {
        "messages": [
            ToolMessage(
                content="Calvin Coolidge (born July 4, 1872, Plymouth, Vermont, 
                    U.S.—died January 5, 1933, Northampton, Massachusetts) was 
                    the 30th president of the United States (1923-29). Coolidge 
                    acceded to the presidency after the death in office of 
                    Warren G. Harding, just as the Harding scandals were coming 
                    to light....",
                name="duckduckgo_search",
                tool_call_id="9ed4328dcdea4904b1b54487e343a373",
            )
        ]
    }
}
{
    "model": {
        "messages": AIMessage(
            content="Calvin Coolidge, the 30th president of the United States, 
                was born on July 4, 1872, and died on January 5, 1933. To 
                calculate his age at the time of his death, we can subtract his 
                birth year from his death year. \n\nAge at death = Death year - 
                Birth year\nAge at death = 1933 - 1872\nAge at death = 61 
                years\n\nCalvin Coolidge was 61 years old when he died.",
        )
    }
}

注意我们首先做了什么:我们查询了检索器,以获取当前用户查询最相关的工具。然后,我们继续执行常规的代理架构。

总结

本章介绍了代理的概念,并讨论了如何使 LLM 应用具有代理性:通过使用外部信息赋予 LLM 在多个选项之间做出决策的能力。

我们通过使用 LangGraph 构建了标准的代理架构,并探讨了两个有用的扩展:如何始终先调用特定工具以及如何处理多个工具。

第七章将探讨代理架构的其他扩展。