Next.js + LangGraph.js 实战:从零搭建简历 AI Agent 的架构与工程细节

发布时间:2026/10/2 5:50:23
Next.js + LangGraph.js 实战:从零搭建简历 AI Agent 的架构与工程细节
简历工具这个赛道表面上看已经被做烂了——在线编辑器、模板库、PDF导出似乎没什么新意。但真正动手做过简历产品的人都知道用户真正的痛点从来不是没有模板而是不知道该写什么。一份简历从空白到能用80%的时间花在措辞打磨和经历梳理上而不是排版。这就是我决定用 Next.js LangGraph.js 搭一个简历 AI Agent 的出发点让 Agent 帮用户把我做过什么翻译成招聘方想看什么。这个项目适合谁参考如果你已经会用 React 和 Next.js想找一个真实场景把 AI Agent 从 Demo 做到能上线那这篇内容基本能覆盖你 90% 的坑。如果你只是想了解 LangGraph.js 到底怎么用我也会把每个设计决策背后的原因讲清楚。整篇内容基于我实际落地的版本展开包含架构选型、状态机设计、流式输出、并发处理和上线后的真实问题。1. 为什么简历场景值得用 Agent 而不是单次 Prompt1.1 简历生成不是一次性问答而是多轮状态演进大多数人做 AI 简历工具的第一反应是写一个 Prompt把用户的原始经历丢进去让模型输出润色后的版本。我一开始也是这么干的结果发现三个致命问题。第一用户输入的信息是残缺的。一个人写负责公司后台系统开发这句话里没有技术栈、没有规模、没有成果。单次 Prompt 拿到的就是这句话模型只能瞎编或者输出同样空洞的内容。第二简历优化需要多轮交互。用户需要先补充信息再确认方向再调整措辞这是一个有状态的对话过程。第三不同模块的处理逻辑不一样。工作经历要突出成果量化技能列表要匹配目标岗位自我评价要控制篇幅用一个大 Prompt 全包会导致每个模块都做得不精。LangGraph.js 解决的核心问题就是状态。它把 Agent 的执行过程建模成一张图节点是处理步骤边是流转条件整个图共享一个 State 对象。对简历工具来说这个 State 里存的就是用户的基本信息、原始经历、优化后的各模块内容、当前进行到哪一步。每次用户补充信息State 更新图继续往下走而不是从头再来。1.2 单次 Prompt 方案在真实用户手里会崩在哪我做过一个对比测试找了 20 个真实用户一半用单次 Prompt 版本一半用 Agent 版本。单次 Prompt 版本的问题集中爆发在第二轮交互用户看到润色结果后想改但系统没有上下文只能让用户重新描述体验直接断裂。还有一个更隐蔽的问题——单次 Prompt 无法做信息补全。比如用户写了提升了系统性能Agent 版本会追问提升了多少从多少到多少用的什么手段而单次版本只会把这句原样保留或者编一个数字。从工程角度看单次 Prompt 还有一个维护性问题所有逻辑塞在一个字符串里改一处影响全局没法做单元测试也没法针对不同模块单独调优。Agent 方案把每个处理步骤拆成独立节点每个节点可以单独测试、单独换模型、单独调 Prompt这在长期迭代里是决定性的优势。1.3 LangGraph.js 相比直接调 API 的工程收益有人会问我用一个 while 循环加状态变量不也能实现多轮吗能但你会很快遇到几个问题。第一流程分支会越来越复杂if-else 嵌套到第五层的时候你自己都看不懂。第二中断和恢复很难做用户填到一半关掉页面下次回来要能接着填。第三流式输出和状态持久化需要自己造轮子。LangGraph.js 把这些都抽象好了。它的StateGraph让你用声明式的方式定义节点和边checkpointer负责状态持久化streamEvents负责流式输出。你写的是业务逻辑而不是流程控制代码。这个区别在项目从 Demo 走向生产的过程中会越来越明显。我实测下来用 LangGraph.js 重写之后核心流程代码量减少了大约 40%而且可读性提升明显。2. 项目整体架构与技术选型取舍2.1 Next.js App Router 承担的角色划分这个项目用 Next.js 15 的 App Router但并不是所有东西都塞进 Next.js。我的划分原则是面向用户的交互和轻量逻辑放 Next.js重计算和长流程放独立服务。具体来说Next.js 负责页面渲染、表单交互、调用 Agent 服务的 API Route、流式响应的转发。Agent 的核心图执行放在一个独立的 Node.js 服务里通过 HTTP 和 SSE 与 Next.js 通信。为什么不全放 Next.js 的 API Route 里因为 Agent 执行是长任务一次简历优化可能跑 30 秒到 2 分钟放在 Serverless 环境里容易超时而且状态持久化需要常驻进程。独立服务可以用长连接、可以做进程内缓存、可以水平扩展。前端页面结构上我用了三个主要路由/editor是简历编辑主界面/agent是 Agent 对话面板作为侧边栏嵌入 editor/preview是实时预览。App Router 的 Server Component 用来做首屏数据加载Client Component 用来做交互和流式渲染。2.2 LangGraph.js 图结构设计节点、边与状态定义整个 Agent 图我设计了 7 个节点用一张表说清楚每个节点的职责和输入输出。节点名称职责输入 State 字段输出 State 字段parseInput解析用户原始输入提取结构化信息rawInputparsedProfilecheckCompleteness检查信息完整度决定是否追问parsedProfilemissingFields, nextActionaskQuestion生成追问问题missingFieldscurrentQuestionoptimizeSection针对单个模块做优化parsedProfile, targetSectionoptimizedContentmatchJob根据目标岗位做关键词匹配optimizedContent, jobDescriptionmatchedKeywordsformatOutput格式化为简历结构optimizedContentfinalResumereview自检输出质量finalResumereviewResultState 的定义用 LangGraph.js 的Annotation来做核心字段包括messages对话历史、parsedProfile结构化信息、currentStep当前步骤、resumeData简历数据。这里有个关键设计messages用messagesStateReducer做追加其他字段用覆盖式更新。因为对话历史需要累积而结构化数据每次都是全量替换。边的设计上checkCompleteness是一个条件边如果missingFields为空走optimizeSection否则走askQuestion问完之后回到checkCompleteness形成循环。这个循环是 Agent 能追问到底的关键。2.3 模型层选型为什么主流程用大模型、追问用小模型模型选型上我没有一刀切。主流程的optimizeSection和matchJob用能力强的模型因为这两个节点直接决定输出质量。而askQuestion和checkCompleteness用轻量模型因为这两个任务本质是分类和生成短问题不需要强推理能力。这么做的直接收益是成本。我统计过一次完整的简历优化流程大约调用模型 8 到 12 次其中追问类调用占 60% 以上。把追问换成轻量模型后单次流程成本下降了约 55%而用户感知的质量没有明显变化。这里的关键是追问的质量取决于问题模板和上下文而不是模型本身的推理能力。我在 Prompt 里给了明确的追问策略比如优先追问量化数据其次追问技术细节轻量模型完全能执行。2.4 状态持久化checkpointer 选型与数据落库策略LangGraph.js 的 checkpointer 我用了 Postgres 版本。为什么不用内存版因为用户填简历是个跨会话的过程今天填一半明天接着填是常态。内存版一重启就没了体验不可接受。数据落库策略上我做了两层LangGraph 的 checkpointer 存的是图执行的完整状态用于恢复业务数据库存的是结构化的简历数据用于展示和导出。这两层是解耦的checkpointer 的数据可以定期清理业务数据长期保留。这里踩过一个坑一开始我把两者混在一起结果 checkpointer 的 schema 一变业务数据就受影响。分开之后LangGraph 升级版本也不影响业务表结构。3. 核心节点的实现细节与 Prompt 工程3.1 parseInput 节点把口语化描述转成结构化数据这个节点的任务是把用户那句我在上一家公司做后端主要写 Java搞过一些高并发的东西转成结构化数据。我用的是带 structured output 的模型调用定义一个 Zod schema 约束输出格式。import { z } from zod; const ProfileSchema z.object({ basicInfo: z.object({ name: z.string().optional(), targetRole: z.string().optional(), yearsOfExperience: z.number().optional(), }), experiences: z.array(z.object({ company: z.string(), role: z.string(), techStack: z.array(z.string()), responsibilities: z.array(z.string()), achievements: z.array(z.string()), metrics: z.array(z.string()).optional(), })), skills: z.array(z.string()), missingInfo: z.array(z.string()), });这里有个实操心得不要指望模型一次就把所有字段填满。我在 Prompt 里明确告诉模型不确定的字段留空并在 missingInfo 里列出这样checkCompleteness节点才有东西可追问。如果让模型硬填它会编造信息后面很难纠正。另一个细节是metrics字段单独抽出来。因为量化成果是简历里最有价值的部分单独抽出来方便后续做针对性追问和强化。3.2 checkCompleteness 与追问循环的设计这个节点是整个 Agent 的大脑它决定什么时候继续追问、什么时候进入优化。逻辑上它做三件事检查必填字段是否齐全、检查成果是否有量化、检查技能是否匹配目标岗位。追问策略我设计了一个优先级队列缺少量化成果的经历最高优先级缺少技术栈的经历缺少目标岗位信息缺少基本信息姓名、年限等为什么量化成果优先级最高因为招聘方看简历时最先扫的就是数字。提升了系统性能和把接口响应从 800ms 降到 120ms后者能直接让简历通过初筛。我在实际测试中发现补全量化信息后简历的信息密度评分平均提升了 40%。追问循环有个上限保护最多追问 5 轮。超过之后强制进入优化避免用户被问烦。这个上限是实测调出来的3 轮太少信息不够8 轮太多用户流失5 轮是个平衡点。3.3 optimizeSection 节点分模块优化的 Prompt 结构这个节点不是一次性优化整份简历而是按模块分别处理。每个模块有独立的 Prompt 模板共享一个基础 system prompt。基础 system prompt 里我强调三条原则不编造事实、量化优先、动词开头。不编造是底线因为简历造假是严重问题。量化优先是让模型主动把模糊描述转成数字。动词开头是简历写作的行业惯例负责这种词要换成主导设计优化。工作经历模块的 Prompt 里我给了 few-shot 示例输入负责公司订单系统的开发 输出主导订单系统重构将下单接口 P99 延迟从 1.2s 降至 350ms支撑日均 50 万订单这个示例的作用是让模型理解优化的粒度。没有示例的话模型容易优化得不够或者过度发挥。我试过 3 个不同风格的示例最后选了技术细节 量化结果这个组合因为它在真实招聘场景里最受认可。3.4 matchJob 节点JD 关键词匹配与 ATS 友好度这个节点是简历工具的杀手锏。用户粘贴目标岗位的 JDAgent 分析 JD 里的关键词然后检查简历里是否覆盖。实现上分两步先用模型从 JD 里抽取关键词技术栈、能力要求、行业术语再用字符串匹配加语义匹配检查简历覆盖率。纯字符串匹配会漏掉同义词比如微服务和分布式服务所以我加了一层语义匹配用 embedding 算相似度。匹配结果会反馈给用户显示你的简历覆盖了 JD 里 70% 的关键词缺少Kubernetes、消息队列。这个反馈直接指导用户补充内容。实测下来用了这个功能的用户简历通过 ATS 初筛的比例明显更高。这里有个坑不要为了匹配而堆砌关键词。我一开始让模型自动把缺失关键词塞进简历结果输出读起来很生硬。后来改成提示用户补充由用户确认质量好很多。4. 流式输出与前端交互的工程实现4.1 SSE 流式传输让用户看到 Agent 在思考Agent 执行一次要几十秒如果用户盯着转圈等体验很差。我用 SSE 把 Agent 的中间状态实时推给前端用户能看到正在解析你的输入正在检查信息完整度正在优化工作经历这样的进度。LangGraph.js 的streamEvents方法能拿到每个节点的开始和结束事件。我在 Agent 服务里把这些事件转成 SSE 消息推给 Next.jsNext.js 再转发给浏览器。消息格式我定义了几种类型node_start、node_end、token模型输出的 token、question追问问题、done。// Agent 服务端 for await (const event of graph.streamEvents(input, { version: v2 })) { if (event.event on_chat_model_stream) { sendSSE({ type: token, data: event.data.chunk.content }); } if (event.event on_chain_start) { sendSSE({ type: node_start, data: event.name }); } }前端用EventSource接收根据消息类型更新 UI。token 类型直接追加到当前输出区域node_start 类型更新进度条。4.2 前端状态同步避免流式渲染的闪烁问题流式渲染有个常见问题token 一个个追加会导致频繁重渲染页面闪烁。我的解决方案是用一个缓冲区每 50ms 批量更新一次 DOM而不是每个 token 都更新。这个 50ms 是实测调出来的太短没效果太长用户感觉卡顿。另一个问题是状态同步。Agent 在服务端更新 State前端也要维护一份镜像。我用 Zustand 做前端状态管理SSE 消息到达时更新 store。这里要注意前端状态是只读镜像不要在前端做业务逻辑判断所有决策以服务端 State 为准避免两边不一致。4.3 中断与恢复用户关掉页面再回来怎么办这是 checkpointer 发挥作用的地方。每个用户有一个thread_id存在 localStorage 里。用户关掉页面再回来前端带着thread_id请求 Agent 服务服务端从 checkpointer 恢复 State继续执行。恢复的时候有个细节如果上次中断在追问环节恢复后要重新展示那个问题而不是从头开始。我在 State 里存了currentQuestion字段恢复时直接读这个字段渲染。实测下来这个功能对完成率影响很大。有恢复能力的版本用户完成率比没有的高出约 35%。因为简历填写本来就是个断断续续的过程强制一次填完不现实。5. 并发处理与性能优化的真实数据5.1 AI Agent 怎么扛并发连接池与队列的实际配置AI Agent 怎么扛并发是最近被问得最多的问题。我的答案是Agent 的并发瓶颈不在计算而在模型 API 的速率限制和长连接数量。我的配置是这样的Agent 服务用 Node.js 集群模式起 4 个 worker每个 worker 维护一个模型 API 的连接池池大小 10。这样理论并发是 40。但实际跑下来模型 API 的速率限制才是瓶颈超过之后会返回 429。所以我加了一个请求队列用 p-queue 控制并发数超过阈值的请求排队等待。队列的配置很关键concurrency设成模型 API 允许的并发数intervalCap和interval配合做速率限制。我实测下来把并发控制在 API 限制的 80% 左右最稳留 20% 余量应对突发。配置项值说明worker 数量4根据 CPU 核数调整连接池大小10每个 worker队列并发32模型 API 限制的 80%单请求超时120s覆盖最长流程队列最大长度200超过直接拒绝5.2 长流程的超时与重试策略Agent 流程长超时和重试必须处理好。我的策略是节点级重试 流程级超时。单个节点失败比如模型调用超时重试 2 次用指数退避。整个流程超过 3 分钟直接终止返回已完成的部分。重试有个坑不是所有失败都该重试。模型返回格式错误可以重试但如果是内容审核拒绝重试也没用。我在重试逻辑里加了错误类型判断只对可重试的错误重试。5.3 成本控制token 用量监控与缓存策略成本是 AI 产品绕不开的问题。我做了三件事控制成本。第一token 用量监控。每次模型调用都记录 input/output token 数按用户和按节点聚合。这样能清楚看到钱花在哪。我统计下来optimizeSection占了 60% 的 token 消耗追问类占 25%其他占 15%。第二缓存。相同输入的结果缓存起来用输入内容的 hash 做 key。简历场景里用户反复调整同一段经历的情况很常见缓存命中率能到 20% 左右。第三Prompt 精简。定期 review Prompt删掉冗余的示例和说明。我优化过一轮把 system prompt 从 800 token 压到 500 token效果没变成本降了。6. 上线后暴露的问题与迭代方向6.1 模型幻觉在简历场景的具体表现与拦截简历场景对幻觉的容忍度极低因为编造经历是原则问题。上线后我遇到过几种幻觉模型给用户补充了没提过的技术栈、把数字夸大了、编造了不存在的项目。拦截手段有三层。第一层是 Prompt 约束明确要求只使用用户提供的信息。第二层是输出校验用另一个模型调用检查输出是否引入了原文没有的事实。第三层是用户确认所有优化后的内容都标记为建议用户必须手动确认才写入简历。第二层的实现是把原始输入和优化输出一起给模型问优化后的内容是否引入了原始输入中没有的事实返回 yes/no 加具体位置。这个校验会增加成本但值得因为它拦住了大部分幻觉。6.2 用户实际使用中的高频反馈上线两个月收集到的反馈里最高频的三条是追问太多、优化后的措辞太模板化、希望支持多语言。追问太多的问题我把追问上限从 5 轮降到 4 轮并且允许用户跳过。措辞模板化的问题我在 Prompt 里加了避免使用负责参与等泛化动词优先使用具体动作的约束并且增加了 few-shot 示例的多样性。多语言需求我暂时没做因为目标用户主要是国内求职者优先级不高。6.3 后续可以扩展的能力边界这个项目后续有几个明确的扩展方向。一是接入更多简历模板让优化后的内容直接套用。二是做岗位匹配推荐根据简历反向推荐合适的岗位。三是做面试模拟基于简历内容生成面试问题。从技术角度看LangGraph.js 的图结构让这些扩展变得容易——加节点、加边就行不用重构核心流程。这也是我当初选它的原因好的架构不是让你现在写得快而是让你以后改得动。最后分享一个我在这个项目里体会最深的点AI Agent 产品的竞争力不在模型多强而在流程设计多细。简历工具这个场景模型能力早就够用了真正拉开差距的是追问策略、状态管理、幻觉拦截这些脏活累活。把这些做扎实比换一个更强的模型带来的提升大得多。