如何创建用于并行节点执行的分支¶
LangGraph 本地支持使用常规边或 conditionalEdges 进行扇出和扇入。
这使您可以并行运行节点以加快总图执行速度。
以下是显示如何添加创建适合您的分支数据流的一些示例。
设置¶
首先,安装 LangGraph.js
yarn add @langchain/langgraph @langchain/core
本指南将使用 OpenAI 的 GPT-4o 模型。 我们将选择性地为 LangSmith 跟踪 设置我们的 API 密钥,这将为我们提供一流的可观察性。
在 [1]
已复制!
// process.env.OPENAI_API_KEY = "sk_...";
// Optional, add tracing in LangSmith
// process.env.LANGCHAIN_API_KEY = "ls__..."
// process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "true";
process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "true";
process.env.LANGCHAIN_TRACING_V2 = "true";
process.env.LANGCHAIN_PROJECT = "Branching: LangGraphJS";
// process.env.OPENAI_API_KEY = "sk_..."; // 可选,在 LangSmith 中添加跟踪 // process.env.LANGCHAIN_API_KEY = "ls__..." // process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "true"; process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "true"; process.env.LANGCHAIN_TRACING_V2 = "true"; process.env.LANGCHAIN_PROJECT = "Branching: LangGraphJS";
Branching: LangGraphJS
扇出,扇入¶
首先,我们将创建一个简单的图,该图向外分支并返回。 当合并回来时,来自所有分支的状态更新将通过您的 reducer(下面的 aggregate
方法)应用。
在 [2]
已复制!
import { END, START, StateGraph, Annotation } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
aggregate: Annotation<string[]>({
reducer: (x, y) => x.concat(y),
})
});
// Create the graph
const nodeA = (state: typeof StateAnnotation.State) => {
console.log(`Adding I'm A to ${state.aggregate}`);
return { aggregate: [`I'm A`] };
};
const nodeB = (state: typeof StateAnnotation.State) => {
console.log(`Adding I'm B to ${state.aggregate}`);
return { aggregate: [`I'm B`] };
};
const nodeC = (state: typeof StateAnnotation.State) => {
console.log(`Adding I'm C to ${state.aggregate}`);
return { aggregate: [`I'm C`] };
};
const nodeD = (state: typeof StateAnnotation.State) => {
console.log(`Adding I'm D to ${state.aggregate}`);
return { aggregate: [`I'm D`] };
};
const builder = new StateGraph(StateAnnotation)
.addNode("a", nodeA)
.addEdge(START, "a")
.addNode("b", nodeB)
.addNode("c", nodeC)
.addNode("d", nodeD)
.addEdge("a", "b")
.addEdge("a", "c")
.addEdge("b", "d")
.addEdge("c", "d")
.addEdge("d", END);
const graph = builder.compile();
import { END, START, StateGraph, Annotation } from "@langchain/langgraph"; const StateAnnotation = Annotation.Root({ aggregate: Annotation({ reducer: (x, y) => x.concat(y), }) }); // 创建图 const nodeA = (state: typeof StateAnnotation.State) => { console.log(`Adding I'm A to ${state.aggregate}`); return { aggregate: [`I'm A`] }; }; const nodeB = (state: typeof StateAnnotation.State) => { console.log(`Adding I'm B to ${state.aggregate}`); return { aggregate: [`I'm B`] }; }; const nodeC = (state: typeof StateAnnotation.State) => { console.log(`Adding I'm C to ${state.aggregate}`); return { aggregate: [`I'm C`] }; }; const nodeD = (state: typeof StateAnnotation.State) => { console.log(`Adding I'm D to ${state.aggregate}`); return { aggregate: [`I'm D`] }; }; const builder = new StateGraph(StateAnnotation) .addNode("a", nodeA) .addEdge(START, "a") .addNode("b", nodeB) .addNode("c", nodeC) .addNode("d", nodeD) .addEdge("a", "b") .addEdge("a", "c") .addEdge("b", "d") .addEdge("c", "d") .addEdge("d", END); const graph = builder.compile();
在 [3]
已复制!
import * as tslab from "tslab";
const representation = graph.getGraph();
const image = await representation.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
await tslab.display.png(new Uint8Array(arrayBuffer));
import * as tslab from "tslab"; const representation = graph.getGraph(); const image = await representation.drawMermaidPng(); const arrayBuffer = await image.arrayBuffer(); await tslab.display.png(new Uint8Array(arrayBuffer));
在 [4]
已复制!
// Invoke the graph
const baseResult = await graph.invoke({ aggregate: [] });
console.log("Base Result: ", baseResult);
// 调用图 const baseResult = await graph.invoke({ aggregate: [] }); console.log("Base Result: ", baseResult);
Adding I'm A to Adding I'm B to I'm A Adding I'm C to I'm A Adding I'm D to I'm A,I'm B,I'm C Base Result: { aggregate: [ "I'm A", "I'm B", "I'm C", "I'm D" ] }
条件分支¶
如果您的扇出不是确定性的,您可以像这样直接使用 addConditionalEdges
在 [5]
已复制!
const ConditionalBranchingAnnotation = Annotation.Root({
aggregate: Annotation<string[]>({
reducer: (x, y) => x.concat(y),
}),
which: Annotation<string>({
reducer: (x: string, y: string) => (y ?? x),
})
})
// Create the graph
const nodeA2 = (state: typeof ConditionalBranchingAnnotation.State) => {
console.log(`Adding I'm A to ${state.aggregate}`);
return { aggregate: [`I'm A`] };
};
const nodeB2 = (state: typeof ConditionalBranchingAnnotation.State) => {
console.log(`Adding I'm B to ${state.aggregate}`);
return { aggregate: [`I'm B`] };
};
const nodeC2 = (state: typeof ConditionalBranchingAnnotation.State) => {
console.log(`Adding I'm C to ${state.aggregate}`);
return { aggregate: [`I'm C`] };
};
const nodeD2 = (state: typeof ConditionalBranchingAnnotation.State) => {
console.log(`Adding I'm D to ${state.aggregate}`);
return { aggregate: [`I'm D`] };
};
const nodeE2 = (state: typeof ConditionalBranchingAnnotation.State) => {
console.log(`Adding I'm E to ${state.aggregate}`);
return { aggregate: [`I'm E`] };
};
// Define the route function
function routeCDorBC(state: typeof ConditionalBranchingAnnotation.State): string[] {
if (state.which === "cd") {
return ["c", "d"];
}
return ["b", "c"];
}
const builder2 = new StateGraph(ConditionalBranchingAnnotation)
.addNode("a", nodeA2)
.addEdge(START, "a")
.addNode("b", nodeB2)
.addNode("c", nodeC2)
.addNode("d", nodeD2)
.addNode("e", nodeE2)
// Add conditional edges
// Third parameter is to support visualizing the graph
.addConditionalEdges("a", routeCDorBC, ["b", "c", "d"])
.addEdge("b", "e")
.addEdge("c", "e")
.addEdge("d", "e")
.addEdge("e", END);
const graph2 = builder2.compile();
const ConditionalBranchingAnnotation = Annotation.Root({ aggregate: Annotation({ reducer: (x, y) => x.concat(y), }), which: Annotation({ reducer: (x: string, y: string) => (y ?? x), }) }) // 创建图 const nodeA2 = (state: typeof ConditionalBranchingAnnotation.State) => { console.log(`Adding I'm A to ${state.aggregate}`); return { aggregate: [`I'm A`] }; }; const nodeB2 = (state: typeof ConditionalBranchingAnnotation.State) => { console.log(`Adding I'm B to ${state.aggregate}`); return { aggregate: [`I'm B`] }; }; const nodeC2 = (state: typeof ConditionalBranchingAnnotation.State) => { console.log(`Adding I'm C to ${state.aggregate}`); return { aggregate: [`I'm C`] }; }; const nodeD2 = (state: typeof ConditionalBranchingAnnotation.State) => { console.log(`Adding I'm D to ${state.aggregate}`); return { aggregate: [`I'm D`] }; }; const nodeE2 = (state: typeof ConditionalBranchingAnnotation.State) => { console.log(`Adding I'm E to ${state.aggregate}`); return { aggregate: [`I'm E`] }; }; // 定义路由函数 function routeCDorBC(state: typeof ConditionalBranchingAnnotation.State): string[] { if (state.which === "cd") { return ["c", "d"]; } return ["b", "c"]; } const builder2 = new StateGraph(ConditionalBranchingAnnotation) .addNode("a", nodeA2) .addEdge(START, "a") .addNode("b", nodeB2) .addNode("c", nodeC2) .addNode("d", nodeD2) .addNode("e", nodeE2) // 添加条件边 // 第三个参数用于支持可视化图 .addConditionalEdges("a", routeCDorBC, ["b", "c", "d"]) .addEdge("b", "e") .addEdge("c", "e") .addEdge("d", "e") .addEdge("e", END); const graph2 = builder2.compile();
在 [6]
已复制!
import * as tslab from "tslab";
const representation2 = graph2.getGraph();
const image2 = await representation2.drawMermaidPng();
const arrayBuffer2 = await image2.arrayBuffer();
await tslab.display.png(new Uint8Array(arrayBuffer2));
import * as tslab from "tslab"; const representation2 = graph2.getGraph(); const image2 = await representation2.drawMermaidPng(); const arrayBuffer2 = await image2.arrayBuffer(); await tslab.display.png(new Uint8Array(arrayBuffer2));
在 [7]
已复制!
// Invoke the graph
let g2result = await graph2.invoke({ aggregate: [], which: "bc" });
console.log("Result 1: ", g2result);
// 调用图 let g2result = await graph2.invoke({ aggregate: [], which: "bc" }); console.log("Result 1: ", g2result);
Adding I'm A to Adding I'm B to I'm A Adding I'm C to I'm A Adding I'm E to I'm A,I'm B,I'm C Result 1: { aggregate: [ "I'm A", "I'm B", "I'm C", "I'm E" ], which: 'bc' }
在 [8]
已复制!
g2result = await graph2.invoke({ aggregate: [], which: "cd" });
console.log("Result 2: ", g2result);
g2result = await graph2.invoke({ aggregate: [], which: "cd" }); console.log("Result 2: ", g2result);
Adding I'm A to Adding I'm C to I'm A Adding I'm D to I'm A Adding I'm E to I'm A,I'm C,I'm D
Result 2: { aggregate: [ "I'm A", "I'm C", "I'm D", "I'm E" ], which: 'cd' }
稳定排序¶
当扇出时,节点作为单个“超级步骤”并行运行。 来自每个超级步骤的更新在超级步骤完成后全部按顺序应用于状态。
如果您需要来自并行超级步骤的一致、预定顺序的更新,您应该将输出(以及标识键)写入状态中的一个单独字段,然后通过从每个扇出节点到会合点的添加常规 edge
在“接收器”节点中组合它们。
例如,假设我想按“可靠性”对并行步骤的输出进行排序。
在 [9]
已复制!
type ScoredValue = {
value: string;
score: number;
};
const reduceFanouts = (left?: ScoredValue[], right?: ScoredValue[]) => {
if (!left) {
left = [];
}
if (!right || right?.length === 0) {
// Overwrite. Similar to redux.
return [];
}
return left.concat(right);
};
const StableSortingAnnotation = Annotation.Root({
aggregate: Annotation<string[]>({
reducer: (x, y) => x.concat(y),
}),
which: Annotation<string>({
reducer: (x: string, y: string) => (y ?? x),
}),
fanoutValues: Annotation<ScoredValue[]>({
reducer: reduceFanouts,
}),
})
class ParallelReturnNodeValue {
private _value: string;
private _score: number;
constructor(nodeSecret: string, score: number) {
this._value = nodeSecret;
this._score = score;
}
public call(state: typeof StableSortingAnnotation.State) {
console.log(`Adding ${this._value} to ${state.aggregate}`);
return { fanoutValues: [{ value: this._value, score: this._score }] };
}
}
// Create the graph
const nodeA3 = (state: typeof StableSortingAnnotation.State) => {
console.log(`Adding I'm A to ${state.aggregate}`);
return { aggregate: ["I'm A"] };
};
const nodeB3 = new ParallelReturnNodeValue("I'm B", 0.1);
const nodeC3 = new ParallelReturnNodeValue("I'm C", 0.9);
const nodeD3 = new ParallelReturnNodeValue("I'm D", 0.3);
const aggregateFanouts = (state: typeof StableSortingAnnotation.State) => {
// Sort by score (reversed)
state.fanoutValues.sort((a, b) => b.score - a.score);
return {
aggregate: state.fanoutValues.map((v) => v.value).concat(["I'm E"]),
fanoutValues: [],
};
};
// Define the route function
function routeBCOrCD(state: typeof StableSortingAnnotation.State): string[] {
if (state.which === "cd") {
return ["c", "d"];
}
return ["b", "c"];
}
const builder3 = new StateGraph(StableSortingAnnotation)
.addNode("a", nodeA3)
.addEdge(START, "a")
.addNode("b", nodeB3.call.bind(nodeB3))
.addNode("c", nodeC3.call.bind(nodeC3))
.addNode("d", nodeD3.call.bind(nodeD3))
.addNode("e", aggregateFanouts)
.addConditionalEdges("a", routeBCOrCD, ["b", "c", "d"])
.addEdge("b", "e")
.addEdge("c", "e")
.addEdge("d", "e")
.addEdge("e", END);
const graph3 = builder3.compile();
// Invoke the graph
let g3result = await graph3.invoke({ aggregate: [], which: "bc" });
console.log("Result 1: ", g3result);
type ScoredValue = { value: string; score: number; }; const reduceFanouts = (left?: ScoredValue[], right?: ScoredValue[]) => { if (!left) { left = []; } if (!right || right?.length === 0) { // 覆盖。 与 redux 类似。 return []; } return left.concat(right); }; const StableSortingAnnotation = Annotation.Root({ aggregate: Annotation({ reducer: (x, y) => x.concat(y), }), which: Annotation({ reducer: (x: string, y: string) => (y ?? x), }), fanoutValues: Annotation({ reducer: reduceFanouts, }), }) class ParallelReturnNodeValue { private _value: string; private _score: number; constructor(nodeSecret: string, score: number) { this._value = nodeSecret; this._score = score; } public call(state: typeof StableSortingAnnotation.State) { console.log(`Adding ${this._value} to ${state.aggregate}`); return { fanoutValues: [{ value: this._value, score: this._score }] }; } } // 创建图 const nodeA3 = (state: typeof StableSortingAnnotation.State) => { console.log(`Adding I'm A to ${state.aggregate}`); return { aggregate: ["I'm A"] }; }; const nodeB3 = new ParallelReturnNodeValue("I'm B", 0.1); const nodeC3 = new ParallelReturnNodeValue("I'm C", 0.9); const nodeD3 = new ParallelReturnNodeValue("I'm D", 0.3); const aggregateFanouts = (state: typeof StableSortingAnnotation.State) => { // 按分数排序(反转) state.fanoutValues.sort((a, b) => b.score - a.score); return { aggregate: state.fanoutValues.map((v) => v.value).concat(["I'm E"]), fanoutValues: [], }; }; // 定义路由函数 function routeBCOrCD(state: typeof StableSortingAnnotation.State): string[] { if (state.which === "cd") { return ["c", "d"]; } return ["b", "c"]; } const builder3 = new StateGraph(StableSortingAnnotation) .addNode("a", nodeA3) .addEdge(START, "a") .addNode("b", nodeB3.call.bind(nodeB3)) .addNode("c", nodeC3.call.bind(nodeC3)) .addNode("d", nodeD3.call.bind(nodeD3)) .addNode("e", aggregateFanouts) .addConditionalEdges("a", routeBCOrCD, ["b", "c", "d"]) .addEdge("b", "e") .addEdge("c", "e") .addEdge("d", "e") .addEdge("e", END); const graph3 = builder3.compile(); // 调用图 let g3result = await graph3.invoke({ aggregate: [], which: "bc" }); console.log("Result 1: ", g3result);
Adding I'm A to Adding I'm B to I'm A Adding I'm C to I'm A Result 1: { aggregate: [ "I'm A", "I'm C", "I'm B", "I'm E" ], which: 'bc', fanoutValues: [] }
在这种情况下,我们的 aggregateFanouts “接收器”节点获取了映射值,然后以一致的方式对它们进行了排序。 注意,因为它为 fanoutValues
返回一个空数组,所以我们的 reduceFanouts
reducer 函数决定覆盖状态中的先前值。
在 [10]
已复制!
let g3result2 = await graph3.invoke({ aggregate: [], which: "cd" });
console.log("Result 2: ", g3result2);
let g3result2 = await graph3.invoke({ aggregate: [], which: "cd" }); console.log("Result 2: ", g3result2);
Adding I'm A to Adding I'm C to I'm A Adding I'm D to I'm A Result 2: { aggregate: [ "I'm A", "I'm C", "I'm D", "I'm E" ], which: 'cd', fanoutValues: [] }