简单示例¶
让我们考虑一个玩具示例:一个系统,它接受来自另一个系统的日志作为输入并执行两个独立的子任务。首先,它将对它们进行总结。其次,它将总结日志中捕获的任何故障模式。这两个操作将由两个不同的子图执行。
最重要的是要认识到图之间信息传递。入口图
是父级,两个子图都定义为入口图
中的节点。两个子图都继承父入口图
的状态;我只需在子图状态中指定它即可访问每个子图中的docs
(请参见图)。每个子图都可以拥有自己的私有状态。我想要传播回父入口图
(用于最终报告)的任何值只需要在我的入口图
状态中定义(例如,摘要报告
和故障报告
)。
定义子图¶
首先,我们将从上图中定义故障分析和汇总子图
在 [1]
已复制!
import { StateGraph, START, Annotation } from "@langchain/langgraph";
// Custom structure for adding logs from subgraphs to the state
interface Log {
type: string;
id: string
question: string
answer: string
grade?: number
feedback?: string
}
// Define custom reducer (see more on this in the "Custom reducer" section below)
const addLogs = (left: Log[], right: Log[]): Log[] => {
let newLeft = left || [];
let newRight = right || [];
const logs = [...newLeft];
const leftIdToIdx = new Map(logs.map((log, idx) => [log.id, idx]));
// update if the new logs are already in the state, otherwise append
for (const log of newRight) {
const idx = leftIdToIdx.get(log.id);
if (idx !== undefined) {
logs[idx] = log;
} else {
logs.push(log);
}
}
return logs;
};
// Failure Analysis Subgraph
const FailureAnalysisAnnotation = Annotation.Root({
// keys shared with the parent graph (EntryGraphState)
logs: Annotation<Log[]>({
reducer: addLogs,
default: () => [],
}),
failureReport: Annotation<string>,
// subgraph key
failures: Annotation<Log[]>,
})
const getFailures = (state: typeof FailureAnalysisAnnotation.State) => {
const failures = state.logs.filter(log => log.grade === 0);
return { failures };
}
const generateSummary = (state: typeof FailureAnalysisAnnotation.State) => {
const failureIds = state.failures.map(log => log.id);
// NOTE: you can implement custom summarization logic here
const failureReport = `Poor quality of retrieval for document IDs: ${failureIds.join(", ")}`;
return { failureReport };
}
const failureBuilder = new StateGraph(FailureAnalysisAnnotation)
.addNode("getFailures", getFailures)
.addNode("generateSummary", generateSummary)
.addEdge(START, "getFailures")
.addEdge("getFailures", "generateSummary");
// Summarization subgraph
const QuestionSummarizationAnnotation = Annotation.Root({
// keys that are shared with the parent graph (EntryGraphState)
summaryReport: Annotation<string>,
logs: Annotation<Log[]>({
reducer: addLogs,
default: () => [],
}),
// subgraph key
summary: Annotation<string>,
})
const generateQuestionSummary = (_state: typeof QuestionSummarizationAnnotation.State) => {
// NOTE: you can implement custom summarization logic here
const summary = "Questions focused on usage of ChatOllama and Chroma vector store.";
return { summary };
}
const sendToSlack = (state: typeof QuestionSummarizationAnnotation.State) => {
const summary = state.summary;
// NOTE: you can implement custom logic here, for example sending the summary generated in the previous step to Slack
return { summaryReport: summary };
}
const questionSummarizationBuilder = new StateGraph(QuestionSummarizationAnnotation)
.addNode("generateQuestionSummary", generateQuestionSummary)
.addNode("sendToSlack", sendToSlack)
.addEdge(START, "generateQuestionSummary")
.addEdge("generateQuestionSummary", "sendToSlack");
import { StateGraph, START, Annotation } from "@langchain/langgraph"; // 用于将子图的日志添加到状态接口的自定义结构 Log { type: string; id: string question: string answer: string grade?: number feedback?: string } // 定义自定义 reducer(请参阅下面“自定义 reducer”部分中的更多信息) const addLogs = (left: Log[], right: Log[]): Log[] => { let newLeft = left || []; let newRight = right || []; const logs = [...newLeft]; const leftIdToIdx = new Map(logs.map((log, idx) => [log.id, idx])); // 如果新日志已存在于状态中,则更新,否则追加 for (const log of newRight) { const idx = leftIdToIdx.get(log.id); if (idx !== undefined) { logs[idx] = log; } else { logs.push(log); } } return logs; }; // 故障分析子图 const FailureAnalysisAnnotation = Annotation.Root({ // 与父图(EntryGraphState)共享的键 logs: Annotation({ reducer: addLogs, default: () => [], }), failureReport: Annotation, // 子图键 failures: Annotation, }) const getFailures = (state: typeof FailureAnalysisAnnotation.State) => { const failures = state.logs.filter(log => log.grade === 0); return { failures }; } const generateSummary = (state: typeof FailureAnalysisAnnotation.State) => { const failureIds = state.failures.map(log => log.id); // 注意:您可以在此处实现自定义汇总逻辑 const failureReport = `检索文档 ID 质量差:${failureIds.join(", ")}`; return { failureReport }; } const failureBuilder = new StateGraph(FailureAnalysisAnnotation) .addNode("getFailures", getFailures) .addNode("generateSummary", generateSummary) .addEdge(START, "getFailures") .addEdge("getFailures", "generateSummary"); // 汇总子图 const QuestionSummarizationAnnotation = Annotation.Root({ // 与父图(EntryGraphState)共享的键 summaryReport: Annotation, logs: Annotation({ reducer: addLogs, default: () => [], }), // 子图键 summary: Annotation, }) const generateQuestionSummary = (_state: typeof QuestionSummarizationAnnotation.State) => { // 注意:您可以在此处实现自定义汇总逻辑 const summary = "问题侧重于 ChatOllama 和 Chroma 向量存储的使用。"; return { summary }; } const sendToSlack = (state: typeof QuestionSummarizationAnnotation.State) => { const summary = state.summary; // 注意:您可以在此处实现自定义逻辑,例如将上一步生成的摘要发送到 Slack return { summaryReport: summary }; } const questionSummarizationBuilder = new StateGraph(QuestionSummarizationAnnotation) .addNode("generateQuestionSummary", generateQuestionSummary) .addNode("sendToSlack", sendToSlack) .addEdge(START, "generateQuestionSummary") .addEdge("generateQuestionSummary", "sendToSlack");
请注意,每个子图都有自己的状态,QuestionSummarizationAnnotation
和 FailureAnalysisAnnotation
。
定义每个子图后,我们将所有内容组合在一起。
定义父图¶
在 [2]
已复制!
// Dummy logs
const dummyLogs: Log[] = [
{
type: "log",
id: "1",
question: "How can I import ChatOllama?",
grade: 1,
answer: `To import ChatOllama, use: 'import { ChatOllama } from "@langchain/ollama";'`,
},
{
type: "log",
id: "2",
question: "How can I use Chroma vector store?",
answer: `To use Chroma, define: "const ragChain = await createRetrievalChain(retriever, questionAnswerChain);".`,
grade: 0,
feedback: "The retrieved documents discuss vector stores in general, but not Chroma specifically",
},
{
type: "log",
id: "3",
question: "How do I create a react agent in langgraph?",
answer: `import { createReactAgent } from "@langchain/langgraph";`,
}
];
// Entry Graph
const EntryGraphAnnotation = Annotation.Root({
rawLogs: Annotation<Log[]>({
reducer: addLogs,
default: () => [],
}),
// This will be used in subgraphs
logs: Annotation<Log[]>({
reducer: addLogs,
default: () => [],
}),
// This will be generated in the failure analysis subgraph
failureReport: Annotation<string>,
// This will be generated in the summarization subgraph
summaryReport: Annotation<string>,
});
const selectLogs = (state: typeof EntryGraphAnnotation.State) => {
return { logs: state.rawLogs.filter((log) => "grade" in log) };
}
const entryBuilder = new StateGraph(EntryGraphAnnotation)
.addNode("selectLogs", selectLogs)
.addNode("questionSummarization", questionSummarizationBuilder.compile())
.addNode("failureAnalysis", failureBuilder.compile())
// Add edges
.addEdge(START, "selectLogs")
.addEdge("selectLogs", "failureAnalysis")
.addEdge("selectLogs", "questionSummarization");
const graph = entryBuilder.compile()
// 虚拟日志 const dummyLogs: Log[] = [ { type: "log", id: "1", question: "如何导入 ChatOllama?", grade: 1, answer: `要导入 ChatOllama,请使用:'import { ChatOllama } from "@langchain/ollama";'`, }, { type: "log", id: "2", question: "如何使用 Chroma 向量存储?", answer: `要使用 Chroma,请定义:“const ragChain = await createRetrievalChain(retriever, questionAnswerChain);”。`, grade: 0, feedback: "检索到的文档总体上讨论了向量存储,但没有专门讨论 Chroma", }, { type: "log", id: "3", question: "如何在 langgraph 中创建 React 代理?", answer: `import { createReactAgent } from "@langchain/langgraph";`, } ]; // 入口图 const EntryGraphAnnotation = Annotation.Root({ rawLogs: Annotation({ reducer: addLogs, default: () => [], }), // 这将在子图中使用 logs: Annotation({ reducer: addLogs, default: () => [], }), // 这将在故障分析子图中生成 failureReport: Annotation, // 这将在汇总子图中生成 summaryReport: Annotation, }); const selectLogs = (state: typeof EntryGraphAnnotation.State) => { return { logs: state.rawLogs.filter((log) => "grade" in log) }; } const entryBuilder = new StateGraph(EntryGraphAnnotation) .addNode("selectLogs", selectLogs) .addNode("questionSummarization", questionSummarizationBuilder.compile()) .addNode("failureAnalysis", failureBuilder.compile()) // 添加边 .addEdge(START, "selectLogs") .addEdge("selectLogs", "failureAnalysis") .addEdge("selectLogs", "questionSummarization"); const graph = entryBuilder.compile()
在 [3]
已复制!
const result = await graph.invoke({ rawLogs: dummyLogs });
console.dir(result, { depth: null });
const result = await graph.invoke({ rawLogs: dummyLogs }); console.dir(result, { depth: null });
{ rawLogs: [ { type: 'log', id: '1', question: 'How can I import ChatOllama?', grade: 1, answer: `To import ChatOllama, use: 'import { ChatOllama } from "@langchain/ollama";'` }, { type: 'log', id: '2', question: 'How can I use Chroma vector store?', answer: 'To use Chroma, define: "const ragChain = await createRetrievalChain(retriever, questionAnswerChain);".', grade: 0, feedback: 'The retrieved documents discuss vector stores in general, but not Chroma specifically' }, { type: 'log', id: '3', question: 'How do I create a react agent in langgraph?', answer: 'import { createReactAgent } from "@langchain/langgraph";' } ], logs: [ { type: 'log', id: '1', question: 'How can I import ChatOllama?', grade: 1, answer: `To import ChatOllama, use: 'import { ChatOllama } from "@langchain/ollama";'` }, { type: 'log', id: '2', question: 'How can I use Chroma vector store?', answer: 'To use Chroma, define: "const ragChain = await createRetrievalChain(retriever, questionAnswerChain);".', grade: 0, feedback: 'The retrieved documents discuss vector stores in general, but not Chroma specifically' } ], failureReport: 'Poor quality of retrieval for document IDs: 2', summaryReport: 'Questions focused on usage of ChatOllama and Chroma vector store.' }
在 [4]
已复制!
import { StateGraph, START, Annotation } from "@langchain/langgraph";
// Define a simple reducer
const reduceList = (left: any[], right?: any[]): any[] => {
const newLeft = left || [];
const newRight = right || [];
return [...newLeft, ...newRight];
};
// Define parent and child state
const ChildStateAnnotation = Annotation.Root({
name: Annotation<string>,
path: Annotation<any[]>({ reducer: reduceList, default: () => [] }),
});
const ParentStateAnnotation = Annotation.Root({
name: Annotation<string>,
path: Annotation<any[]>({ reducer: reduceList, default: () => [] }),
});
// Define a helper to build the graph
const makeGraph = (parentSchema: any, childSchema: any) => {
const childBuilder = new StateGraph(childSchema)
.addNode("childStart", (_) => ({ path: ["childStart"] }))
.addEdge(START, "childStart")
.addNode("childMiddle", (_) => ({ path: ["childMiddle"] }))
.addNode("childEnd", (_) => ({ path: ["childEnd"] }))
.addEdge("childStart", "childMiddle")
.addEdge("childMiddle", "childEnd");
const builder = new StateGraph(parentSchema)
.addNode("grandparent", (_) => ({ path: ["grandparent"] }))
.addEdge(START, "grandparent")
.addNode("parent", (_) => ({ path: ["parent"] }))
.addNode("child", childBuilder.compile())
.addNode("sibling", (_) => ({ path: ["sibling"] }))
.addNode("fin", (_) => ({ path: ["fin"] }))
// Add nodes
.addEdge("grandparent", "parent")
.addEdge("parent", "child")
.addEdge("parent", "sibling")
.addEdge("child", "fin")
.addEdge("sibling", "fin");
return builder.compile();
};
// Create the graph
const graphWithReducer = makeGraph(ParentStateAnnotation, ChildStateAnnotation);
import { StateGraph, START, Annotation } from "@langchain/langgraph"; // 定义一个简单的 reducer const reduceList = (left: any[], right?: any[]): any[] => { const newLeft = left || []; const newRight = right || []; return [...newLeft, ...newRight]; }; // 定义父和子状态 const ChildStateAnnotation = Annotation.Root({ name: Annotation, path: Annotation({ reducer: reduceList, default: () => [] }), }); const ParentStateAnnotation = Annotation.Root({ name: Annotation, path: Annotation({ reducer: reduceList, default: () => [] }), }); // 定义一个构建图的辅助函数 const makeGraph = (parentSchema: any, childSchema: any) => { const childBuilder = new StateGraph(childSchema) .addNode("childStart", (_) => ({ path: ["childStart"] })) .addEdge(START, "childStart") .addNode("childMiddle", (_) => ({ path: ["childMiddle"] })) .addNode("childEnd", (_) => ({ path: ["childEnd"] })) .addEdge("childStart", "childMiddle") .addEdge("childMiddle", "childEnd"); const builder = new StateGraph(parentSchema) .addNode("grandparent", (_) => ({ path: ["grandparent"] })) .addEdge(START, "grandparent") .addNode("parent", (_) => ({ path: ["parent"] })) .addNode("child", childBuilder.compile()) .addNode("sibling", (_) => ({ path: ["sibling"] })) .addNode("fin", (_) => ({ path: ["fin"] })) // 添加节点 .addEdge("grandparent", "parent") .addEdge("parent", "child") .addEdge("parent", "sibling") .addEdge("child", "fin") .addEdge("sibling", "fin"); return builder.compile(); }; // 创建图 const graphWithReducer = makeGraph(ParentStateAnnotation, ChildStateAnnotation);
在 [5]
已复制!
const result2 = await graphWithReducer.invoke({ name: "test" });
console.dir(result2, { depth: null });
const result2 = await graphWithReducer.invoke({ name: "test" }); console.dir(result2, { depth: null });
{ name: 'test', path: [ 'grandparent', 'parent', 'grandparent', 'parent', 'childStart', 'childMiddle', 'childEnd', 'sibling', 'fin' ] }
请注意,此处["grandparent", "parent"]
序列重复了!
这是因为我们的子状态已收到完整的父状态,并在其终止后返回完整的父状态。
为了避免状态重复或冲突,您通常会执行以下一项或多项操作
- 在您的
reducer
函数中处理重复项。 - 从函数内部调用子图。在该函数中,根据需要处理状态。
- 更新子图键以避免冲突。但是,您仍然需要确保父级可以解释输出。
让我们使用技术 (1) 重新实现该图,并为列表中的每个值添加唯一的 ID。这就是MessagesAnnotation
中所做的操作。
在 [6]
已复制!
import { v4 as uuidv4 } from 'uuid';
const reduceList2 = (left: any[], right?: any[]): any[] => {
const newLeft = left || [];
const newRight = right || [];
const left_ = [];
const right_ = [];
for (const [orig, new_] of [[newLeft, left_], [newRight, right_]]) {
for (let val of orig) {
if (typeof val !== "object") {
val = { val };
}
if (typeof val === "object" && !("id" in val)) {
val.id = uuidv4();
}
new_.push(val);
}
}
// Merge the two lists
const leftIdxById = new Map(left_.map((val, i) => [val.id, i]));
const merged = [...left_];
for (const val of right_) {
if (leftIdxById.has(val.id)) {
merged[leftIdxById.get(val.id)] = val;
} else {
merged.push(val);
}
}
return merged;
}
const ChildAnnotation = Annotation.Root({
name: Annotation<string>,
// note the updated reducer here
path: Annotation<any[]>({ reducer: reduceList2, default: () => [] }),
})
const ParentAnnotation = Annotation.Root({
name: Annotation<string>,
// note the updated reducer here
path: Annotation<any[]>({ reducer: reduceList2, default: () => [] }),
})
import { v4 as uuidv4 } from 'uuid'; const reduceList2 = (left: any[], right?: any[]): any[] => { const newLeft = left || []; const newRight = right || []; const left_ = []; const right_ = []; for (const [orig, new_] of [[newLeft, left_], [newRight, right_]]) { for (let val of orig) { if (typeof val !== "object") { val = { val }; } if (typeof val === "object" && !("id" in val)) { val.id = uuidv4(); } new_.push(val); } } // 合并两个列表 const leftIdxById = new Map(left_.map((val, i) => [val.id, i])); const merged = [...left_]; for (const val of right_) { if (leftIdxById.has(val.id)) { merged[leftIdxById.get(val.id)] = val; } else { merged.push(val); } } return merged; } const ChildAnnotation = Annotation.Root({ name: Annotation, // 注意此处更新的 reducer path: Annotation({ reducer: reduceList2, default: () => [] }), }) const ParentAnnotation = Annotation.Root({ name: Annotation, // 注意此处更新的 reducer path: Annotation({ reducer: reduceList2, default: () => [] }), })
由于我们的图拓扑结构没有改变,我们可以重用之前定义的相同makeGraph
辅助函数,并为父图和子图传递新的模式。
在 [7]
已复制!
const graphWithNewReducer = makeGraph(ParentAnnotation, ChildAnnotation)
const result3 = await graphWithNewReducer.invoke({ name: "test" });
console.dir(result3, { depth: null });
const graphWithNewReducer = makeGraph(ParentAnnotation, ChildAnnotation) const result3 = await graphWithNewReducer.invoke({ name: "test" }); console.dir(result3, { depth: null });
{ name: 'test', path: [ { val: 'grandparent', id: 'be824684-5107-41c2-b78e-71ef9878289b' }, { val: 'parent', id: '1d6a392e-d350-4bef-a8a5-d36e07308cb7' }, { val: 'childStart', id: '367a76a2-bae9-41c7-8a82-26f42cd393b3' }, { val: 'childMiddle', id: '9052bffa-1d18-491a-b78b-d1763cafb42a' }, { val: 'childEnd', id: '1299922c-5b0a-42bb-bc30-70afd3111d4a' }, { val: 'sibling', id: '5eef5e4c-fac3-4d60-a486-a115d32410cf' }, { val: 'fin', id: '2f8244b1-7cdc-41e8-8dd4-ef390deb8323' } ] }
您可以看到,由于我们上面引入的更新后的 reducer,路径值现在不再重复了。