ARTICLE DETAIL

资讯详情

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

Next.js+LangGraph.js构建简历优化AI Agent

Next.js+LangGraph.js构建简历优化AI Agent 最近这段时间一直在折腾 AI Agent 的实际落地前前后后试了好几个技术栈组合。这套Next.js LangGraph.js 简历工具 AI Agent是我个人完整跑通、并且觉得最值得拿出来分享的一套方案。它解决的不是做个聊天机器人这种玩具问题而是把一份简历从上传、解析、评估到给出可执行修改建议的完整流程真正交给了 Agent 来驱动。先说说为什么选这个方向。简历优化这个场景看起来简单其实特别适合 Agent 来干输入是非结构化的文本中间涉及多轮推理哪里薄弱、为什么薄弱、怎么改输出又需要结构化的评估和建议。单纯用提示词模板硬套效果很不稳定用普通后端流程写死又没法处理各种意外情况。而用 LangGraph.js 把整个流程编排成有状态、可中断、可恢复的图配合 Next.js 做交互层体验和可维护性都能兼顾。这篇文章我会从需求拆解、架构选型、状态图设计、核心实现、前端联调、再到实际踩坑记录把整套落地过程完整讲一遍。代码和方案都是实际可复现的适合已经熟悉 React/Next.js、想往 Agent 方向深入或者正打算做类似文档处理类 Agent的朋友参考。1. 简历工具为什么值得做成 Agent普通表单和提示词模板的边界在哪里在动手写代码之前我花了不少时间想一个问题简历优化这个需求到底需不需要用 Agent用传统方案行不行这个想清楚了后面才不会把架构做复杂。传统方案无非两种。第一种是写一个表单页面用户粘贴简历文本后端用一个写死的 prompt 调一次大模型返回一篇修改建议。这种方案的问题在于简历内容差异极大——有人发来的是一段纯文本有人是 PDF 里扒出来的乱格式有人只有三五行工作经历有人写了密密麻麻的十几页。一个固定 prompt 很难同时处理这些情况结果是给资深工程师的建议和对应届生的建议几乎一样。第二种是用多轮对话的方式做每次用户上传简历之后让大模型你问我答地了解情况。但这又引入了另一个问题普通对话没有强制结构用户不问模型就不会主动深入分析最后产出的东西深度完全不可控。Agent 的介入改变了这个局面。它的核心不是多轮对话而是把一个复杂的任务拆成多个可以由模型自主决策的步骤每一步都有明确的输入、输出和检查点。落到简历这个场景里就是第一步把用户上传的原始文本清洗成结构化数据基本信息、教育经历、工作经历、技能清单第二步基于岗位方向对结构化简历做逐项评估第三步根据评估结果生成针对性的修改建议第四步把这些内容组装成一份用户能直接看、直接用的报告。这个流程里每一步的结果都可能影响下一步的执行方式。比如第一步发现用户简历里完全没写技能清单第二步评估的时候就不能判技能缺失然后给一句建议补充技能就完事而应该结合用户的目标岗位方向主动推荐该岗位的核心技能关键词。这种根据中间结果动态调整后续动作的能力就是 Agent 相对普通表单流程最大的优势。还有一个重要的点是用户要的不是一篇泛泛的建议而是一份能照着改的清单。所以输出格式必须严格结构化。传统提示词模板虽然也能要求输出 JSON但一旦某个环节需要重新处理整个流程就要从头跑一遍。用了 Agent 编排之后我可以随时把流程暂停在某个节点上让用户补充信息再从这个节点恢复执行成本和体验完全不一样。2. 技术选型与总体架构为什么是 Next.js 和 LangGraph.js它们各自扛什么职责选 Next.js 做前端和 BFF后端 for 前端层理由很直接生态成熟、可以前后端一体部署而且 API Route 处理流式响应非常顺手。LangGraph.js 则是整个 Agent 的运行时核心它提供了一套将 Agent 状态机化的框架。先看一个很多人会混淆的问题既然 LangChain.js 也有 Agent 概念为什么我还要上 LangGraph.js我最初也是从 LangChain.js 的create_agent这类高层 API 开始试的但很快就碰到了三个问题流程控制不够精细。LangChain.js 的高层 Agent API 更适合对话型交互对于解析-评估-生成建议这种有固定流水线但中间又需要动态分支的场景它给不了清晰的 DAG有向无环图控制状态管理是隐式的。我需要把每一步的中间结果结构化简历、评分明细、用户补充信息持久化下来方便出错重试和断点续跑LangChain.js 的默认实现里这块不够透明人工介入不方便。简历这个场景里用户经常需要在流程进行中修改信息比如纠正公司名称、补充一个项目经历这意味着 Agent 要能暂停等待用户输入而不是一条路跑到黑。LangGraph.js 的interrupt机制正好干这个。LangGraph.js 给的核心抽象就两个StateGraph和State。State是一个全局共享的数据结构每个节点执行完后可以更新它StateGraph定义节点之间的连接关系和条件分支。你可以把它理解成一个带记忆的流水线产品沿着传送带前进每个工位节点读取产品当前状态、做加工、把加工结果写回产品然后根据结果决定下一站去哪个工位。整体架构上我把它分成了三层层级技术组件职责交互层Next.js App Router React简历上传、流式展示评估过程、用户补充信息的弹窗交互编排层LangGraph.js运行在 API Route 内状态图构建、节点执行、中断/恢复控制模型层大模型 API兼容 OpenAI 协议 结构化输出文本解析、评分推理、建议生成这里强调一点LangGraph.js 本身是大模型无关的你接哪家模型都行。我这里用的是兼容 OpenAI 协议的接口因为这套协议是目前兼容性最好的。实际开发中你的大模型 API 只需要两个能力支持函数调用或 JSON mode支持流式输出。前者用于让模型的输出可以被程序安全解析后者用于把中间过程实时推送给前端。3. LangGraph.js 状态图设计简历 Agent 的核心流程图与数据模型这个部分是整个项目的灵魂。很多人用 LangGraph.js 失败不是不会写代码而是把状态图设计得太复杂或者太简单。太复杂导致链路调试困难太简单又把 Agent 退化成了普通流水线。我的设计思路是把一个简历评估拆成五个节点每个节点只干一件大事节点之间用清晰的 State 字段传递信息。3.1 状态数据模型先定义ResumeState。这个状态是整个图执行的公共黑板所有节点都读写它type ResumeState { // 用户输入的原始内容 rawText: string; // 用户指定的目标岗位方向如 前端工程师 targetRole: string; // 也可以让用户粘贴岗位 JD作为评估依据 targetJD?: string; // 节点输出区 parsedResume: ParsedResume | null; // 节点1 输出 gapAnalysis: GapAnalysis | null; // 节点2 输出 scores: ScoreReport | null; // 节点3 输出 suggestions: SuggestionItem[] | null; // 节点4 输出 // 人工介入标记 userConfirmations: Recordstring, unknown; // 中断恢复时用的恢复数据 pendingUserInput?: { type: confirm_company_name | select_role | supply_missing_skills; context: unknown; }; };每个节点写完自己的结果就更新对应字段。后面的节点只需要读前面的字段不需要关心它们是怎么生成的。这样设计的好处是图执行到一半挂了我可以直接把整个ResumeState序列化保存下来下次从断点恢复。3.2 五个节点的职责划分我用的是五个节点按顺序分别是解析节点parse_resume把用户上传的原始文本转成结构化对象。关键设计是输出必须符合严格的 JSON Schema字段包括personalInfo、education、workExperience[]、projects[]、skills[]等。如果文本里信息缺失字段用空数组或 null 表示不要凭空编造。岗位对齐节点align_role这一步很多人会忽略但恰恰是简历评估质量的分水岭。它把目标岗位方向或者岗位 JD 转成一份该岗位能力需求清单比如前端岗需要React 生态、工程化、性能优化、跨端能力等。这份清单是后续评分和建议的标尺。差距分析节点analyze_gap把结构化简历和岗位需求清单做比对逐条判断匹配度输出GapAnalysis。内容包含每一项的匹配状态match匹配、partial部分匹配、missing缺失、irrelevant无关。评分节点score_resume基于差距分析结果从技术深度、项目经验、技能匹配度、表达清晰度四个维度打分每个维度附一段 80 字以内的解释。分数不是凭空生成的而是由差距分析里的具体条目推导出来的这样用户追问分数原因时我们给得出解释。生成建议节点generate_suggestions根据评分和差距分析输出优先级从高到低的修改建议。建议必须具体到把你第 2 段的第一个项目描述改成这样XXX而不是泛泛说建议加强项目描述。3.3 边的设计与条件路由连接这些节点的边也不是简单的一条直线。我加了两处条件路由解析节点 - 岗位对齐节点如果parsedResume中关键字段全为空比如用户只贴了三行字则直接路由到一个人工介入节点提示用户补充基本信息而不是继续往下走。这个判断在真实场景里救了很多次——真有用户就贴一句我是前端工程师5年经验就想让 AI 给出评估。评分节点 - 建议生成节点如果评分结果里有任一维度低于阈值比如低于 60 分就额外调用一次模型对低分维度做深度分析生成专项改进建议挂载到对应的suggestionItem里。这样避免所有建议都堆在一个大 JSON 里内容层次更清晰。下面用一个表格总结路由逻辑方便你对照设计起点终点路由依据parse_resumealign_roleparsedResume非空parse_resumehuman_interruptparsedResume关键字段为空align_roleanalyze_gap岗位需求清单生成成功analyze_gapscore_resumegapAnalysis条目数 0score_resumegenerate_suggestions无条件score_resumegenerate_suggestionsscores存在低分维度时同时追加专项深挖节点这里有个设计取舍我没有把人类介入当做一个独立的 State 节点而是通过interrupt()函数在任意节点内暂停执行等外部程序补充状态后继续。这个后面在实现部分详细说。4. 核心节点实现细节结构化输出、评分规则与流式推送的代码落地状态图设计好之后真正写代码实现节点反而是最顺手的事。这一节我把几个关键节点的实现要点拆开讲每个都有可复制的代码片段。4.1 解析节点用 JSON Schema 约束输出别信自由文本解析节点的核心是让大模型输出严格的、符合预期结构的 JSON。我推荐用StructuredOutputParser或者直接使用大模型的response_format: {type: json_object}。不过我更推荐在 LangGraph 里用输出校验函数做二次保险模型返回 JSON 后程序先校验必填字段如果不合法就让模型基于错误信息重新生成一次。const parseResumeNode: NodeFunctionResumeState async (state) { const { rawText, targetRole } state; const prompt 你是一个简历解析器。请从以下原始文本中提取结构化信息。 原始文本 --- ${rawText} --- 要求 1. 只提取文本中真实存在的信息不要编造。 2. 工作经历里的时间统一成 YYYY.MM - YYYY.MM 格式。 3. 如果某项信息缺失使用空数组或 null。 4. 严格按照下面的 JSON 输出不要输出其他内容。 { personalInfo: { name: string, phone: string, email: string }, education: [{ school: string, degree: string, major: string, duration: string }], workExperience: [{ company: string, title: string, duration: string, highlights: string[] }], projects: [{ name: string, description: string, techStack: string[], highlights: string[] }], skills: string[] } ; const parser new StructuredOutputParser(JSONSchema); let result; try { result await llm.invoke(prompt, { response_format: { type: json_object }, }); result parser.parse(result); } catch (e) { // 解析失败带着错误信息重试一次 result await repairJsonWithError(prompt, e.message); } return { parsedResume: result }; };这里repairJsonWithError是一个很实用的工具函数把模型上次的原始输出和校验报错一起丢回给模型让它修正后重新输出。这个机制比单纯重试好用得多因为模型往往只是小毛病多了一个逗号、字段名拼错看到具体错误提示之后很容易改对。4.2 评分节点四条评分维度如何从差距分析推导出来评分节点如果直接用模型打分很容易出现分数忽高忽低的情况。我的做法是先让模型对每个维度做证据枚举再基于证据给分数。打个比方不是让考官直接给面试表现 80 分而是让他先说面试者项目经历深挖时条理清晰、能讲清难点但系统设计环节没答上来然后据此打分。const scoreResumeNode: NodeFunctionResumeState async (state) { const { parsedResume, gapAnalysis } state; const prompt 根据如下差距分析结果为简历打分。 差距分析 ${JSON.stringify(gapAnalysis, null, 2)} 我只需要四个维度的分数每个维度的评分说明必须先列出2-3条具体证据再给出分数。 维度定义 - techDepth技术深度项目是否涉及复杂业务、性能优化、架构设计 - projectMatch项目匹配项目经验与目标岗位的相关度 - skillHit技能匹配技能清单与岗位需求清单的覆盖率 - expression表达清晰描述是否量化、是否突出个人贡献 输出格式 { techDepth: { score: 0-100, evidence: string[], comment: string }, projectMatch: { score: 0-100, evidence: string[], comment: string }, skillHit: { score: 0-100, evidence: string[], comment: string }, expression: { score: 0-100, evidence: string[], comment: string } } ; const result await llm.invoke(prompt, { response_format: { type: json_object }, }); return { scores: result }; };注意我把evidence和comment放在同一个 JSON 对象里。前端展示时证据可以折叠展示评论直接显示在分数旁边。这样用户看到分数时不觉得是黑箱打分而是AI 给出了理由我认可或反驳它再决定信不信。这里有个很重要的经验不要让模型直接对原始简历打分一定要先经过差距分析这一步。差距分析本质上是把复杂的评分任务拆成了一个个小的、可验证的判断这个人的技能清单里有没有 React——有。这些判断合在一起分数推导就变得稳定且可信。如果你让模型直接跳到最后一步评分理由往往写得空泛而且分数和具体建议会对不上。4.3 流式推送把每个节点的执行状态实时送到前端LangGraph.js 对流式的支持做得不错但我用的是更朴素的方法在每个节点内部通过回调把当前状态推给前端。这样我可以控制推送的粒度和格式不让前端感知到 LangGraph 的存在。const nodeWithStream (nodeFn, stageLabel) async (state) { const start Date.now(); const eventId crypto.randomUUID(); // 通知前端当前阶段开始 await pushToSSE(eventId, { type: stage_start, stage: stageLabel, state: extractPublicState(state), }); try { const updates await nodeFn(state); await pushToSSE(eventId, { type: stage_complete, stage: stageLabel, state: extractPublicState(updates), durationMs: Date.now() - start, }); return updates; } catch (e) { await pushToSSE(eventId, { type: stage_error, stage: stageLabel, error: e.message, }); throw e; } };extractPublicState是为了避免把完整的、可能包含敏感信息的 state 推给前端只挑几个必要字段推送。前端收到stage_start就显示正在解析简历...收到stage_complete就更新对应板块的内容。对于大模型生成建议这种耗时长的阶段我还会在节点内部启用 token 级别的流式输出把生成中的建议文本逐段推送到前端。这个实现看起来像这样const generateSuggestionsNode async (state) { const stream await llm.stream(prompt); let accumulated ; for await (const chunk of stream) { accumulated chunk.content; await pushToSSE(streamId, { type: suggestion_delta, stage: generate_suggestions, delta: chunk.content, }); } return { suggestions: parseAccumulated(accumulated) }; };这里和stage_complete事件有一个轻微的设计冲突需要理清suggestion_delta是给用户看的正在生成效果而stage_complete是给前端逻辑用的可以正式更新 UI 数据了。两者不冲突前端分别监听即可。5. Next.js 侧接入API Route、流式响应和人工中断恢复的实战处理App Router 的 API Route 是 LangGraph.js 运行的地方。我用route.ts接收前端请求创建图的执行实例然后通过Response返回 SSE 流。5.1 创建运行时和图实例// app/api/analyze-resume/route.ts import { NextRequest } from next/server; import { runResumeAgentGraph } from /lib/agent/graph; export async function POST(request: NextRequest) { const body await request.json(); const { rawText, targetRole, targetJD } body; // 校验输入 if (!rawText || rawText.length 10) { return new Response( JSON.stringify({ error: 简历文本太短请补充内容 }), { status: 400 } ); } const encoder new TextEncoder(); // 使用 SSE 流式返回 const stream new ReadableStream({ async start(controller) { // 收到前端断开信号时中止 Agent 执行 const abortController new AbortController(); request.signal.addEventListener(abort, () { abortController.abort(); }); const sendEvent (event: unknown) { controller.enqueue( encoder.encode(data: ${JSON.stringify(event)}\n\n) ); }; try { await runResumeAgentGraph({ rawText, targetRole, targetJD, onEvent: sendEvent, signal: abortController.signal, }); } catch (e) { sendEvent({ type: fatal_error, error: (e as Error).message }); } finally { controller.close(); } }, }); return new Response(stream, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, no-transform, Connection: keep-alive, }, }); }runResumeAgentGraph内部做的就是初始化ResumeState构建StateGraph编译然后调用graph.invoke(initialState, { onEvent })。把onEvent回调透传进图内节点就实现了前面说的节点内部推送。5.2 中断恢复怎么让 Agent停下来等用户再继续跑这是整套方案里最有 Agent 感的一个设计。用户上传的简历中经常有信息缺失的情况比如没有目标岗位或者公司名称被 OCR 识别得乱七八糟。此时如果流程继续往下跑最后生成的报告质量一定很差。传统的做法是报错让用户重来。用 LangGraph.js 的interrupt()流程会在特定位置暂停等待外部数据补给后从暂停处继续。我封装了一个工具函数import { interrupt } from langchain/langgraph; export async function askUser( state: ResumeState, question: { id: string; type: input | select | confirm; question: string; options?: string[]; } ): PromiseRecordstring, unknown { // 将 pendingUserInput 写入 state前端能感知到 state.pendingUserInput { type: question.id, context: question, }; const userResponse await interrupt({ question, }); // 恢复后清空 pending 标记 state.pendingUserInput undefined; return userResponse; }在前端当收到stage_complete事件中带有pendingUserInput字段时我会渲染一个中断弹窗展示给用户的是类似这样的交互// 前端伪代码处理中断弹窗 function InterruptModal({ pending, onResolve }) { if (pending.type select_role) { // 渲染岗位方向选择器 return RoleSelector options{pending.options} onConfirm{onResolve} /; } if (pending.type confirm_company_name) { // 渲染文本确认框 return ( TextConfirm original{pending.context.original} suggested{pending.context.suggested} onConfirm{(editable) onResolve({ confirmedName: editable })} / ); } }用户确认之后前端把结果通过一个POST /api/agent/resume的resume请求发回后端。后端拿到回复之后调用graph.invoke带上Command(resume userResponse)恢复执行。LangGraph.js 会从上次interrupt的位置继续往下跑已经完成过的节点不会重复执行。这里有个细节中断恢复必须用一个新的图实例但状态要从上次执行完的ResumeState里加载。所以我在数据库或记忆中保存了上次的执行状态快照。最简单的方式就是把ResumeState直接存在服务端 session 里恢复时取出来塞回图里。如果你是部署在 Serverless 环境可以考虑存 Rediskey 用本次任务的runId。5.3 前端的事件协议设计为了让前后端配合不出岔子我设计了下面这套 SSE 事件协议。每个事件都带type前端根据type分派处理事件 type触发时机前端动作stage_start节点刚开始显示阶段状态栏正在解析...、正在评估...stage_complete节点正常结束更新对应板块的正式数据stage_error节点抛错显示错误提示提供重试按钮suggestion_delta建议文本流式生成中实时追加显示生成中的建议interrupt等待用户输入弹窗展示待确认信息run_complete全流程图执行完展示最终报告隐藏状态栏run_idle图停了但没有走完提示用户手动继续或放弃前端的整体状态机很简单running - waiting_for_input - resume_running - done。收到interrupt时进入waiting_for_input此时停止展示状态栏的加载中动画改成弹窗交互。用户提交后置回resume_running并把弹窗关闭恢复状态栏。6. 完整跑通之后的踩坑记录LangGraph.js 版本差异、中断恢复丢失与流式乱序代码写完之后真正让我掉头发的不是架构设计而是各种环境、版本和细节问题。这一节分享三个我觉得最有代表性的坑以及完整的排查思路希望能帮你少走点弯路。6.1 坑一LangGraph.js 不同小版本的事件序列差异项目初期我用的langchain/langgraph版本比较旧graph.invoke结束后会默认吐出一个完整的事件数组。后来升级版本发现 invoke 的返回结构变了很多事件的type从on_llm_end变成了新的格式。最直观的现象是前端突然收不到stage_start事件了因为我在回调里判断了event.event on_node_start而新版本改成了event.name node_start之类的字段。排查路径是这样走的先在前端看网络面板发现 SSE 流是通的有数据返回说明问题出在后端事件翻译层在后端打印event结构对比新旧版本的类型定义发现旧代码里的事件名判断完全失效需要改成读取统一的event.meta字段由于我们的事件协议是自己定义的stage_start等所以最终修复方案是把 LangGraph 的原生事件彻底隐藏不依赖它的具体格式只用我们自己的节点包裹函数发事件。这个坑给我最大的教训是不要让业务代码直接依赖 LangGraph 原生事件的字段名因为小版本升级就会破坏它们。正确做法是在节点内部显式发送自定义事件原生事件只用来做执行流兜底。6.2 坑二中断恢复后状态丢失了一半我用interrupt()做的用户确认功能在上线初期碰到了一个问题用户确认公司名称之后继续跑后面的节点都正常但最终报告里展示的解析结果居然还是 OCR 纠正之前的旧数据。一开始很迷惑用户明明确认了新公司名我也把Command(resume { confirmedName: 某某科技有限公司 })传进去了。后来仔细排查才发现问题出在我的节点更新方式上。我写的恢复逻辑是const updatedState await graph.invoke(initialState, { command: new Command(resume { ... }), // 只更新了部分字段 });但 LangGraph.js 的Command恢复更新的是当前节点的返回字段它是在当前状态快照的基础上合并的。而我后面节点读parsedResume时用的还是旧状态里的对象引用并没有把用户确认的新值回写到parsedResume.company。修复方案是在askUser恢复之后显式地修改状态const userResume await askUser(state, question); if (question.type confirm_company) { const target state.parsedResume.workExperience.find( (item) item.company question.context.original ); if (target) { target.company userResume.confirmedName; } } return state;也就是说中断之后的恢复逻辑不能只把用户输入塞进 Command 就完事还要根据业务规则决定这份输入到底更新状态里的哪个位置。这个业务判断是模型做不了的必须开发者自己写清楚。6.3 坑三多路流式输出导致前端 UI 顺序错乱在建议生成阶段我用了流式 token 推送同时在评分节点也有一个正在评分中的动画提示。结果前端偶尔会出现评分动画还在转建议已经出来了这种怪现象。排查之后发现是并发问题评分节点和后续的建议节点在某种情况下会并行触发推送而 SSE 是单通道的前端收到的顺序就是交错的了。这里我做了三个层面的修复在节点内部明确把业务完成事件和流式 token 事件分成两个通道前者用stage_complete后者统一用delta前端严格按照type分派不再混用一套事件名前端对每个板块的 UI 状态只管是否收到stage_complete收到之前统一显示分析中不再单独根据 token 流做动画后端给每个事件打上runId和stageId标记前端收到事件后先校验这两个 id 是否匹配当前任务不匹配直接丢弃。这些修改之后前端再也没出现过板块错乱的情况。我给的建议是不要低估前端事件消费的复杂度事件协议宁可多设计一些字段也不要事后打补丁。6.4 预算与耗时控制简历报告的 token 消耗实测最后说一个很多人忽略的工程问题token 成本。一个简历从解析到生成最终建议如果全程不节制很容易一次消耗 3-5 万 token接口耗时 30 秒以上。我对节点做了下面这些优化解析节点不要求模型输出格式化好的 markdown只输出紧凑 JSON省掉大量空白 token评分节点的输入是gapAnalysis而不是原始简历这样评分模型只需要看一份精简的中间结果而不是啰嗦的原文建议节点的 prompt 限定输出总长度比如每条建议不超过 120 字而且要求直接给结论不要铺垫全局给大模型请求加max_tokens解析节点和建议节点分开设置避免某个节点失控。优化后一次完整评估的 token 消耗压在了 1.2 万到 1.8 万之间耗时控制在 15 到 25 秒用户等待的体感也还行。如果你用的是按量计费的商业模型这个成本控制是必须考虑的不然功能做出来也没人用得起。7. 内容扩展方向从简历 Agent 到通用文档分析 Agent 的迁移思路简历工具做完之后我发现这套解析-对齐-差距分析-评分-建议的图结构可以非常自然地迁移到其他文档处理场景。比如招聘 JD 与候选人履历匹配把parsedResume换成 JD 和候选人简历双端解析gapAnalysis变成双向匹配报告合同审查解析节点提取合同关键条款align_role节点换成业务需求规则库差距分析变成条款风险清单学习计划生成把用户输入换成当前技术栈和目标岗位差距分析变成知识点补全计划。核心的图结构不需要大改主要调整的是三个节点解析节点的 JSON Schema、对齐节点的参照物、建议节点的输出模板。这种换数据不动架构的迁移方式让我觉得当初花时间优化状态图和事件协议是值得的。如果你准备基于这套方案做自己的 Agent 工具我个人建议先不要急着堆功能而是把一条主流程做到 90 分的稳定度再把中断恢复、流式展示、低成本运行这些体感细节补上。Agent 落地最难的不是模型能力而是工程边界——你要非常清楚地知道每一步该做什么、什么情况下停下来问用户、什么情况下报错重试。把这个边界控制住了后面的一切都顺。最后再分享一个实际操作中的小技巧在图的入口处一定要做输入长度校验并且在每个节点里加一个简单的输入状态是否合法的判断函数。这个函数不需要太复杂只要能在数据明显异常时抛出一个带具体原因的异常就能避免很多因为用户粘贴格式千奇百怪而导致的流水线崩溃。简历工具这种面向真实用户的 Agent用户不会按照你预设的格式输入把容错做好比把模型调对还重要。
返回列表