尧图精选

基于Seed-2.1-pro-0915与Claude Code的求职岗位信息聚合系统实战

🕒 发布时间:2026/10/2 4:48:42 📁 来源:尧图网络
1. 求职雷达这个想法是怎么来的找工作这件事最让人头疼的其实不是投简历也不是面试而是信息筛选。我前阵子帮一个朋友看机会打开几个招聘平台搜同一个岗位关键词出来的结果能差出三倍。有的岗位挂了大半年还在有的薪资写着“面议”点进去发现是销售岗还有的JD写得天花乱坠仔细一看是外包派遣。更离谱的是同一个公司在不同平台挂的薪资范围能差出5K到8K你根本不知道哪个是真的。我当时就想能不能做一个东西把多个来源的岗位信息拉到一个地方自动去重、自动标注薪资可信度、自动把每条岗位的原始链接保留下来让我点一下就能跳回去验证。这个想法其实不复杂核心就是三个动作采集、清洗、呈现。但真动手做的时候我发现难点不在采集而在于怎么让整个流程跑得快、结果可验证、而且不用我手动去一个个平台翻。后来我用了 Seed-2.1-pro-0915 这个模型来做信息抽取和语义匹配配合 Claude Code 做快速原型开发前端用 Next.js 搭了一个带 SSE 实时推送的界面。整个东西从零到能跑出第一份报告大概花了12分钟。这个时间不是说我写了12分钟代码而是从我把需求描述清楚、模型开始处理、到前端把结果渲染出来整个链路跑通的时间。下面我把整个思路、技术选型、实操步骤和踩过的坑都拆开讲一遍。2. 整体架构设计与技术选型逻辑2.1 为什么选 Seed-2.1-pro-0915 做核心抽取岗位信息的非结构化程度很高。同一个“前端开发工程师”有的JD写“负责Web端界面开发”有的写“参与产品需求评审并完成前端实现”还有的写“与后端协作完成数据交互”。你要把这些归到同一个岗位类别下靠关键词匹配根本不够。我试过用正则去抓“薪资”字段结果发现有的写“15-25K”有的写“15K-25K”有的写“15~25K”还有的写“15万-25万/年”。正则写到最后我自己都乱了。Seed-2.1-pro-0915 在这个环节的优势是语义理解稳定。我给它一段JD原文让它输出结构化的JSON包含岗位名称、薪资下限、薪资上限、薪资周期、工作地点、经验要求、学历要求、公司名称、岗位描述摘要。实测下来它对薪资周期的识别准确率很高能把“年薪”和“月薪”分开也能识别“13薪”“14薪”这种变体。这个能力直接决定了后面薪资对比的可信度。注意模型抽取不是100%准确所以我在前端保留了原始JD文本的展开查看功能每条岗位都能点开看到原文这样即使抽取有偏差用户也能自己判断。2.2 为什么用 Claude Code 做开发主力Claude Code 在这个项目里的角色是快速把想法变成可运行代码。我不是专业前端Next.js 的 App Router 和 SSE 的配合我一开始并不熟。Claude Code 的好处是它能理解整个项目上下文我不用把每个文件的代码都贴给它它自己会去读。比如我要加一个“薪资可信度评分”的逻辑我只需要描述清楚规则薪资范围跨度超过10K的扣分、薪资周期为“面议”的扣分、公司名称包含“外包”“派遣”的扣分。Claude Code 会帮我把这个评分函数写出来并且自动接到数据流里。这里有个细节Claude Code 在 Windows 下的安装和配置比 macOS 稍微麻烦一点。我是在 Ubuntu 环境下开发的直接通过命令行安装然后配置好模型接入点就能用。如果你在 Windows 上建议用 WSL2不然路径和权限问题会浪费很多时间。2.3 为什么前端用 Next.js 加 SSESSE 在这个场景里比 WebSocket 更合适。原因很简单数据流是单向的服务端把处理好的岗位信息一条条推给前端前端不需要往回发消息。WebSocket 的双向能力在这里是浪费而且连接管理更复杂。SSE 基于 HTTP天然支持断线重连浏览器兼容性也好。Next.js 的 App Router 里做 SSE 需要注意一点Route Handler 默认是静态的你要显式声明export const dynamic force-dynamic否则流式响应会被缓存前端收不到实时推送。这个坑我踩过调试了半小时才发现是缓存问题。整个数据流是这样的后端从多个来源拉取原始岗位数据经过 Seed-2.1-pro-0915 抽取结构化字段再经过去重和评分模块最后通过 SSE 推送到前端。前端每收到一条数据就渲染一张岗位卡片用户看到卡片逐渐填满屏幕的过程体验上比等所有数据加载完再一次性渲染要好得多。3. 核心细节解析与实操要点3.1 岗位数据采集的边界与合规采集这件事我的原则是只拿公开可见的信息并且保留原始链接。每条岗位卡片上都有一个“查看原文”的按钮点开直接跳转到原始页面。这样做有两个好处一是用户能自己验证信息真伪二是避免了我把数据二次加工后变成“不可追溯”的内容。采集频率我控制在合理范围内不会对目标站点造成压力。具体实现上我用了一个简单的队列机制每个来源之间间隔几秒避免并发过高。如果你要做类似的东西建议也加上这个限制不然容易被封IP而且也不符合基本的网络礼仪。3.2 薪资字段的归一化处理薪资归一化是这个项目里最琐碎但最重要的环节。我整理了一个处理流程原始格式归一化结果处理逻辑15-25K15000-25000/月识别“K”为单位乘以100015K-25K15000-25000/月同上兼容不同连接符15~25K15000-25000/月波浪号统一替换为短横线15万-25万/年12500-20833/月年薪除以12保留整数面议标记为“未知”不参与薪资排序单独分组13薪月薪×13/12折算为等效月薪这个表看起来简单但实际数据里还有“15-25K·13薪”“15K-25K·14薪”这种组合写法。我的处理方式是先按“·”分割分别解析薪资和薪数再合并计算。Seed-2.1-pro-0915 在这个环节的表现是它能直接输出结构化的薪资对象我只需要在Prompt里把规则写清楚它就能按格式返回。实操心得不要试图用一套正则搞定所有薪资格式。我一开始就是这么干的结果维护成本极高。后来改成“模型抽取规则校验”的组合模型负责理解语义规则负责兜底和格式化稳定性提升了很多。3.3 去重逻辑的设计去重不是简单的字符串匹配。同一个岗位可能在不同平台有不同的标题比如“高级前端开发”和“资深前端工程师”可能是同一个职位。我的做法是公司名称工作地点薪资范围三个字段组合成一个指纹如果三个都相同或高度相似就判定为重复。但这里有个例外有些公司确实在招多个同岗位的人薪资范围也一样。这种情况我保留两条但在卡片上标注“可能为同一岗位的多个HC”。这个判断逻辑我交给了模型去做因为纯规则很难区分“重复”和“多HC”。3.4 SSE 流式推送的实现细节SSE 的服务端实现我用的是 Next.js 的 Route Handler 返回ReadableStream。核心代码结构是这样的export const dynamic force-dynamic; export async function GET() { const encoder new TextEncoder(); const stream new ReadableStream({ async start(controller) { const jobs await fetchAndProcessJobs(); for (const job of jobs) { controller.enqueue( encoder.encode(data: ${JSON.stringify(job)}\n\n) ); await new Promise(r setTimeout(r, 50)); } controller.close(); } }); return new Response(stream, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }, }); }前端用EventSource接收const eventSource new EventSource(/api/jobs/stream); eventSource.onmessage (event) { const job JSON.parse(event.data); setJobs(prev [...prev, job]); }; eventSource.onerror () { eventSource.close(); };这里有个细节setTimeout的50毫秒延迟是为了让前端有“逐条加载”的视觉效果实际生产环境可以去掉。但如果你去掉数据会瞬间全部推完用户感觉不到流式的优势。我保留了这个延迟因为体验上更像“雷达在扫描”。注意SSE 连接在服务端关闭后浏览器会自动重连。如果你不希望重连需要在onerror里手动close()。我在开发阶段遇到过无限重连导致重复数据的问题后来加了去重逻辑才解决。4. 实操过程与核心环节实现4.1 环境准备与依赖安装我用的开发环境是 Ubuntu 22.04Node.js 20 LTS。Next.js 项目初始化用npx create-next-applatest选择 App Router 和 TypeScript。然后安装必要的依赖npm install eventsource-parser npm install -D types/nodeClaude Code 的安装我是在终端里直接跑的配置好模型接入点后用claude命令启动。这里提醒一句如果你用的是国内网络环境模型接入点的配置需要根据你的实际情况调整确保能正常访问。Seed-2.1-pro-0915 的调用我是通过 API 方式接入的封装了一个extractJobInfo函数输入是原始JD文本输出是结构化对象。这个函数的Prompt我调了大概七八版最终稳定下来的版本是这样的你是一个岗位信息抽取助手。请从以下JD文本中提取 - 岗位名称标准化为常见称呼 - 薪资下限数字单位元/月 - 薪资上限数字单位元/月 - 薪资周期月薪/年薪/日薪 - 工作地点城市级别 - 经验要求年限或“不限” - 学历要求大专/本科/硕士/不限 - 公司名称 - 岗位描述摘要50字以内 如果某个字段无法确定返回null。只返回JSON不要其他内容。4.2 数据管道的搭建整个数据管道分四步拉取原始数据从配置好的来源列表里逐个获取岗位列表页提取详情页链接。抽取结构化信息对每个详情页的JD文本调用 Seed-2.1-pro-0915 进行抽取。归一化与去重对抽取结果做薪资归一化然后按指纹去重。评分与排序根据薪资可信度、信息完整度、发布时间等维度计算综合评分按评分降序排列。每一步都有日志输出方便排查问题。我在本地跑的时候控制台会打印类似这样的信息[采集] 来源A 获取到 45 条岗位 [抽取] 完成 45 条成功 43 条失败 2 条 [归一化] 薪资字段处理完成面议岗位 8 条 [去重] 去重前 43 条去重后 31 条 [评分] 评分完成最高分 92最低分 45 [推送] SSE 流开始推送共 31 条这个日志格式是我自己定的目的是让每一步的输入输出都清晰可见。如果你要做类似的项目建议也加上这种分步日志不然出了问题很难定位是哪个环节的锅。4.3 前端卡片的渲染逻辑前端每收到一条岗位数据就渲染一张卡片。卡片包含以下信息岗位名称大字号公司名称中字号灰色薪资范围醒目颜色如果是“面议”则显示为灰色工作地点、经验要求、学历要求小标签形式薪资可信度评分用颜色条表示绿色高分红色低分“查看原文”按钮跳转到原始链接卡片的布局我用的是 CSS Grid响应式适配。在桌面端一行显示三张卡片平板两张手机一张。这个布局没什么特别的但有一个细节卡片的进入动画。每张卡片从下方淡入延迟50毫秒这样用户看到的是卡片逐张出现的效果而不是一次性全部弹出。这个动画让整个页面看起来更有“雷达扫描”的感觉。4.4 12分钟出报告的完整时间线我记录了一下从启动到出报告的时间分布阶段耗时说明数据拉取约3分钟多个来源串行拉取每个来源间隔2秒模型抽取约5分钟43条JD平均每条7秒归一化去重约30秒纯本地计算速度很快评分排序约10秒规则计算几乎瞬时SSE推送约3分钟31条数据每条间隔50毫秒加上前端渲染时间合计约12分钟从启动到前端完整显示这个时间分布说明模型抽取是瓶颈。如果你要优化速度可以考虑并发调用模型但要注意控制并发数避免触发限流。我试过并发5路速度提升到约2分钟但偶尔会有超时失败。后来改成并发3路稳定性和速度比较平衡。5. 常见问题与排查技巧实录5.1 SSE 连接断开与重连问题现象前端收到部分数据后连接断开控制台报stream disconnected before completion: idle timeout waiting for sse。原因服务端处理时间过长超过了代理或浏览器的空闲超时时间。默认情况下很多代理的空闲超时是60秒如果两条数据之间间隔太久连接就会被切断。解决在SSE流中定期发送心跳注释。SSE协议支持以冒号开头的注释行浏览器会忽略但能保持连接活跃const heartbeat setInterval(() { controller.enqueue(encoder.encode(: heartbeat\n\n)); }, 15000);在流结束时清除定时器clearInterval(heartbeat); controller.close();这个心跳机制我加上之后再也没有出现过空闲超时断开的问题。5.2 模型抽取结果不稳定现象同样的JD文本两次调用模型一次返回了薪资上限一次返回null。原因模型的温度参数设置过高导致输出有随机性。另外Prompt里如果没有明确要求“必须返回所有字段”模型可能会省略它认为不重要的字段。解决把温度调到0并且在Prompt里明确列出所有必须返回的字段即使值为null也要返回。我还在Prompt末尾加了一句“确保返回的JSON包含所有上述字段不要省略任何字段”。这个改动之后抽取稳定性明显提升。5.3 前端重复渲染问题现象SSE重连后前端收到了重复的岗位数据列表里出现了相同的卡片。原因SSE自动重连时服务端会重新推送所有数据而前端没有做去重。解决在前端维护一个已接收岗位ID的Set每次收到新数据先检查ID是否已存在const receivedIds useRef(new Set()); eventSource.onmessage (event) { const job JSON.parse(event.data); if (receivedIds.current.has(job.id)) return; receivedIds.current.add(job.id); setJobs(prev [...prev, job]); };这个改动很小但解决了大问题。如果你做SSE流式推送强烈建议加上这个去重逻辑。5.4 薪资归一化的边界情况现象某些岗位的薪资字段是“15-25K·13薪”归一化后变成了“15000-25000/月”丢失了13薪的信息。原因归一化逻辑只处理了薪资范围没有处理薪数。解决在归一化之前先按“·”分割字符串分别解析薪资部分和薪数部分。薪数部分提取数字然后计算等效月薪function normalizeSalary(raw) { const parts raw.split(·); const salaryPart parts[0]; const monthsPart parts[1] || ; const monthsMatch monthsPart.match(/(\d)薪/); const months monthsMatch ? parseInt(monthsMatch[1]) : 12; const [min, max] parseSalaryRange(salaryPart); const factor months / 12; return { min: Math.round(min * factor), max: Math.round(max * factor), months, }; }这个逻辑我后来补上的之前漏掉了薪数信息导致薪资对比不公平。5.5 常见问题速查表问题可能原因排查方法解决方案SSE连接断开空闲超时查看控制台报错加心跳注释数据重复重连后重复推送检查前端是否有去重用Set记录已接收ID抽取字段缺失Prompt不明确对比多次调用结果明确要求返回所有字段薪资归一化错误未处理薪数检查原始数据格式先分割再解析模型调用超时并发过高查看API返回状态降低并发数前端渲染卡顿数据量过大检查浏览器性能面板虚拟滚动或分页实操心得这个项目里最耗时的不是写代码而是调试数据管道的边界情况。我建议你在开发阶段就把日志打全每一步的输入输出都记录下来。这样出问题的时候你能快速定位是采集、抽取、归一化还是推送环节的锅。6. 这个雷达还能怎么扩展我现在用的这个版本核心功能就是“采集-抽取-推送”这条链路。但实际用下来我觉得还有几个方向可以继续加第一个是历史对比。同一个岗位如果之前出现过这次又出现了可以标注“该岗位已挂N天”。这个信息对判断岗位真实性很有帮助挂了半年的岗位大概率是虚假招聘或者已经招到人了但没下架。第二个是薪资分布图。把所有岗位的薪资归一化后画一个分布直方图让用户一眼看到自己搜的岗位在市场上的薪资区间。这个用简单的Canvas或者SVG就能画不需要引入图表库。第三个是订阅推送。用户设定好关键词和薪资下限有新岗位出现时通过邮件或站内通知提醒。这个需要加一个定时任务和持久化存储复杂度会高一些但实用性很强。第四个是公司维度聚合。把同一个公司的所有岗位聚合在一起显示这家公司目前在招的所有职位。这个对判断公司规模和发展阶段很有参考价值。这些扩展我还没有全部实现但架构上已经预留了空间。数据管道是模块化的加新的处理步骤只需要在管道里插入一个环节就行。前端也是组件化的加新的卡片类型或者视图模式不需要改动核心逻辑。最后说一个我在实际使用中的体会这个工具最大的价值不是帮你找到工作而是帮你过滤掉不靠谱的机会。我朋友用这个雷达筛了一遍之后发现之前投的岗位里有三分之一是外包或者薪资虚标。省下来的时间足够他多准备几轮面试了。如果你也在找工作或者帮别人看机会不妨试试这个思路自己搭一个轻量版的雷达比在多个平台之间来回切换要高效得多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →