ARTICLE DETAIL

资讯详情

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

用LangGraph.js构建简历AI Agent:从状态图设计到并发优化实战

用LangGraph.js构建简历AI Agent:从状态图设计到并发优化实战 1. 为什么做这个简历AI Agent最近我把一个做了几年的简历工具推倒重写核心原因很直接传统表单式的简历生成器用户填完了模板得到的只是一份“排版好看但内容平庸”的文档。而真正决定简历能不能过初筛的是经历描述、技能匹配、项目亮点这些内容层面的东西——这恰恰是机器最难帮上忙的部分。所以我决定用AI Agent的思路重做用户丢进来一份旧简历或者一段零散的个人经历Agent自动拆分信息、判断岗位方向、生成优化建议、输出定制化的简历文档。整套流程不是一个单次问答而是像一位专业的职业顾问在处理你的材料先读再问再改最后给你一份能直接投递的成品。这里有一个技术选型的判断市面上很多简历工具用纯前端加一个LLM接口把prompt模板拼一拼就算完事。但这样做的缺点很明显——流程是死的状态是散的用户和模型之间无法形成多轮协作。我需要的是一套能管理“多步骤、有状态、可中断恢复”的Agent框架于是选了Next.js做应用壳LangGraph.js做Agent编排层。这篇博文适合谁看想用AI Agent做Web工具落地的人、正在调研LangGraph.js的开发者、以及所有在Next.js项目里跑LLM工作流的同学。我会把这套方案的架构决策、状态图设计、并发处理、Token控制、踩坑记录全部摊开讲你拿回去可以直接复刻。2. 整体架构设计与状态流转设计2.1 前后端一体化的Agent架构这套项目的前后端没有分离整个应用跑在Next.js的App Router上。API Routes承担Agent入口前端页面通过Server-Sent Events接收流式输出。选择前后端一体不是因为懒而是因为简历场景的数据流太紧凑用户上传文件、Agent解析、生成草稿、用户在线修改、最终导出PDF每个环节都需要共享同一个任务状态和会话上下文。如果拆成独立前端加独立后端你至少要多维护一套REST接口契约、一套跨域配置、一套部署链路。而Next.js的Route Handler天然支持流式响应和React前端的useEffect消费流可以无缝衔接。更关键的是LangGraph.js的State对象需要在内存或存储层保持一体化部署可以把这个状态挂到应用进程内减少了网络序列化的开销和出错面。项目里我做了三层结构表现层Next.js页面组件负责简历预览、交谈UI、进度展示。接口层Route Handler接收HTTP请求创建Agent任务把事件流转发到前端。Agent层LangGraph.js定义的状态图负责解析、规划、调用模型、生成文件。还有一层容易被忽视的是任务隔离。每个用户上传简历后我都会生成一个taskIdAgent的所有状态变更都挂在taskId下面。这样即使用户刷新页面重新连接时也能恢复之前的进度——这个能力对LangGraph.js这种以状态为核心的工作流引擎来说是天然的加分项。2.2 LangGraph.js的状态图设计LangGraph.js的核心概念是“图”——你把Agent的工作流定义成节点和边的集合每个节点是一个函数每次执行后更新一个共享的State对象。比起用代码顺序调用LLM这种方式的最大优势是流程中的每一个分支、每一次循环、每一个条件判断都是显式可追踪的不会像LangChain时代那样长链调用失控。简历Agent的状态图我设计了五个核心节点文件解析节点parse接收上传的PDF或DOCX提取纯文本。信息抽取节点extract让模型把非结构化的文本整理成结构化的JSON简历数据。岗位分析节点analyze读取目标岗位描述输出关键词匹配度分析和差距列表。内容改写节点rewrite基于差距列表逐段生成修改建议和重写内容。文档渲染节点render把最终JSON数据渲染成PDF或Word文档。边上的设计参考了“人类反馈回路”rewrite节点生成建议后图会进入一个Conditional Edge判断是否存在用户确认节点——如果用户不满意就回到analyze再迭代一轮如果确认通过才进入render。这里有个关键设计不要把整个流程压成一个节点。如果你把“解析抽取分析改写渲染”全塞进一个大prompt调用里出了问题你根本不知道是哪一步错了Token消耗也会爆炸。图的好处是把一次大任务拆成多个小的可观测步骤每步都有自己的输入输出和校验逻辑。我在每个节点结束后都把State的部分内容打印到日志里调试效率提升了不止一个量级。3. 核心功能实现与代码拆解3.1 简历解析与信息结构化简历解析是整个Agent的地基。上传的简历格式五花八门有PDF、DOCX、也有纯文本粘贴。最麻烦的是PDF——很多简历是设计软件导出的没有文本层直接读出来是乱码或空白。项目中的兜底方案是先尝试pdf-parse提取文本如果发现提取结果字数少于50就转为调用OCR接口做识别。不过这层技术细节还不是最核心的。最重要的是不要让LLM去处理一堆乱糟糟的原始文本。我的做法是先用正则规则做一轮粗拆分按“基本信息/教育经历/工作经历/项目经历/技能列表/自我评价”这几个板块切分再把这几个板块分别喂给抽取节点。这样做的好处很明显一是减少了单次输入的Token量二是让模型可以聚焦不会把项目经历写进基本信息里。抽取节点使用的结构体我定义如下interface ResumeData { basic: { name: string; phone: string; email: string; city: string; yearsOfExperience: number; }; education: Array{ school: string; degree: string; major: string; startDate: string; endDate: string; }; experience: Array{ company: string; title: string; startDate: string; endDate: string; highlights: string[]; }; projects: Array{ name: string; role: string; techStack: string[]; description: string; achievements: string[]; }; skills: string[]; summary: string; }这里有一个容易踩坑的地方JSON Schema约束。你必须在提示词里把上面这个TypeScript接口直接贴进去并要求模型“严格输出符合该结构的JSON”否则模型会自己发挥字段名千奇百怪后续的渲染层会崩溃。实测下来配合结构化输出的解析器模型输出非法JSON的概率会从百分之十几降到百分之二以内。3.2 基于工作流的AI建议生成与多轮迭代简历优化不能只做一次“格式化改写”。同样是“负责xx系统的开发”优化空间可以是“主导xx系统的架构升级将接口响应时间从800ms优化至200ms”——但模型不知道这些细节它需要追问用户。于是analyze节点被设计成一个交互式工作节点。它的内部逻辑是这样的从State中取出抽取后的ResumeData和用户填写的目标岗位。让模型基于岗位要求产出三个层级的分析结果匹配项、缺失项、可量化改造点。把可量化改造点提取成问题列表发送给前端由用户补充实际数据。举例来说如果用户的简历写着“负责订单系统的后端开发”目标岗位是“高级后端工程师”analyze节点会给出一个建议“建议补充订单系统的日均请求量、QPS峰值、数据库规模、优化前后的延迟数据。”然后前端会显示这个追问用户在对话框里输入“日均请求10万QPS峰值2000优化后P95延迟从800ms降到了150ms。”这些数据被写回State后rewrite节点再生成完整的优化文案。这种“先分析、再追问、后改写”的节奏就是LangGraph.js工作流最典型的用法。它不是一次性输出而是把对话过程建模成状态转移。每个节点只做一件定义明确的事状态里保存用户的所有反馈和中间产物。你回头看整个交互过程会发现Agent的表现非常接近一个真人顾问不是上来就改而是先搞清楚你到底做了什么。3.3 流式输出与打字机交互体验Agent跑简历分析的时候如果前端是一个转圈的loading图标用户的耐心最多撑过十秒。而一次完整的多节点工作流执行顺利的话要20秒遇到模型响应慢可能到40秒。所以流式输出不是锦上添花是刚需。实现方式上我用的是Next.js Route Handler直接返回一个ReadableStreamexport async function POST(req) { const encoder new TextEncoder(); const stream new ReadableStream({ async start(controller) { const events runResumeAgent(reqBody); for await (const event of events) { controller.enqueue(encoder.encode(data: ${JSON.stringify(event)}\n\n)); } controller.close(); }, }); return new Response(stream, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }, }); }前端用fetch加ReadableStream解析逐段更新UIconst reader response.body.getReader(); const decoder new TextDecoder(); while (true) { const { value, done } await reader.read(); if (done) break; const chunk decoder.decode(value); // 解析 SSE 事件更新界面 }这一段代码看起来简单但有一个隐蔽的坑如果你在Node.js运行时里跑这个Route Handler需要确保不在路由内做同步阻塞操作并且要小心反向代理的超时配置。本地开发一切正常部署到云平台后却发现连接45秒被断开——那就是平台侧的streaming超时设得太短了。解决方法有两个方向一是在平台配置里调大响应超时时间二是把普通HTTP请求转换为任务ID轮询。简历场景我推荐前者因为代码改动最小。4. 并发性能优化实战4.1 Agent任务如何扛并发热搜词里“AI Agent怎么扛并发”这个话题我看到很多讨论都停留在“加机器”这个层面。实际上Agent场景和传统Web请求有一个本质区别传统接口是短连接几十毫秒就返回了而Agent任务动辄几十秒甚至几分钟直接同步处理很快会打满进程。我的方案是两层解耦第一层HTTP请求进入后立即创建一个任务记录并返回taskId不等待Agent执行完成。任务记录写入内存MAP或Redis包含State初始化数据和执行状态。第二层后台有一个任务执行器从队列里取出任务逐节点推进LangGraph图。每个节点执行前检查任务状态执行后把最新State回写。这种设计让Web服务器和Agent执行器彻底解耦。Web服务器永远是轻量的瞬间响应真正吃CPU和Token的任务在worker侧排队执行。并发能力提升的关键不在于模型接口多快而在于队列的吞吐和worker的个数。我把worker做成可水平扩展的Node.js进程需要扛更高并发时直接拉升worker数量即可。4.2 Token消耗的监控与限流Agent比传统调接口贵得多因为一个简历任务可能要调用5到8次模型接口。如果不做控制一个热情的、多轮追问的用户可能让单次任务消耗几十万Token。我上线第二天就收到了账单提醒所以必须加三层控制第一层是模型分级。解析和抽取用便宜快速的模型改写和渲染用高质量模型。这个策略能省不少钱。第二层是单轮输出Token上限强制在调用参数里设置maxTokens同时用stop序列让模型在应该结束的地方收住。第三层是任务级预算每个任务在创建时就带一个Token预算值钱花完了还在继续追问就自动降级为模板建议不再调用大模型。具体到监控我在Agent执行器的每个节点完成时记录本次调用的Token数累计到任务上下文里同时上报到日志平台。做一张简单的看板看“平均每个任务的Token消耗”和“昂贵节点占比”数据增长了就回去看是哪一层失控了。这个动作我建议每个做Agent应用的人上线第一天就做别等账单炸了才想起来。5. 常见问题与排查技巧实录5.1 状态丢失与任务恢复问题LangGraph.js的State默认在内存里进程重启、部署更新、函数超时都可能导致状态丢失。我在本地开发时从来不觉得有问题直到部署后经常收到用户反馈“重新打开页面后任务卡住了”。排查下来是两个原因一是Next.js在Serverless环境下的实例会被回收内存数据跟着消失二是浏览器端刷新后没有重新拉取任务状态。解决方式是引入一个外置存储层把State序列化后存进Redis。每次节点执行前后都更新一次前端重新连接时按taskId恢复。这里有个实践教训不要把“恢复任务”只当作后端问题。前端也要设计好断线重连的逻辑——后端把状态恢复得再好前端如果认为任务是新的会重新创建taskId双重折磨。我在全局状态里维护了connectionStatus和taskId映射刷新时先尝试恢复恢复失败才走新建流程。5.2 流程分支判断失效的排查Conditional Edge是LangGraph.js里很灵活的功能但也容易出问题。我遇到典型的case是用户确认节点模型判断用户已经满意走render分支但用户实际只是开了个玩笑说“还行吧”结果Agent就真的开始导出PDF了。后来我在判断逻辑里加了一层“显式确认校验”用户的回复必须包含“确认/没问题/可以导出”这类明确肯定的关键词同时要求当前模型输出一个布尔值continue和reason。两者同时满足才走确认分支否则一律回到改写节点。这种“双保险”让误判率大幅下降。5.3 文件渲染环境的依赖问题最后一个经常被忽略的坑是PDF渲染。开发时用的是本地环境的字体库部署到容器后字体缺失生成的PDF中文全变成方块。排查了半天才发现是Linux容器里没有中文字体。解决方式是在Dockerfile里安装fonts-noto-cjk并且把自定义的字体路径显式配置给渲染引擎不依赖系统默认字体。这个经历告诉我简历工具这类场景“最后的产物长什么样”直接决定用户对AI能力的判断。前面Agent流程再顺滑生成一份乱码PDF前面的努力就白费了。所以我把文档渲染这一环的自动化测试做得特别重每次修改Agent逻辑后都要跑一版真实简历的端到端回归。6. 项目扩展与后续方向简历工具Agent只是LangGraph.js应用的一个切片。做完这个项目后我最大的体会是Agent框架的价值不在于帮你“一键调用大模型”而在于把复杂业务流程变成可编排、可恢复、可观测的状态机。同一个图引擎换一套节点就能变成周报生成器、合同审查助手或面试模拟器。后续我计划在这个项目里加入两个能力一是多文件输入让用户同时上传多个简历版本做对比合并二是引入记忆层把用户的历史偏好长期保存这样第二次优化时Agent可以直接沿用上次的措辞风格。这两个扩展都能在现有的图结构上加节点实现不需要推翻架构这也是我当初选LangGraph.js而不是自己撸一个状态管理器的原因。最后给想动手做的同学一个建议不要一开始就想着把Agent做得无所不能先圈定一条窄业务流程把状态图和流式体验跑通。简历优化就是一个很好的起步场景——流程明确、反馈直接、产物体感强。等你把这一整套走完把并发、Token、状态恢复这些问题都磨平了再去复制到其他场景会发现复用的不只是代码还有一整套对Agent应用的工程直觉。
返回列表