跳到内容

如何创建和控制循环

先决条件

本指南假设您熟悉以下内容

当创建一个带有循环的图时,我们需要一种终止执行的机制。这通常通过添加一个 条件边 来完成,该条件边在达到某些终止条件后路由到 END 节点。

您还可以在调用或流式传输图时设置图的递归限制。递归限制设置了图在引发错误之前允许执行的 超步 数量。在此处阅读更多关于递归限制概念的 信息

让我们考虑一个简单的带有循环的图,以更好地理解这些机制是如何工作的。

概述

当创建循环时,您可以包含一个条件边,该条件边指定终止条件

const route = async function (state: typeof StateAnnotation.State) {
  if (terminationCondition(state)) {
    return "__END__";
  } else {
    return "a";
  }
}

const graph = StateGraph(State)
  .addNode(a)
  .addNode(b)
  .addConditionalEdges("a", route)
  .addEdge("b", "a")
  .compile();

要控制递归限制,请在配置中指定 "recursionLimit"。这将引发一个 GraphRecursionError,您可以捕获并处理它

import { GraphRecursionError } from "@langchain/langgraph";

try {
  await graph.invoke(inputs, { recursionLimit: 3 });
} catch (error) {
  if (error instanceof GraphRecursionError) {
    console.log("Recursion Error");
  } else {
    throw error;
  }
}

设置

首先,让我们安装所需的软件包

npm install @langchain/langgraph @langchain/core

设置 LangSmith 以进行 LangGraph 开发

注册 LangSmith 以快速发现问题并提高 LangGraph 项目的性能。LangSmith 允许您使用跟踪数据来调试、测试和监控使用 LangGraph 构建的 LLM 应用程序 — 在 此处 阅读更多关于如何开始使用的信息。

定义图

让我们定义一个带有简单循环的图。请注意,我们使用条件边来实现终止条件。

import { StateGraph, Annotation } from "@langchain/langgraph";

// Define the state with a reducer
const StateAnnotation = Annotation.Root({
  aggregate: Annotation<string[]>({
    reducer: (a, b) => a.concat(b),
    default: () => [],
  }),
});

// Define nodes
const a = async function (state: typeof StateAnnotation.State) {
  console.log(`Node A sees ${state.aggregate}`);
  return { aggregate: ["A"] };
}

const b = async function (state: typeof StateAnnotation.State) {
  console.log(`Node B sees ${state.aggregate}`);
  return { aggregate: ["B"] };
}

// Define edges
const route = async function (state: typeof StateAnnotation.State) {
  if (state.aggregate.length < 7) {
    return "b";
  } else {
    return "__end__";
  }
}


// Define the graph
const graph = new StateGraph(StateAnnotation)
  .addNode("a", a)
  .addNode("b", b)
  .addEdge("__start__", "a")
  .addConditionalEdges("a", route)
  .addEdge("b", "a")
  .compile();
import * as tslab from "tslab";

const drawableGraph = graph.getGraph();
const image = await drawableGraph.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();

await tslab.display.png(new Uint8Array(arrayBuffer));

此架构类似于 ReAct agent,其中节点 "a" 是一个工具调用模型,节点 "b" 代表工具。

在我们的 route 条件边中,我们指定在状态中的 "aggregate" 列表通过阈值长度后应该结束。

调用该图,我们看到我们在节点 "a""b" 之间交替,直到达到终止条件。

await graph.invoke({ aggregate: [] });
Node A sees 
Node B sees A
Node A sees A,B
Node B sees A,B,A
Node A sees A,B,A,B
Node B sees A,B,A,B,A
Node A sees A,B,A,B,A,B
{
  aggregate: [
    'A', 'B', 'A',
    'B', 'A', 'B',
    'A'
  ]
}

设置递归限制

在某些应用程序中,我们可能无法保证会达到给定的终止条件。在这些情况下,我们可以设置图的 递归限制。这将在给定数量的 超步 后引发 GraphRecursionError。然后我们可以捕获并处理此异常

