这两年我一直在帮团队搭各种内部工具,从简单的表单收集到复杂的业务流自动化都做过。前段时间接到一个需求:做一个简历优化工具,不是那种套模板的排版工具,而是能真正读懂用户简历内容、针对目标岗位给出量化评估和修改建议的AI工具。试了好几版方案,最后选了Next.js + LangGraph.js组合,把整个Agent完整落地了。这篇文章就聊聊这个项目的设计思路、关键实现,以及我踩过的一些坑。
先说结论:简历工具是特别适合用Agent架构来做的一类场景。原因很简单,简历优化不是一个“一次提问一次回答”的简单对话,它包含了拆解简历、理解岗位需求、分维度评估、生成修改建议、甚至生成定制化简历这一整条链路的多个环节。如果用普通的prompt工程硬写,代码会变成一坨互相嵌套的if-else,而且每加一个新功能就要动一遍主流程。用了LangGraph.js之后,整条链路被拆成了图上的一个个节点,状态流转清晰,每个节点可以单独调试,后续加功能也只是在图上加节点的事。
1. 这类简历工具为什么值得用Agent来做
1.1 简历优化的本质是一连串决策
很多人觉得简历工具就是“把简历丢给大模型,让它给点建议”,这想法太天真了。真实场景里,一个完整的简历优化需求至少包含以下步骤:
- 把用户粘贴的原始文本或上传的PDF解析成结构化数据,这需要识别个人信息、工作经历、项目经历、技能清单,还要把时间线对齐。
- 结合用户选择的目标岗位(比如“后端开发工程师”),对简历进行打分,判断是否匹配。这里要拆好几个维度,包括结构完整性、量化成果数量、关键词覆盖、STAR法则使用情况。
- 基于评分结果,生成具体的修改建议。注意,修改建议不是泛泛而谈的“请补充量化数据”,而是要结合用户的原文,指出具体哪一条工作经历描述太平淡、哪些技能关键词缺失。
- 用户确认后,生成一份优化后的简历全文,保留原格式风格,同时替换掉有问题的描述。
这四步之间存在明确的数据依赖关系:第二步依赖第一步的结构化数据,第三步依赖第二步的评分结果,第四步又依赖前三个步骤的所有产出。用传统的Service层写法,每一步都要手动串起来,中间状态需要自己定义一堆DTO来传递。而LangGraph.js的核心机制就是把这一步一步封装成图节点,靠共享的State对象自动传递数据,代码结构会干净非常多。
1.2 LangGraph.js到底解决了什么问题
LangGraph.js是LangChain.js生态里的图编排框架,核心思想是让开发者用“图”的方式定义Agent的行为路径。每个节点就是一个函数或者一个封装好的组件,节点之间用边来连接,边上还可以定义条件路由。
我之前用LangChain.js做Agent时,最大的痛点就是复杂的多轮工具调用流程很难控制。LangChain.js的Agent executor虽然能循环调用工具,但只要你需要中断流程、插入人工确认、或者在特定条件下走不同的分支,代码复杂度立刻爆炸。LangGraph.js把这些问题变成了图上的“条件边”,比如“如果评分低于60分,就走‘深度优化’分支;如果评分高于85分,就直接跳到‘生成简历’分支”,这种写法完全跟着业务逻辑走,不需要自己去维护状态机。
另外LangGraph.js有一个很有用的机制叫持久化检查点(checkpointer)。图每次执行到某个节点时,会把状态快照保存下来。中断之后可以从指定的检查点继续执行,不需要重头跑一遍。我之前觉得这个功能没用,直到做简历生成这个场景才真正体会到它的价值:大模型的中间输出可能很慢,如果用户在第三步调整了目标岗位,Agent可以从“评分”节点重新跑,而不是重新解析一遍简历。
1.3 这个项目适合谁参考
如果你也在做内容生成、文档处理、数据分析这类偏“重流程”的AI应用,这篇文章的架构思路可以直接抄。哪怕你完全没接触过LangGraph.js,只要会一点TypeScript和React,跟着后面的代码走一遍,也能搭出一个可运行的简历Agent。
2. 技术选型:Next.js与LangGraph.js的组合逻辑
2.1 为什么用Next.js而不是纯前端方案
最初我在两个方案之间犹豫过:方案A是纯React SPA + 后端单独起一个Node服务;方案B是直接用Next.js全栈搞定。最后选了Next.js,核心原因有三个。
第一是API Route的天然整合。LangGraph.js需要跑在Node环境,而Next.js的Route Handler可以直接挂载Graph服务,前端页面和Agent接口在同一个项目里维护,不需要为Agent的API单独开一个Git仓库,也不存在跨域问题。对于我这种经常单兵作战、维护多个内部工具的开发者来说,少一个服务就少一倍的运维成本。
第二是流式渲染体验。简历优化这个场景,大模型生成建议动不动就要十几秒,如果做成一次性返回,用户会以为网站卡死了。Next.js的App Router对流式响应支持得很好,Route Handler里可以直接写Web标准流,前端配合ReadableStream逐个字展示生成结果。这套体验跟现在主流AI产品完全一致。
第三是团队协作的边界清晰。前端组件和Agent的Graph定义虽然在一个仓库里,但目录结构上分开,前端的人不用关心Agent内部长什么样,只需要对“启动任务”和“获取结果”两个API。这个边界在项目中后期特别重要,不然每个人都在改Agent的代码,方案会越搞越乱。
2.2 为什么Agent层选了LangGraph.js而不是LangChain.js
LangChain.js的Agent执行器也能实现多工具调用,但我做了个对比实验,发现两个核心差异:
- 状态模型不同。LangChain.js的Agent本质上是一个循环:把当前prompt传给模型,模型决定调用哪个工具,拿到结果再传回模型,直到模型认为可以回答了。整个循环里没有“阶段”的概念,很难表达“先解析再评分再生成”这种明显有先后顺序的流程。LangGraph.js可以显式定义节点和边的顺序,执行顺序完全可控。
- 人机交互的时机不同。简历优化流程里,我希望在“解析简历”完成后暂停,让用户确认一下解析出的关键信息有没有错,比如“工作年限识别对了没”。如果在LangChain.js里做人工确认,要么得写复杂的回调,要么得把整个流程拆成多个接口手动串联。在LangGraph.js里,一个
interrupt_before参数就能实现中途暂停,等用户确认后调用invoke恢复执行。
这两个差异基本决定了选型,后面所有开发都围绕LangGraph.js展开。
另外提一嘴版本问题:现在LangChain.js的官方包里已经直接集成了langgraph,安装@langchain/langgraph包就能用,不需要单独引一个框架。官方文档里最新的例子也都是基于这个包在做,社区生态已经比较成熟了。
2.3 整体技术栈清单
项目最终用到的核心依赖如下:
| 模块 | 选型 | 说明 |
|---|---|---|
| 前端框架 | Next.js 14(App Router) | 路由、服务端渲染、API Route |
| Agent编排 | @langchain/langgraph | 定义状态图、节点、条件路由 |
| LLM接入 | @langchain/openai | 兼容OpenAI接口的模型都可用 |
| 结构化输出 | zod | 校验Agent节点返回的数据结构 |
| 状态持久化 | @langchain/langgraph-checkpoint | 支持中断续跑和任务恢复 |
| 部署 | Vercel / Node服务器 | 取决于是否需要长时任务 |
模型层面,生产环境我是接了兼容OpenAI协议的自建服务,开发调试直接用DeepSeek的API也能跑通,因为LangChain.js的OpenAI封装支持自定义baseURL。这一层灵活性很大,别被“必须用GPT”这种想法框住。
3. 核心实现:简历Agent的状态图与节点
3.1 图结构设计:四个节点加一条人工确认边
整个Agent的图结构很清晰,总共四个核心节点:
parse_resume(解析简历) → evaluate_resume(评分诊断) → optimize_suggestions(生成修改建议) → generate_resume(生成定制简历)其中parse_resume之后会暂停,等前端把解析结果返回给用户确认,确认通过才继续执行evaluate_resume。这样设计的原因很实际:如果模型第一轮就把工作时长解析错了,后面所有评分和建议都建立在错误数据上,整份优化结果会跑偏。
图定义的核心代码如下:
import { StateGraph, Annotation, START, END } from "@langchain/langgraph"; // 定义Agent的全局状态 const AgentState = Annotation.Root({ rawResume: Annotation<string>({ reducer: (a: string, b: string) => b ?? a ?? "", }), targetRole: Annotation<string>({ reducer: (a: string, b: string) => b ?? a ?? "", }), parsedResume: Annotation<any>({ reducer: (a: any, b: any) => b ?? a, }), evaluation: Annotation<any>({ reducer: (a: any, b: any) => b ?? a, }), suggestions: Annotation<any[]>({ reducer: (a: any[], b: any[]) => (b ?? a) as any[], }), generatedResume: Annotation<string>({ reducer: (a: string, b: string) => b ?? a ?? "", }), messages: Annotation<any[]>({ reducer: (a: any[], b: any[]) => (a ?? []).concat(b ?? []), }), }); async function parseResumeNode(state: typeof AgentState.State) { const llm = getLLM(); const parser = structuredParser(); // zod定义的JSON Schema const result = await llm.invoke([ { role: "system", content: RESUME_PARSE_PROMPT }, { role: "user", content: state.rawResume }, ]); const parsed = parser.parse(result.content); return { parsedResume: parsed }; } async function evaluateResumeNode(state: typeof AgentState.State) { // 基于解析后的简历 + 目标岗位生成评分 } async function optimizeSuggestionsNode(state: typeof AgentState.State) { // 基于评分结果生成逐条建议 } async function generateResumeNode(state: typeof AgentState.State) { // 生成完整优化版简历 } const graph = new StateGraph(AgentState) .addNode("parse_resume", parseResumeNode) .addNode("evaluate_resume", evaluateResumeNode) .addNode("optimize_suggestions", optimizeSuggestionsNode) .addNode("generate_resume", generateResumeNode) .addEdge(START, "parse_resume") .addEdge("parse_resume", "evaluate_resume") .addEdge("evaluate_resume", "optimize_suggestions") .addEdge("optimize_suggestions", "generate_resume") .addEdge("generate_resume", END) .compile({ checkpointer: memorySaver });这个图的可读性非常好,后面任何人接手都能一眼看懂整个Agent的执行链路。
3.2 解析节点的结构化输出实现
简历解析是整个Agent里最容易出错也最影响体验的环节。大模型直接输出一段话很容易,但要让后续节点能编程式地访问“工作经历列表”“技能关键词”,就必须要求解析结果是一个严格的JSON结构。
我的做法是定义一个zod Schema,把它作为JSON Schema传给模型:
import { z } from "zod"; import { ChatPromptTemplate } from "@langchain/core/prompts"; import { JsonOutputFunctionsParser } from "langchain/output_parsers"; const ResumeSchema = z.object({ basicInfo: z.object({ name: z.string().describe("候选人姓名"), email: z.string().describe("邮箱地址"), phone: z.string().describe("联系电话"), yearsOfExperience: z.number().describe("工作年限总和,按年计算"), }), workExperience: z.array(z.object({ company: z.string(), title: z.string(), startDate: z.string(), endDate: z.string(), achievements: z.array(z.string()), })), projects: z.array(z.object({ name: z.string(), description: z.string(), techStack: z.array(z.string()), })), skills: z.array(z.string()), });这里有个特别重要的经验:一定要求模型把“工作年限总和”作为显式字段输出,而不是让后续节点自己推断。因为简历文本里的时间描述经常不完整,比如只写了“2020 - 2023”,没写月份,模型被迫推断时会有很大误差,但把它当成一个独立字段让模型认真数一遍,准确率会高很多。
3.3 评分节点的分维度诊断逻辑
评分节点的Prompt是整个Agent里最需要反复调优的部分。我测试过好几种写法,最后稳定下来的是让它按五个维度打分:结构完整性、量化成果、关键词匹配、语言质量、STAR法则使用。每个维度满分20分,总分100分。
在写这个节点的Prompt时有个小技巧,就是让它先输出自己的推理过程,再给出分数。比如“我看到简历中项目部分有两个项目用到了React、TypeScript,与目标岗位要求的关键词匹配度较高,我认为该维度得分较高”。有了推理过程之后再打分,分数会稳定很多。这是我在实际测试里发现的——如果直接让模型打分,它经常给每个维度都打差不多的分数,得不到有区分度的诊断结果。
评分节点的伪代码:
async function evaluateResumeNode(state: typeof AgentState.State) { const llm = getLLM(); const prompt = `你是一名资深技术招聘专家。请根据以下目标岗位和简历结构化内容,对简历进行分维度评估。 目标岗位:${state.targetRole} 简历内容:${JSON.stringify(state.parsedResume)} 评估要求: 1. 先逐维度分析,说明理由 2. 再给出每个维度的分数(0-20分) 3. 最后输出总分(0-100分) 输出格式必须为JSON。`; const result = await llm.invoke([ { role: "system", content: EVALUATE_SYSTEM_PROMPT }, { role: "user", content: prompt }, ]); return { evaluation: JSON.parse(stripCodeFence(result.content)) }; }这里stripCodeFence是我自己写的一个小工具函数,专门处理模型把JSON输出包在```json代码块里的情况。这个问题出现的频率比想象的高,后面排查章节会专门聊。
3.4 生成定制简历时的流程控制
最后一个节点是生成完整简历,这个节点跟前几个不太一样:它不是一次性输出,因为一份完整的优化简历可能超过三千字,大模型一次生成的token长度有限,而且生成不稳定。我在这里引入了流式输出,通过回调用ReadableStream把内容逐步推给前端。
在LangGraph.js里,节点内部可以直接调用模型的stream方法:
async function generateResumeNode(state: typeof AgentState.State, config) { const llm = getLLM(); const stream = await llm.stream([ { role: "system", content: GENERATE_RESUME_PROMPT }, { role: "user", content: buildGenerateInput(state) }, ], { signal: config.signal }); let fullText = ""; for await (const chunk of stream) { fullText += chunk.content; // 通过回调把chunk推给前端SSE } return { generatedResume: fullText }; }前端接流的写法,我放在后面“实操环节”统一说。
3.5 人工确认节点的中断恢复
这个机制是LangGraph.js最值回票价的功能。我用interruptBefore实现“解析完成后暂停,等用户确认”:
const compiledGraph = graph.compile({ checkpointer: memorySaver, interruptBefore: ["evaluate_resume"], });当图执行到evaluate_resume之前,框架会自动暂停,状态保存在checkpointer里。前端收到解析结果后,把thread_id和用户的确认/修正信息传回后端,后端调用graph.invoke(updatedState, { thread_id }),图会从暂停的地方继续往下执行。
我建议给每个用户的任务创建一个唯一的thread_id,用UUID就行。后续无论用户刷新页面还是隔几个小时再回来,只要thread_id不变,任务状态就能无缝恢复。这体验比重新填一遍表单强太多了。
4. 前端接入与流式交互的落地细节
4.1 Next.js Route Handler封装Agent接口
在Next.js的App Router里,我新建了app/api/resume-agent/route.ts来承接Agent调用。这个路由处理两种请求:POST启动任务,GET获取任务状态,另外SSE流式推送生成结果。
启动任务的简化实现:
import { NextRequest } from "next/server"; export async function POST(req: NextRequest) { const { rawResume, targetRole, threadId } = await req.json(); const initialState = { rawResume, targetRole, messages: [], }; const result = await compiledGraph.invoke(initialState, { thread_id: threadId, }); return Response.json({ threadId, parsedResume: result.parsedResume, status: "NEED_CONFIRM", // 表示需要用户确认解析结果 }); }流式生成简历的端点单独走SSE:
export async function GET(req: NextRequest) { const threadId = req.nextUrl.searchParams.get("threadId"); const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { const config = { thread_id: threadId, streamMode: "messages", callbacks: [ { handleLLMNewToken(token: string) { controller.enqueue(encoder.encode(`data: ${token}\n\n`)); }, handleLLMEnd() { controller.close(); }, }, ], }; await compiledGraph.invoke(null, config); }, }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }, }); }这里需要注意,我给invoke传的initialState是null,因为图会从checkpointer里恢复之前保存的状态,不需要再传一遍。这是LangGraph.js的checkpointer机制带来的好处。
4.2 前端实时展示token流
前端我用了一个自定义hook来管理SSE连接,核心逻辑是把fetch返回的response.body转成reader,循环读取decoded chunk并拼接到state里:
async function streamAgentOutput(threadId: string) { const response = await fetch(`/api/resume-agent?threadId=${threadId}`); const reader = response.body?.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader!.read(); if (done) break; const text = decoder.decode(value); const tokens = text .split("\n") .filter((line) => line.startsWith("data: ")) .map((line) => line.replace("data: ", "")); setGeneratedText((prev) => prev + tokens.join("")); } }这里有个坑:TextDecoder默认不会自动处理跨chunk的多字节字符。如果流式输出的是中文,一个汉字被拆成两个chunk传输时,用TextDecoder单独decode每个chunk会偶尔出现乱码。解决办法很简单——用TextDecoder的stream选项,让它保持内部状态:
const decoder = new TextDecoder("utf-8", { stream: false });实际测试下来,比较稳妥的是在每次decode时传{stream: true},然后最后一次读取时再decode(undefined, {stream: false})收尾。这个细节很容易被忽略,但会导致部分用户偶发看到乱码,排查起来还特别费力。
4.3 组件状态机的设计
简历工具的整个交互流程我用了一个简单的状态机来管理:IDLE、PARSING、NEED_CONFIRM、EVALUATING、SUGGESTING、GENERATING、DONE。
每个状态对应页面上不同的UI区块。比如NEED_CONFIRM状态时,页面展示一个表格,让用户核对解析出的姓名、工作年限、技能列表,旁边有“确认无误”和“修正”两个按钮。这种边做边让用户纠错的交互模式,比全部自动化但结果不准确要好很多,因为用户对AI的容忍度很低,但对“自己确认过的结果”接受度很高。
5. 实操过程中遇到的问题与排查实录
5.1 模型输出的JSON永远被Markdown代码块包裹
这个是出现频率最高的问题。即使是明确要求“只输出JSON”的Prompt,模型也经常输出```json开头和结尾的代码块。直接用JSON.parse会抛异常。
我的处理思路分两层。第一层写一个健壮的提取函数:
function stripCodeFence(text: string): string { const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/); if (jsonMatch) return jsonMatch[1].trim(); const braceMatch = text.match(/\{[\s\S]*\}/); if (braceMatch) return braceMatch[0]; return text.trim(); }第二层是在解析失败时走“重试节点”。LangGraph.js里可以在节点内部catch异常,然后重新调用一次模型,并额外加一句“上次的回复格式不是有效JSON,请只输出JSON对象”。实测重试一次的成功率接近98%。
这两层兜底加上之后,解析节点基本稳了。
5.2 流式输出到一半断了,前端永久loading
这个问题排查了很久。症状是前端收到一段内容后SSE连接突然中断,页面一直转圈。排查后发现两个原因叠加产生:
第一个原因是Vercel部署时路由默认有超时时间,长任务跑太久会被强制掐断。解决方法是给该路由单独配置export const maxDuration = 60;,并且不要使用默认的Edge Runtime(因为LangGraph.js需要Node API)。
第二个原因是我的Node版本太低,ReadableStream的某些API行为不一致。后来把项目Node版本升到20.x就稳定了。
我的建议是:如果你用LangGraph.js做这种长链路生成,本地调试没问题后,上生产环境前一定要用真实的慢模型(比如带上较长的简历文本)跑一遍完整流程。别用Mock数据测,Mock测不出超时问题。
5.3 Agent状态被“污染”:多个用户共用同一个thread_id
我在联调阶段发生过一个很尴尬的bug:用户A填了一份简历,生成了建议;用户B进来不用填简历,直接看到了用户A的生成结果。查了日志才发现,前端没有正确为每个新会话生成threadId,导致所有用户都共用了一个默认的thread_id。
解决办法是前端每次进入页面时用crypto.randomUUID()生成一个全新的threadId,而不是放在全局变量里。这个其实是Next.js开发者很容易掉进去的坑,因为本地开发时组件默认不会重新mount,看起来一切正常,但生产环境并发请求一上来就暴露了。
5.4 recursionLimit导致Agent执行失败
LangGraph.js有一个默认的recursionLimit(默认25),意思是整个图最多执行25个节点。对大多数Agent来说25次足够用,但我的简历Agent有个重试节点,如果模型连续输出坏格式触发了多次重试,再加上主链路的4个节点,偶尔会超出限制报错。
处理方式有两种:一是在compile()时调大recursionLimit;二是控制重试次数,超过3次就用固定的兜底JSON返回。我最终用的是第二种,因为它保证响应时间可控,不会因为模型反复出错无限拖下去。
5.5 成本控制:别让大模型反复解析同一份简历
简历解析这个节点调用的模型如果用的是高精度大参数模型,成本并不低。一份典型简历大约1500字,解析一次对应几千个token的输入输出。如果用户在“解析确认”环节修改了简历原文(比如发现AI漏了一段经历),重新跑整个图会导致解析节点再跑一次,成本直接翻倍。
我的优化方案是把rawResume的哈希值作为缓存key,如果用户没有改动原始文本,直接复用之前的parsedResume,跳过解析节点。在LangGraph.js里,可以在节点入口判断缓存,命中就直接返回已有结果,不调用模型。这个优化在长期运行后能省下不少钱。
6. 部署与性能调优的实战建议
6.1 把LangGraph.js的状态存进数据库,而不是内存
项目早期我用的checkpointer是MemorySaver,所有状态都存在进程内存里。测试环境没问题,但生产环境有两个隐患:一是重启后所有任务状态丢失;二是多实例部署时,用户可能被负载均衡打到不同实例,导致invoke时找不到对应的thread_id。
后来换成了数据库支持的checkpointer。LangGraph.js官方提供了多种checkpointer实现,可以直接接入Postgres。核心改动只有一行:
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!); const compiledGraph = graph.compile({ checkpointer });换成数据库持久化之后,任务状态跨实例、跨重启都稳稳的。这是我认为“从Demo到生产”最关键的一步,很多人忽略了这个,上线第二天就因为进程重启丢了一堆用户任务。
6.2 长任务用队列还是直接同步等待
简历生成任务耗时通常15到30秒,如果直接在Route Handler里同步等待,用户在请求期间要保持HTTP连接不中断,一方面体验不够好,另一方面在Serverless环境(比如Vercel)很容易触发超时限制。
我的落地方式是:生成类长任务启动后立即返回task_id,前端轮询状态;只有SSE流式返回是在一个持续连接里做。这样既兼顾了实时性,也规避了同步等待的部署限制。如果你不想自己写轮询,也可以用Trigger.dev或者Inngest这类任务队列服务,把生成任务塞进队列再回调通知,架构上更干净。
6.3 模型选型与prompt调优的参数建议
我调试过程中对比了几组模型参数,最终稳定的配置是:解析和评分用temperature 0.1,建议生成和简历生成用temperature 0.5。原因很简单:解析和评分要的是稳定和准确,温度越低越好;建议生成要有一定的发散性,温度太低会导致所有建议都长得很像,千篇一律。
另外,所有节点的Prompt我都显式声明了“以JSON格式返回”,并且把zod schema转换成JSON Schema塞进Prompt里。这比让模型自由发挥返回格式的效果好太多,也大大减少了前面提到的“JSON被代码块包裹”问题的出现概率。
7. 复盘:LangGraph.js简历Agent的架构价值与可扩展性
这个项目做完之后,我最大的体会是:Agent类应用不要一上来就写代码,先把“图”画出来。LangGraph.js的真正价值不是帮你会用大模型,而是逼着你去梳理业务链路里的节点、状态、条件分支和人工干预点。梳理清楚之后,实现就是按图施工,后面加需求也是按图加节点,思路会特别清晰。
这个架构的可扩展性也体现在多个方面。比如现在只支持“按目标岗位优化简历”,后续完全可以加一个“面试题预测”节点:在optimize_suggestions之后,基于简历内容和目标岗位生成一份模拟面试题。只需要在图上加一个节点,再加一条边,不需要动现有的任何代码。
再比如可以做“简历版本对比”:把generate_resume节点备份一份原始文本,然后生成多个风格版本的简历,让用户选择。这也是在图上加并行分支的事情,LangGraph.js天然支持并行节点。
我现在已经把同一套图架构复用到了部门里的其他文档处理工具上,只是换了节点内容,状态的流转逻辑完全不用动。这就是图编排框架带来的架构红利——先在逻辑层面把流程梳理清楚,再落到框架里,开发效率会大幅提升。
最后再分享一个小技巧:调试LangGraph.js的图时,可以用它官方提供的drawMermaid()方法,把图直接生成Mermaid格式代码,然后拿到任意Mermaid渲染器里可视化。我看了一眼生成的图,整个Agent的执行链路一目了然,给同事讲代码的时候用它做图示,比自己画半天流程图高效多了。不过生产环境记得关掉这个调试输出,避免暴露内部结构。