ARTICLE DETAIL

资讯详情

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

用Next.js和LangGraph.js构建简历AI Agent的实战指南

用Next.js和LangGraph.js构建简历AI Agent的实战指南 做这个简历工具的起因很实际年前帮学弟改了一轮简历发现大部分人的问题根本不是措辞而是结构、匹配度和可量化结果。当时手头正好在调研 AI Agent 的落地场景就想着干脆用 Next.js 加上 LangGraph.js 撸一个完整的简历 AI Agent 出来。从简历解析、诊断评分、定向优化到模拟面试一整条链路全部跑通。这篇文章不写宣传稿就是把从 0 到 1 落地这个 Agent 全过程的选型思考、状态图设计、核心代码实现和踩过的坑如实记录下来。想用 LangGraph.js 做实际业务 Agent、或者正考虑把 AI 能力嵌进现有 Web 应用的开发者看完应该能少走不少弯路。这个项目本身不算大但它的价值在于完整覆盖了一个 Agent 从有想法到能上线的所有环节前端交互、后端流式响应、LLM 调用、状态管理、结构化输出、错误恢复全都碰了一遍。而且简历这个场景非常典型——输入是非结构化的 PDF/文本经过多个子任务处理最终生成多份结果正好适合用图状态编排来做。如果你也在纠结AI Agent 到底怎么落地到实际产品这篇可以当成一份最小可行参考。1. 为什么用 Next.js LangGraph.js 做简历 Agent而不是一把梭1.1 AI Agent 主流架构里为什么选 LangGraph.js 当编排层先说结论AI Agent 的底层模型很成熟真正难的是流程可控。你让大模型自由发挥它能帮你写简历但也能在第三个环节突然跑偏把一个改简历的任务变成写情书。像 Coze、Dify 这类平台也能搭 Agent但对开发者来说最大的问题是流程不透明、扩展困难、代码不能进 Git。自己写编排我又不想跟复杂的回调地狱纠缠。LangGraph.js 解决的核心问题是把 Agent 的推理和执行过程建模成一张有向状态图。每个节点是一个函数比如解析简历、诊断问题、生成优化建议节点之间通过边连接并定义好状态如何流转。和 LangChain 里那种chain prompt1.pipe(model).pipe(parser)的线性串联相比LangGraph 的主角变成了图——有分支、有循环、有条件的跳转这才是 Agent 该有的样子。比如简历诊断完发现某个板块信息缺失Agent 可以回到解析节点要求补充而不是像线性链一样直接死磕到生成端。你可能会问LangChain 不也能做 Agent 吗LangChain 里已经内置了AgentExecutor等高层封装但它把大量决策细节隐藏了出了问题你不太清楚状态在哪一步挂掉的。LangGraph 则把每一个节点、每一条边摊开给你看调试体验更好也更容易精确控制 token 消耗和分支逻辑。对我这个项目来说我需要做的简历诊断、优化、模拟面试恰好就是一条带条件分支的流水线用 LangGraph 写出来的代码读起来和画在纸上的流程图一样直观这对后期维护是致命的友好。1.2 技术选型对比为什么不用纯 Next.js API 硬写 Agent很多人会问你直接用 Next.js 的 API Routes 写几个接口轮询调用 OpenAI不也能实现吗确实能但区别在于有状态和无状态。如果你只是调一次POST /api/resume/optimize把简历全文丢给模型返回一段优化文本那完全不用 LangGraph。这种模式本质是单次问答。但真实业务里的简历工具一定是多轮、多步骤、有记忆的第一步解析产生的结果摘要要传给第三步做匹配度分析用户在第二步说我的经历偏运营后续的优化方向就要跟着变。这种跨步骤的上下文状态你用无状态 API 写只能靠数据库字段硬存然后到处传递参数写到最后满屏幕都是if (step 3 mode interview flag a)。LangGraph 把状态管理做进了框架里。它的StateGraph自带一个全局的State对象每个节点函数都能读取和更新它节点之间天然共享上下文。我只需要定义好状态类型节点之间通过边传递信息状态的读写集中在同一份数据上不用再操心参数传递顺序。还有一点LangGraph.js 原生支持流式输出节点结果这正好解决了我想要的前端打字机效果。1.3 这个项目用 Next.js 而不是纯后端框架的原因我见过不少团队做 AI 项目前端用 React后端用 FastAPI中间跨两个仓库、两套部署联调一次要半天。这个简历工具本身不大我一个人全干没必要搞成分布式。Next.js 的强项在于全栈App Router 里可以直接写 Route Handler 做流式接口前端组件可以放在同一代码库部署到 Vercel 或自己的 Node 服务器都方便。还有一个实际的好处Next.js 的流式渲染和服务端组件对 LLM 输出有天然的配合。比如简历优化结果可以一边流式生成、一边渐进式渲染到页面组件里用户感知到的延迟从等待 10 秒变成首字 1 秒内出现。这种体验在没有 Next.js 的纯前端架构里很难做顺。更关键的是Next.js 生态里有成熟的ai库Vercel AI SDK它和 LangGraph.js 可以直接打通前端用useChat或者自定义 streaming handler 接后端流一套流程非常顺。选 Next.js 的核心逻辑不是跟风而是在全栈能力、流式体验、部署成本这三个维度上它是我这个规模项目的最优解。2. 基于 LangGraph.js 的简历 Agent 核心状态图设计2.1 把简历 Agent 拆成一张流水线状态图整个简历 Agent 的流程我在落地前画了一张纸面图然后直接照着翻译成 LangGraph 的StateGraph。既有线性流程也有条件分支正好把 LangGraph 的看家本领都用上。先看整体的图状态设计用文字描述你可以照着画出同样的图Start → parseResume解析简历 → checkMissingInfo判断关键信息是否缺失条件分支 ├─ 缺失 → requestMissingInfo向用户询问补充 ├─ 完整 → diagnoseResume简历诊断输出评分 → generateOptimizedResume生成优化后简历 → matchJobDescription对JD匹配度分析 → simulateInterview模拟面试 → End这张图看起来简单但落地时每个节点内部都有完整的 prompt 工程和输出解析逻辑。checkMissingInfo本身不是一个简单的条件它需要从解析结果里判断教育经历、工作经历、项目经历、技能清单、联系方式这五类信息是否齐备。如果哪一类缺失或者内容过短State 里会挂起一个waitingUserInput的标记Agent 会先向用户提问拿到补充后再继续往下走。设计的关键在于节点即函数、状态即全局变量这一思路。每个节点函数长这样输入整个 State输出 State 的一部分更新。节点之间不直接通信只依赖 State。这让每个节点都能独立测试——我可以在 REPL 里单独喂给diagnoseResume一段简历文本看它的诊断结果是否合理而不用把整条流程跑通。2.2 状态对象设计和各节点职责拆分State 是整个 Agent 的数据中枢。我定义的状态结构大概是这样的TypeScript 类型后续代码会展开interface ResumeAgentState { rawText: string; // 用户上传的原始简历文本 parsedResume: ParsedResume | null; // 结构化解析后的简历对象 missingInfo: string[]; // 缺失的关键信息清单 diagnosis: DiagnosisResult | null; // 诊断结果含各项评分和改进点 optimizedMarkdown: string; // 优化后的简历按板块拆分的Markdown jdSummary: string; // 岗位JD的关键要素提炼 matchScore: number; // 简历与JD的匹配度评分0-100 interviewQuestions: InterviewQuestion[]; // 模拟面试问题及参考思路 userInputs: Recordstring, string; // 用户在对话中补充的信息 }State 里每个字段的职责尽量单一避免大杂烩。比如parsedResume是纯数据对象missingInfo是判断条件的分支依据userInputs是对用户反馈的临时存储。节点函数拿到 State 后只读取自己需要的字段更新也只更新自己要负责的那几个字段互不干扰。这样做的最大好处是某个字段出了问题五分钟内就能定位到是哪个节点改坏了。节点职责的拆分我遵循了一个节点做一件事的原则。解析节点只负责把纯文本变成结构化 JSON诊断节点只负责评分和改进建议不做改写优化节点只负责生成优化后的简历内容匹配度分析节点只负责算分数和给出匹配点分析。这样拆完后每个节点的 prompt 都很短逻辑清晰token 消耗也更可控。如果你把多个职责塞进一个节点prompt 一长输出的稳定性就会断崖式下跌模型很容易把不该丢的信息丢掉。2.3 条件分支的设计如何让 Agent 决定下一步走向LangGraph 比线性链强的地方就是条件边。在checkMissingInfo节点之后我加了一个路由函数LangGraph 会根据这个函数的返回值决定走哪条边function routeAfterCheck(state: ResumeAgentState): requestInfo | diagnose { if (state.missingInfo.length 0) { return requestInfo; } return diagnose; }这个函数本身不复杂关键在于missingInfo是怎么生成的。我没有直接让 LLM 返回一个是/否的布尔值因为 LLM 的自由文本结果不稳定。我让解析节点输出结构化的ParsedResume然后用正则硬校验必填字段如果parsedResume.education为空数组 → 判定缺教育经历如果parsedResume.workExperience为空数组 → 判定缺工作经历如果parsedResume.contact.phoneOrEmail为空 → 判定缺联系方式如果parsedResume.skills.length 3→ 判定技能太单薄用正则和规则判断悬着的分支条件而不是依赖模型二次判断是我在这个项目里悟到的关键一点。Agent 的智能应该体现在大模型擅长的内容生成和理解上规则分支该硬的时候就该硬否则 Agent 就变成了一个失控的随机数生成器。后面我在实操中还发现让 Agent 主动提问补充信息时也要限制它只能针对识别出的缺失项提问避免它发散问一些和简历无关的隐私问题。3. 简历 AI Agent 完整落地从后端流程到前端交互的实战实现3.1 环境准备和项目初始化我用的实验环境是 Node.js 20 LTS npm。项目基于 Next.js 14 App RouterTypeScript。初始化命令直接用npx create-next-applatest resume-agent --typescript --tailwind --app cd resume-agent npm install langchain/langgraph langchain/openai ai zod依赖里zod很关键它是定义结构化输出的 schema 校验工具。LangGraph 本身不强制你用什么 schema 库但你做结构化输出的时候没有 zod 你就要自己手写一堆类型守卫写起来很痛苦。后面解析节点的代码里我会展示怎么用zod定义输出结构然后让 LLM 严格遵循格式。还需要注意.env里配置模型 API Key。如果你用的是国内可直连的模型LangGraph.js 也支持通过ChatOpenAI的兼容接口接进来只要 model 名称和 baseURL 对应即可。我自己本地调试用的是标准 OpenAI 接口线上换成了国内模型厂家的 OpenAI 兼容端点代码层面只改了一行 baseURLLangGraph 和这个兼容性处理得挺好。3.2 核心代码实操定义 StateGraph 和四个核心节点先把最核心的编排代码写出来。基于langchain/langgraph定义一张状态图串起全部节点。import { StateGraph, END } from langchain/langgraph; import { ChatOpenAI } from langchain/openai; import { z } from zod; const model new ChatOpenAI({ model: gpt-4o-mini, temperature: 0.3, }); // 定义状态 const graphState { rawText: { value: (prev?: string) prev ?? }, parsedResume: { value: (prev?: any) prev ?? null }, missingInfo: { value: (prev?: string[]) prev ?? [] }, diagnosis: { value: (prev?: any) prev ?? null }, optimizedMarkdown: { value: (prev?: string) prev ?? }, matchScore: { value: (prev?: number) prev ?? 0 }, jdSummary: { value: (prev?: string) prev ?? }, interviewQuestions: { value: (prev?: any[]) prev ?? [] }, }; const workflow new StateGraph(graphState) .addNode(parseResume, parseResumeNode) .addNode(checkMissingInfo, checkMissingInfoNode) .addNode(requestInfo, requestInfoNode) .addNode(diagnoseResume, diagnoseResumeNode) .addNode(generateOptimizedResume, optimizeResumeNode) .addNode(matchJob, matchJobNode) .addNode(simulateInterview, interviewNode) .addEdge(__start__, parseResume) .addEdge(parseResume, checkMissingInfo) .addConditionalEdges(checkMissingInfo, routeAfterCheck, [requestInfo, diagnoseResume]) .addEdge(requestInfo, checkMissingInfo) .addEdge(diagnoseResume, generateOptimizedResume) .addEdge(generateOptimizedResume, matchJob) .addEdge(matchJob, simulateInterview) .addEdge(simulateInterview, END); export const resumeAgent workflow.compile();这段代码读起来就是一张图。routeAfterCheck是条件边缺信息时回到requestInfo节点这个节点会用一个专门的交互 prompt 向用户提问。请求到信息后回到checkMissingInfo重新检查直到信息齐了才继续往下走。这一步是 Agent 和普通一次性 API 的本质区别——它能根据自身状态发起多轮对话而不是把用户输入丢进模型返回结果就完事。有些刚接触 LangGraph 的朋友疑惑为什么value字段要用这样一个函数形式这是 LangGraph 的状态更新机制。你可以传入一个 reducer它会决定当节点返回部分状态更新时如何把新值和旧值合并。我第一次用默认值只写了value: null结果节点返回数据后状态没变查了半天文档才发现要定义{ value: (prev) prev ?? }这种带默认值的 reducer让它接受首次写入。这个坑后面第 4 节会继续聊。3.3 解析节点用结构化输出让 Agent 学会读简历parseResume节点是整个流水线的入口。它的任务是把用户上传的 PDF/文本转成结构化的简历对象。我在代码里嵌入了一个 zod schema交给模型做结构化输出然后把结果写回状态。import { z } from zod; const ResumeSchema z.object({ personal: z.object({ name: z.string().describe(姓名), phone: z.string().nullable().describe(手机号), email: z.string().nullable().describe(邮箱), location: z.string().nullable().describe(所在城市), }), education: z.array(z.object({ school: z.string(), major: z.string(), degree: z.string(), startDate: z.string(), endDate: z.string().nullable(), })), workExperience: z.array(z.object({ company: z.string(), title: z.string(), startDate: z.string(), endDate: z.string().nullable(), achievements: z.array(z.string()), })), projects: z.array(z.object({ name: z.string(), description: z.string(), highlights: z.array(z.string()), })), skills: z.array(z.string()), });解析节点的完整流程是把 rawText 塞进 prompt告诉模型你是简历解析引擎只负责提取结构化信息不要补全或编造简历里不存在的内容然后调用model.withStructuredOutput(ResumeSchema)直接拿到符合 schema 的 JSON。这个withStructuredOutput的用法是 LangChain.js 提供的它会用函数调用的方式强制模型按 JSON Schema 输出配合 zod 做了运行时校验。对 LLM 输出有了解的人都知道这一步要紧不然模型输出的 JSON 十个里有三四个是坏的。实际测试中解析节点用温度 0.1 的低温因为我希望它只做提取不做创作。如果温度太高模型可能会自作主张给简历里添加他觉得应该有的内容而不是用户真的写过的。这件事在简历场景里很敏感——添加不存在的经历属于严重的信任危机所以解析模型必须极端保守。我甚至在 prompt 里刻意强调没有提取到的字段使用 null 或空数组不要猜测。最终解析出来的数据直接铺在页面上用户修改后存为可信数据后续诊断和优化都基于这份数据而不是再回去读原文。3.4 诊断、优化、匹配、面试四个节点的 prompt 设计思路诊断节点是给用户获得感最直接的一步。它接收解析后的结构化简历输出一份诊断报告包括总体评分百分制、分维度评分结构、内容、措辞、量化、匹配度、以及每项需要改进的具体建议。评分我用严格的 JSON 结构输出让前端可以直接渲染成雷达图。prompt 的核心设计原则是必须逐条指向原文证据、给出具体的修改建议不要输出大而化之的鸡汤。优化节点是诊断节点的输出再加工。它把diagnosis和parsedResume一起传给模型在保留基本信息不变的前提下逐条应用改进建议输出一版标记了修改点的 Markdown 简历。这里我做了个细节要求优化结果用自定义格式标注每一处修改的原因比如// [改动] 将负责XX系统改为主导XX系统覆盖用户量提升40%。这样用户看到的不是一版黑箱重写内容而是能清楚知道模型动了哪些地方、为什么改。对简历这种牵一发动全身的文档黑箱式整体重写是最不可取的。匹配度分析节点是要附加一个岗位 JD 输入。它的输出包含一个matchScore0-100和若干条匹配点与缺口点。我要求它基于 JD 里高频关键词和硬性要求做对比而不是泛泛而谈。这里的分数计算是让模型按关键词出现情况和技能覆盖率来给的还会输出一个missingKeywords数组方便前端高亮展示。模拟面试节点是整条链路里唯一会多轮交互的节点。它的任务不是生成一份面试题列表而是根据用户正在应聘的岗位生成 5 道与简历内容强相关的面试问题然后逐题和用户对话。这里我把每个问题设计成带questionType的枚举比如行为面试、项目深挖、技能考察、情景题等并附上参考回答思路和追问列表。如果用户回答完Agent 会基于回答内容生成追问实现真的对话感而不是一锤子买卖。这个节点也是 LangGraph 循环边发挥作用最明显的地方用户答完一题状态会回到面试节点生成下一题内容同时保留历史问答记录。3.5 前端流式交互Next.js Route Handler 对接 LangGraph后端图编译好后前端需要通过 HTTP 调用并流式拿到结果。这里走的是 Next.js App Router 的 Route Handler 方案在app/api/agent/route.ts里实现一个 POST 接口接收用户的简历文本和当前回复消息调用resumeAgent.stream()并逐块转发给前端。import { resumeAgent } from /lib/agent; import { NextRequest } from next/server; export async function POST(req: NextRequest) { const body await req.json(); const { rawText, jd, userInputs } body; const config { configurable: { thread_id: body.threadId || default } }; const stream await resumeAgent.stream( { rawText, jd, userInputs }, config ); const encoder new TextEncoder(); const readable new ReadableStream({ async start(controller) { for await (const chunk of stream) { const payload data: ${JSON.stringify(chunk)}\n\n; controller.enqueue(encoder.encode(payload)); } controller.close(); }, }); return new Response(readable, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }, }); }前端我写了一个自定义 hook用fetch调用这个接口再从ReadableStream里逐步解析 SSE 数据块把最新的输出追加到 UI 上。这里没有用useChat库原因是 LangGraph 返回的 chunk 结构不是简单的对话文本而是{ nodeName: { ...partialState } }这种结构。我需要根据 node 名来分区块展示如果直接用现成的useChat反而要绕一层适配。封装一个useAgentStreamhook 反而更灵活它可以按节点名分别存储输出在界面上一边跑流程图一边实时显示节点进度。这个逐步点亮流程图的效果也成了整个产品的面子担当——用户看着 Agent 从解析简历走到模拟面试对 AI 的信任感比纯黑盒等待强得多。流式读取的细节有个容易踩的坑SSE 的分包可能会把一个事件拆成两次传输。前端的解析逻辑不能只看\n\n就一次性切割需要用TextDecoder配合一个 buffer 把残留数据累积起来。这里我贴一下关键代码后面踩坑清单里还会细讲const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n\n); buffer lines.pop() ?? ; for (const line of lines) { if (line.startsWith(data: )) { const json JSON.parse(line.slice(6)); handleChunk(json); } } }3.6 部署到服务器时需要处理的两个核心配置本地跑通后部署到服务器时有两个配置必须提前处理好否则上线了就是一堆乱码报错。第一个是 Node.js 版本。我的服务器环境是 Node 20但如果你用 Node 18有些流式 API 的表现会有细微差异。LangGraph 依赖的比较新的 Web Streams API 需要 Node 18但平滑体验建议直接用 20 LTS。第二个是函数超时设置。Next.js 单独部署成 Node 服务时我遇到了一个很坑的情况默认的路由处理器没有做超时限制但如果部署到某些 Serverless 平台默认函数超时只有 10 秒或 30 秒而一个完整的简历 Agent 流程跑完可能要 40 到 60 秒要调多个模型节点。这个问题导致线上会看到用户跑了一半接口直接 504。解决的方法有两个自托管 Node 服务器覆盖超时限制或者把长任务队列化处理。我选择的是自托管毕竟这个场景的核心价值就是实时流式等队列再回来取结果会让体验大打折扣。但如果你用的是 Serverless 平台最好把流程图拆成多个小接口每一步单独部署成一个短函数。4. 实测踩坑记录流式返回、JSON 解析、上下文管理4.1 流的 eta 信息到底怎么解析别把每个 chunk 当完整 JSON流式返回是用户体验的关键但也是我前期调试最费时间的一块。LangGraph 的stream()返回的 chunk 结构比一般 LLM 流式响应复杂得多。我第一次接的时候想当然地认为每个 chunk 都是类似{ text: 你好 }的增量文本直接往前端拼。结果界面上出现了大量[object Object]控制台也是各种 JSON 解析错误。实际打印后发现stream()返回的每个 chunk 通常是{ nodeName: { ...updates } }比如{ parseResume: { parsedResume: { ... } } } { diagnoseResume: { diagnosis: { score: 75, ... } } }所以在前端解析事件时必须做二次处理先判断当前事件的nodeName是哪个节点再提取该节点返回的部分状态数据更新对应的 UI 区域。同时由于流式传输是持续不断的界面接收到的数据可能不是按节点完整到达的——比如一个节点的输出很长会被拆成若干块。我用一个agentProgress的状态对象维护每个节点的完成度和输出前端再根据完成度渲染进度条和内容区。这里有个建议先用浏览器的 Network 面板直接看接口返回的原始流看清数据结构再写解析逻辑比凭空猜要高效得多。4.2 JSON 结构化输出不稳定用 zod 和 withStructuredOutput 兜底简历 Agent 的每一个节点都要返回结构化 JSON这看起来是小事实际上却是最容易翻车的环节。最开始我图省事直接在 prompt 里写以 JSON 格式返回结果模型要么在 JSON 前后加说明文字要么把注释写进 JSON 里要么返回的是 Markdown 代码块包裹的 JSON。解析起来手忙脚乱。现在的做法是统一走model.withStructuredOutput(schema)。这个函数内部会用 tool calling 方式让模型严格按 schema 生成 JSON并且自带校验。如果校验失败LangChain 会尝试重试。这里我还加了一层保险即使有了withStructuredOutput我仍会在节点函数里用 zod 的safeParse做一次手动校验。因为就算模型生成的 JSON 符合 schema也可能存在语义上错了但结构上对的情况。比如解析简历时把公司名填到了学校名里结构合法但内容错误这种只能靠 prompt 和模型能力控制无法靠 parser 解决。所以我在解析 prompt 里会特别强调公司名和学校名不能混淆并给出几个常见反例。还有一个隐藏问题深层嵌套的 schema 有时会超出某些模型的函数调用能力限制。我一开始定义了非常复杂的嵌套 schema结果小模型输出质量明显下降。解决方法是把大 schema 拆成几个小的子 schema分多次调用模型完成。比如先解析基本信息再分别调用解析教育经历、工作经历和项目经历。虽然多调了几次模型但稳定性提升显著。这个经验也验证了一个通用原则对 Agent 来说给模型的每一个任务都尽量小、边界清晰输出质量远比一个巨型 prompt 更可控。4.3 LangGraph 状态 reducer 的坑默认值不是你以为的那样LangGraph 的 StateGraph 类型定义里每个字段往往是{ value: (prev?: T) T }这样的 reducer 函数。这个设计容易让人困惑我就是在这里浪费了不少时间。一开始我写状态定义时给字段做了简单的直接赋值const graphState { rawText: { value: (prev?: string) prev ?? }, parsedResume: { value: (prev?: any) prev ?? null }, };结果节点函数的返回值总是覆盖不了旧值或者只生效一次。后来弄明白 LangGraph 的节点返回有两种模式如果节点返回的是一个PartialState它会和现有状态做 merge如果返回的是{ state: ..., update: ... }这种对象则需要你显式声明如何 merge。我在这里建议直接用{ value: (prev) prev ?? }这样的 reducer 方式让每个字段自己实现首次赋值、后续跳过的更新逻辑。如果某个字段需要累积追加比如interviewQuestions数组要不断 push 新题就写一个数组 concat 的 reducer。这段逻辑看起来不起眼但不理清楚会导致状态更新行为玄学化。4.4 token 消耗不设限一个用户能把你的预算打到爆Agent 是 token 消耗大户这点不亲自上线你不会意识到。简历 Agent 一个完整流程会调用多次大模型解析一次、诊断一次、优化一次、匹配一次、模拟面试每轮问答一次。如果用户上传一份很长的简历再上传一段很长的 JD每次上下文里都塞着全量的 rawText、parsedResume 和对话历史平均单次完整流程可能消耗 8k 到 12k tokens。这还只是良性使用。没人盯着的话模拟面试这个环节可以让用户无限轮询钱直接打水漂。我的处理方案分三层第一用更小更快的模型跑解析和诊断这类子任务temperature 低、输出结构固定不需要顶级模型第二给每个环节设置上下文窗口上限解析结果和诊断结果同步做内容截断不会把十万字简历全文带进后续所有节点第三也是最重要的给模拟面试设置最大轮数和最大 token 上限达到后自动终止循环并输出总结。具体实现上我用recursionLimit参数限制整张图的执行步数同时在后端接口层做了每用户每小时调用次数限制。这种能省则省、能限制就限制的思路是所有 AI 应用上生产环境的必修课。4.5 常见问题排查速查表现象可能原因解决办法流式输出变成[object Object]前端把 chunk 当文本直接拼接没解析 JSON按nodeName提取数据并在前端逐步渲染withStructuredOutput偶尔抛错模型输出与 zod schema 不匹配用safeParse捕获错误并让节点重试一次状态更新只生效一次LangGraph 状态字段的 reducer 写错直接覆盖而非合并检查value的逻辑数组字段用 concat 风格 reducer整图运行超时某个节点模型等待过长或节点死循环设置 recursionLimit 单步 API 超时简历文本和 JD 都很长时响应极慢上下文里塞了过多原始文本解析后用摘要字段替代全文后续节点只读摘要模型在模拟面试中忘记之前的问答没有把历史对话写入 state在 State 中新增conversationHistory字段每轮结束后追加上一条排查问题最笨但最有效的方法是给每个节点打日志打印进入节点时的 state 摘要和节点输出。LangGraph 本身支持在stream时预留 verbose 参数能把每一步的调用链打出来。把日志打出来基本一眼就能定位到是解析坏了、prompt 坏了还是 reducer 坏了比我对着错误消息瞎猜要快得多。5. 后续还能怎么扩展这个简历 Agent 目前的链路已经完整覆盖了从解析到模拟面试的全程但以 LangGraph 的底子扩展方向还有不少。比如接入简历附件上传时做 PDF 文本抽取或者对接招聘网站 JD 自动抓取再比如把不同角色的面试官观点做成多个子 Agent然后编排进一张更大的图里让技术面试官 Agent、HR 面 Agent并行产出。甚至可以把多个用户的简历数据聚合到一个独立的手动触发节点跑批量分析生成岗位适配度报告。我自己最想做的下一步是引入用户反馈闭环。现在优化节点的输出是单向的如果用户手动改了某个板块Agent 不会感知到。加入一个userEdits字段把用户手动修改的内容同步回状态让诊断节点的后续回合理解用户偏好这样才真正形成了一个有记忆、能进化的 Agent 循环。做这个扩展其实不需要改流程结构只需改 state 类型定义和对应节点的 promptLangGraph 这种图的架构在这个场景下就显得很舒服。最后分享一个我个人的体会AI Agent 的开发核心不在模型不在炫技而在把流程拆得足够细、把状态管得足够稳、把成本控得足够死。LangGraph.js 最大的价值不是替代 LangChain而是逼着你用图的方式思考 Agent——每个节点干一件事状态统一流转一切可观测、可回放。如果你打算做类似的项目先在纸上把流程图画明白再把手伸向代码可能比我当初直接动手写要顺利得多。
返回列表