ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Next.js与LangGraph.js实战:构建稳定可控的简历优化AI Agent

Next.js与LangGraph.js实战:构建稳定可控的简历优化AI Agent 干过简历工具这类项目的人应该都有同感需求看着简单无非是帮我看看简历、按这个JD优化一下、给一段经历润色可一旦要做得像回事背后就是一连串的脏活。解析格式、提取结构化信息、理解岗位要求、生成修改建议每一步都在跟文本打交道。我最初也是先上了Next.js把界面和接口写好再用LangGraph.js把Agent逻辑串起来最终落地了一个简历工具AI Agent。整个过程下来我最想说的是技术栈本身不是难点难的是把Agent的工作流设计得既稳定又可控让AI真正下地干活。这套方案选型有它很实际的理由。Next.js负责前端界面和服务端API路由一份代码同时处理页面渲染和接口逻辑团队里前端同学上手成本低。LangGraph.js则提供了Agent状态机和工具调用的编排能力比用纯LangChain的链式调用灵活得多也比纯手写状态机省掉一堆重复代码。如果你正在做类似的文档分析文本生成工具或者想做简历、合同、周报、知识库问答这类偏文档处理的Agent应用这篇文章能给你一份可以直接参考的落地路径。1. 为什么是LangGraph.js它解决了简历工具最头疼的状态流转问题先说一个很现实的痛点。简历Agent不是问一句答一句的聊天机器人它的完整流程是解析简历内容、判断用户意图、决定要不要优化、调用工具执行修改、生成解释说明。中间任何一个环节出了岔子比如解析失败、意图判断模棱两可、工具调用超时都需要有明确的兜底方案。这种多步骤、有分支、需要来回决策的场景用线性链式调用根本写不清楚。我之前试过用普通的fetch循环去编排大意是判断完意图就调用下一步。结果代码很快变成一团乱麻条件分支散落在各个函数里中途要插入一个新逻辑就得动整个主干出错以后排查起来特别痛苦。LangGraph.js的核心贡献在于它把Agent的执行过程显式建模成一张有向图。节点是干什么比如解析简历、分析JD边是什么时候去干下一件事比如解析完成才进入优化环节状态对象在节点之间传递就像流水线上的一张工单。用Lifecycle的一句话来概括LangGraph.js相当于给你的Agent画了一张工作流程图代码按图执行每个环节出错都看得见、可回退。这对于简历工具这种流程长、结果要稳定的应用来说价值是实打实的。从选型对比上看当时摆在面前的其实有两条路。一是纯用LangChain.js做链式调用缺点是分支逻辑复杂以后链路会很僵硬二是自己写状态机但那样状态定义、节点注册、重试机制全要手工实现工作量直接翻倍。LangGraph.js正好卡在中间既有LangChain的模型调用和工具封装能力又提供了图结构的流程编排能力还能在后端用Python生态的成熟库做补充。实际用起来还有个意外收获它的状态定义是显式的每个节点的输入输出都写在类型里。我拿着这份状态定义去跟产品经理对齐需求对方一眼就能看懂原来你是先解析再判断再优化这个流程沟通成本低了很多。2. 工作流设计简历Agent的节点拆解与状态流转简历工具看起来是丢一份简历进去出来一堆优化建议但内部拆开看至少需要四个核心节点意图识别节点、简历解析节点、JD匹配节点、优化生成节点。没有这套拆分直接让模型一条龙输出结果会很飘用户也控制不了过程。2.1 意图识别先搞清用户到底要干什么用户的输入千奇百怪。帮我把这份简历改成英文、照着这个JD优化一下、我就想看看简历里有没有错别字、这段经历帮我扩写一下。如果上来就丢给模型处理模型很容易自己给自己加戏。我的做法是先用一个快速分类节点模型只输出一个JSON结构包含intent字段optimize_by_jd、fix_typo、translate、expand、general_review。这一步的模型调用不需要太贵快速分类即可。关键点在于分类结果要进入状态对象后续节点会根据它决定走哪条边。比如意图是fix_typo那就直接跳到文本修正工具不再去做JD匹配省掉无谓的调用。2.2 简历解析从一段文本变成结构化数据简历解析是整个Agent的地基。模型再怎么聪明面对一张排版混乱、字体多样的PDF或Word直接读全文也很容易漏信息。我的方案是优先用解析库先把文件转成文本再把文本交给模型整理成结构化JSON个人信息、教育经历、工作经历、项目经历、技能清单五大部分。这里有件特别重要的事解析环节的模型输出需要校验只比对的是JSON结构里的字段不是在页面上渲染的内容。简历内容本身千奇百怪但解析字段必须是稳定的。我写了一个轻量校验函数输出里缺少关键字段就触发重试最多重试两次超过就返回明确的错误给用户。2.3 JD匹配让优化真正对症下药简历优化不能脱离岗位需求。这步节点的输入是解析后的JSON和用户贴的JD文本输出是一个匹配报告——列出简历中与JD高度相关的能力、缺失的关键词、需要加强的表述。这个报告不需要直接给用户看而是作为后续生成节点的参考上下文。我习惯让模型在这步输出一个启发式评分从0到10分别对技能吻合度、经历相关度、关键词覆盖度打分。这个分数写进状态里后面生成优化建议时可以直接引用你在技能吻合度上只有6分建议补充XX关键词。2.4 优化生成用结构化上下文约束大模型输出最后是生成节点它拿到的输入非常完整原始简历文本、结构化JSON、JD匹配报告、用户的具体指令。这四个要素拼成一个system prompt再让模型输出修改后的简历片段外加一条解释。这里最大的经验是给大模型的输入越结构化输出质量越稳定。如果你直接把简历原文和用户模糊指令丢进去模型很容易自由发挥过度。比如用户说的是按照这个JD帮我优化一下听起来是模糊指令但状态里已经有了匹配报告和结构化数据生成节点就能把指令转化为具体的动作补充缺失关键词、调整经历描述顺序、强化与岗位相关的数字表达。2.5 状态定义让每个节点都拿到该拿的数据LangGraph.js最核心的设计就是State类型。我定义了一个ResumeState包含这些字段interface ResumeState { rawFileType: string; rawContent: string; parsedResume: ParsedResume | null; userIntent: IntentType | null; jdContent: string | null; matchReport: MatchReport | null; optimizedResult: OptimizedResult | null; errorMessages: string[]; retryCount: number; }每个节点拿到整个State修改自己负责的那部分字段就能返回图框架会帮我把更新合并回状态池。这比手写传参干净得多。调试的时候我只需要打印每一轮State的diff就能看到哪个节点的输出有问题。我经常在本地跑一个模拟文件把整条链路串起来看日志确认无误再接到前端。这一步做完之后Agent的核心流程已经通了。接下来要做的就是接真实工具因为解析、文档转换、PDF处理这些操作如果全让模型幻想出来的结果基本不能用。3. 工具层接入简历解析、文档转换与在线能力不靠模型硬编刚开始做的时候我犯过一个典型错误让模型直接处理PDF二进制内容然后生成解析结果。模型根本看不了二进制输出的所谓解析基本是编的。后来我把工具层和模型层彻底分开凡是能确定性完成的操作一律交给工具模型只负责需要理解和判断的部分。这一步是你和大多数人拉开差距的关键。3.1 文件解析工具PDF、Word、纯文本各有各的解法简历上传格式无非三种PDF、docx、纯文本。纯文本最简单直接读字符串即可但要留意编码问题UTF-8和GBK混在一起会出现乱码。PDF我优先用pdf-parse这类纯JS方案拿到文本后再做分页拼接。docx相对麻烦一点本质是zip包里面是XML文档我直接用mammoth把它转成带基本格式的HTML或纯文本。这些工具调用方式在LangGraph.js里很直接用createTool或者直接定义普通函数就能注册成工具节点。要注意的是文件解析工具不要做成异步等待超长的模式文件一多容易堆积。我的做法是加一个20秒的超时解析失败直接返回错误文案让Agent走兜底分支提示用户文件似乎损坏了请换个格式试试。3.2 在线检索能力JD之外的隐性要求哪里来只靠用户贴的那段JD文本往往不够。很多JD写得很简略比如熟悉性能优化五个字但背后隐含的是大规模系统、高并发场景、基准测试经验。我接了一个检索工具把JD里的关键技能拆成查询词去索引里找相关的岗位能力描述再把检索结果作为上下文传给JD匹配节点。这一步显著拉高了匹配报告的丰富度。当然做这一步需要提前准备好检索数据源。我用的是自己收集整理的一部分公开岗位描述拆成索引后跑本地检索。如果你不想自建数据源也可以用外部API搜索总之让模型有据可查而不是脑补需求就好。3.3 Agent决策与工具调用的边界LangGraph.js的Agent节点可以自主决定调用哪个工具这是它比普通链式代码强的地方。但自主不等于失控我给工具调用加了白名单和参数校验。白名单里只有解析、检索、文本修正三个工具模型最多在一个轮次内调用两次工具。超过次数就强制进入生成节点用已有的上下文完成输出。有个真实案例用户上传了一份带表格的简历说把这份简历整理一段自我介绍。意图识别节点判成了general_review但解析节点输出的结构化数据里表格部分丢了。结果模型生成的自我介绍漏了项目成果。排查以后发现问题不在模型而在解析工具没有把表格转换成Markdown再进模型上下文。修好解析工具后同样输入直接得到了完整结果。这个案例说明一个道理Agent的问题很多时候不是模型笨而是工具层没有把信息完整地喂给模型。工具层到位后接下来要考虑的就是用户怎么跟这个Agent交互。单纯做一个上传文件→显示结果的页面简单但用户更希望看得见过程Agent正在解析、正在匹配、正在生成。这就涉及到前端流式输出的设计。4. 前端集成Next.js路由、流式输出与交互体验Next.js在这个项目里的角色是双重的前端页面使用React组件渲染同时服务端API路由处理Agent的调用。我选了App Router因为它的Server Actions和流式响应能力刚好匹配需求。用户上传简历后前端调一个API接口接口内部驱动LangGraph.js的Agent执行执行过程中通过流式通道把每一节点的状态推给前端。4.1 最省心的提交方式Server Actions如果你用的是Next.js的App Router提交表单用Server Actions很方便。前端组件里定义一个action函数直接调用后端逻辑不需要额外写fetch封装。示例大致是这样// app/actions/resumeActions.ts use server; import { runResumeAgent } from /lib/agent/runAgent; export async function processResume(formData: FormData) { const file formData.get(resume) as File; const jd formData.get(jd) as string; const instruction formData.get(instruction) as string; const result await runResumeAgent({ fileBuffer: Buffer.from(await file.arrayBuffer()), fileType: file.type, jdContent: jd, instruction, }); return result; }页面组件在表单的action属性里直接挂这个函数配合useActionState管理提交状态代码量比传统API路由少一半。但Server Actions有个注意点如果Agent执行时间很长用户会一直等着这时候就必须上流式输出或者任务队列。4.2 流式输出让用户看见Agent在干活用户等待AI回复时最受不了的是静默状态。我做了两件事一是把Agent的执行过程拆成阶段事件parsing、matching、optimizing、done用Server-Sent Events推给前端二是在前端用EventSource监听按阶段渲染进度提示。LangGraph.js本身支持在节点执行前后发出回调我用它在关键节点上主动发送当前节点名称和进度信息。实现思路上接口返回一个ReadableStream每个事件序列化为JSON行通过text/event-stream格式输出。前端拿到数据后逐行解析在界面上显示正在解析简历…、正在匹配JD要求…这些实时状态。用户不再是干等而是在看着AI干活体验差别很大。4.3 上传组件与文件类型校验上传组件看起来简单但有几个坑。第一是前端要限制文件类型accept属性写application/pdf,.docx,.txt但后端一定要再校验一次MIME类型因为浏览器给的type不可全信。第二是大小限制建议5MB以内后端用中间件拦截大文件。第三是文件名处理最好统一以用户ID加时间戳重命名避免中文名和特殊字符带来的路径问题。我踩过一个坑用户传的docx文件MIME类型是application/octet-stream前端accept拦不住后端又因为类型不匹配直接拒绝。后来我改成用文件扩展名加MIME双重判断解决得干净利落。前端集成的核心目标是让用户的每一步操作都有及时反馈。上传时有进度Agent执行时有阶段提示输出时结果分区块展示。如果用户能看完整个Agent的思考过程他会更信任这个工具也更愿意把简历完完整整地提交上来。5. 并发与长任务AI Agent怎么扛住真实世界的请求AI Agent做demo很容易但Agent落地和Agent能扛住用户请求是两码事。简历工具的特点是用户请求不密集但单次耗时很长动辄10秒到30秒如果直接同步阻塞式处理一台服务器很快就被拖死。热词里有人问AI Agent怎么扛并发本质上要解决的是长任务并发下的资源调度问题。5.1 不要同步等Agent跑完再返回最差的方案前端提交后后端同步等待Agent跑完全部节点再返回JSON。这样接口耗时等于Agent耗时每个请求都占据一个工作线程并发稍微一高就全线超时。正确做法是提交后立即返回一个任务IDAgent在后台异步执行前端轮询或通过SSE订阅任务状态。我用了一个内存任务表键是任务ID值是任务状态和结果。Agent执行时把阶段进度写进去前端通过EventSource或者定时轮询拿到进度。这个方案在单机场景下完全够用但要注意内存任务表在服务重启后会丢所以生产环境最好换成Redis做任务状态存储。如果你只要一个能跑的工具站内存方案省事要真正上线服务用户建议直接上Redis。5.2 队列与并发池限制同时运行的Agent数量即便做了异步化也不能让Agent无限并发。每个Agent任务都会持有LLM调用的连接、占用CPU做解析、占内存存上下文。给模型API的并发上限也有限制。我实现了简单的并发池单个用户最多同时运行2个任务整个服务最多同时运行10个Agent任务超出就排队。// lib/agent/queue.ts class AgentQueue { private activeCount 0; private waitQueue: Array() void []; private maxConcurrent 10; async runT(task: () PromiseT): PromiseT { if (this.activeCount this.maxConcurrent) { await new Promisevoid((resolve) this.waitQueue.push(resolve)); } this.activeCount; try { return await task(); } finally { this.activeCount--; this.waitQueue.shift()?.(); } } }这只是个极简版本但足够说明思路。生产环境完全可以换成BullMQ之类更强壮的队列。我的经验是先把并发池个数写死在环境变量里上线后根据实际负载再调不要上来就做弹性伸缩那种复杂方案。5.3 超时、重试与幂等Agent任务的三道保险AI应用的请求天然具有不确定性模型偶发返回格式错误、工具调用超时、网络瞬时抖动都可能导致任务失败。我给每个Agent节点包了三层保护节点级超时每个节点的执行时限为30秒超时抛错并走重试。整体任务超时整个Agent任务最多运行2分钟超过就终止并返回处理超时请简化指令后重试。幂等性同一个任务ID的重复请求不会重新执行直接从状态表取出已有结果返回。当初有个用户投诉说我点了两次提交结果给我生成了两份完全不同的简历。排查后发现是因为前端提交按钮没做防抖同一份文件被同时提交了两次。后来除了前端禁用按钮后端也加了任务ID去重双保险。有很多人认为AI Agent天然不适合高并发这是误解。适合与否取决于你有没有把这三点做扎实异步化、并发池、超时重试。做到了Agent照样能扛住真实流量的考验。6. 生产化落地可观测性、降级方案与模型选配的实操细节流程跑通、并发处理好之后剩下的事情是把项目推向生产环境。这一步没有太多魔法就是踏踏实实解决三类问题出了问题怎么查、模型挂了下游怎么兜底、预算怎么控制。6.1 可观测性每个节点都要有日志和度量Agent应用的排查难度远高于普通接口因为问题可能出在任意一个节点而且同一个节点在不同输入下表现可能差异很大。我做的第一件事是给每个节点写结构化日志节点名、输入摘要、输出摘要、耗时、token数、错误信息。输出摘要不要打印全文太占存储截取前200个字符就够定位问题了。前端上报也做了埋点用户从哪个入口进来、上传了什么类型文件、Agent跑到哪一步失败、用户是否重试。有了这些数据之后这个工具到底好不好用就不再是感觉了而是可以量化检验的指标。6.2 模型降级不要把所有鸡蛋放进一个篮子里简历工具的核心模型调用有三个场景意图识别、JD匹配、优化生成。这三个场景的模型要求不一样。意图识别和JD匹配用便宜快速的模型就能搞定优化生成则建议用能力更强的模型。我在环境变量里配置了三个模型名分别对应三个节点这样某个模型API偶发抽风时只影响单个环节不会整个Agent全瘫。更稳妥的方案是设置降级链主模型失败时自动切换到备用模型并记录一条告警。这个逻辑写得不复杂但能救命。有一次主模型供应商出了服务故障我的Agent自动降到备用模型跑了一整晚用户完全无感。6.3 大模型输出的最后一道防线规则校验前面说过模型输出需要校验这里再展开一点。优化生成节点返回的文本可能有几种问题格式不对、引用了不存在的关键词、生成了完全重复的内容。我做了三道规则校验JSON解析校验、关键字段存在性校验、长度合理性校验。任何一道不过就触发一次重试重试时把失败原因写进prompt里告诉模型上一次输出格式不符合要求请重新生成并确保返回合法JSON。这样做的好处是最终返回给前端的结构永远是稳定的前端不用针对模型可能返回任何东西做防御。稳定结构是前后端协作的前提也是Agent应用能不能对接真实业务流程的关键。6.4 预算控制token用量要在每个节点都有记录AI工具如果没做预算控制上线后就是烧钱机器。每个节点执行完把token用量累加进任务记录。单次任务如果token超了预设阈值直接停止后续节点并提示用户本次处理内容过于复杂请精简后重试。我还会在每天跑一个定时任务统计当天各节点的token消耗占比用来判断哪些模型调用值得保留、哪些该换更便宜的模型。预算控制的另一个有效手段是缓存相同简历和相同JD的任务解析结果可以直接缓存只有优化生成必须重新调用模型。这样用户反复微调指令时解析成本不会重复产生。实测下来这一步能省掉三成以上的调用费用。7. Node.js运行时与LangGraph.js兼容性这个坑我替你踩过了说一个很多人会忽略的细节。LangGraph.js在文档里强调的是浏览器不运行它依赖Node.js的一些原生能力。但我把项目部署到Serverless边缘运行时比如Vercel部署到Edge运行时时发现部分依赖在边缘环境没法正常工作。原因很直接边缘运行时对Node.js标准库的兼容是降级的流、Buffer等模块要么缺失要么实现不完整。我的解决方案很朴素Agent执行逻辑固定跑在Node.js运行时不让它跑在边缘环境。Next.js的页面和轻接口可以放在边缘或默认环境但涉及LangGraph.js和文件解析的任务一律标成runtime nodejs。如果你的业务服务器用的是纯Node服务这个问题大概率不会遇到但如果你是Serverless架构就要特别注意。另外还有一个更隐蔽的坑LangGraph.js在序列化状态时如果状态里放了Date对象、Buffer或者不可序列化的类实例恢复状态时会报错。我在设计状态时把所有字段都限制为纯JSON类型string、number、boolean、数组、普通对象传文件二进制时先用Buffer转成base64字符串再进状态。这样既避免了序列化问题也让状态可以在网络间传递。还有一点值得提LangGraph.js的版本更新比较快API有变更风险。我的做法是锁版本号升级时先跑一遍全流程回归测试确保所有节点行为没有变化再合入主分支。8. 实操经验总结从demo到正式工具我踩过的坑和验证过的路最后把这几个月实操中真正有价值的经验浓缩成几条供你参考先把确定性逻辑和模型逻辑分清楚文件解析、格式转换、超时重试这些能确定做的事全部用代码搞定不要指望模型模型只负责意图判断、评分和理解生成。这个边界划得越清晰Agent越稳定。给模型所有节点都加结果校验不要相信模型一次就能输出对。JSON解析失败就重试重试时把错误原因喂回给模型。这一步能让最终成功率从七成直接升到九成五以上。不要让Agent代码和页面逻辑绞在一起Agent的核心逻辑放在独立的lib目录通过接口对外暴露能力。Next.js只是它的一个调用方以后要把同一套Agent搬到其他前端或做成API服务都很容易。并发问题不是上线以后才想的哪怕内测阶段只有几个人用也要把任务队列和超时机制先做上。不然某天你发给朋友试用同时点了三个请求服务直接卡死那个场面非常尴尬。日志和埋点是被低估的功臣AI Agent效果不好说不清为什么大部分时候是因为没有数据和日志支撑判断。做Agent和做普通业务系统一样先有可观测性才能迭代优化。再补充一个真实的参考数据。我盯着后台日志观察了一个月用户的请求分布大概是意图识别节点消耗最快但最便宜JD匹配节点因为要读取检索结果token消耗在总成本里占比最高优化生成节点的单次耗时最长最长的任务跑到了50秒。做完缓存和模型分层之后单次请求成本降了四成平均响应时间也明显缩短。这说明性能优化不是盲目赶时髦而是先摸清自己哪个环节最贵、哪个环节最慢再对症下药。这套Next.jsLangGraph.js简历AI Agent其实不局限于简历。你把简历解析换成合同解析、JD匹配换成需求匹配、优化生成换成报告生成它就是一套通用的文档分析智能生成Agent骨架。如果你刚好想做一个吃文档的AI工具这个结构可以直接当成起点去改造。我自己做完这个项目最大的体会是Agent真正落地的标志不是每一步都由AI决定而是你知道它每一步在做什么、什么时候会错、错了怎么兜底。把这些想明白AI工具就真的能下地干活了。
返回列表