尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Next.js+LangGraph.js实战:构建简历优化AI Agent的完整指南

Next.js+LangGraph.js实战:构建简历优化AI Agent的完整指南 聊一聊最近完整落地的一个项目基于 Next.js 和 LangGraph.js 构建的简历工具 AI Agent。这不是一个简单的简历模板生成器而是一个有状态、多步骤、带分支决策和循环反馈的智能代理系统。用户上传一份简历、粘贴目标岗位 JDAgent 会自己去解析简历结构、分析岗位要求、评估匹配度、生成针对性优化建议、逐段重写履历条目最后产出一份可直接导出的新版简历。整个过程中页面会实时展示Agent 当前正在做什么、下一步要做什么用户能看到每一步的中间结果而不是干等一个最终回答。这篇分享适合正在做 AI Agent 类应用、被 LangGraph.js 的状态图设计绕晕过、或者想把大模型能力真正落成可运营产品流程的人。我会把项目最初踩的坑、架构选型的真实理由、LangGraph 状态图的完整设计、Next.js 侧的流式对接方案、以及上线后遇到的性能问题都摊开讲。项目前后跑了三周中间推翻过一次架构这些经验全部来自实际操作不是从文档里抄出来的。1. 为什么要用 AI Agent 做简历工具以及整体架构怎么定1.1 简历工具的核心需求拆解先说清楚这个产品要解决什么问题。市面上已有的简历工具大多是表单式的用户手动填写教育经历、工作经历、项目经历然后套一个模板导出 PDF。这种方式对大多数人来说太痛苦了因为大部分用户手里只有一份旧简历他们要的不是从零填表而是帮我改好现在这份。更深一层的需求是简历优化这件事本身是强上下文依赖的。同样一份简历投技术岗和投产品岗优化方向完全不同。用户在招聘网站上看到某个岗位希望知道自己能不能投、差距在哪、简历上哪些条目需要重写。这要求系统同时理解简历文本和目标岗位 JD并且有能力做逐条对比和改写。把需求拆开来看整个流程包含这些环节解析上传的 PDF/DOCX/文本简历提取出教育背景、工作经历、项目经历、技能清单等结构化信息。分析目标岗位 JD提取出硬性要求、技能关键词、职责范围、可能期望的工作年限。对比简历和 JD计算匹配度评分定位差距点。根据差距生成具体的修改建议比如第一条工作经历需要补充量化成果缺少 Docker 相关经验描述。按照建议逐段重写简历条目要求在不夸大事实的前提下突出与目标岗位的关联。把所有重写过的段落合并生成完整的优化后简历并支持导出。这些环节如果用一次大模型调用硬做Prompt 会膨胀到无法维护输出格式也不稳定。更关键的是用户在实际使用中发现某个环节不满意比如匹配度那步算得不对系统需要能够单独重跑那一段而不是让用户重新上传一次简历。这种可拆分、可重试、可追踪状态的需求天然适合用 Agent 工作流来承载。1.2 技术选型Next.js 和 LangGraph.js 各自承担什么先交代一下我为什么选 Next.js。这个项目需要的是重交互前端 服务端 API AI 流式输出一体化的方案。Next.js 的 App Router 支持 Server Components页面首屏渲染很快API Routes 可以直接写在同一个项目里不用单独维护后端服务部署时无论是上 Vercel 还是自建 Node 服务都很顺手。更重要的是AI 场景下的流式文本输出在 Next.js 的 Route Handler 里实现 SSE 推送非常自然不需要额外搭一个 WebSocket 服务。LangGraph.js 是这次架构调整的核心。项目最初版本用的是 Chain 式调用就是线性地解析 - 评分 - 建议 - 重写一步步串下来。跑通之后发现问题很大一旦评分偏低我想让它多跑一轮优化建议就做不到了因为 Chain 是写死的顺序而且中间任何一步出错整个链就要从头再来浪费大量 token。LangGraph.js 的核心能力正好补上这两个短板它允许在节点之间画条件边根据运行时数据决定下一步走哪个节点也允许画回边让流程可以循环迭代。它本质上是一个图状态机状态 State 在节点之间显式传递每一步的输入输出都透明可追踪。我还对比过自己手写状态机。手写状态机在只有三四个步骤的时候没什么问题但简历优化这个场景有六个以上的节点还要做超时重试、条件路由、会话持久化手写代码会迅速复杂化。LangGraph.js 把这些都封装好了尤其是内置的 Checkpoint 机制可以把每一步的状态存下来用户中途刷新页面还能接着跑。这个能力在普通后端开发里得自己写一套快照系统用 LangGraph 就省掉了。1.3 整体架构从页面到 LLM 的完整链路架构上分三层前端展示层、服务编排层、模型调用层。前端就是 Next.js 页面负责两件事用户上传简历和 JD展示 Agent 运行过程中的实时状态。页面上有一个主区域显示 Agent 当前执行的节点名称、已生成的中间结果、步骤日志另一个区域在流程结束后展示最终简历内容。服务编排层是核心跑在 Next.js 的 Route Handler 里。收到请求后先在服务端构建 LangGraph 的 StateGraph传入初始状态原始简历文本、JD 文本然后调用graph.stream()以流式方式执行整个图。每一步节点的输出都会包装成 SSE 事件推到前端。为什么放在服务端执行而不是浏览器里跑因为模型 API Key 不能暴露在客户端而且解析 PDF、调用 OCR 这些操作也必须在服务端完成。模型调用层就是封装好的 LLM 调用模块。我基于 LangChain.js 的 ChatModel 接口做了统一封装默认用 GPT-4o-mini 做解析和评估用 GPT-4o 做重写这种需要更高质量的节点。底层模型可以配置切换换 Claude 或本地模型只需要改环境变量因为 LangGraph 节点里只依赖 ChatModel 接口不绑定具体厂商。这里有个架构上的取舍值得说我没有把前后端拆成两个独立项目。简历工具这种场景页面交互和 Agent 执行有强关联前端要实时展示 Agent 的事件流如果拆成两个服务还得额外处理 CORS、鉴权、事件推送协议对齐。合在一个 Next.js 项目里Route Handler 直接调用编排层代码事件经过 ReadableStream 推到前端链路最短排查问题也方便。等以后并发量上来了再拆架构上留好接口就行不必一开始就微服务化。2. LangGraph.js 实现简历 Agent 的状态图设计2.1 State 数据结构一切中间产物都进 StateLangGraph.js 的整个执行模型都围绕 State 展开。State 是一个可序列化的对象节点函数接收当前 State返回部分更新图的运行时负责把这些更新合并回完整状态。这意味着所有节点之间共享信息只能通过 State不能靠模块级全局变量也不能偷偷在节点内部缓存什么。我最初犯过一个错在解析节点里把结构化简历保存到了模块级变量想着后续节点直接读那个变量就行。结果图跑到一半会话恢复时那个变量已经没了因为 LangGraph 的 Checkpoint 只保存 State不保存模块内存。后来乖乖把所有中间产物都放进 State才解决了恢复问题。最终 State 的结构设计如下interface ParsedResume { personalInfo: { name: string; email: string; phone: string }; education: EducationItem[]; workExperience: WorkItem[]; projects: ProjectItem[]; skills: string[]; } interface GapItem { category: hardSkill | softSkill | experience | achievement; description: string; severity: high | medium | low; } interface ResumeState { originalText: string; parsedResume: ParsedResume | null; targetJob: string; analyzedJob: JobRequirement | null; matchScore: number | null; scoreBreakdown: Recordstring, number | null; gaps: GapItem[] | null; suggestions: string[] | null; rewrittenSections: Recordstring, string | null; finalResume: string | null; error: string | null; events: AgentEvent[]; }设计 State 的时候有两条原则。第一只放跨节点需要的数据节点内部使用的临时变量不必入 State否则 State 会越来越臃肿Checkpoint 存储成本也会增加。第二写入 State 的数据尽量是结构化数据而不是大段原始文本。比如解析节点产出的是ParsedResume对象而不是把原文再存一遍。这样后续节点每次读取都不需要重新处理文本节省 token 也减少重复计算。2.2 节点编排从解析到成稿的六步流程整个图一共有六个主要节点每个节点职责单一只做一件事。下面这个表格记录了每个节点的输入来源、输出字段和实现方式。节点主要输入输出字段实现方式parse_resumeoriginalTextparsedResumePDF 文本抽取 LLM 结构化解析analyze_jobtargetJobanalyzedJobLLM 提取岗位要求和关键词evaluate_matchparsedResume, analyzedJobmatchScore, scoreBreakdown, gaps规则打分 LLM 语义评分混合generate_suggestionsparsedResume, analyzedJob, gapssuggestionsLLM 生成逐条修改建议rewrite_sectionsparsedResume, suggestionsrewrittenSectionsLLM 按 section 分批重写compose_finalparsedResume, rewrittenSectionsfinalResume模板合并 LLM 整体润色节点之间的连接方式如下。parse_resume和analyze_job是两个并行入口都可以从初始状态直接进入没有先后依赖所以我在图里用了两条入口边让这两个节点可以同时执行。它们都完成之后汇合到evaluate_match。后面evaluate_match根据评分结果走条件边分数高于阈值就直接进入generate_suggestions分数低但还有优化余地也会进入generate_suggestions只是 Prompt 会带上更严格的指令。关键的分支在generate_suggestions之后根据是否已经重写过决定回到evaluate_match再来一轮还是进入compose_final。代码上大概长这样const workflow new StateGraph(ResumeStateSchema) .addNode(parse_resume, parseResumeNode) .addNode(analyze_job, analyzeJobNode) .addNode(evaluate_match, evaluateMatchNode) .addNode(generate_suggestions, generateSuggestionsNode) .addNode(rewrite_sections, rewriteSectionsNode) .addNode(compose_final, composeFinalNode) .addEdge(parse_resume, evaluate_match) .addEdge(analyze_job, evaluate_match) .addConditionalEdges(evaluate_match, routeAfterEvaluation) .addConditionalEdges(generate_suggestions, routeAfterSuggestions); const app workflow.compile({ checkpointer: postgresSaver, recursionLimit: 30, });2.3 条件路由与循环机制Agent 真正的决策点条件路由是 LangGraph.js 比 Chain 式调用强的一个核心点。routeAfterEvaluation这个函数读取 State 里的matchScore和rewriteCount返回下一步要走的目标节点名称。我用它在两个地方做了分支。第一个分支在evaluate_match之后。如果匹配度低于 60 分说明简历和目标岗位差距很大直接生成建议可能覆盖不全我会让流程先进入一个隐藏的deep_gap_analyze节点做细粒度差距分析再回到generate_suggestions。如果分数高于 60就直接走常规建议路径。第二个分支在generate_suggestions之后控制循环次数。简历优化这种场景一轮改写往往不够。我给 State 加了一个rewriteRound字段每执行完一轮评估 - 建议 - 重写就加一。routeAfterSuggestions判断如果rewriteRound 2并且matchScore还有提升空间就返回evaluate_match再跑一轮否则进入compose_final。循环必须要有终止条件否则一旦模型输出异常图会无限跑下去。我做了双重保险一是routeAfterSuggestions里限制最多两轮二是编译时设置recursionLimit: 30这是整个图执行步骤的上限相当于一个安全熔断开关。一旦触发recursionLimitLangGraph.js 会抛出一个专门的错误我在 API 层捕获后转成友好的提示返回给前端。这里想多说一句很多人第一次用 LangGraph.js 会把它当成更好的 Chain只画一条直线完全没用上条件边。其实 Agent 和普通 LLM 工作流的分水岭就在这里——运行时根据实际数据决定路径而不是写死执行顺序。简历工具这个场景天然适合图编排因为你无法预知用户的简历质量和岗位匹配情况必须让系统根据中间结果自己决定下一步做什么。3. Next.js 侧的实现细节3.1 API 路由与 SSE 流式响应Next.js 里实现 SSE 流式响应我用的 Route Handler 配合 ReadableStream。路由收到前端 POST 请求后先解析上传文件和 JD 文本构造初始 State然后编译并运行 LangGraph。关键点是调用graph.stream()而不是graph.invoke()因为前者会逐节点返回执行事件可以用来组装 SSE 消息。Route Handler 的简化代码如下export async function POST(req: Request) { const formData await req.formData(); const file formData.get(resume) as File; const targetJob formData.get(jd) as string; const originalText await extractTextFromFile(file); const initialState: ResumeState { originalText, targetJob, // 其余字段初始化为 null }; const encoder new TextEncoder(); const stream new ReadableStream({ async start(controller) { const run app.stream(initialState, { recursionLimit: 30 }); for await (const event of run) { const payload encoder.encode(event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n); controller.enqueue(payload); } controller.close(); }, }); return new Response(stream, { headers: { Content-Type: text/event-stream }, }); }这里有一个实际测试中容易忽略的细节SSE 消息的data字段必须是一行 JSON不能包含换行否则浏览器端的解析会错乱。LangGraph 输出的结构化数据里如果含有换行符发送前一定要先序列化并替换掉。我在生产环境遇到过几次前端事件丢了一半的情况排查到最后都是因为 JSON 里有未转义换行。3.2 前端状态管理与实时展示前端消费 SSE 的方式我选了fetch配合 ReadableStream 手动解析而不是用 EventSource。原因是 EventSource 只能用 GET 请求而上传简历文件必须用 POSTEventSource 根本带不上 FormData。用fetchReadableStream的方式可以保持 POST 语义同时逐块读取响应文本。前端维护 Agent 事件列表的状态很简单我用useReducer就够了没有引入 zustand 或 redux。因为事件流本质上是追加型数据Reducer 的appendaction 能完美覆盖这个场景。界面上有一个步骤时间线组件每收到一个node_started事件就在时间线上加一个节点标题每收到一个node_finished事件就在对应节点下追加输出摘要。用户能非常直观地看到Agent 正在跑第几步。还有一个交互细节rewrite_sections节点执行时间比较长模型要逐段生成重写后的内容。我在前端做了逐段展示的效果每当后端推送一小段文本页面就局部更新用户可以看到重写内容像打字机一样出现在屏幕里。这种体验比等一个完整 JSON 再渲染要好太多用户会觉得系统真的在思考。3.3 会话快照与持久化刷新也不丢进度LangGraph.js 的 Checkpoint 机制是我选它的一个重要加分项。在编译图时传入checkpointerLangGraph 会自动把每一步的 State 存下来并为每次运行分配一个threadId。用户刷新页面、甚至关闭浏览器再打开只要还能拿到threadId就可以从上次中断的地方继续执行。生产环境里我用了 Postgres 做 Checkpoint 存储实现了一个PostgresSaver继承 LangGraph 的BaseCheckpointSaver。核心接口就两个方法put和getTuple把序列化后的 State 存进数据库表按threadIdcheckpointId做索引。这个表结构很简单字段是 thread_id、checkpoint_id、state_json、created_at。这里有一个重要的架构边界所有 LangGraph 的执行和 Checkpoint 读写必须放在服务端客户端永远只能持有threadId字符串。模型 API Key、数据库连接串这些都是服务端机密。Next.js 的 Route Handler 天然在服务端运行只要注意不要把这些值传到客户端组件就算安全。4. 核心功能实现简历解析、评分与职位匹配4.1 简历解析PDF、DOCX 到结构化 JSON简历解析是整条链路的地基如果这一步产出脏数据后面所有节点都会受影响。我同时支持 PDF 和 DOCX 两种格式PDF 用pdf-parseDOCX 用mammoth抽取纯文本后交给 LLM 做结构化。PDF 有个经典的坑扫描件。用户上传的 PDF 如果是图片扫描版pdf-parse抽出来的是一堆二进制乱码或者空文本。我做了两层兜底先检测抽取文本的长度如果低于 200 个字符就判定为扫描件走 OCR 通道把 PDF 转成图片后调用 OCR 服务识别。这个逻辑在解析节点内部做分支对用户完全透明。文本抽取完成之后交给 LLM 用 function calling 输出结构化 JSON。我定义了一个 zod schema 描述ParsedResume结构传给模型的 tools 参数。这样模型输出的就一定是合法的 JSON不会出现{不闭合这种问题。schema 片段const ParsedResumeSchema z.object({ personalInfo: z.object({ name: z.string(), email: z.string(), phone: z.string() }), education: z.array(EducationItemSchema), workExperience: z.array(WorkItemSchema), projects: z.array(ProjectItemSchema), skills: z.array(z.string()), });解析节点里还会做一次合理性校验防止模型把工作经历重复塞进项目经历。规则很简单检查每个 work item 的起止时间和项目时间是否冲突检查教育经历是否包含学校名称如果缺字段就标记为needsManualReview不会直接丢弃。这样做的好处是即使模型解析出错后面的评分节点也能通过标记数据理解这份简历信息不完整给出针对性的改进建议。4.2 评分算法规则打分与 LLM 语义评分的混合方案匹配度评分是用户最看重的指标也是我改得最多的地方。最初版本的评分完全靠 LLM让模型看一眼简历和 JD直接输出一个 0 到 100 的分数。结果非常不稳定同一个简历换个措辞问分数能差 15 分以上。后来我改成混合方案规则打分保证稳定LLM 做语义补充。具体打分逻辑分三步。第一步从 JD 里提取硬性要求。这是analyze_job节点产出的结构化数据包含技能关键词列表、学历要求、经验年限、职责描述。第二步对硬性技能做关键词匹配。我从简历的技能列表和工作经历描述里提取关键词集合和 JD 的技能关键词做交集计算得到技能覆盖率。这一步是纯规则计算结果稳定可解释。第三步对软性匹配度调用 LLM 打分。技能覆盖率只能反映要素匹配但无法判断表达方式是否加分。比如简历里写了负责系统开发JD 要求主导过分布式系统架构关键词不直接匹配但语义上是相关的这时候靠 LLM 判断更准。最终评分是把两部分加权合并finalScore hardSkillScore * 0.6 semanticScore * 0.4hardSkillScore是规则计算的技能覆盖率乘以 100semanticScore是 LLM 返回的 0 到 100 语义匹配度。加权权重不是拍脑袋定的我取了 50 份测试简历做回归评估发现 0.6/0.4 的权重和人工评分的相关性最高。这个权重也可以通过配置文件调整不同行业可能最优权重不同。scoreBreakdown里记录了每个维度的得分前端会展示一个雷达图或者柱状图用户能直观看到技能匹配 80 分、经验匹配 65 分、成果量化 40 分。这部分透明度很重要用户不会因为一个笼统的 72 分就信服看到具体维度才有优化方向。4.3 优化建议与重写结构化输出加逐段处理优化建议节点读取gaps列表逐条生成建议。prompt 里明确要求每条建议必须包含三部分问题位置哪个 section、哪条经历、问题原因为什么和 JD 不匹配、具体改法怎么改写、补充什么内容。输出格式用固定 JSON方便前端直接渲染成建议卡片。重写节点是整个流程中最耗 token 的部分也是最容易出质量问题的地方。我的处理原则是一次只重写一个 section不要试图让模型一次性输出整份简历。工作经历这种长文本更要拆成单条经历来处理。为什么一方面是为了控制上下文长度单条经历的上下文只有两条 Prompt 那么大模型不容易遗忘重要的原始信息另一方面是方便用户局部确认重写完的工作经历逐条展示用户觉得哪条改得不行可以单独再调不用整份返工。重写提示词里有一个非常重要的约束不许编造事实。我反复强调这一点因为简历是我的核心信誉问题。LLM 很容易在润色过程中把参与项目扩写成主导项目或者编造根本不存在的量化指标。我在 prompt 里明确写了一条规则只要原文没有给出数字就不许编造数字如果觉得这条经历缺少量化成果应该在建议中提醒用户自行补充而不是自动代写。5. 常见问题与排查技巧实录5.1 State 更新冲突并发节点互相覆盖LangGraph.js 允许多个节点并行执行。我在第一个版本里让parse_resume和analyze_job并行结果发现两个节点都向 State 里写入了progress这个字段最后总是只有一个节点的进度被保留另一个被覆盖。这个问题排查了很久因为 LangGraph 的运行时不会报错它只是把后写入的覆盖先写入的。解决办法有两个我两个都用了。最快的办法是给并行节点写不同的 State key比如parse_progress和analyze_progress从源头避免冲突。另一个办法是用函数式更新返回值不是具体数据而是(current: State) PartialState形式的函数LangGraph 会按顺序应用这些更新而不是直接覆盖。对于需要合并多个来源数据的场景函数式更新是更优雅的方案。5.2 SSE 流式中断与超时处理SSE 流式响应在开发环境跑得好好的部署到线上就经常中断。排查下来有两个原因一是 serverless 平台的函数执行时长限制简历重写这种重任务很容易超时二是代理层对 SSE 的长连接不友好连接空闲一段时间后被掐断。针对超时我的做法是把 Agent 执行拆成两段耗时较短的解析和评估接口保持普通 POST前端等待完整响应耗时较长的重写流程改走异步任务提交后接口立即返回taskId前端轮询任务状态。这样即便 serverless 函数超时任务也已经在后台跑完不会丢失进度。如果坚持要实时流式展示可以把 Next.js 部署到 Node 常驻进程或者是支持长时间运行的托管平台不要在默认 serverless 配置里硬扛长任务。前端也加了断线重连机制。收到 SSE 超时或者网络错误时不是直接提示失败而是记录当前已经收到的节点数重新请求时带上这个游标后端从游标之后继续推送。配合 Checkpoint 机制这个恢复流程实现起来不算复杂但用户体验提升非常明显。5.3 Token 消耗与上下文裁剪策略Token 成本是这个项目上线后最头疼的问题。一次完整的简历优化流程如果全程用 GPT-4o平均消耗在 3 万 token 左右成本相当可观。我做了三件事来压成本。第一分级用模型。解析和评估这种对创造力要求不高的节点用便宜的 GPT-4o-mini只有重写段落这种真正需要高质量文本的节点才用 GPT-4o。实测下来解析和评估环节换用 mini 后准确率几乎没下降但 token 成本降了将近一半。第二裁剪传入上下文的粒度。重写单条工作经历时不需要把整份简历都塞进去。我实现了一个trimContext工具函数根据当前要重写的 section 类型只传相关的上下文片段。比如重写工作经历时只传这条经历原文、JD 中相关的职责要求、以及评分节点给出的针对性建议其他无关内容一律不传。第三JD 分析结果做缓存。同一个 JD 如果被多个用户使用我按 JD 文本的 hash 缓存analyzedJob结果。很多热门岗位的 JD 是重复出现的命中缓存后能省掉一个节点的模型调用。这个缓存逻辑放在analyze_job节点里命中就直接读取没命中才调用模型。5.4 模型输出格式不稳定与校验兜底LLM 输出 JSON 尽管用了 function calling偶尔还是会出现字段缺失或者类型不对的情况。我在重写节点上遇到过几次rewrittenSections里只有一半的 section导致最终简历缺段落。处理思路是接入 zod 校验校验不通过就进入重试逻辑。每个节点函数的输出先过safeParse失败时最多重试两次每次重试稍微调整 prompt 强调格式要求。重试仍然失败的就把该节点的输出标记为失败但不阻塞整个流程最终简历里保留原始段落并注明此段建议人工复核。这种部分成功策略比整体失败用户体验好得多用户不会因为一个段落格式错误就丢掉整份产出。6. 部署与性能调优心得6.1 部署架构容器化常驻 vs serverless 之争部署方案我前前后后折腾了三次经验教训不少。最初直接部署到 Vercel因为 Next.js 是它的亲儿子几行配置就搞定。但很快发现长任务不合适serverless 函数的时长限制和冷启动放在 AI 场景里非常难受。SSE 流式输出本来应该是持续的被平台一掐就废掉了。于是我把架构改成了两层Next.js 应用部署到 Docker 容器用 Node 常驻进程跑同一台机器上跑一个轻量的任务队列用 Redis BullMQ负责承接重写和生成这类耗时的后台任务。前端发起请求后轻量请求直接由 Next.js 进程响应耗时任务投递到队列消费者进程处理完后写回 Redis前端轮询获取结果。这套架构上线后稳定多了不再受 serverless 平台限制。6.2 流式输出与缓存优化SSE 在常驻 Node 进程下表现很好但我额外加了两个优化。第一模型输出用流式 API而不是等服务端完整生成再返回。第二静态资源全部走 CDN简历工具的页面主要是 JS 包和样式体积不大但缓存命中率提升后首屏加载明显更快。模型调用层面还有一个值得做的优化温度参数调低。简历重写这种任务对确定性要求高我把重写节点的 temperature 设为 0.2让输出更稳定解析节点的 temperature 直接设 0避免纯提取任务出现随机偏差。如果你是做内容创作类 Agent温度可以调高但简历工具追求可预测性低温度更合适。6.3 并发任务与成本控制上线运营之后用户同时提交简历的情况越来越多。我按threadId做并发控制同一个用户同一时刻只允许一个 Agent 任务在跑新的请求直接排队。这个限制主要是成本考量AI Agent 任务不像普通 Web 请求可以无限并发token 消耗是实打实的账单。我给用户做了等额配额免费用户每天可以跑一次完整流程付费用户不限次数但限制同时运行的任务数量。配额逻辑挂在任务队列入口超出部分返回明确的提示文案你已有任务正在运行请等待完成或取消。实测下来这个限制对用户体验影响不大简历优化本来就不是高频操作用户看到Agent 正在思考中反而觉得系统更专业。最后再分享一点个人体会。项目上线后我发现用户真正高频使用的不是一键生成完整简历这个最终功能而是针对目标岗位逐条优化的过程本身。他们会反复调整 JD 文本对比不同岗位的匹配度评分单独修改某一条工作经历的改写结果。这给我的启发是AI Agent 工具的价值不在于代替用户做完所有事而在于把过程拆开、让用户看到每一步逻辑、在关键节点上给人控制权。LangGraph.js 的状态图设计正好契合这个理念每一步都可追踪、可回放、可单独重跑。这种透明感和可控性是用户在真实场景里愿意长期使用的核心原因。
返回列表