恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Next.js与LangGraph.js实战:构建企业级AI Agent简历生成系统
首页
资讯中心
/
Next.js与LangGraph.js实战:构建企业级AI Agent简历生成系统
Next.js与LangGraph.js实战:构建企业级AI Agent简历生成系统
发布时间:2026/10/8 21:07:29
最近把一个简历工具做成了完整的AI Agent落地项目前端界面用Next.js搭Agent的整个工作流编排交给LangGraph.js从用户填写原始信息、粘贴目标JD到Agent内部完成信息抽取、JD分析、简历初稿生成、HR视角评审、多轮修订最后输出一份可以直接用的Markdown简历全程不再需要“复制粘贴到ChatGPT反复调教”。这篇文章就是把这次落地过程完整拆开——选型思路、Agent工作流设计、提示词工程、Next.js接入、真实环境下踩过的坑全写在这里。适合两类人看一类是想把AI Agent从“聊天Demo”推向“真正能交付结果”的前端或全栈工程师另一类是正在评估Agent编排框架选型的技术负责人。1. 方案选型为什么用Next.js做壳LangGraph.js做大脑1.1 Next.js在全栈Agent应用里到底扮演什么角色很多人一看到“AI Agent Web应用”下意识就用“前端Vue/React 后端FastAPI/Spring Boot”的经典结构。但这次我直接选了Next.js作为整个应用的主干原因不是因为Next.js有SSR或者静态导出这些常规卖点而是因为它天然能当“Agent应用的外壳”。简历工具的核心交互不是传统的“点按钮→拿结果”而是“用户提交信息→Agent多步骤处理→中间状态持续反馈→最终结果流式呈现”。这种场景下Next.js的全栈能力价值就体现出来了API路由可以承接Agent的调用入口服务端逻辑和前端组件在同一个TypeScript工程里共享类型定义Agent的状态对象可以直接被前端类型引用不用像前后端分离那样维护两份协议。另外一个实际原因是部署心智负担极低。Next.js构建完以后可以直接跑一个standalone Node.js服务加一层Docker就能丢上云整个项目只有一个进程。而FastAPI方案意味着前端一套部署、后端一套部署中间还要考虑跨域、鉴权会话同步、日志聚合这些开销对于一个以Agent逻辑为核心的项目来说都是不必要的成本。1.2 LangGraph.js为什么比LangChain.js更适合这事儿LangGraph.js是LangChain团队推出的图编排框架。在选型时我对比过两条路线一条是继续用LangChain.js的链式调用Chain另一条就是LangGraph.js这种基于图状态机的方式。简历工具的实际需求不是“一串顺序调用”而是“有分支、有循环、有状态”的完整工作流JD分析结果决定了简历生成策略简历初稿要被评审节点打分分数不达标就得回到修订节点重写修订次数多了还得强制退出。这个逻辑用标准的线性Chain表达很别扭要靠一堆if-else硬塞进代码里。用LangGraph.js的定义方式会清晰得多把每个处理环节看成图上的节点Node节点之间是边Edge节点和边共同操作一份全局的共享状态State。每当一个节点执行完把结果写回State下一个节点从State里读取自己需要的数据。节点之间的流转可以条件判断评审分数低就走“refineDraft”分支分数够高或者修订次数达到上限就走“formatOutput”分支所有控制流都在图结构里显式表达而不是散落在业务代码里。也顺便聊一下现在的Agent主流架构。行业里讨论比较多的是ReActReasonAct模式也就是Agent反复“思考→调用工具→观察结果”直到解决问题另一类则是Plan-and-Execute式的多步骤工作流。简历工具更偏后者因为简历生成不是一个开放式探索问题而是一条有明确阶段的目标管线。LangGraph.js这种图编排思路恰好就是Plan-and-Execute的工程落地硬化这也是我没用纯LangChain链式调用的根本原因——链表达不了“循环”和“条件回溯”。1.3 替代方案的真实对比做选型时我拉了一张对比表算是给技术评审留个参考方案组合优点核心痛点适合场景纯前端 直连LLM API搭建最快适合10分钟出DemoAPI Key会暴露无法做多步骤编排和状态管理原型验证Next.js LangGraph.js前后端同构图编排能力强部署轻量需要额外理解图状态机概念生产级Agent应用FastAPI LangChain.jsPython生态的Agent工具链非常成熟前后端割裂部署两套系统类型不互通已有Python技术栈、需要深度定制NLP能力的团队这个表不是说Python那套不好而是针对“简历工具”这种Web形态、以流程编排为主、以TypeScript全栈为目标的场景Next.js LangGraph.js的综合成本最低。2. 简历Agent的工作流拆解从零散信息到完整成稿2.1 核心流程的五个阶段把整个简历生成过程拆开看用户真正想要的是“给我一份贴合目标岗位的简历”。但直接让模型生成效果会非常不稳因为用户输入的原始信息通常是零散的JD里要求的技能可能用户没写进原始信息里模型要么编造经历要么把不相关的项目排在最前面。所以我按照人工改简历的思路把过程拆成了五段信息抽取collectInfo把用户填写的零散文本结构化提取姓名、工作年限、公司、职位、项目经历、技能标签、教育背景。JD分析analyzeJD把用户粘贴的目标JD拆成硬性技能、加分技能、资历信号、文化关键词。初稿生成generateDraft结合结构化个人信息和JD画像生成第一版简历。HR视角评审reviewDraft模拟资深HR对初稿进行多维打分输出的分数直接决定是否进入修订。迭代修订与格式化refineDraft / formatOutput根据评审意见和用户反馈反复修改达到标准后输出为可复制的简历文本。这五个阶段里最核心的设计决策是“评审必须数字化”。如果评审结果只是一段含糊的文字程序无法判断要不要进入循环修订。我给评审节点约定了结构化输出——四个维度的0-10分数加上三条最致命的改进建议程序根据分数决定图的流向既保留了人的判断逻辑又让机器能执行控制流。2.2 图状态与节点设计LangGraph.js的核心抽象是State状态所有节点共用一个State对象。我在这个项目里定义的State包含以下字段const AgentState Annotation.Root({ // 用户输入的原始文本 rawInput: Annotationstring, // 从原始信息里抽取出的结构化简历档案 parsedProfile: AnnotationRecordstring, unknown, // 目标JD的分析结果 jdAnalysis: AnnotationRecordstring, unknown, // 当前最新的简历文本 draft: Annotationstring, // HR评审结果 review: AnnotationRecordstring, unknown, // 用户对修订方向的反馈 feedback: Annotationstring, // 已经迭代了几轮 revisionCount: Annotationnumber, });每一个节点函数接收当前State处理完后返回一个增量对象LangGraph会把它合并回State。比如analyzeJD节点只负责返回{ jdAnalysis: {...} }不改动其他字段reviewDraft节点返回{ review: {...} }并可能同时更新draft。图结构的定义大致是这样const workflow new StateGraph(AgentState) .addNode(collectInfo, collectInfoNode) .addNode(analyzeJD, analyzeJDNode) .addNode(generateDraft, generateDraftNode) .addNode(reviewDraft, reviewDraftNode) .addNode(refineDraft, refineDraftNode) .addNode(formatOutput, formatOutputNode) .addEdge(START, collectInfo) .addEdge(collectInfo, analyzeJD) .addEdge(analyzeJD, generateDraft) .addEdge(generateDraft, reviewDraft) .addConditionalEdges(reviewDraft, routeAfterReview, { refine: refineDraft, done: formatOutput, }) .addEdge(refineDraft, reviewDraft) .addEdge(formatOutput, END); const app workflow.compile();routeAfterReview这个函数是条件边的核心它读取State里的review和revisionCount字段返回一个字符串决定下一步走哪个分支。我给它定了三条规则评审总分低于7分且修订次数没到3次走refine达到条件就返回done。这样无限循环从根本上就不会发生因为任何情况下修订到第3轮都会强制进入格式化输出。2.3 用户反馈是怎么注入到循环里的简历工具不能只靠模型评审自动迭代因为只有用户自己知道哪些经历是真实的、哪些方向是愿意调整的。所以我在界面上专门留了一个“补充修改意见”的输入框用户看完初稿和评审意见后可以写一句“项目经历里的数字不要夸大”“第二条建议我做不到因为那个技术栈我没用过”之类的话。这串文字会被拼进refineDraft节点的输入修订时模型会把用户反馈和评审意见一起考虑。这个设计本质上是把“多轮对话”改造成了“状态迭代”。用户在对话式Agent里要一遍遍重复自己的需求而在这个图状态机方案里反馈会被持久化到State的feedback字段修订节点每次都从State里读取不存在“模型忘记前文”的问题也方便后续追溯到底改了什么。3. 核心实现质量的关键提示词工程与节点细节3.1 提示词不是“写一段话”而是“设计一次任务交接”做Agent项目最容易忽略的一件事是提示词工程的目标不是让模型“回答得更聪明”而是让不同的模型调用之间能“准确交接”。简历工具里有六次LLM调用每一次调用的输出都会被下一次当作输入任何一个节点输出格式不稳定整条流水线就断了。我自己的提示词模板有一套固定结构概括成四件套角色、任务、输入项、输出约束。角色约束模型的知识经验和语气任务说明这一个节点要完成的具体事输入项通过模板变量注入State里的数据输出约束明确格式尤其是JSON的键名。每一部分都不能省尤其是输出约束必须让模型在“自由发挥”和“结构化”之间优先选择结构化。3.2 JD分析节点的提示词设计这是整个Agent里最吃提示词功底的一个节点。JD分析结果决定后续简历生成的侧重点如果分析错了后面全是白搭。我的提示词模板大致长这样你是招聘领域有8年经验的资深HR。现在给出一段目标职位的JD请完成以下分析 1. 提取硬性技能标签最多8个指岗位上明确要求必须掌握的技能 2. 提取加分技能标签最多5个指“优先考虑”“熟悉者优先”等描述中的技能 3. 判断该岗位的资历要求输出 junior / mid / senior 三选一 4. 提取文化关键词最多5个例如“自驱”“结果导向”“跨部门协作” 只输出JSON键名固定为 hard_skills、plus_skills、seniority、culture_terms。 不要输出任何解释性文字。注意几个细节“最多8个”和“最多5个”是为了把输出长度锁死防止模型给出一大串标签导致后续prompt挤爆上下文“资历三选一”是为了让后续写简历的语气有依据senior岗可以多强调架构设计junior岗多强调学习能力“不要输出任何解释性文字”是防止模型在JSON外面包一层废话导致解析器读不到纯JSON。3.3 生成和评审节点的提示词设计generateDraft节点的目标是输出一版简历草稿所以提示词里最重要的约束是“不要编造”。我把这条写成了硬性规定候选人的结构化档案里没有的信息一律不允许补充项目经历里没有量化结果就写“负责/主导”不能自己编一个“提升30%”出来。评审节点的提示词是这个Agent的“品控关卡”。我把它设计成四个维度的打分每个维度都有明确的参考行为例如“匹配度”考察简历内容是否围绕JD中的硬性技能展开“量化程度”考察项目经历是否有数字支撑“格式”考察标题层级、时间顺序、条目是否统一“重点突出”考察核心项目是否排在了简历前半部分。评审输出同样是固定JSON键名固定为scores、topIssues、summary其中topIssues要求恰好三条且必须是可以执行的修改指令不能是“内容需优化”这种空话。3.4 版本控制和可观测性的经验图状态机的另一个优势是可以自然实现版本控制。我每次进入refineDraft节点前都会把当前这版draft快照存一份放到内存数组里。用户侧能看到每一轮评审的分数变化是往上走还是原地打转。这个功能当时只是顺手写的结果在测试阶段非常有用——一眼就能判断哪条提示词改动产生了真实效果而不是靠感觉猜。另外建议在开发早期就接入LangSmith这类可观测性工具。LangGraph.js在传参时只要额外带一个langsmith配置每次节点调用、每次token消耗、每次prompt完整内容都会被记录下来。项目上线以后排查模型输出异常没有这类工具就只能靠打印日志一条条找效率差太远了。4. 完整落地Next.js API路由与LangGraph.js的工程化整合4.1 项目初始化和依赖安装我习惯直接用Next.js官方脚手架TypeScript和App Router都是默认项。命令很简单npx create-next-applatest resume-agent --ts --app cd resume-agent npm i langchain/langgraph langchain/openai注意langchain/openai这个包是专门负责OpenAI兼容接口调用的。如果公司的模型网关只提供OpenAI兼容API也可以通过configuration参数传入自定义的baseURL和apiKey这正好解决了国内直连不稳定、需要走公司网关的问题。另外生产环境千万别把API Key写死在.env里提交到Git仓库用部署平台的密钥注入功能更稳妥。4.2 图对象封装成可调用函数不要把LangGraph的图对象直接散落在API路由文件里。我在src/lib/下建了一个graph.ts把State定义、全部节点函数、图编译封装成一个内部模块对外只暴露一个runAgent(initialState)函数返回最终State。这样API路由、测试脚本、将来可能的命令行工具都能复用同一份Agent逻辑。节点函数的写法也有讲究。比如analyzeJD节点async function analyzeJDNode(state: typeof AgentState.State) { const prompt analyzeJDPrompt(state.rawInput); const response await chatModel.invoke(prompt); const parsed parseJsonLoose(response.content as string); return { jdAnalysis: parsed }; }chatModel我统一在另一个模块里初始化比如用ChatOpenAI设置temperature: 0.2。温度设低一点是因为简历分析这类任务需要稳定性和一致性宁可少一点创造性也要保证输出格式不乱。生成初稿那个节点温度会稍微抬高到0.5让语句更自然但不会超过0.6否则简历容易写得“过于有文采”而失实。4.3 从app.invoke到流式输出的实现一开始我用的是app.invoke()一次调用等全部节点跑完才返回最终结果。实测下来体验非常差模型生成一份简历初稿可能就要20多秒加上评审和修订整个链路跑下来可能要一两分钟用户看到的就是一个一直转圈的白屏没有任何中间反馈非常劝退。解决方法是换成app.streamEvents()把每个阶段的实时变化推给前端for await (const event of await app.streamEvents(initialState, { version: v2, })) { if (event.event on_chat_model_stream event.data?.chunk) { const text event.data.chunk.text(); controller.enqueue(encoder.encode(data: ${JSON.stringify({ text })}\n\n)); } else if (event.event on_chain_end) { // 节点结束时把当前阶段名推给前端 controller.enqueue(encoder.encode(data: ${JSON.stringify({ stage: event.name })}\n\n)); } }这里有个容易踩坑的点streamEvents需要显式传version: v2否则拿不到on_chat_model_stream事件。前端用fetch读取响应体按SSE格式逐行解析然后做两件事一是刷新顶部“当前阶段”的进度条提示用户“正在分析JD”“正在生成初稿”“正在评审”二是把模型输出的文本增量实时追加到预览区让用户看着简历一行一行写出来。进度提示这件事特别重要。用户愿意等一分钟前提是知道这一分钟里系统一直在干活而不是卡死了。我实测加了阶段提示以后用户的等待焦虑明显下降反馈“至少知道它没死”。4.4 API路由与前端交互的细节API路由的关键是处理好流式响应和中断。我写的POST处理函数返回ReadableStream同时设置Content-Type: text/event-stream。如果用户在生成过程中点“停止”前端可以调用AbortController中断fetch后端虽然还在执行但用户侧不再接收数据等执行完成后靠状态自查补通知。前端交互的布局也比较直接左侧是表单区收集基本信息、目标JD、补充意见右侧是预览区实时展示生成进度、评审结果和最终Markdown简历。这里我特意没引入Markdown编辑器那种重型依赖就用一个轻量的marked把Agent输出的Markdown渲染成HTML用户复制到Word或语雀也不会丢格式。预览区右上角加一个“复制全文”按钮一键复制整份简历省去手动选中的麻烦。表单区还有一个容易被忽略的细节原始信息输入框允许用户一次性粘贴整段杂乱经历不需要按字段填表。这个设计是为了降低使用门槛把结构化拆解的脏活累活交给Agent。有些人可能习惯填表但我自己的经验是真正急着写简历的人根本不会耐心填完20个表单字段一个大的textarea 一个JD粘贴框这个交互成本最低。5. 真实环境里踩过的坑问题排查与避坑技巧5.1 流式输出与节点设计之间的冲突第一次接流式输出时我以为只要把streamEvents套上去就行结果前端只收到了生成简历节点的文本其他节点比如JD分析、评审的中间输出全都没有。排查发现on_chat_model_stream只会在模型正在生成token时触发而很多节点的输出是“先调模型再解析JSON再返回结构数据”模型生成token之后立即被解析用户自然看不到中间内容。后来我调整了策略阶段提示不依赖模型token而是用on_chain_end事件在每个节点结束时推送一个stage字段给前端前端根据阶段名显示静态文案例如“正在分析JD→”“正在生成初稿→”“HR正在评审→”。只有真正写简历正文和修订稿的两个节点才把模型token流透传给前端展示。这个组合方案既保证用户能看到阶段变化又不会把大量JSON中间参杂在流里干扰阅读。5.2 Agent Token消耗失控一次账单带来的教训做内测的时候我发现一次完整生成流程的token消耗比我预想的高出快一倍。细细追踪才发现问题出在“把完整JD文本反复塞进每次模型调用”上。JD最长可能有几千字每轮评审、生成、修订都要把完整的JD原文再发一遍三轮修订下来光JD就重复发了五六次。AI Agent里的token不是“字数”而是每一次模型调用都会消耗上下文的计量单位。上下文里塞的每一段文本不管是用户输入的还是系统生成的全都要按token计费。一个Agent链路跑完如果同一个长文本被引用多次消耗就是成倍的。我做了三个优化JD分析只做一次分析完以后把结果压缩成摘要对象存进State后续节点要用JD信息时读jdAnalysis而不是读原始JD文本rawInput也只在collectInfo节点用用完后不再传给后续节点每次进入refineDraft节点时先检查feedback有没有新内容没新内容就直接跳过修订避免空转。这套优化做完平均一次完整流程的token消耗降了大约40%生成质量没有明显下降。5.3 循环退出机制防死循环的硬保护第一次测试迭代逻辑时我给了个很宽松的循环条件“评审总分低于7就继续修订”。结果遇到一次极端案例模型一直给6.9分前端转圈转了十几分钟。看日志发现评审节点每次给的修改建议都不一样改了以后分数反而更低彻底卡死在refineDraft和reviewDraft之间。排查下来根子在于“路由函数必须在所有情况下都能退出”。我给routeAfterReview加了一个强制退出条件revisionCount 3时无条件返回done。同时State里的revisionCount由refineDraft节点自己累加每次进节点先1再执行修订这样即使路由判断和节点执行之间有异步时序问题计数也不会丢。另外还给整个图调用加了超时控制。LangGraph.js支持在整个invoke外包一层超时逻辑我用Promise.race实现了一个简单的超时保护超过90秒直接返回当前最新状态宁可给用户一份未完工但已保存的草稿也不能无限耗下去。这在实际生产里非常重要任何一个环节出问题都要保证用户兜底能拿到东西。5.4 环境变量、鉴权与部署的坑最典型的坑是.env.local里的模型API Key在构建时没有被正确复制到容器里。用Next.js standalone模式部署时记得环境变量要在运行时注入而不是构建时注入否则每次重新部署都要重新构建而且密钥容易残留到镜像层。我最终的方案是用Docker Compose统一管理环境变量OPENAI_API_KEY这类敏感变量从宿主机环境读取不写进镜像。FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine WORKDIR /app ENV NODE_ENVproduction COPY --frombuilder /app/.next/standalone ./ COPY --frombuilder /app/.next/static ./.next/static EXPOSE 3000 CMD [node, server.js]还有一个小坑Next.js的fetch在API路由里默认有缓存策略如果Agent返回的结果被缓存了用户第二次提交相同输入时可能拿到的是上一次的旧结果。我的解决方案是在API路由的Response响应头里显式加上Cache-Control: no-store同时在fetch请求里传cache: no-store彻底关闭默认缓存确保每次提交都是真实跑一遍Agent流程。6. 扩展方向从简历工具到通用Agent平台6.1 复用这套图编排逻辑做更多Agent简历Agent只是这个架构的第一个落地场景。现在StateGraph框架里的节点函数、提示词四件套、条件边路由函数、流式输出封装、token预算控制都已经沉淀成了独立的模块。后续接面试Agent根据JD生成模拟面试题、岗位匹配Agent分析简历匹配多个JD、甚至写周报的Agent只需要新增对应节点和State字段不需要动整体骨架。这也是图编排架构最强的扩展能力每加一个Agent就是再加一张图图的节点可以完全复用底层的模型调用封装、日志追踪、流式输出。我目前已经在内部把“简历生成”和“面试模拟”这两个流程接到同一个Next.js应用里前端通过类型选择路由到不同的编译后的Graph共用同一套API出口和状态监控面板。6.2 从LangGraph.js到更多运行时选择的思考聊到最后分享一个最近关注的方向。LangGraph.js这个生态发展很快官方除了支持JS/TS还支持Python社区里也已经有人开始尝试用Rust重写高性能的Agent运行时。对大多数业务场景来说TypeScript生态的开发效率依然是最高的前端后端一套语言Agent状态类型定义一次就全局复用。但如果将来遇到超大并发的Agent服务或者需要极低延迟的流式场景Rust这类系统级语言确实值得关注。选择LangGraph.js最大的理由不完全在于框架本身而在于它背后的运行模型——图状态机。这个模型和具体语言无关换到Python的LangGraph、甚至自己手写一套状态机核心思想都是把原来藏在一堆if-else里的Agent控制流变成显式、可观测、可维护的图结构。理解了这一点以后无论Agent技术怎么演进你都能比别人更快看清楚新框架到底解决了什么问题。我自己在这套项目里最大的收获不是某一段代码而是逐渐养成了一个习惯接手任何Agent项目先问自己“它的状态是什么”“它的退出条件是什么”想清楚这两个问题再动手写代码。这比一开始就追各种新框架的抽象概念要实在得多。如果你正准备做一个类似的Agent工具建议先把主线流程画成一张带节点和箭头的图哪怕先不用LangGraph用纸笔画也行——这张图就是你未来排错和优化时最可靠的底图。