ARTICLE DETAIL

资讯详情

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

Next.js+LangGraph.js构建简历优化Agent实战拆解

Next.js+LangGraph.js构建简历优化Agent实战拆解 如果你最近在研究 AI Agent肯定能感觉到 2025 年这个赛道已经从“能聊天”卷到了“能干活”。我的一个比较典型的落地项目就是用 Next.js 做前端和 BFF 层用 LangGraph.js 编排一个简历优化 Agent——用户上传简历或者粘贴一段工作经历Agent 会自动完成解析、结构化、评分、优化建议生成最后在前端流式渲染出来。整个链路跑通的体验和传统的“套个 prompt 调 API”完全不一样这里面有状态流转、有分支决策、有流式输出也有真刀真枪的并发问题。这篇文章就完整拆一下这个项目的设计思路和落地过程适合已经用过 Next.js 和 AI SDK、但对 Agent 工作流还停留在“一问一答”阶段的开发者。1. 整体架构与方案选型1.1 为什么用 Next.js 而不是单独拆前后端先说结论如果 Agent 的前端交互足够复杂用 Next.js 全栈方案是最省事的。这个项目最初也想过去搞一个独立的 Node.js 服务端再用 React 前端去对接但很快发现一个尴尬的问题——AI 应用的前端和后端边界太模糊了。举个例子简历解析的结果要在前端实时展示同时解析过程中产生了中间状态后端需要根据这些状态决定下一步是继续追问用户还是直接生成优化建议。如果前后端拆开你需要额外定义 WebSocket 或者 SSE 协议还得自己维护连接状态。但 Next.js 的 Route Handler 天然支持流式响应App Router 下的 Server Actions 可以直接在服务端调用 Agent 流程再把流式结果通过ReadableStream推给前端省掉一层网络协议约束。这个项目最终的结构是Next.js App Router 负责页面路由和 API RouteAgent 核心逻辑放在服务端通过 Route Handler 暴露接口前端用useChat或自定义的fetch流式读取实现打字机效果整个 Agent 的状态由 LangGraph.js 的StateGraph统一管理不需要单独部署一个 API 服务一个 Next.js 应用全搞定部署的时候也只需要一个环境。1.2 LangGraph.js 相比 LangChain.js 的演进LangChain.js 大家都很熟链式调用chain.pipe()一个接一个。但做简历工具这种场景链式调用的局限性非常明显——它不适合有条件的流程控制。我在早期版本里用 LangChain.js 写过一版遇到的最大问题是用户可能上传 PDF、可能直接粘贴文本、也可能描述不清想让我先给个模板。这种场景下流程的分支和循环逻辑在链式结构里写得像一堆 if 嵌套维护性很差。LangGraph.js 出来之后把流程改成图结构节点与节点之间的边可以带条件判断整个 Agent 的执行逻辑一下子清晰了。LangGraph.js 带来的核心优势有三个图结构天然支持分支和循环就像给 Agent 画了张流程图节点之间通过共享的 State 传递数据不用自己搞全局变量每一步都有清晰的类型约束和 TypeScript 配合起来非常舒服1.3 并发扛不住先分清楚瓶颈在哪热词里有个“AI Agent 怎么扛并发”我实测下来Agent 应用的并发瓶颈几乎不在框架而在大模型 API 的响应速度和外部依赖的稳定性上。一个简历解析请求从进入 Agent 工作流到最终生成优化建议通常要经历解析简历文本、调用大模型做结构化抽取、评分、生成建议。这里每一步都是 IO 密集型操作等待模型响应的时间往往占 80% 以上。所以扛并发的关键是对耗时步骤做超时控制和重试策略而不是盲目加大并发数用 Next.js 的unstable_cache或 Redis 做结果缓存相同简历重复请求直接命中模型调用层面做并发池限制同时进行的模型请求数量避免触发 API 限流我在 Node.js 环境下用p-limit做了并发池把同时执行的模型请求控制在 5 个以内实测比无限制并发更稳定整体吞吐率反而更高——因为没有被 429 拖慢。2. Agent 工作流设计与状态流转2.1 能力不应该靠“一个巨大的 prompt”实现简历优化听上去简单实际拆解下来至少包含四个子能力简历解析从 PDF 或文本中抽取个人信息、工作经历、技能标签结构化评分按岗位匹配度、经历描述质量、技能完整性打分优化建议针对每一段经历给出可落地的修改建议结果生成按用户设置的模板格式输出最终简历如果把这四个能力塞进一个 prompt 里效果大概率不会好。因为指令太长模型会选择性地失忆尤其对于长简历文本前面的解析规则到后面可能就被忽略了。用 LangGraph.js 把每个能力独立成一个节点每个节点只做一件事反而准确率高得多。2.2 图结构设计节点、边和条件分支我用StateGraph设计了一个包含 5 个节点的图import { StateGraph, Annotation } from langchain/langgraph; const AgentState Annotation.Root({ input: Annotationstring, parsedResume: AnnotationResumeData, scores: AnnotationScoreRecord, suggestions: AnnotationSuggestion[], finalOutput: Annotationstring, error: Annotationstring, }); const workflow new StateGraph(AgentState) .addNode(parseResume, parseResumeNode) .addNode(checkMissingInfo, checkMissingInfoNode) .addNode(scoreResume, scoreResumeNode) .addNode(generateSuggestions, generateSuggestionsNode) .addNode(formatOutput, formatOutputNode) .addEdge(parseResume, checkMissingInfo) .addConditionalEdges(checkMissingInfo, routeByMissingInfo) .addEdge(scoreResume, generateSuggestions) .addEdge(generateSuggestions, formatOutput) .addEdge(formatOutput, END); export const graph workflow.compile();整个流程是这样的用户输入进入parseResume节点产出结构化数据进入checkMissingInfo判断信息完整度如果缺少关键信息有一条边指向追问用户的节点如果信息完整直接进入打分scoreResume按岗位匹配、描述质量等维度打分generateSuggestions针对低分项生成优化建议formatOutput按模板输出最终结果这里最关键的是routeByMissingInfo这个条件路由函数它让 Agent 有了“决策”能力——不是每个请求都走同样的路径而是根据中间状态动态切换。2.3 State 设计直接影响开发体验LangGraph.js 的 State 是全局共享的但设计得不好会直接影响开发体验。我在第一版把整个简历文本直接塞进 State导致每次节点更新都要传一遍大字符串浪费 token 不说调试时看状态也很痛苦。后来改成“State 里只存任务标识和小型结构化数据”原始文本在进入图之前先存到 Redis 或者临时存储State 里只放一个resumeId节点需要原文时按 ID 去取。这样 State 的序列化快了很多也更容易排查问题。对于每个节点的返回值要显式声明要更新哪个字段async function parseResumeNode(state: typeof AgentState.State) { // 解析逻辑省略 return { parsedResume: result }; }这样 LangGraph 会自动把返回值合并到全局 State 中。注意不要在一个节点里同时更新太多字段尽量让每个节点职责单一后面如果要加日志、加埋点也容易。3. 关键实操模型选型、Prompt 与工具调用3.1 模型选型的取舍不是越贵越好简历解析这种任务我用过 GPT-4o 级别的模型也试过便宜很多的轻量模型最终选了中间档位的模型做主力。原因是解析和打分这类任务需要较强的指令遵循能力轻量模型容易漏字段但纯生成建议的任务轻量模型完全够用。我的方案是双模型策略解析、打分节点用强模型保证结构化输出稳定建议生成、格式整理节点用轻量模型降低成本、提高响应速度强模型和轻量模型的价格差距很大但质量和延迟差距在可控范围。这个取舍上线后整体 API 成本降了 40% 左右。另外一点经验把大模型当成“实习生”别让它做太复杂的任务。一开始设计得分项时我想让模型一次性输出简历解析结果、评分、建议结果经常出现评分和文本脱节——评分很高建议却说“需要提升”。拆开之后基本杜绝了这个问题。3.2 用工具调用替代“自由发挥”简历优化里有个环节是“根据用户选择的岗位方向进行调整”早期版本我让模型自己决策从哪些维度优化结果不可控。有的回复给的是通用建议压根没有结合用户真实经历。后来改成工具调用方式把“获取简历数据库记录”、“获取岗位技能要求”、“解析 HR 关注点”都做成工具模型的行为变成了根据用户输入自主决定调用哪些工具再结合工具结果做分析。工具的返回结果是确定的模型的发挥空间被约束在“分析”和“表达”层面稳定性好很多。LangGraph.js 的createAgent或者直接给模型绑定 tools 都可以实现import { ChatOpenAI } from langchain/openai; import { tool } from langchain/core/tools; const getSkillRequirements tool(async ({ jobTitle }) { // 从数据库或静态配置中拿技能要求 return loadSkillMap(jobTitle); }, { name: getSkillRequirements, description: 根据岗位名称获取技能要求列表, schema: { type: object, properties: { jobTitle: { type: string }, }, required: [jobTitle], }, }); const model new ChatOpenAI({ model: gpt-4o-mini, temperature: 0.1 }) .bindTools([getSkillRequirements]);3.3 流式输出体验优化AI 应用如果没有流式输出用户等待 10 秒看到一整段文字体验会很差。简历优化建议通常几百字到上千字我必须用流式渲染。LangGraph.js 本身支持streamMode: messages可以边跑边吐 token。但在我的图结构里不是所有节点都应该流式输出。我在实现上分了两段解析、打分阶段不流式输出只显示状态标签例如正在解析简历...、历史经历分析中...建议生成阶段开启流式逐 token 推给前端这样用户既能感觉到进度又不会看到 JSON 解析过程的中间产物交互更干净。前端我用fetch配合ReadableStream解析 SSE 格式数据在 Next.js Route Handler 里用TextEncoder转码export async function POST(req: Request) { const encoder new TextEncoder(); const stream new ReadableStream({ async start(controller) { for await (const chunk of runAgent(req)) { controller.enqueue(encoder.encode(data: ${JSON.stringify(chunk)}\n\n)); } controller.close(); }, }); return new Response(stream, { headers: { Content-Type: text/event-stream }, }); }4. 常见问题与排查技巧实录4.1 状态污染这是 LangGraph 新手最容易踩的坑这里有个需要注意的地方LangGraph 的 State 在不同节点间传递时必须保证返回对象的字段类型一致。我遇到过一个问题某个节点返回了空数组[]下一个节点读到的 State 里对应字段变成了undefined直接抛错。排查了半天发现是类型标注问题。解决方法是在StateGraph定义时给每个字段设置合理的初始值和类型守卫。比如scores字段初始化成空对象节点返回时确保是有值的数据。JS 的弱类型在这里会带来隐患用 TS 严格模式 zod 校验接口返回基本能规避大部分问题。4.2 并发场景下的实测与优化项目做压测的时候我用 20 个并发请求打一个没有做任何处理的 Agent 接口结果惨不忍睹——平均响应时间从 2 秒飙到 18 秒有不少请求直接超时。排查后发现几个关键问题每个请求都重新初始化 LangGraph 的图实例导致内存和 CPU 消耗巨大模型调用没有并发限制多个请求同时打 API触发限流后互相等待简历解析用的外部 PDF 解析接口是同步的阻塞了事件循环优化后的方案是图实例做成单例复用模型调用包一层并发池PDF 解析换成异步任务队列。改完之后同样 20 并发响应时间稳定在 3-4 秒基本没有超时。这里得到的经验是Agent 应用的瓶颈大多数不在框架本身而在外部 IO 依赖上做并发设计时要先把这部分看清楚。4.3 结构化输出不稳定的解法用大模型抽取简历字段时偶尔会出现 JSON 格式错误或者字段缺失。尤其对于排版混乱的简历模型可能漏掉“工作经历”整块内容。LangGraph 节点抛错会导致整个图中断这是我早期上线时最头疼的问题。我的解法是在每个 Agent 节点外层包一个重试机制并针对出错的情况做 prompt 修复。比如遇到 JSON 解析失败就把原始报错信息拼进下一个请求的 prompt让模型“重新整理”而不是简单地重新调用一遍原 prompt。实测下来结构化输出的成功率从 92% 提到了 99% 以上。还有一个技巧是给输出加上few-shot示例喂两个简历 JSON 的样例模型对格式的理解会稳定很多。别嫌示例占 token格式稳定省下的调试时间更值得。4.4 版本兼容性注意LangGraph.js 的 API 还比较年轻社区版本迭代很快。我写这个项目时用的langchain/langgraphStateGraph的 API 和早期版本有差异。如果你照着教程写遇到方法不存在先去查一下版本。另一种做法是把核心依赖锁定版本号避免部署时意外升级导致行为变化。可以额外说明的一点是LangGraph.js 并不是唯一的选择实际项目里我也试过自己用状态机实现简单编排但在复杂场景下图结构带来的可视化和可调试能力确实值回学成本。5. 一点真实的经验分享这个项目从最开始简单的“聊天窗口 prompt”演进到完整的 Agent 工作流最重要的体会是AI Agent 的本质不是“让模型更聪明”而是“用工程手段让模型的行为可控”。LangGraph.js 的图结构、平台无关的 State 设计、流式输出本质上都是在给大语言模型的随机性套上缰绳。如果你也想做类似的项目我的建议是不要一上来就追求复杂先把最小可用链路跑通——一个节点一次模型调用一个流式输出。跑通了之后再往图里加分支、加条件判断、加缓存每一步都小步验证会比直接照搬一整套复杂架构稳妥得多。简历工具这个方向还有很大的优化空间比如多轮追问实现千人千面的调整或者对接更多的简历模板这些在现有图结构上都只要增加节点就能扩展。
返回列表