1. 项目概述一个自动更新的AI职位聚合平台如果你在2026年寻找一份AI工程师的工作你的日常大概率是这样的打开十几个浏览器标签页在Anthropic的Greenhouse页面、OpenAI的招聘网站、DeepMind的职位公告板、Cohere的Lever页面之间来回切换。像LinkedIn、Indeed这样的综合招聘平台确实有AI相关的职位但它们的筛选功能简直是一场灾难。搜索“AI工程师”你可能会得到“AI驱动的客服专员”或者“一家业务与AI毫不相干的AI初创公司的工程师”这类结果。这种信息过载与精准度缺失的双重困境正是我决心构建LLMHire的起点。我的核心目标很简单打造一个单一页面自动聚合所有主流AI/ML/LLM公司的每一个相关职位并且保持实时更新。这个项目不仅仅是一个工具它是我对当前AI招聘市场信息碎片化问题的一次技术性回应。通过自动化爬取和智能分类我希望为求职者提供一个纯净、高效的信息入口。整个系统基于现代Web技术栈构建Next.js 14使用App Router作为前端框架Supabase底层是PostgreSQL处理数据存储Vercel则承担了部署和定时任务Cron Jobs的重任。下面我将详细拆解从架构设计、核心实现到数据洞察的完整过程。2. 核心架构与设计思路拆解2.1 技术栈选型背后的逻辑选择Next.js 14的App Router并非盲目追新。对于这样一个内容驱动、需要优秀SEO方便求职者通过搜索引擎发现和极快初始加载速度的项目Next.js的服务端渲染SSR和静态生成SSG能力是决定性因素。App Router提供的基于文件系统的路由、服务端组件以及流式渲染使得构建复杂的、数据预取的应用变得异常清晰。例如职位列表页可以大部分静态生成而搜索和过滤功能则利用客户端组件实现交互这种混合模式在性能和体验上取得了很好的平衡。数据层选择Supabase看中的是其开箱即用的PostgreSQL数据库、实时订阅功能和简单的REST/GraphQL API。职位数据具有明显的结构化特征公司、职位、地点、描述等关系型数据库是最自然的选择。Supabase的pg_cron扩展也能方便地执行数据库内的定时清理任务如标记过期职位虽然本项目主要使用Vercel Cron但这种灵活性为未来扩展留下了空间。部署在Vercel上几乎是顺理成章的。它与Next.js的无缝集成、全球CDN、以及最重要的——内置的Cron Jobs功能使得设置定时抓取任务变得轻而易举无需额外维护一个服务器或复杂的Worker脚本。Vercel的Cron可以可靠地每隔一段时间触发我们的数据更新管道。2.2 核心挑战异构ATS系统的统一项目的技术核心在于对接不同的申请人跟踪系统ATS。经过调研绝大多数科技公司使用三大平台Greenhouse、Ashby和Lever。它们虽然都提供公开的职位API但接口规范、认证方式和数据格式迥然不同。我的设计思路是采用“适配器模式”Adapter Pattern为每个ATS系统编写一个专用的数据获取器Fetcher。为什么是适配器模式这保证了系统核心逻辑的稳定性。无论底层对接的是Greenhouse还是Ashby我的数据处理流水线去重、分类、存储都只需要与一个统一的、规范化的数据格式打交道。当未来需要支持新的ATS如Workable时我只需新增一个适配器模块而不必修改核心业务代码。这种解耦设计极大地提升了系统的可维护性和可扩展性。3. 核心细节解析与实操要点3.1 数据获取器的具体实现每个ATS的Fetcher模块都需要处理网络请求、错误重试和数据清洗。下面以Greenhouse为例详细说明实现细节// adapters/greenhouseFetcher.js import axios from axios; /** * Greenhouse ATS 职位获取器 * param {string} companySlug - 公司在Greenhouse上的唯一标识符 * returns {PromiseArray} - 规范化后的职位列表 */ async function fetchGreenhouseJobs(companySlug) { const url https://boards-api.greenhouse.io/v1/boards/${companySlug}/jobs; try { const response await axios.get(url, { timeout: 10000, // 10秒超时 headers: { User-Agent: LLMHireBot/1.0 (compatible; https://llmhire.com) } // 友好的User-Agent }); if (response.status ! 200) { throw new Error(Greenhouse API返回错误状态码: ${response.status}); } const jobs response.data.jobs || []; // 核心数据规范化 return jobs.map(rawJob ({ // 生成唯一ID用于后续去重 externalId: greenhouse_${companySlug}_${rawJob.id}, title: rawJob.title?.trim() || , company: rawJob.company?.name || companySlug, location: parseLocation(rawJob.location?.name), // 需要解析地点字符串 department: rawJob.departments?.[0]?.name || Uncategorized, // Greenhouse API不直接提供远程信息需从标题或地点推断 workMode: inferWorkMode(rawJob.title, rawJob.location?.name), description: rawJob.content || , applyUrl: rawJob.absolute_url, publishedDate: new Date(rawJob.updated_at || rawJob.created_at), // 原始数据保留以备后续分析或调试 rawData: rawJob, source: greenhouse })); } catch (error) { console.error(获取Greenhouse公司 ${companySlug} 职位失败:, error.message); // 实现简单的指数退避重试逻辑 return []; // 本次返回空数组由上游调度器决定是否重试 } } // 辅助函数解析复杂的地点字符串如“Remote - US”或“San Francisco, CA | Hybrid” function parseLocation(locationStr) { if (!locationStr) return Not Specified; // 移除“| Hybrid”等后缀提取主要地点 return locationStr.split(|)[0].trim(); } // 辅助函数从职位标题和地点推断工作模式 function inferWorkMode(title, location) { const titleLower title.toLowerCase(); const locationLower location?.toLowerCase() || ; if (locationLower.includes(remote) || titleLower.includes(remote)) { return remote; } else if (locationLower.includes(hybrid) || titleLower.includes(hybrid)) { return hybrid; } else { return onsite; } }注意在实际请求ATS的公开API时务必设置合理的超时如10秒和请求间隔避免对对方服务器造成压力。同时使用一个清晰的User-Agent头如包含你的项目名称和网站是一种良好的网络公民行为方便对方识别你的爬虫。Ashby和Lever的适配器结构类似但需注意Ashby通常使用POST请求其请求体和响应格式是自定义的Lever的API路径和分页方式也略有不同。关键在于为每个平台编写正确的HTTP请求逻辑和对应的数据映射函数。3.2 自动分类系统的构建与优化将杂乱的职位标题自动归类为“LLM工程师”、“ML工程师”等是提升产品可用性的关键。我采用了一个基于规则的模式匹配系统其核心是一个优先级匹配列表// classifiers/roleClassifier.js const ROLE_PATTERNS [ { category: LLM Engineer, keywords: [llm, large language model, language model, nlp, natural language processing, generative ai, gpt, transformer], priority: 10 // 优先级最高因为更具体 }, { category: AI Research Scientist, keywords: [research scientist, ai research, applied scientist], mustAlsoContain: [ai, machine learning, llm], // 必须同时包含这些词避免误判 priority: 9 }, { category: ML Engineer, keywords: [machine learning engineer, ml engineer, ml/ai engineer, ai/ml engineer], priority: 8 }, { category: AI Infrastructure Engineer, keywords: [ml infrastructure, ai platform, ai infrastructure, ml systems, model deployment], priority: 7 }, { category: Prompt Engineer, keywords: [prompt engineer, prompting], priority: 6 // 优先级较低因为可能被其他更宽泛的类别覆盖 }, // ... 其他类别如 Data Scientist, AI Product Manager 等 ]; function classifyRole(jobTitle, jobDescription ) { const textToSearch (jobTitle jobDescription).toLowerCase(); let matchedRole Other AI Role; // 默认类别 let highestPriority -1; for (const pattern of ROLE_PATTERNS) { // 检查关键词 const hasKeywords pattern.keywords.some(keyword textToSearch.includes(keyword)); // 检查“必须包含”条件如果存在 const meetsMustContain !pattern.mustAlsoContain || pattern.mustAlsoContain.some(word textToSearch.includes(word)); if (hasKeywords meetsMustContain pattern.priority highestPriority) { highestPriority pattern.priority; matchedRole pattern.category; } } return matchedRole; }实操心得纯关键词匹配的准确率最初只有70%左右。例如“Software Engineer, ML Platform”可能被错误地归类为“ML Engineer”而它更可能是“AI Infrastructure Engineer”。我通过以下策略将准确率提升至90%以上引入优先级系统更具体的角色如“LLM Engineer”优先级高于更宽泛的角色如“ML Engineer”。添加“必须同时包含”规则对于“Research Scientist”要求标题或描述中必须同时出现“AI”或“Machine Learning”以避免将生物、化学领域的研究科学家误判进来。结合部门信息如果ATS提供了部门信息如“Machine Learning Department”可以将其作为强有力的分类依据。建立人工审核队列对于置信度低的匹配如匹配到的关键词非常少系统会将其标记放入一个管理后台供我每周花少量时间集中审核和修正。这些修正后的数据又可以反过来作为训练数据未来用于优化一个简单的机器学习分类器。4. 实操过程与核心环节实现4.1 数据流水线与定时任务整个系统的数据流由Vercel Cron Job驱动每4小时执行一次。我将其设计为一个无状态的、幂等的流水线确保即使某次执行失败也不会导致数据混乱。// app/api/cron/update-jobs/route.js (Next.js App Router API Route) import { fetchAllJobs } from /lib/fetchers; import { deduplicateAndStore } from /lib/database; import { updateSearchIndex } from /lib/search; export const maxDuration 60; // Vercel函数最大执行时间秒 export const dynamic force-dynamic; export async function GET(request) { // 简单的Cron验证可选增加安全性 const authHeader request.headers.get(authorization); if (authHeader ! Bearer ${process.env.CRON_SECRET}) { return new Response(Unauthorized, { status: 401 }); } console.log(开始执行定时职位更新任务...); const startTime Date.now(); try { // 阶段一获取数据 const allRawJobs await fetchAllJobs(); // 并行调用所有公司的Fetcher console.log(从所有公司获取到 ${allRawJobs.length} 个原始职位); // 阶段二去重与存储 const { newJobsCount, updatedJobsCount } await deduplicateAndStore(allRawJobs); console.log(新增职位: ${newJobsCount}, 更新职位: ${updatedJobsCount}); // 阶段三更新搜索索引例如使用Algolia或Meilisearch await updateSearchIndex(); console.log(搜索索引已更新); // 阶段四标记过期职位例如超过30天未更新的 await markStaleListings(); const duration ((Date.now() - startTime) / 1000).toFixed(2); return Response.json({ success: true, message: 任务完成耗时 ${duration} 秒。新增 ${newJobsCount} 个职位。 }); } catch (error) { console.error(定时任务执行失败:, error); // 这里可以集成错误通知如发送邮件或Slack消息 return Response.json({ success: false, error: error.message }, { status: 500 }); } }去重逻辑详解这是保证数据清洁度的关键。我并非简单地根据职位标题去重因为同一公司可能在不同地点招聘相同标题的职位。我采用的去重键是一个由公司ID、规范化后的职位标题和规范化后的地点三者生成的哈希值。只有这个哈希值完全相同的职位才会被视为重复。对于已存在的职位我会比较publishedDate如果新抓取的数据更新则覆盖旧记录。4.2 前端展示与用户体验优化前端使用Next.js 14的App Router和服务器组件构建。职位列表页/jobs在构建时或定时重验证时会直接从Supabase获取数据并静态生成保证了极快的加载速度。// app/jobs/page.js import { createClient } from /lib/supabase-server; import JobList from /components/JobList; import SearchAndFilters from /components/SearchAndFilters; export const revalidate 3600; // 每1小时重新验证并可能生成新页面 export default async function JobsPage({ searchParams }) { const supabase createClient(); // 初始加载获取所有活跃职位按发布日期排序 let query supabase .from(jobs) .select(*) .eq(is_active, true) .order(published_date, { ascending: false }); // 根据URL查询参数应用过滤客户端交互后 const { role, location, mode } searchParams; if (role) query query.ilike(role_category, %${role}%); if (location) query query.ilike(location, %${location}%); if (mode) query query.eq(work_mode, mode); const { data: initialJobs, error } await query; if (error) { console.error(Error fetching jobs:, error); } return ( div classNamecontainer mx-auto px-4 py-8 h1 classNametext-3xl font-bold mb-2AI ML 职位聚合/h1 p classNametext-gray-600 mb-8实时追踪来自 160 家顶尖AI公司的职位自动更新。/p SearchAndFilters initialFilters{searchParams} / JobList initialJobs{initialJobs || []} / /div ); }搜索与过滤的实现为了在静态页面上实现动态过滤我采用了Next.js的useSearchParams和useRouter钩子。当用户选择过滤器时前端会更新URL的查询参数触发页面的软导航router.push服务器组件会根据新的searchParams重新获取数据并渲染。这种方式既保持了服务器端渲染的优势又提供了流畅的客户端交互体验。对于更复杂的全文搜索我集成了Meilisearch它提供了即时的、模糊的搜索能力远超PostgreSQL的ilike查询。5. 数据洞察与市场分析运行数周后系统积累了超过670个活跃职位每日更新50-100个。这些数据揭示了AI人才市场的一些有趣趋势1. “LLM工程师”已成为一个明确的独立角色。与传统的“机器学习工程师”相比LLM工程师的职位描述更强调对Transformer架构、提示工程、大模型微调Fine-tuning、检索增强生成RAG以及相关工具链如LangChain, LlamaIndex的深入理解。而ML工程师的职位则更侧重于传统的模型开发、特征工程和MLOps。2. 远程工作机会在减少。尽管早期许多AI公司提供远程岗位但数据显示像OpenAI、Anthropic、DeepMind这样的头部实验室越来越倾向于混合或现场办公模式。这可能与涉及敏感研究、需要高强度协作或使用受限的计算基础设施有关。3. AI基础设施领域正在蓬勃发展。“AI基础设施工程师”或“ML系统工程师”是增长最快的子类别之一。这反映了行业重点正从纯粹的研究和模型开发转向如何高效、可靠、规模化地部署和运行这些大模型。相关技能包括分布式系统、GPU集群管理、模型服务化如使用Triton Inference Server和成本优化。4. 提示工程师的热度在变化。纯粹的“提示工程师”职位数量似乎达到了一个峰值并开始趋于平稳或略有下降。相反这项技能正被吸收到更广泛的角色中如“LLM工程师”、“AI产品经理”甚至“解决方案架构师”。这表明提示工程正在从一门独立的“手艺”转变为AI从业者工具箱中的一项基础技能。为了更直观地展示这些洞察我构建了一个简单的内部仪表盘并考虑将这些分析通过博客分享出去。角色类别占比 (约)趋势关键技能关键词LLM工程师25% 快速增长Transformer, Fine-tuning, RAG, LangChain, Prompting机器学习工程师35%→ 保持稳定Python, TensorFlow/PyTorch, MLOps, Feature EngineeringAI研究科学家15%→ 稳定Publications (NeurIPS, ICML), Novel Research, PhDAI基础设施工程师18% 快速增长Distributed Systems, Kubernetes, GPU, Cloud (AWS/GCP/Azure)提示工程师5% 略有下降Prompt Design, LLM Evaluation, Few-shot Learning其他2%→AI Product, Data, Ethics6. 常见问题与排查技巧实录在开发和维护LLMHire的过程中我遇到了不少典型问题以下是其中一些及其解决方案问题一ATS API限制或封禁。现象定时任务突然开始大量失败返回403或429状态码。排查首先检查请求头确保User-Agent设置得当。查看日志确认是否因请求频率过高导致。解决增加延迟在每个公司API请求之间添加随机延迟如1-3秒模拟人类浏览行为。实现指数退避重试对于临时性错误如网络波动、服务器5xx错误实现重试机制每次重试前等待时间指数级增加。使用IP轮换如果规模扩大考虑使用可靠的代理服务池避免单个IP被限制。尊重robots.txt定期检查目标网站的robots.txt文件确保你的爬取行为是被允许的。问题二职位分类错误率高。现象用户反馈某些职位被分错了类别。排查在管理后台查看低置信度匹配的队列分析误判案例的共同模式。解决丰富规则库针对新的误判模式添加或调整关键词和规则。例如发现“AI Safety Researcher”被分到“Other”就为“AI Safety”创建新规则。引入描述文本分析最初只分析标题后来加入职位描述的前200个字符进行分析准确率显著提升。建立反馈循环在网站上添加一个简单的“分类有误”按钮收集用户反馈用于持续优化分类器。问题三数据库性能随着数据量增长而下降。现象职位数量超过1万条后列表页加载和复杂过滤查询变慢。排查使用Supabase的仪表板或EXPLAIN ANALYZE命令分析慢查询。解决添加索引为最常用的过滤字段如role_category,work_mode,company,is_active和排序字段published_date创建数据库索引。数据归档将标记为is_active false且超过60天的职位移动到历史表保持主表精简。引入专用搜索服务将全文搜索功能从PostgreSQL的ilike迁移到Meilisearch或Typesense后者为搜索场景做了极致优化。问题四Vercel Cron Job执行时间超过限制。现象数据抓取公司数量增加到160后单个Cron Job执行时间超过Vercel免费计划的10秒限制或Pro计划的60秒。解决并行化抓取使用Promise.all()或Promise.allSettled()并发执行多个公司的数据抓取任务大幅减少总耗时。任务分片如果并行后仍超时可以将公司列表分成多个批次设置多个Cron Job例如/api/cron/update-group1,/api/cron/update-group2错峰执行。考虑边缘函数或队列对于超大规模抓取可以将抓取任务拆解通过消息队列如Redis分发由多个边缘函数并发处理。7. 项目演进与未来规划LLMHire目前已经稳定运行但仍有不少可以改进和扩展的方向。短期规划集成更多ATS正如原文提到的Workable是下一个目标。许多中型AI初创公司使用Workable。其API的集成方式将与现有模式类似但需要仔细研究其认证通常需要API Key和分页机制。薪资数据聚合这是用户需求最强烈的功能之一。计划通过多种方式获取a) 从职位描述中提取如果明确列出b) 集成第三方薪资数据API如Levels.fyi的API如果可用c) 鼓励用户匿名提交offer数据。展示薪资范围时会明确标注数据来源和样本量。增强搜索与提醒实现更智能的语义搜索“找一份需要PyTorch经验的远程LLM工作”并允许用户创建保存的搜索当有新职位匹配时通过邮件或Telegram/Bot发送通知。中期构想技能标签提取使用NLP技术如简单的关键词提取或微调一个小模型从职位描述中自动提取技术栈如Python, PyTorch, AWS, Kubernetes让求职者能按技能过滤。公司信息聚合除了职位聚合公司的基本信息、技术博客、融资情况等帮助求职者更全面地了解目标公司。社区与内容启动每周AI招聘市场分析简报Newsletter分享趋势数据、热门技能和招聘解读。建立一个博客深入探讨如何准备AI面试、如何构建AI作品集等话题。长期愿景将平台从一个单向的信息聚合器转变为一个双向的AI人才生态连接器。可能的形态包括基于技能的个性化职位推荐、匿名的候选人-职位匹配度分析、或是与AI教育平台合作为学习者指明技能提升路径以匹配市场需求。构建LLMHire的过程是一个典型的“用技术解决自身痛点”的案例。它始于一个简单的需求通过合理的架构设计、持续的数据处理和对细节的打磨最终成为一个有价值的工具。最让我有成就感的不是技术本身而是看到它真正帮助到那些在AI浪潮中寻找方向的开发者。如果你也在构建类似的数据驱动型产品我的核心建议是从最小可行产品MVP开始尽快让数据流动起来然后在真实用户的反馈和数据的指引下持续迭代。数据管道中每一个环节的稳定性和可观测性远比追求功能的复杂更重要。