☰
Next.js+LangGraph.js构建简历优化Agent实战拆解
2026/10/8 10:39:35 网站建设 项目流程

如果你最近在研究 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 Route
  • Agent 核心逻辑放在服务端,通过 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: Annotation<string>, parsedResume: Annotation<ResumeData>, scores: Annotation<ScoreRecord>, suggestions: Annotation<Suggestion[]>, finalOutput: Annotation<string>, error: Annotation<string>, }); 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();

整个流程是这样的:

  1. 用户输入进入parseResume节点,产出结构化数据
  2. 进入checkMissingInfo判断信息完整度,如果缺少关键信息,有一条边指向"追问用户"的节点;如果信息完整,直接进入打分
  3. scoreResume按岗位匹配、描述质量等维度打分
  4. generateSuggestions针对低分项生成优化建议
  5. 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/langgraph,StateGraph的 API 和早期版本有差异。如果你照着教程写遇到方法不存在,先去查一下版本。另一种做法是把核心依赖锁定版本号,避免部署时意外升级导致行为变化。

可以额外说明的一点是:LangGraph.js 并不是唯一的选择,实际项目里我也试过自己用状态机实现简单编排,但在复杂场景下,图结构带来的可视化和可调试能力确实值回学成本。

5. 一点真实的经验分享

这个项目从最开始简单的“聊天窗口 + prompt”演进到完整的 Agent 工作流,最重要的体会是:AI Agent 的本质不是“让模型更聪明”,而是“用工程手段让模型的行为可控”。LangGraph.js 的图结构、平台无关的 State 设计、流式输出,本质上都是在给大语言模型的随机性套上缰绳。

如果你也想做类似的项目,我的建议是不要一上来就追求复杂,先把最小可用链路跑通——一个节点,一次模型调用,一个流式输出。跑通了之后再往图里加分支、加条件判断、加缓存,每一步都小步验证,会比直接照搬一整套复杂架构稳妥得多。简历工具这个方向还有很大的优化空间,比如多轮追问实现千人千面的调整,或者对接更多的简历模板,这些在现有图结构上都只要增加节点就能扩展。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询