ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LangGraphJS:基于有向图编排AI Agent工作流的JavaScript框架

LangGraphJS:基于有向图编排AI Agent工作流的JavaScript框架 1. 项目概述为什么我们需要 LangGraphJs如果你正在探索如何构建一个能自主思考、规划并执行复杂任务的AI应用那么“Agent”智能体这个概念你一定不陌生。传统的LangChain或LlamaIndex等框架为我们串联大模型调用和工具使用提供了基础但当任务流程变得复杂、需要循环、分支或状态管理时简单的链式结构就显得力不从心了。这时一个更强大的工具——LangGraphJs就进入了我们的视野。简单来说LangGraphJs是一个基于有向图Graph来编排和运行AI Agent工作流的JavaScript/TypeScript框架。它把Agent的每一步决策、每一个工具调用、每一次状态更新都抽象为图中的一个节点Node节点之间的连线Edge则定义了工作流的流转逻辑。这就像为你的AI应用绘制了一张“思维导图”或“程序流程图”让Agent的执行路径变得清晰、可控且强大。无论是处理一个需要多轮对话和工具调用的客服机器人还是一个能自动分析数据、撰写报告并发送邮件的自动化助手LangGraphJs都能提供坚实的架构支持。对于前端、全栈开发者或者任何希望在Node.js环境中构建复杂AI应用的工程师来说掌握LangGraphJs的核心概念和工作流程无疑是打开下一代AI应用开发大门的钥匙。2. LangGraphJs 核心概念深度拆解要玩转LangGraphJs必须吃透它的几个核心抽象。这些概念共同构成了其强大灵活性的基石。2.1 状态State工作流的记忆中枢在LangGraph中State是整个工作流运行时唯一的核心数据容器。你可以把它想象成一个全局的、共享的“白板”或者“上下文对象”。所有节点Node都读取和修改这个状态对象。核心特性与设计考量单一数据源整个图的所有操作都围绕这一个状态对象进行。这避免了数据在多个组件间传递的复杂性使得状态管理变得清晰。可预测性给定相同的输入状态一个节点或整个图的行为是可预测的这非常有利于调试和测试。结构定义状态的结构通常由一个TypeScript接口或类来定义。这不仅是类型安全的需要也明确了工作流需要处理哪些信息。一个典型的状态定义可能如下interface AgentState { // 用户输入或任务目标 input: string; // 与大模型的对话历史 messages: BaseMessage[]; // 从外部工具获取的数据 knowledge: string; // 控制流程的标志例如“是否需要进一步搜索” needsResearch: boolean; // 最终输出的结果 finalAnswer?: string; }注意状态的设计是工作流设计的第一步。你需要仔细思考完成这个任务需要记录哪些信息哪些信息需要在节点间共享过于臃肿的状态会增加复杂性而过于精简的状态可能导致信息不足。我的经验是初期可以适当冗余随着流程清晰再逐步重构精简。2.2 节点Node执行具体任务的单元Node是图的基本执行单元。每个节点都是一个函数它接收当前的State作为输入执行一些操作如调用大模型、使用工具、处理数据然后返回一个更新后的State或包含State的对象。节点的关键职责业务逻辑封装一个节点应该只做一件事。例如一个“搜索节点”只负责调用搜索API并更新状态中的knowledge字段一个“推理节点”只负责将问题和知识组织成提示词调用大模型并将回复添加到messages中。状态更新节点通过返回一个新对象来更新状态。LangGraph采用不可变更新模式通常是返回一个包含更新字段的对象框架会将其与旧状态合并。async function searchNode(state: AgentState): PromisePartialAgentState { const searchResult await tavilySearch(state.input); // 返回需要更新的部分状态 return { knowledge: searchResult }; }纯函数理念虽然节点可以执行异步操作如网络请求但理想情况下它应该是一个“纯函数”输出仅由输入状态决定没有隐藏的副作用。这使节点易于测试和复用。2.3 边Edge决定流程走向的路径Edge定义了在某个节点执行完毕后下一步应该走到哪个节点。这是LangGraph实现复杂逻辑如条件分支、循环的核心机制。边分为两种主要类型普通边Fixed Edge无条件地指向下一个节点。用于构建线性流程。条件边Conditional Edge根据当前State中的某个条件动态决定下一个节点。这是实现“智能”决策的关键。条件边通过一个路由函数Router Function来实现。这个函数检查状态并返回下一个要执行的节点的名称一个字符串。function shouldContinue(state: AgentState): ‘generate_answer’ | ‘search_web’ { // 如果已有足够知识则生成答案否则继续搜索 if (state.knowledge state.knowledge.length 100) { return ‘generate_answer’; } else { return ‘search_web’; } }2.4 图Graph与编译Compilation从蓝图到可执行程序Graph是节点和边的集合它定义了一个工作流的静态结构。但光有结构还不行我们需要一个可以“运行”的实体。编译Compile这个过程就是将静态的图定义转化成一个可执行的、有状态的Runnable对象。在编译时LangGraph会进行一系列检查和优化比如验证节点和边引用的名称是否存在是否存在无法到达的节点或死循环等。import { StateGraph } from “langchain/langgraph”; const workflow new StateGraphAgentState() .addNode(“search”, searchNode) .addNode(“generate”, generateNode) .addEdge(“search”, “generate”) // 从search节点无条件指向generate节点 .addConditionalEdges( “generate”, shouldContinue, // 条件路由函数 { search_web: “search”, generate_answer: END } // 映射函数返回值 - 下一个节点名 ); // 关键一步编译图获得可执行对象 const app workflow.compile();编译后得到的app就是一个强大的AI工作流引擎。你可以像调用函数一样调用它const finalState await app.invoke({ input: “今天天气如何” })。3. LangGraphJs 工作流程全景解析理解了核心概念后我们来看一个完整的工作流是如何从设计到运行的。我将以一个“研究型问答Agent”为例拆解其生命周期。3.1 工作流设计阶段绘制你的Agent思维地图在设计阶段你需要像架构师一样思考。我的习惯是先在白板或绘图工具上画出草图。定义目标Agent要完成什么例如“根据用户问题自动搜索网络信息综合信息后生成准确、全面的答案并能根据答案质量决定是否进行新一轮搜索。”拆解步骤将目标分解为原子任务。对应我们的例子解析问题理解用户意图可能提取关键词。搜索网络调用搜索工具。生成草稿根据已有信息生成初步答案。评估答案判断答案是否充分、可靠。完善/结束如果评估通过则输出最终答案否则基于不足点发起新一轮搜索。映射到图元素节点每个原子任务成为一个节点parse,search,generate_draft,evaluate,finalize。状态设计状态结构来承载这些任务需要的信息流。边设计节点间的流转逻辑。parse-search是固定的。search-generate_draft是固定的。从evaluate到finalize或search则是条件边由评估结果决定。这个设计阶段至关重要草图的质量直接决定了后续编码的顺畅程度。3.2 运行时执行流程状态驱动的智能舞步当你调用app.invoke(initialState)时一场精密的、状态驱动的舞蹈就开始了。初始化你提供的initialState至少包含input和空的messages成为当前状态。图有一个预定义的入口节点通常是第一个添加的节点或通过setEntryPoint指定。节点执行运行时引擎将当前状态传递给入口节点函数执行。节点函数运行可能调用LLM、工具并返回一个状态更新补丁PartialState。状态合并引擎将返回的补丁与当前状态进行合并产生新的当前状态。这里有一个关键细节合并是浅合并shallow merge。如果你的状态中有嵌套对象如数组messages直接返回{ messages: newMessagesArray }会整个替换掉原数组而不是追加。对于追加操作LangGraph通常建议在节点函数内部处理或者使用框架提供的特定更新器。路由决策根据当前节点配置的边决定下一步。如果是固定边直接跳转到下一个节点。如果是条件边则调用路由函数传入最新的状态根据其返回值决定下一个节点。循环与终止重复步骤2-4沿着图的路径前进。直到遇到特殊的END节点流程终止并返回最终的状态。中断与持久化LangGraph支持“检查点”Checkpoint它可以在每个步骤后保存状态的快照。这使得暂停和恢复长时运行的工作流成为可能对于处理耗时任务如等待人工审核的Agent至关重要。整个流程就像是一个状态机状态是驱动机器运转的燃料而图的结构是机器的蓝图。3.3 与LangChain生态的集成站在巨人的肩膀上LangGraphJs 并非孤岛它深度集成在LangChain JS/TS生态中。这意味着无缝使用LangChain模型你可以轻松地将ChatOpenAI、ChatAnthropic等LangChain封装的LLM集成到你的节点中。直接调用LangChain工具海量的LangChain Tools搜索、计算、API调用等可以直接在节点函数中使用。利用LangChain提示模板使用ChatPromptTemplate等来高效构建发送给LLM的提示。状态消息集成状态中的messages字段通常就是BaseMessage[]类型与LangChain的对话记忆组件天然兼容。这种集成极大地降低了开发门槛你无需重复造轮子可以专注于工作流本身的逻辑编排。4. 核心应用场景与实战模式LangGraphJs的图抽象非常灵活能支持多种经典的Agent模式。下面我们深入探讨几种最实用的模式。4.1 ReAct模式思考与行动的循环ReActReason Act是Agent的经典范式。LangGraph是实现ReAct的绝佳框架。工作流设计状态设计需要包含messages对话历史其中包含LLM的“思考”过程、scratchpad临时记录思考内容、以及tool_calls记录上次工具调用的结果等。核心循环节点通常是一个“代理节点”Agent Node。该节点接收当前状态包含问题、历史、上次工具结果。构造一个特殊的提示词要求LLM按照“Thought: ... Action: ... Action Input: ...”的格式输出。解析LLM的输出。如果是“Thought”则更新scratchpad并循环再次调用该节点让LLM继续思考。如果是“Action”则提取要调用的工具名和参数。工具调用节点根据代理节点的输出动态调用对应的工具并将结果格式化后更新到状态如tool_result。边路由从“代理节点”出发的条件边根据LLM输出内容解析出的下一个动作类型继续思考、调用工具、结束来决定路由。实操心得实现ReAct时最大的挑战在于提示词工程和输出解析的稳定性。LLM必须严格遵守指定的格式。使用LangChain的OutputFixingParser或StructuredOutputParser与Pydantic模型结合可以极大地提高解析的鲁棒性。此外要为循环设置最大迭代次数防止LLM陷入死循环。4.2 多Agent协作模式构建专家团队对于复杂问题可以设计多个各司其职的Agent如“研究员”、“写手”、“校对员”让它们在LangGraph的协调下共同工作。工作流设计状态设计需要有一个字段来标识当前任务处于哪个阶段或由哪个Agent负责例如current_stage: ‘research’ | ‘writing’ | ‘review’。同时状态中需要有共享的工作区如research_materials: string[],draft: string,feedback: string。专家节点每个Agent是一个独立的节点拥有自己擅长的提示词和工具集。“研究员”节点调用搜索工具并整理资料“写手”节点根据资料生成草稿“校对员”节点评估草稿质量并提出修改意见。编排逻辑通过条件边实现工作流转。例如开始后进入“研究员”节点完成后将current_stage改为’writing’。一条条件边检查该字段路由到“写手”节点。写手完成后再路由到“校对员”。校对员可能提出反馈此时可以将current_stage改回’writing’并附带反馈触发新一轮的修改直到校对通过。这种模式模拟了人类团队的协作流程非常适合需要多步骤、多技能融合的复杂任务生成如撰写深度报告、制定项目方案等。4.3 带有监督与自省的工作流提升可靠性让Agent完全自主运行有时是危险的。我们可以引入“监督”节点或“自省”机制来提升其可靠性和质量。应用模式人工审核节点在关键节点如最终输出前设置一个“人工审核”节点。该节点可以暂停工作流将中间状态通过某种方式如发送邮件、写入数据库待办项呈现给用户等待用户输入approve/reject/modify后再更新状态并决定后续流程。这通过LangGraph的“检查点”和“外部线程”功能可以很好地实现。自动验证节点在生成答案后增加一个“验证”节点。这个节点可以调用另一个LLM或同一LLM的不同提示基于原始问题和生成答案判断答案的事实准确性、完整性、有无幻觉。如果验证不通过则重新路由到搜索或生成节点。循环优化将“生成 - 评估 - 优化”作为一个子图循环。评估节点对当前结果打分如果分数低于阈值则生成具体的修改指令并路由回生成节点进行迭代优化。这常用于代码生成、文案润色等场景。重要提示增加监督环节必然会增加延迟和成本更多的LLM调用。需要在自动化程度和可靠性之间做出权衡。对于高风险场景如金融、医疗建议人工审核是必要的对于一般场景自动验证可以作为有效的安全网。5. 实战构建一个能自我验证的问答Agent让我们将上述理论付诸实践构建一个完整的、带有自我验证循环的问答Agent。这个Agent会先搜索再生成答案最后验证答案的准确性如果不达标则重新搜索。5.1 环境准备与依赖安装首先初始化项目并安装核心依赖。# 初始化一个新的npm项目如果还没有 mkdir self-validating-agent cd self-validating-agent npm init -y # 安装LangChain和LangGraph核心包 npm install langchain/langgraph langchain/core # 安装OpenAI模型包和社区工具包以Tavily搜索为例 npm install langchain/openai langchain-community # 安装TypeScript和类型定义推荐使用TS以获得更好的开发体验 npm install -D typescript types/node tsx npx tsc --init在package.json的scripts中添加”start”: “tsx src/index.ts”。5.2 定义状态与工具在src/types.ts中定义我们的状态结构并在src/tools.ts中创建工具。// src/types.ts import { BaseMessage } from “langchain/core/messages”; export interface AgentState { // 用户原始问题 question: string; // 对话历史 messages: BaseMessage[]; // 搜索到的资料 research: string; // 生成的答案草稿 draftAnswer: string; // 验证结果 verification: { score: number; // 0-10分 feedback: string; // 验证模型的反馈 isApproved: boolean; // 是否通过验证 }; // 最终答案 finalAnswer?: string; } // src/tools.ts import { TavilySearchResults } from “langchain/community/tools/tavily_search”; // 初始化搜索工具你需要一个Tavily API密钥 export const searchTool new TavilySearchResults({ apiKey: process.env.TAVILY_API_KEY, maxResults: 3, // 控制搜索结果的量 });5.3 实现核心功能节点我们将创建四个节点searchNode,generateNode,verifyNode,finalizeNode。放在src/nodes.ts。// src/nodes.ts import { HumanMessage, AIMessage, SystemMessage } from “langchain/core/messages”; import { ChatOpenAI } from “langchain/openai”; import { AgentState } from “./types”; import { searchTool } from “./tools”; const llm new ChatOpenAI({ modelName: “gpt-4-turbo-preview”, temperature: 0.2, // 较低的温度使输出更稳定 }); // 节点1搜索节点 export async function searchNode(state: AgentState): PromisePartialAgentState { console.log(“[Node] Executing: Search”); const searchResult await searchTool.invoke(state.question); // 将搜索结果格式化存储 return { research: Search results for ${state.question}:\n${searchResult}, messages: [ …state.messages, new SystemMessage(I have gathered the following information: ${searchResult}), ], }; } // 节点2生成答案节点 export async function generateNode(state: AgentState): PromisePartialAgentState { console.log(“[Node] Executing: Generate Draft”); const prompt You are a helpful assistant. Answer the user’s question based SOLELY on the provided research context. If the context does not contain enough information to answer fully, say so and do not make up information. User Question: ${state.question} Research Context: ${state.research} Provide a comprehensive, accurate, and well-structured answer: ; const response await llm.invoke(prompt); return { draftAnswer: response.content as string, messages: […state.messages, new AIMessage(response.content as string)], }; } // 节点3验证节点 export async function verifyNode(state: AgentState): PromisePartialAgentState { console.log(“[Node] Executing: Verify Answer”); const verificationPrompt You are a strict fact-checker. Evaluate the following answer against the provided research. Question: ${state.question} Research: ${state.research} Draft Answer: ${state.draftAnswer} Please provide: 1. A score from 0 to 10, where 10 means the answer is perfectly accurate and complete based on the research, and 0 means it’s completely wrong or fabricated. 2. Specific feedback: Point out any inaccuracies, missing key points, or unsupported statements. 3. A simple “APPROVE” or “REJECT” verdict. REJECT if the score is below 8 or if there are any major factual errors. Output your evaluation in the following JSON format: { “score”: number, “feedback”: string, “verdict”: “APPROVE” or “REJECT” } ; const response await llm.invoke(verificationPrompt); let evaluation; try { evaluation JSON.parse(response.content as string); } catch (e) { console.error(“Failed to parse verification output”, e); evaluation { score: 0, feedback: “Parse error”, verdict: “REJECT” }; } return { verification: { score: evaluation.score, feedback: evaluation.feedback, isApproved: evaluation.verdict “APPROVE”, }, }; } // 节点4终结节点 export async function finalizeNode(state: AgentState): PromisePartialAgentState { console.log(“[Node] Executing: Finalize”); return { finalAnswer: **Verified Answer:**\n${state.draftAnswer}\n\n*Verification Score: ${state.verification.score}/10*, }; }5.4 组装工作流图并运行在src/index.ts中我们将节点和边组装起来并编译运行。// src/index.ts import { StateGraph, END } from “langchain/langgraph”; import { AgentState } from “./types”; import { searchNode, generateNode, verifyNode, finalizeNode } from “./nodes”; import * as dotenv from “dotenv”; dotenv.config(); // 加载环境变量如 OPENAI_API_KEY, TAVILY_API_KEY // 1. 定义条件路由函数 function shouldContinue(state: AgentState): string { // 检查验证结果 if (state.verification?.isApproved) { return “finalize”; // 验证通过去终结 } else { // 验证不通过我们选择重新搜索也可以选择重新生成 // 在实际应用中可以加入重试计数器防止无限循环 console.log(Verification failed (Score: ${state.verification?.score}). Restarting search.); return “search”; } } // 2. 构建图 const workflow new StateGraphAgentState() .addNode(“search”, searchNode) .addNode(“generate”, generateNode) .addNode(“verify”, verifyNode) .addNode(“finalize”, finalizeNode) // 设置入口点 .setEntryPoint(“search”) // 固定边搜索 - 生成 .addEdge(“search”, “generate”) // 固定边生成 - 验证 .addEdge(“generate”, “verify”) // 条件边验证 - (终结 或 重新搜索) .addConditionalEdges( “verify”, shouldContinue, { finalize: “finalize”, // 如果 shouldContinue 返回 “finalize” search: “search”, // 如果 shouldContinue 返回 “search” } ) // 固定边终结 - END .addEdge(“finalize”, END); // 3. 编译图 const app workflow.compile(); // 4. 运行Agent async function runAgent(question: string) { const initialState: AgentState { question, messages: [], research: “”, draftAnswer: “”, verification: { score: 0, feedback: “”, isApproved: false }, }; console.log(\n Starting Agent for question: “${question}” ); const finalState await app.invoke(initialState, { recursionLimit: 5 }); // 设置递归限制防止无限循环 console.log(“\n Final Result ); console.log(finalState.finalAnswer); console.log(“\n Full State Summary ); console.log(Research length: ${finalState.research?.length} chars); console.log(Draft length: ${finalState.draftAnswer?.length} chars); console.log(Verification Score: ${finalState.verification.score}); console.log(Total steps (approx): ${finalState.messages?.length}); } // 5. 执行 runAgent(“What are the main causes and potential solutions for climate change according to recent scientific consensus?”).catch(console.error);6. 开发中的常见陷阱与进阶调试技巧即使理解了概念和流程在实际开发中依然会踩坑。以下是我从多个项目中总结出的核心问题和解决方案。6.1 状态管理中的“坑”问题状态更新未生效或覆盖错误。原因LangGraph采用浅合并。如果你直接返回{ messages: [newMessage] }它会完全替换掉旧的messages数组而不是追加。解决方案在节点函数内部处理合并逻辑。例如对于messages总是返回{ messages: […state.messages, newMessage] }。对于其他字段如果是要追加字符串也需手动拼接。问题状态类型定义过于宽泛导致节点间协作困难。原因使用大量可选的 (?) 字段或any类型使得节点难以确定所需数据是否已存在。解决方案精心设计状态接口明确哪些字段在哪个阶段会被填充。可以使用联合类型或更精细的状态机来区分不同阶段的状态结构。6.2 流程控制与循环问题问题Agent陷入无限循环。原因条件边逻辑有缺陷或者LLM在ReAct模式下不断输出“Thought”而不采取“Action”。解决方案设置硬性限制在compile()或invoke()时传入{ recursionLimit: 20 }参数。增强条件判断在路由函数中加入更严格的终止条件例如检查迭代次数或时间。优化提示词在ReAct提示中明确要求LLM在几次思考后必须行动或给出最终答案。问题条件边路由函数过于复杂难以调试。原因路由函数里塞满了业务逻辑。解决方案保持路由函数精简。它应该只做路由决策。复杂的条件判断逻辑应该放在前一个节点中并将结果以清晰的标志如nextStep: ‘approve’写入状态路由函数只需读取这个标志即可。6.3 调试与监控实战技巧利用console.log和状态快照在每个节点的开始和结束打印状态的关键部分。LangGraph的invoke方法返回最终状态其中包含了完整的历史消息和所有中间状态如果你使用了检查点。将其输出到文件进行仔细分析。可视化工具虽然LangGraph JS官方没有像Python版那样的可视化工具但你可以通过记录每个节点的执行和边路由用第三方库如vis-network自己绘制执行流程图这对于理解复杂工作流的执行路径非常有帮助。使用LangSmith进行追踪如果你使用OpenAI模型强烈建议集成LangSmith。它能以时间线的形式完整记录每一次LLM调用、工具调用和状态变化是调试LangGraph应用最强大的武器。你可以清晰地看到工作流在哪一步出了错输入输出具体是什么。单元测试节点函数由于节点函数理想上是纯函数它们非常易于进行单元测试。你可以模拟输入状态断言输出的状态更新是否符合预期。这能确保每个“齿轮”都运转正常。6.4 性能与成本优化减少不必要的LLM调用在条件边判断时尽量使用简单的状态字段判断而不是调用另一个LLM来做决策除非必要。将多次连续的、信息互补的LLM调用思考是否能合并到一个提示中完成。流式输出与用户体验对于生成最终答案的节点可以利用LLM的流式响应通过回调函数逐步将结果返回给前端提升用户体验。这需要在前端调用和后端流式处理之间做好衔接。持久化与恢复对于长时任务一定要利用好Checkpoint机制。将检查点ID与用户会话关联即使服务器重启也能从最近一步恢复任务避免功亏一篑。构建基于LangGraphJs的Agent应用是一个将抽象思维图设计转化为具体智能工作流执行的过程。它要求开发者同时具备清晰的逻辑架构能力和对AI模型行为的深刻理解。从简单的线性链起步逐步尝试引入条件分支和循环最终驾驭多Agent协作和复杂的状态管理你会发现自己构建AI应用的能力将获得质的飞跃。记住所有复杂的系统都是由简单的模块和清晰的规则组合而成LangGraphJs正是为你提供了这样一套强大而优雅的组合工具。
返回列表