
最近一直在折腾 AI Agent 的实际落地从最开始拿 Python 写一堆脚本做验证到后来把整套服务搬到 Web 端跑通踩了不少坑。这次想聊的是一个比较完整的案例用 Next.js 做前端和 API 层用 LangGraph.js 编排 Agent 的流程最终做出一个能用的简历工具。这个工具不是简单调用大模型接口生成一段文字而是真正能把分析简历–定位问题–给出修改建议–产出优化稿这条链路串起来。标题里写的完整落地意思是它不是一个 Demo而是能放在生产环境里给真实用户用的东西。这篇内容适合三类人看一是想在 Next.js 项目里集成 AI Agent 的前端工程师二是已经会用 LangChain 但想知道 LangGraph.js 编排流程怎么做的开发者三是想把自己的 AI 工具从代码片段变成完整产品的人。我会把架构设计、核心代码、并发处理、部署细节都拆开讲后面还会提到一些常规文档里不会写的坑。1. 为什么是 Next.js LangGraph.js而不是别的组合市面上做 Agent 的主流方案不少Python 生态有 LangChain、LangGraph、AutoGen前端圈子也有很多人在尝试直接把 Agent 跑在 Node.js 环境里。我在选型时对比过几套组合这里直接把我的思考过程列出来。1.1 这套组合解决了我之前的三层割裂问题最开始我用的是 FastAPI 写后端 Agent 逻辑React 单独写前端两边通过 REST API 通信。问题很快就出来了Agent 的流式输出要先从 Python 后端推到 Node 服务再转给前端中间多了一层代理Agent 内部的状态要存在后端内存里前端刷新一下就全丢了还有前后端两个项目分开部署调试的时候老要对着一堆跨域日志发呆。Next.js 把 API 路由和前端页面放在同一个应用里LangGraph.js 则把 Agent 的状态、节点、工具调用都管理起来两者结合之后一层服务就能承担所有职责。前端调用/api/agent接口Next.js 的 Route Handler 直接拉起 LangGraph.js 的图执行流程流式结果通过 ReadableStream 推给浏览器不再有多层转发。1.2 LangGraph.js 和 LangChain.js 的本质区别图 vs 链很多人第一次接触 LangGraph.js 时容易把它理解成 LangChain.js 的升级版其实它们的定位完全不同。LangChain.js 是给出一堆预置的 Chain比如先调用模型、再解析输出、再调用工具这种线性执行LangGraph.js 则把执行流程建模成一张图每个节点是一个处理步骤节点之间靠边连接支持条件分支、循环、并行、人工介入。用简历工具来举例很直观。一个简历分析流程如果写成一串代码大概是读文本 - 提取结构化信息 - 评估 - 给建议四步一顺到底。但真实场景里有分支如果简历里没有工作经历就不能走评估经历丰富度这个分支如果用户要求的是优化措辞而不是整体评估就要走另一条路径。用 LangGraph.js 写这种带分支和循环的流程比硬编码 if/else 清晰得多。我之前也看到有不少人在问ai agent 主流架构到底是什么我的理解是Agent 核心就三块——状态State、决策LLM 选择下一步做什么、工具Tools 执行动作。LangGraph.js 给这三块提供了一个很好的表达框架状态是共享数据对象决策是节点之间的条件边工具是注册给模型调用的函数。1.3 几个备选组合的对比我把实际测试过或深度调研过的组合放在一起比较过方案适合场景主要问题纯 Next.js 直接调用大模型 API简单问答、一次性生成没有流程管理复杂业务无法编排FastAPI LangChain 独立前端Python 生态友好的团队部署和运维成本高流式传输复杂纯前端 Agent 框架如 Vercel AI SDK快速原型、轻交互复杂工具链和状态管理欠缺Next.js LangGraph.js需要长流程和工具调用且希望前后端同构参考资料较少中文案例稀缺最后一行的参考资料少是真痛点我写这篇文章也是希望把这个坑填上。2. LangGraph.js 的核心概念与简历 Agent 的状态流转设计先花点篇幅把 LangGraph.js 里最核心的几个概念讲清楚后面代码你才能看懂。它本质上是一个可编程的状态机几个核心概念对应关系如下State状态贯穿整个图执行过程的数据对象类似于一个全局 store所有节点共享、可读取、可修改。Node节点一个普通的异步函数输入是当前 State输出是 State 的更新片段。Edge边定义节点的执行顺序。Conditional Edge条件边根据 State 当前内容动态选择下一个节点。Graph图把所有节点和边组合起来的执行流程。2.1 简历 Agent 的状态定义简历工具的状态我设计成这样import { v4 as uuidv4 } from uuid; export interface ResumeState { // 原始输入数据 requestId: string; resumeText: string; rawText: string; // 解析中间产物 parsedResume: { name: string; contact: string; summary: string; skills: string[]; experience: Array{ company: string; position: string; duration: string; description: string; }; education: Array{ school: string; degree: string; major: string; duration: string; }; } | null; // 分析结果 analysisResult: { overallScore: number; dimensionScores: Recordstring, number; issues: Array{ type: critical | warning | suggestion; content: string; relatedSection: string; }; } | null; // 优化结果 optimizedResume: string; chatHistory: ArrayRecordstring, string; // 流程控制标志 needMoreInput: boolean; lastStep: string; }在 LangGraph.js 里定义 State 时每次节点返回的都是部分更新框架会自动合并进全局状态。上面这种写法里我没有引入 Reducer因为简历场景每个字段都是全量更新即可不需要数组累加之类的复杂逻辑。如果是做多轮对话工具那么chatHistory这种就需要定义updater否则新消息会把旧消息覆盖掉。2.2 图的节点设计与流转路径简历 Agent 的完整流程我分成 6 个节点parseResume接收原始文本提取结构化信息evaluateResume基于结构化信息打分、找问题checkNeedMoreInfo判断信息是否完整决定走哪条边askClarification信息不全时向用户提问optimizeResume针对问题生成优化稿formatOutput把结果整理成用户友好的 Markdown 格式图的定义用 LangGraph.js 的StateGraphimport { StateGraph, END } from langchain/langgraph; const workflow new StateGraphResumeState({ channels: { resumeText: { value: (a: string, b?: string) b ?? a }, parsedResume: { value: (a: any, b?: any) b ?? a }, // ... 其他通道 }, }) .addNode(parseResume, parseResume) .addNode(evaluateResume, evaluateResume) .addNode(checkNeedMoreInfo, checkNeedMoreInfo) .addNode(askClarification, askClarification) .addNode(optimizeResume, optimizeResume) .addNode(formatOutput, formatOutput) .addEdge(__START__, parseResume) .addEdge(parseResume, evaluateResume) .addEdge(evaluateResume, checkNeedMoreInfo) .addConditionalEdges(checkNeedMoreInfo, (state) { return state.needMoreInput ? askClarification : optimizeResume; }) .addEdge(askClarification, parseResume) .addEdge(optimizeResume, formatOutput) .addEdge(formatOutput, END); export const graph workflow.compile();这里有几个细节值得单独说明。为什么要加checkNeedMoreInfo节点而不是在evaluateResume里直接判断原因有两个一是让执行流程可视化你可以在日志里清楚看到评估之后进入了信息补充分支二是后续要加人工审批节点时只需要在中间插一个节点就够了不需要改动现有逻辑。askClarification回到parseResume会形成循环这也是 LangGraph.js 作为图框架的优势——循环执行不是 bug而是有状态支撑的迭代流程。用户补充信息之后整个分析重新跑一遍直到信息足够才往下走。2.3 每个节点的实现细节拿parseResume节点举例干的事情如下async function parseResume(state: ResumeState): PromisePartialResumeState { const { resumeText } state; // 调用大模型用结构化输出解析简历 const model getModel(); const parsed await model.invoke( [ [system, SYSTEM_PROMPT_RESUME_PARSER], [human, resumeText], ], { response_format: { type: json_object }, } ); return { parsedResume: JSON.parse(parsed.content), }; }在 Node.js 环境里调用大模型时我强烈建议在invoke参数里带上response_format: { type: json_object }。如果不带模型返回的内容里经常会多出以下是简历的结构化分析结果这类废话后面 JSON.parse 就会直接崩。这算是第一个小坑。然后是evaluateResume核心是让模型输出多维度的评分同时必须保证输出结构稳定这样才能跟前端展示层对接。async function evaluateResume(state: ResumeState): PromisePartialResumeState { const { parsedResume } state; if (!parsedResume) { return { needMoreInput: true }; } const model getModel(); const result await model.invoke( [ [system, SYSTEM_PROMPT_EVALUATOR], [human, JSON.stringify(parsedResume)], ], { response_format: { type: json_object }, } ); const parsedResult JSON.parse(result.content); return { analysisResult: parsedResult, needMoreInput: parsedResult.isInfoComplete false, }; }isInfoComplete这个字段很有用——很多简历没有工作起止时间、没有项目链接、没写技术栈的熟练度模型在评估前如果发现关键信息缺失可以把判断结果带回来触发澄清分支。2.4 关于模型选择的一个建议整条链路里解析、评估、优化三个节点可以用同一个模型也可以分开用不同模型。我的经验是解析用 JSON 输出稳定的模型评估可以用强推理模型优化阶段要选写作能力强的模型。如果你在做一个追求极致效果的 Agent这个拆分值得做如果只是做个 MVP统一用一个支持 function calling 和 JSON 输出的模型也能跑通。3. 简历 Agent 的核心工具与 Function Calling 设计Agent 的价值很大程度体现在工具使用上。如果只是模板化地生成内容用普通的大模型接口就够了但是要做能干活的 AI必须让模型能主动调用外部能力。ai agent 让 AI 真的下地干活说白了就是 Tools 设计要落地。3.1 简历工具注册了哪些工具我给 Agent 设计了 5 个工具覆盖简历处理的完整流程const tools [ new DynamicStructuredTool({ name: parse_resume_document, description: 解析简历文本提取姓名、联系方式、技能、工作经历、教育经历等结构化信息。当用户上传简历原文或粘贴简历文字时调用。, schema: zodSchema, func: async (input) { ... } }), new DynamicStructuredTool({ name: analyze_resume_gap, description: 对比目标职位要求与简历内容找出差距项。入参为职位描述jobDescription和当前简历文本。, schema: zodSchema, func: async (input) { ... } }), new DynamicStructuredTool({ name: rewrite_section, description: 重写简历中的某个指定板块比如工作经历、项目经历或自我评价。, schema: zodSchema, func: async (input) { ... } }), new DynamicStructuredTool({ name: generate_cover_letter, description: 根据简历和目标职位自动生成匹配的求职信。, schema: zodSchema, func: async (input) { ... } }), new DynamicStructuredTool({ name: export_markdown, description: 将优化后的简历导出为 Markdown 格式文本。, schema: zodSchema, func: async (input) { ... } }) ];每个工具都给模型提供了清晰的 description这直接决定了模型能不能在正确的场景调用正确的工具。3.2 工具入参的 zodSchema 定义zodSchema 不仅是类型校验也是给模型的工具说明书。我以analyze_resume_gap为例const analyzeResumeGapSchema zodSchema({ type: object, properties: { jobDescription: { type: string, description: 目标职位的 JDJob Description文本尽量完整。 }, resumeText: { type: string, description: 当前简历的完整文本。 }, targetRole: { type: string, description: 目标职位名称比如高级前端工程师、产品经理等。 } }, required: [jobDescription, resumeText] });有个很实用的经验description里一定要写明什么时候应该调用这个工具这比描述里写这是一个简历分析工具有效得多。大模型是通过 description 来推理工具使用场景的描述越像使用说明书工具被正确调用的概率越高。3.3 绑定工具到模型上LangGraph.js 社区版里绑定工具的方式是通过bindToolsimport { ChatOpenAI } from langchain/openai; const model new ChatOpenAI({ model: gpt-4o-mini, temperature: 0.2, }).bindTools(tools);bindTools会让模型可以感知到这些工具的存在在回答内容时自行判断是否调用工具以及调用哪个。这一点跟纯提示词工程有本质区别——模型是在决策而不是在猜。在 LangGraph.js 里工具的执行并非自动完成你需要一个executeTools节点来做实际的调用。推荐用ToolNode它是现成的工具执行节点import { ToolNode } from langchain/langgraph/prebuilt; const toolNode new ToolNode(tools);然后在图的build中把它加入import { END, StateGraph } from langchain/langgraph; const workflow new StateGraphResumeState({ channels: { ... } }); workflow.addNode(agent, agentNode); workflow.addNode(tools, toolNode); workflow.addEdge(__START__, agent); workflow.addConditionalEdges(agent, (state) { return state.lastStep shouldCallTool ? tools : END; }); workflow.addEdge(tools, agent);注意tools - agent这条回边它让模型在完成工具调用后能拿到结果再做下一步决策。这个思考 - 调用 - 观察结果 - 再思考的循环才是 Agent 区别于普通 API 封装的核心。3.4 遇到 tool calling 不生效时的排查思路我初学时被这个问题卡过很长时间。模型明明加了bindTools但它就是不调用工具。后来排查了一圈发现是模型的参数没对具体来说确保版本兼容langchain/openai和langchain/core要使用匹配版本版本一变工具调用的数据格式可能就变了。确保模型支持 function calling并不是所有模型默认就带这种能力。你用的是国产模型或开源模型需要确认是否支持工具类接口。使用defaultStoppingCriteria或stop参数时不要设置stop: [|endoftext|]这类影响模型输出的内容。最重要的是模型temperature不要太高0.1~0.3之间对工具调用的稳定性最好。3.5 Agent 处理简历的多轮对话机制简历工具不是一次问答就结束的用户可能会问帮我改成 SQL 方向再精简一下项目经历那块重写一下。我用 LangGraph.js 做了多轮对话管理做法是在 State 中加入chatHistory存历史对话Agent 节点每次执行时把当前用户消息和chatHistory一起传给模型为了让模型清楚何时调用工具我给它配置了一段 System Promptconst AGENT_SYSTEM_PROMPT 你是一个资深的简历顾问 AI Agent。你的任务是根据用户的简历文本进行深度分析和优化。 你可以使用以下工具 - parse_resume_document: 当用户提供简历文本或上传简历文件时调用结构化解析简历。 - analyze_resume_gap: 当用户提供目标职位 JD 或告知目标职位时调用对比分析差距。 - rewrite_section: 当用户要求修改简历的某个板块时调用。 - generate_cover_letter: 当用户要求生成求职信时调用。 - export_markdown: 当用户要求导出为 Markdown 时调用。 注意不要在没有明确目标前凭空生成内容不要在没有调用工具前就假装完成了分析。每次回复前先判断是否需要调用工具需要则先调用工具再回复。 ;有了这个 Prompt 和bindTools的搭配多轮对话中模型才能该出手时就出手。之前的血泪教训是没有把工具用途写清楚结果模型经常绕过工具直接生成内容跟普通的聊天机器人无异。4. 前端流式输出与 Route Handler 的实时交互实现Agent 的场景天生就是流式的用户在上传简历之后如果界面一直转圈 10 秒才出结果体验非常差。我把大模型流式输出一路打通到浏览器让用户能看到正在解析简历……正在分析技能缺口……正在重写优化……的完整过程。这也回应了很多人关心的ai agent 怎么扛并发的基础——先把一条链路的效率拉满再做横向扩展。4.1 Next.js Route Handler 与 LangGraph 的接入Next.js 的 Route Handler 写在app/api/agent/route.ts里接口入口长这样import { NextRequest, NextResponse } from next/server; import { graph } from /lib/langgraph/agent; import { HumanMessage } from langchain/core/messages; export const runtime nodejs; export async function POST(req: NextRequest) { const body await req.json(); const { resumeText, userInput, chatHistory } body; // 初始化 Agent 状态 const initialState { resumeText, requestId: crypto.randomUUID(), chatHistory, lastStep: start, }; // 直接编译执行图 const result await graph.invoke(initialState); return NextResponse.json({ result, }); }这是同步等待版实现简单但用户体验一般。生产级方案要改成流式返回。4.2 流式输出的实现细节要流式的核心在于 LangGraph.js 的stream方法以及 Next.js 的ReadableStream。export async function POST(req: NextRequest) { const body await req.json(); const { resumeText, userInput, chatHistory } body; const encoder new TextEncoder(); const stream new ReadableStream({ async start(controller) { const initialState { ... }; const eventStream await graph.stream(initialState, { streamMode: updates, recursionLimit: 25, }); for await (const event of eventStream) { // event 里带有 node 执行结果拼成 SSE 格式 const parsedEvent JSON.stringify({ type: node-update, data: event, }); controller.enqueue(encoder.encode(data: ${parsedEvent}\n\n)); } controller.enqueue(encoder.encode(data: [DONE]\n\n)); controller.close(); }, }); return new Response(stream, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }, }); }streamMode: updates这样的模式有这么几种我这里按实际使用场景做个整理values每次返回全部状态适合调试但数据量大。updates只返回发生变化的节点输出生产推荐。messages专门用于流式返回聊天消息LangGraph 0.2.20 支持。4.3 前端如何消费这个 SSE 流前端我用了原生的fetchReadableStream来做没有额外引 SSE 库减少依赖async function runAgent({ resumeText, userInput, chatHistory }) { const response await fetch(/api/agent, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ resumeText, userInput, chatHistory }), }); const reader response.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { value, done } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop() ?? ; for (const line of lines) { if (line.startsWith(data: )) { const payload line.slice(6).trim(); if (payload [DONE]) continue; const event JSON.parse(payload); updateUI(event); // 更新日志区、分步展示等 } } } }这里有一个实测中的关键点decoder.decode(value, { stream: true })必须带stream: true参数否则遇到多字节 Unicode 字符会被截断导致乱码。简历里大量出现中文、特殊符号这个细节不处理好前端很容易显示一堆问号。4.4 前端交互组件设计简历 Agent 的前端交互我分了三个区域界面不做复杂设计但信息架构必须清晰左侧输入区粘贴简历文本、上传 PDF/Word、后端用 pdf-parse 和 mammoth 抽取文本、填写目标职位 JD。中间过程展示区实时显示 Agent 当前在做什么正在解析简历结构化信息 - 正在对比 JD 技能要求 - 正在重写项目经历每步一个卡片用户可以直观看到思考过程。右侧结果区展示优化后的简历 Markdown支持一键复制、导出 .md 文件。那些一眼就是 AI 生成的套话在 UI 上还有一层设计讲究流式交互时推理过程和最终产物分开展示。把模型中间过程——比如调用了哪些工具、分析了哪些问题——跟最终产出物分开用户才不会被一大段文字刷屏也更信任结果的真实性。5. 并发场景下的状态隔离与性能优化ai agent 怎么扛并发这个热搜词背后本质是很多人在做 Agent 服务时发现直接把 Agent 跑起来很简单但用户一多就崩。我自己在生产环境中体会最深的是Agent 的并发瓶颈一般不在模型 API而在状态管理和资源占用上。5.1 用 requestId 隔离状态别把 Agent 实例做成全局单例很多人看着官方例子就把graph定义成一个全局变量谁来了都往同一个State上灌数据。这会出大问题用户 A 的简历状态可能被用户 B 的请求覆盖。解决方式很简单每次请求都创建一个独立的state而 LangGraph 的graph本身是无状态的状态是传入的所以可以安全共享const graph compileAgentGraph(); // 全局只编译一次 const initialState createInitialState(req.body); // 每个请求独立状态graph.invoke/graph.stream在接受输入时内部会创建一个新的状态副本因此不会串数据。真正要注意的是你不要在 Node 模块的顶层用一个可变变量去存当前用户输入。所有状态都必须随请求传入、随请求结束释放。5.2 Streaming 模式下的 CPU 与内存控制Agent 迭代过程中每次模型调用都在消耗 CPU/内存。在graph.stream的配置里我建议设置这样几个参数const config { recursionLimit: 10, // 限制循环次数防止死循环 streamMode: updates, // 减少每次传输的数据量 runConfig: { maxConcurrency: 3 // 如果有并行节点限制并发数 } };recursionLimit太重要了。LangGraph 默认递归上限是 25但如果你设计的 Agent 是多轮对话且有循环边模型可能陷入反复调用工具的循环里。我实际调优过简历 Agent 的最长链路是 8 步recursionLimit设为 16 就足够。再高的话一旦出错就会白白消耗 token 和时间。5.3 加一层 Redis 缓存来扛重复请求很多用户会在一次会话中反复上传同一份简历每次都全量调用大模型非常浪费。我用极简的 Redis 缓存来挡掉这类重复请求import { createClient } from redis; const redis createClient({ url: process.env.REDIS_URL }); export async function getCachedResult(key: string) { const value await redis.get(resume-agent:${key}); return value ? JSON.parse(value) : null; } export async function setCachedResult(key: string, data: any) { await redis.set(resume-agent:${key}, JSON.stringify(data), { EX: 60 * 60 * 6 }); }这里的缓存 key 我使用内容哈希 目标 JD 哈希 模型名基本相同文本和 JD 的请求可以直接复用分析类任务 6 小时过期足够大多数场景使用还能显著节省 API 开销。实测下来的效果很可观重复请求的响应时间从 10 秒级降到 1 秒内。5.4 Redis 缓存失效与安全如果用户的简历内容非常相似但微有不同哈希会变化所以不会误命中。同时注意不要把用户的简历明文存储在缓存里我是把文本喂给哈希函数后只保存哈希值避免敏感信息滞留。真要存原文你也应该考虑用加密后再入 Redis。6. 部署到 Vercel 时的坑与虚拟机部署方案Next.js 项目最常见的部署目标就是 Vercel但 AI Agent 场景有它的特殊性。我在这上面踩了不少坑这里完整记录一下。6.1 Vercel Serverless 函数的超时限制Vercel 的 Hobby 计划里Serverless 函数的最大执行时长是 10 秒Pro 是 60 秒、Enterprise 可以到 300 秒。我的简历 Agent 在调用多个模型节点后总耗时通常超过 15 秒Hobby 直接超时。如果你用的是免费版 Vercel长耗时 Agent 会直接被切断。你会看到几乎必然发生的现象本地跑得好好的部署上去就超时。解决思路有两个把 Vercel 升级到 Pro/Enterprise适合验证期。用异步任务模式比如将请求丢到队列中用 Webhook 或轮询获取结果避免同步等待。缺点是要额外做前端轮询逻辑。对于个人项目或内部工具我更推荐部署到虚拟机上比如一台 2 核 4G 的云主机用 Docker 跑 Next.js 的 standalone 模式。这样你有完整的 Node.js 进程不受 Serverless 时间限制。6.2 Docker 部署 Next.js standalone默认next build产物跑不起来是因为要依赖 node_modules但我用输出standalone模式来解决体积问题FROM node:20-alpine AS base FROM base AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci FROM base AS builder WORKDIR /app COPY --fromdeps /app/node_modules ./node_modules COPY . . RUN npm run build FROM base AS runner WORKDIR /app ENV NODE_ENVproduction COPY --frombuilder /app/public ./public COPY --frombuilder /app/.next/standalone ./ COPY --frombuilder /app/.next/static ./.next/static EXPOSE 3000 CMD [node, server.js]注意next.config.js里需要加module.exports { output: standalone, };这套部署方案有几个最直接的收益不再有冷启动问题、函数执行无超时限制、可以常驻内存做长连接而且日志直接看 Docker logs方便排查问题。我个人更推荐你在产品正式上线时直接用这种方案尤其是 AI Agent 这种生来就是长耗时交互的物种。6.3 环境变量管理模型 API Key、Redis 连接串、LangSmith 的 API Key 这些都要走环境变量建议不要在代码里写死。部署到云端时用云厂商的 Secrets 管理或者至少用.env.local做本地开发。还要注意 Next.js 的NEXT_PUBLIC_前缀变量会暴露给浏览器任何模型 API Key 都不要带这个前缀。7. 简历 Agent 落地中绕不开的失败与调优经验最后这部分算是全文的精华把我在完整落地过程中踩过的坑、绕过的弯、调过的参数一次性列清楚。这些偏方很少出现在官方文档里但基本都是生产环境才会暴露的问题。7.1 不要迷信大模型一次生成完美简历普通 API 调用中长期存在只输出固定措辞的问题简历 Agent 则要避免模板化因为所有用户都拿同一版式这个产品就废了。我的做法是优化节点里把重写拆成多次迭代const iterativeRewritePrompt 你是一个资深简历优化专家。请基于以下分析结果逐步优化简历 第一轮工作经历描述确保每条描述以动词开头并包含量化结果。 第二轮技能匹配度针对目标 JD 中的关键技能调整技能列表和技能描述位置。 第三轮整体风格与排版结构控制在一页到两页避免无关内容。 现在开始第一轮优化 ;分轮次生成的优化结果在观感和质感上远比一次成型自然得多模型输出质量也有明显提升。7.2 在 LangGraph 中使用人机协同Human-in-the-loop高级别简历场景经常需要用户确认。LangGraph.js 的interrupt功能可以暂停图执行等用户操作后再继续。我在导出简历节点前加了确认环节import { interrupt } from langchain/langgraph; export async function waitForUserConfirm(state: ResumeState) { const confirmed await interrupt(是否确认导出该简历); if (!confirmed) { return { lastStep: user_cancelled }; } return { lastStep: user_confirmed }; }这种交互机制在 Agent 落地中很实用能避免模型在条件不确定时强行生成错误内容。7.3 可观测性用 LangSmith 追踪每一次链路执行Agent 链路一旦复杂调试就会变成噩梦。我最推荐的方案是接入 LangSmith在 LangGraph.js 初始化时设置环境变量LANGCHAIN_TRACING_V2true LANGCHAIN_API_KEYxxx LANGCHAIN_PROJECTresume-agent之后每一次 graph 执行你都可以在 LangSmith 后台看到完整的节点调用顺序、token 消耗、每个节点的耗时、工具调用的输入输出。这个在排查模型为什么没调用工具为什么走到不该走的分支时简直是神器。7.4 别忽视 token 成本简历 Agent 一次完整执行解析 评估 优化大约消耗 6000~10000 token。如果用户反复上传简历成本很快就上去。我的经验是这样控成本解析和评估用便宜的模型比如 gpt-4o-mini 或 qwen-turbo。优化阶段才用更强的模型。加缓存缩短重复请求。设置单用户调用频率限制比如一分钟最多 3 次 Agent 调用。7.5 针对ai agent token 是什么意思这类新手的速通解释最新热词里有很多是新手扫盲类的问题这里顺手给个极简版解释token 是模型处理文本的最小单位大约 1 个汉字 ≈ 1~2 个 token1 个英文单词 ≈ 1 个 token。Agent 的 token 消耗包含输入、输出、工具调用产生的上下文。因为 LangGraph 会在每次执行时把当前状态传给模型所以状态越大token 越多。控制 token 的核心就是控制状态的精简程度不要把一堆历史记录全部塞进每次调用。这个是ai agent token 是什么意思的完整回答能解决很多人对成本炸裂的焦虑——先算清楚自己的 Agent 每次执行需要的 token再配置上下文裁剪策略就不会出现月底账单吓一跳的情况。7.6 关于基于 Rust 语言 AI Agent的一点参考很多人在搜索热词里关注 Rust 做 Agent说明性能敏感型场景越来越受注意。如果你想做高并发、吞吐量大的 Agent 服务Rust 确实有性能优势但 Next.js LangGraph.js 的组合更适合产品迭代速度快的 Web 应用开发效率高、生态成熟。两者不是取代关系而是场景分工。简历 Agent 这种重 I/O、重 LLM 调用、重交互的产品形态选择 TypeScript 全栈仍是综合效率更高的方案。8. 简历工具 AI Agent 的完整代码目录结构为了方便你复现我贴一下当前项目的目录结构这份结构经过多个项目验证足够清晰也方便后续加新功能时快速定位代码位置。resume-agent/ ├── app/ │ ├── api/ │ │ ├── agent/ │ │ │ └── route.ts # Agent 流式接口 │ │ └── upload/ │ │ └── route.ts # 简历文件上传与文本抽取 │ ├── page.tsx # 简历工具主页 │ └── layout.tsx ├── components/ │ ├── ResumeInput.tsx # 简历输入区域 │ ├── AgentProcess.tsx # Agent 过程展示区域 │ └── ResumeOutput.tsx # 优化结果展示区域 ├── lib/ │ ├── langgraph/ │ │ ├── agent.ts # 图定义与编译 │ │ ├── nodes.ts # 各节点实现 │ │ ├── tools.ts # 工具定义 │ │ └── state.ts # 状态类型与 Reducer │ ├── models/ │ │ └── index.ts # 模型实例工厂 │ ├── cache/ │ │ └── redis.ts # Redis 缓存 │ └── utils/ │ ├── pdf.ts # PDF 文本抽取封装 │ └── file.ts # 文件处理工具 ├── next.config.js ├── package.json └── .env.local按照这个结构你拿到代码之后不需要再猜文件应该放哪里直接一条指令npm run dev就能跑起来。如果你需要完整仓库的克隆地址可以在评论区留言我可以基于我的生产版本整理一份可直接运行的模板工程。写在最后的个人体会这套简历 Agent 从最初的一坨脚本到现在的完整产品最大的感受是Agent 的难度不在技术选型而在状态设计和流程编排。你一旦把状态定义清楚了、图结构设计合理了剩下的实现就是水到渠成的事。Next.js LangGraph.js 组合最大的价值不是代码有多酷而是把用户输入 - 状态流转 - 工具调用 - 流式返回这条链路变成了可维护、可观测、可扩展的工程结构。如果你正在设计自己的 AI Agent我的建议是从一个垂直场景切入先把一条链路跑通不要一上来就做通用型 Agent。简历工具就是个很好的起点它领域明确、工具边界清晰、用户痛点强烈非常适合练手和落地。后面如果你想把它扩展成AI 求职助手加上职位搜索、面试模拟、内推匹配其实都是在现在的图上加节点和工具的事情架构完全不用推翻。