import { GraphRecursionError } from "@langchain/langgraph";

try {
  await graph.invoke({ aggregate: [] }, { recursionLimit: 4 });
} catch (error) {
  if (error instanceof GraphRecursionError) {
    console.log("Recursion Error");
  } else {
    throw error;
  }
}
Node A sees 
Node B sees A
Node A sees A,B
Node B sees A,B,A
Recursion Error
请注意,这次我们在第四步后终止。

带分支的循环

为了更好地理解递归限制是如何工作的,让我们考虑一个更复杂的例子。下面我们实现一个循环,但一个步骤会扇出到两个节点

import { StateGraph, Annotation } from "@langchain/langgraph";

// Define the state with a reducer
const StateAnnotationWithLoops = Annotation.Root({
  aggregate: Annotation<string[]>({
    reducer: (a, b) => a.concat(b),
    default: () => [],
  }),
});

// Define nodes
const nodeA = async function (state: typeof StateAnnotationWithLoops.State) {
  console.log(`Node A sees ${state.aggregate}`);
  return { aggregate: ["A"] };
}

const nodeB = async function (state: typeof StateAnnotationWithLoops.State) {
  console.log(`Node B sees ${state.aggregate}`);
  return { aggregate: ["B"] };
}

const nodeC = async function (state: typeof StateAnnotationWithLoops.State) {
  console.log(`Node C sees ${state.aggregate}`);
  return { aggregate: ["C"] };
}

const nodeD = async function (state: typeof StateAnnotationWithLoops.State) {
  console.log(`Node D sees ${state.aggregate}`);
  return { aggregate: ["D"] };
}

// Define edges
const loopRouter = async function (state: typeof StateAnnotationWithLoops.State) {
  if (state.aggregate.length < 7) {
    return "b";
  } else {
    return "__end__";
  }
}

// Define the graph
const graphWithLoops = new StateGraph(StateAnnotationWithLoops)
  .addNode("a", nodeA)
  .addNode("b", nodeB)
  .addNode("c", nodeC)
  .addNode("d", nodeD)
  .addEdge("__start__", "a")
  .addConditionalEdges("a", loopRouter)
  .addEdge("b", "c")
  .addEdge("b", "d")
  .addEdge(["c", "d"], "a")
  .compile();
import * as tslab from "tslab";

const drawableGraphWithLoops = graphWithLoops.getGraph();
const imageWithLoops = await drawableGraphWithLoops.drawMermaidPng();
const arrayBufferWithLoops = await imageWithLoops.arrayBuffer();

await tslab.display.png(new Uint8Array(arrayBufferWithLoops));

这个图看起来很复杂,但可以概念化为 超步 的循环

  1. 节点 A
  2. 节点 B
  3. 节点 C 和 D
  4. 节点 A
  5. ...

我们有一个四个超步的循环,其中节点 C 和 D 并发执行。

像以前一样调用该图,我们看到我们完成了两个完整的“圈”,然后才达到终止条件

await graphWithLoops.invoke({ aggregate: [] })
Node A sees 
Node B sees A
Node C sees A,B
Node D sees A,B
Node A sees A,B,C,D
Node B sees A,B,C,D,A
Node C sees A,B,C,D,A,B
Node D sees A,B,C,D,A,B
Node A sees A,B,C,D,A,B,C,D
{
  aggregate: [
    'A', 'B', 'C',
    'D', 'A', 'B',
    'C', 'D', 'A'
  ]
}
但是,如果我们将递归限制设置为四,我们只完成一圈,因为每圈是四个超步

import { GraphRecursionError } from "@langchain/langgraph";

try {
  await graphWithLoops.invoke({ aggregate: [] }, { recursionLimit: 4 });
} catch (error) {
  if (error instanceof GraphRecursionError) {
    console.log("Recursion Error");
  } else {
    throw error;
  }
}
Node A sees 
Node B sees A
Node C sees A,B
Node D sees A,B
Node A sees A,B,C,D
Recursion Error