ARTICLE DETAIL

资讯详情

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

基于Seed-2.1-pro-0915与Next.js SSE的求职薪资雷达实战

基于Seed-2.1-pro-0915与Next.js SSE的求职薪资雷达实战 1. 这个求职雷达到底解决了什么问题求职这件事最让人抓狂的从来不是“投简历”本身而是信息筛选的效率。我身边不少朋友包括我自己都经历过这样的循环打开招聘平台输入关键词翻十几页发现一半岗位是重复的、三分之一是猎头挂的假岗、剩下几个看起来靠谱的点进去发现薪资范围写得像谜语——“8k-20k”你永远不知道自己值 8 还是 20。更麻烦的是岗位信息是动态的。今天看到的岗位明天可能就关闭了今天标 15k 的下周可能变成 12k。你手动记录、手动对比等你整理完市场已经变了。所以我用Seed-2.1-pro-0915搭了一个专用求职雷达。它的核心逻辑很简单定时抓取目标岗位、结构化解析薪资与要求、生成可验证的报告。12 分钟出一份报告每一条薪资数据都能点回原始岗位页面核对。不是那种“AI 帮你编一个薪资范围”的玩具而是每一条数据都有来源、有链接、可追溯。这个方案适合谁如果你是正在找工作、想快速摸清某个岗位在市场里的真实薪资分布的人这套东西可以直接抄作业。如果你是想学Agent 开发、SSE 流式推送、Next.js 全栈的开发者这套架构也是一个完整的实战案例。下面我把整个设计思路、核心实现、踩过的坑全部拆开讲。2. 整体架构设计与技术选型思路2.1 为什么选 Seed-2.1-pro-0915 做核心解析引擎市面上能做大模型解析的方案很多我选Seed-2.1-pro-0915的原因有三个都是实际用下来的体会。第一是结构化输出稳定。求职岗位的原始文本非常脏——有 HTML 标签、有平台自己的排版符号、有“急招”“高薪”这种干扰词。我需要模型把“岗位名称、公司、薪资下限、薪资上限、经验要求、学历要求、技能标签”这些字段干净地抽出来。Seed-2.1-pro-0915 在 JSON 模式下的字段遵循度很高不会出现那种“让它输出 JSON它给你输出一段解释文字”的情况。第二是长上下文处理能力。一个岗位详情页加上公司信息动辄三四千字。如果要做批量对比一次要喂进去十几个岗位的文本。上下文不够的模型会截断截断就意味着丢数据。Seed-2.1-pro-0915 在这块的表现让我比较放心批量解析时没有出现明显的“后半段失忆”。第三是成本可控。求职雷达是要定时跑的一天跑几次每次解析几十个岗位。如果单次调用成本太高这个项目就没法长期跑。Seed-2.1-pro-0915 在解析质量与调用成本之间的平衡点是我实测下来比较合适的。提示模型选型没有绝对的最优解。如果你的岗位文本以英文为主或者需要更强的推理能力来做薪资预测可以换其他模型。但如果你要的是“稳定抽取字段 批量处理 成本可控”Seed-2.1-pro-0915 是一个很稳的选择。2.2 Next.js SSE 的前后端分工整个系统分成三层采集层、解析层、展示层。采集层负责按关键词去目标平台拉取岗位列表和详情。这部分我用的是服务端定时任务不放在前端因为前端触发容易被反爬也不稳定。解析层就是 Seed-2.1-pro-0915 的工作区。采集层把原始文本丢过来解析层输出结构化 JSON存进数据库。展示层用Next.js做。为什么用 Next.js 而不是纯前端框架因为我要做SSEServer-Sent Events流式推送。求职雷达的运行过程是“边抓边解析边出结果”用户不需要等 12 分钟才看到东西而是可以实时看到“正在解析第 3 个岗位”“已发现 5 条薪资数据”。SSE 天然适合这种服务端主动推送的场景比 WebSocket 轻比轮询省资源。Next.js 的 Route Handler 可以直接返回text/event-stream配合 React 端的EventSource整个流式链路非常干净。这也是为什么热词里Next.js和SSE会同时出现——它们在这个场景里是绝配。2.3 Agent 编排让雷达自己决定下一步这个项目里我用了一个轻量的Agent编排逻辑。不是那种复杂的多智能体框架而是一个“决策循环”采集 Agent 判断当前关键词下还有没有未抓取的岗位如果有抓取并交给解析 Agent解析 Agent 输出结构化数据后判断薪资字段是否完整如果不完整触发一次“补全”调用尝试从公司页面或岗位描述里二次提取全部完成后报告 Agent 汇总生成可验证报告。这个循环的价值在于容错。纯脚本的问题是遇到一个字段缺失就断了。Agent 编排让系统能自己决定“这一步没拿到我要不要再试一次”。热词里agent架构、agent框架与编排之所以火就是因为大家发现真正能落地的 Agent 不是炫技而是把这种“判断-执行-再判断”的循环做稳。3. 核心细节解析与实操要点3.1 岗位文本清洗别让脏数据污染解析结果原始岗位文本有多脏我举几个真实例子。有的平台会把薪资写成“1.5-2万/月”有的写成“15K-20K·13薪”还有的写成“面议”。技能标签有的是“Java,Spring,MySQL”有的是“Java / Spring / MySQL”还有的混在正文里“熟悉 Java、Spring 框架”。如果你直接把这些丢给模型模型也能解析但字段一致性会很差。我的做法是先做一轮规则清洗再交给模型。清洗规则包括统一薪资单位把“万/月”换算成“元/月”把“K”换算成“千元”统一分隔符把“、”“/”“”统一成逗号剥离 HTML 标签和平台特有的装饰符号把“面议”“薪资面谈”单独标记不参与数值统计。这一步看起来不起眼但实测下来清洗后的解析准确率比直接丢原始文本高了至少两成。原因很简单模型再强也不该让它去处理本该用规则解决的格式问题。注意清洗规则不要写得太死。比如“13薪”这种信息如果你直接删掉就丢了重要数据。我的做法是把“13薪”“14薪”提取成单独字段在报告里单独展示。3.2 薪资字段的抽取与校验逻辑薪资是这份报告的核心也是最容易出错的地方。我的抽取逻辑分三步第一步模型抽取。让 Seed-2.1-pro-0915 输出salary_min、salary_max、salary_unit、salary_months四个字段。第二步规则校验。检查salary_min是否小于salary_max检查单位是否在允许范围内检查月数是否合理比如 12 到 16 之间。第三步异常标记。如果校验不通过比如salary_min大于salary_max或者单位缺失就把这条记录标记为“待人工确认”不进入统计。为什么要做校验因为模型偶尔会把“15k-20k”解析成min20, max15这种错误如果不拦报告里的薪资分布就全乱了。校验规则就是最后一道防线。3.3 SSE 流式推送的实现要点SSE 这块我踩过坑重点说三个。第一个坑连接超时。热词里有一条stream disconnected before completion: idle timeout waiting for sse这个我太熟了。SSE 连接如果长时间没有数据推送中间层会认为连接空闲并断开。解决办法是定期发送心跳。我的做法是每 15 秒发一个注释行: heartbeat保持连接活跃。第二个坑数据格式。SSE 要求每条消息以data:开头以两个换行结尾。如果你直接JSON.stringify一个对象丢过去前端EventSource解析会出问题。正确做法是// Next.js Route Handler 中的 SSE 推送 const encoder new TextEncoder(); function sendEvent(controller, eventName, data) { const payload event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n; controller.enqueue(encoder.encode(payload)); }第三个坑错误处理。如果解析过程中某个岗位出错不能让整个流断掉。我的做法是捕获错误后推送一个event: error的消息前端展示“该岗位解析失败”然后继续处理下一个。3.4 可验证报告的设计每条薪资都能点回去这是这个项目最核心的差异化点。报告里的每一条薪资数据都带一个source_url字段指向原始岗位页面。前端渲染时薪资数字旁边有一个“验证”链接点开就是原始页面。这样做的好处是建立信任。AI 生成的报告用户天然会怀疑“这数据是不是编的”。但如果你能点回去看到原始岗位信任感就建立起来了。实现上采集层在抓取每个岗位时必须把source_url一起存下来解析层输出时带上这个字段展示层渲染成链接。整条链路不能丢这个字段。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先说环境。我用的是 Node.js 20 以上版本Next.js 14 的 App Router 模式。数据库用的是 PostgreSQL因为要做结构化查询和统计。依赖清单# 核心依赖 npm install next14 react react-dom npm install prisma/client prisma npm install seed-sdk # Seed-2.1-pro-0915 的调用 SDK # 采集与解析 npm install cheerio axios npm install zod # 用于校验模型输出的 JSON 结构这里重点说zod。模型输出的 JSON 虽然看起来对但字段类型可能不对比如salary_min本该是数字模型给了字符串15。用 zod 定义 schema解析后立即校验不通过就触发重试。这一步能省掉后面大量的脏数据排查时间。4.2 采集层的定时任务实现采集层我用的是 Next.js 的 Route Handler 配合外部定时触发。核心逻辑是// app/api/crawl/route.js export async function POST(request) { const { keyword, pages } await request.json(); const jobs []; for (let page 1; page pages; page) { const list await fetchJobList(keyword, page); for (const item of list) { const detail await fetchJobDetail(item.url); jobs.push({ ...detail, source_url: item.url, crawled_at: new Date().toISOString(), }); } } // 存入数据库状态标记为 pending await prisma.job.createMany({ data: jobs }); return Response.json({ count: jobs.length }); }这里的关键是每个岗位都带source_url。没有这个字段后面的可验证报告就无从谈起。4.3 解析层的 Agent 循环实现解析层是整个系统的核心。我用了一个简化的 Agent 循环async function parseJobWithAgent(job) { let attempts 0; const maxAttempts 3; while (attempts maxAttempts) { const raw await callSeedModel(job.raw_text); const parsed safeParseJSON(raw); const validation jobSchema.safeParse(parsed); if (validation.success) { return validation.data; } // 校验失败把错误信息反馈给模型让它重试 attempts; job.raw_text 上次输出有以下问题${validation.error.message}请修正后重新输出。\n\n${job.raw_text}; } return { error: parse_failed, source_url: job.source_url }; }这个循环的价值在于自我修正。模型第一次输出格式不对把错误信息反馈回去第二次通常就能修正。实测下来第一次成功率大概七成加上重试后能到九成五以上。4.4 SSE 推送与前端实时展示后端推送这块Next.js 的 Route Handler 返回一个ReadableStream// app/api/radar/route.js export async function GET(request) { const stream new ReadableStream({ async start(controller) { const encoder new TextEncoder(); const send (event, data) { controller.enqueue( encoder.encode(event: ${event}\ndata: ${JSON.stringify(data)}\n\n) ); }; // 心跳防止空闲超时 const heartbeat setInterval(() { controller.enqueue(encoder.encode(: heartbeat\n\n)); }, 15000); try { const jobs await prisma.job.findMany({ where: { status: pending } }); send(start, { total: jobs.length }); for (let i 0; i jobs.length; i) { const parsed await parseJobWithAgent(jobs[i]); await prisma.job.update({ where: { id: jobs[i].id }, data: { parsed_data: parsed, status: done }, }); send(progress, { current: i 1, total: jobs.length, job: parsed }); } send(complete, { message: 报告生成完毕 }); } catch (err) { send(error, { message: err.message }); } finally { clearInterval(heartbeat); controller.close(); } }, }); return new Response(stream, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }, }); }前端用EventSource接收const es new EventSource(/api/radar); es.addEventListener(progress, (e) { const data JSON.parse(e.data); setProgress(data.current / data.total); setJobs((prev) [...prev, data.job]); }); es.addEventListener(complete, () { es.close(); });这套链路跑通后用户打开页面就能看到岗位一条条被解析出来体验比“等 12 分钟看一个静态报告”好太多。4.5 报告生成与薪资统计所有岗位解析完成后报告 Agent 做汇总统计薪资中位数、平均值、分位数25%、75%按经验要求分组的薪资对比按技能标签分组的岗位数量每条数据的来源链接。统计逻辑用 SQL 就能做不需要再调模型。模型只负责抽取统计交给数据库这样结果更可控。5. 常见问题与排查技巧实录5.1 SSE 连接频繁断开怎么办这是最高频的问题。热词里stream disconnected before completion说的就是这个。排查顺序现象可能原因解决办法连接几秒就断中间层空闲超时加心跳每 15 秒发一次连接建立后立即断响应头不对检查Content-Type是否为text/event-stream部分消息丢失数据格式错误检查是否以\n\n结尾长时间无数据后断服务端处理太慢先推送“处理中”状态再推送结果我踩过最坑的一次是中间层有 60 秒空闲超时而我的解析逻辑有时候一个岗位要处理 40 秒中间没有推送任何数据连接就被断了。后来加了心跳问题解决。5.2 模型输出 JSON 解析失败这个问题的根源通常是模型在 JSON 外面包了一层解释文字比如“好的以下是解析结果{...}”。解决办法有两个一是用response_format参数强制 JSON 模式如果模型支持二是在解析前用正则提取第一个{到最后一个}之间的内容。function safeParseJSON(text) { try { return JSON.parse(text); } catch { const match text.match(/\{[\s\S]*\}/); if (match) { try { return JSON.parse(match[0]); } catch { return null; } } return null; } }5.3 薪资数据出现异常值异常值通常来自两种情况一是模型解析错误二是原始岗位本身写的就是异常值比如“100k-200k”其实是年薪但被当成月薪。我的处理方式是双重校验先检查数值范围是否合理月薪超过 100k 的标记为可疑再检查单位是否明确。可疑数据不进入统计但在报告里单独列出标注“待确认”。5.4 批量解析时速度太慢如果串行解析几十个岗位要跑很久。我的优化是并发解析但并发数要控制。实测下来并发 5 到 8 个比较稳再高容易触发模型端的限流。async function parseBatch(jobs, concurrency 5) { const results []; for (let i 0; i jobs.length; i concurrency) { const batch jobs.slice(i, i concurrency); const batchResults await Promise.all(batch.map(parseJobWithAgent)); results.push(...batchResults); } return results; }5.5 采集层被目标平台限制这个问题的处理原则是控制频率、模拟正常访问。具体做法包括每次请求之间加随机延迟、使用合理的请求头、不要短时间内重复抓同一个页面。这部分不展开太多核心思路是“像正常用户一样访问”。6. 这套雷达还能怎么扩展跑通基础版本后我做了几个扩展效果不错。第一个扩展是薪资趋势追踪。每次运行都把结果存一份快照这样过一段时间就能看到同一个岗位的薪资变化或者同一个关键词下的薪资中位数走势。这个功能对判断“现在是不是跳槽好时机”很有参考价值。第二个扩展是技能热度分析。把所有岗位的技能标签抽出来做词频统计能看出当前市场最缺什么技能。我实测下来这个数据比很多行业报告都及时。第三个扩展是订阅推送。当雷达发现符合条件的新岗位时通过邮件或站内通知推送给用户。这部分用 SSE 做实时推送或者用定时任务做批量推送都可以。第四个扩展是多关键词并行。现在支持同时跑多个关键词比如“前端”“后端”“全栈”一起跑最后生成一份综合报告。并发控制还是老规矩别贪多。7. 一些实操心得最后分享几个我在这个项目里踩过的坑和总结的经验。关于模型调用不要迷信“一次调用解决所有问题”。把任务拆细抽取字段是一个调用校验是规则统计是 SQL。模型只做它最擅长的事——从非结构化文本里抽信息。关于 SSE心跳是必须的错误处理是必须的数据格式是必须严格的。这三点做到SSE 就很稳。关于可验证性source_url这个字段从采集层到展示层全程不能丢。这是整个报告可信度的基石。关于 Agent 编排不要为了 Agent 而 Agent。我这个项目里的 Agent 循环很简单就是“解析-校验-重试”。但就是这三步把成功率从七成提到了九成五。复杂的多智能体框架不一定适合每个人先把这种小循环做稳价值就已经很大了。关于并发并发数不是越高越好。模型端有限流数据库有连接数限制采集端有频率限制。找到那个平衡点比盲目调高并发更重要。这套东西我从零搭到跑通大概花了两个周末。中间踩的坑主要集中在 SSE 连接稳定性和模型输出校验上。但跑通之后每次求职季都能用而且数据是自己的、可验证的比看别人的报告踏实多了。如果你也在做类似的东西希望这些经验能帮你少走点弯路。
返回列表