Elevatus 是一个真实的产品。它现在在 elevatus.jobs 上运行,为中东各地的企业客户和政府部委处理招聘事务。我是在 2018 到 2020 年于 Talentera 期间构建它所运行的多租户架构的团队的一部分。这篇文章讲的是那实际上意味着什么——不是你在 Stripe 工程博客上能找到的那种抽象多租户演讲,而是构建一个政府部委和零售连锁共用同一套 codebase、同一套数据库架构、同一个工作流引擎的平台的具体的、尴尬的、迷人的细节——而且两者都永远不能知道对方的存在。

租户隔离问题首先不是数据问题

工程师想到多租户时,通常先想到数据:共享表还是独立 schema 还是独立数据库。这是真实存在的问题,也很重要。在 Elevatus,我们用的是行级隔离模型,每行都带租户 ID,同时对某些敏感配置表做 schema 级隔离。这能处理基础问题。

真正难的问题是身份和访问。不是”租户 A 能读到租户 B 的数据吗”——那是一个查询过滤器。难的问题是:在一个平台上,同一个电子邮件地址可能属于一个向三个不同租户投过简历的候选人,你如何管理用户的身份?对那个人来说”已登录”意味着什么?哪个租户的配置支配他们的会话?当租户 A 和租户 B 在同一个职位上跑招聘轮次,而同一个候选人同时向两家投了简历时会发生什么?

这些不是假设性的边缘情况。在 MENA 招聘市场,大量候选人投得很广。政府招聘轮次——集中化、大规模运作——可能有数万名申请人,其中很多人同时也在同一个平台上投了私营部门的职位。

我们围绕 Keycloak 构建的答案:一个联合身份模型,每个租户有自己的 realm,候选人作为跨 realm 的身份类型存在。候选人的核心档案存在于共享身份空间里。他们的申请数据存在于租户 realm 里。认证流程是租户范围的——你不能用租户 B 的凭证登录租户 A 的界面——但候选人可以在多个租户同时持有活跃申请,平台不会创建冲突的身份记录。Keycloak 的 realm 联合和跨 realm token 交换让这成为可能。没有它,我们就得手工解决身份联合,而那是产生安全事故的那类工作。

2018 年”AI 驱动”意味着什么

Elevatus 的卖点包括”AI 驱动的招聘”,这在 2018 年是一个真实的表述——和很多真实的表述一样,略微比听起来复杂一点。

我们没有在做大语言模型。GPT-3 还不存在。我们做的是 ML 驱动的简历解析和评分、基于加权标准匹配的候选人排名,以及视频面试分析的早期实验。简历解析是在生产中真正好用的部分——从五种语言(阿拉伯语、英语、法语、印地语以及一些其他语言)的非结构化简历中提取结构化数据,把这些数据归一化成系统可以按职位标准排名的候选人档案。当时的挑战不是 ML 本身有多难——而是阿拉伯语简历解析的标注训练数据真的很稀缺,而模型质量反映了这一点。我们构建了一个反馈循环,招聘官的决策(录用/拒绝/进入下一阶段)反馈回排名模型,随时间在每个租户基础上改进它。

最后那部分——每租户模型个性化——在架构上很有趣,在运营上很烦人。每租户模型变体意味着一个政府部委的招聘官决策(监管合规和资质验证主导招聘标准)训练出来的模型,和一个科技公司的决策(作品集和已证明的技能主导)训练出来的模型有着实质性的不同。这是正确的结果。它也是一个如果你只跑一个全局模型就不存在的存储和服务问题。多租户应用到 ML 基础设施上是一整套额外的约束。

视频面试分析还没有生产就绪。自然语言处理视频录制的面试回答,为招聘官生成摘要评估。它能工作。但回过头来看,它是那种在 2018 年需要比我们拥有的工具支撑更仔细的偏见和公平性设计的系统。这个领域已经成熟了很多。我能说的是:我们在那些问题成为主流之前就在问”这个模型公平地反映了招聘池的多样性吗”——因为我们的客户要求如此——在明确多样性指令下运作的政府部委会对你的 AI 输出提出尖锐的问题,“模型说是这样的”不是向部长办公室给出的可接受答案。

每租户而非全局的工作流编排

Camunda BPMN 集成是那段时间我最满意的结构性决策之一。招聘工作流真的很复杂:它们有条件分支(如果职位需要安全许可,加上这些步骤),有并行轨道(技术面试和 HR 面试可以同时进行),有时间依赖的状态(如果候选人 7 天内没有响应,触发提醒,然后升级,然后关闭申请),而且在租户之间差异极大。

一个政府部委的公务员职位招聘工作流可能有 14 个阶段,涉及三个审批委员会。一个创业公司的工作流可能有 5 个阶段,一周内跑完。这两种你都没法硬编码。你也没法构建一个覆盖两者的可配置 UI。你能做的是把它们都建模成 BPMN 流程定义,每租户存储,让 Camunda 用同一个引擎执行它们。

这意味着平台的工作流层在流程定义层是每租户的,但在执行基础设施层是共享的。为一个新客户添加新的工作流变体是一次 BPMN 文件 deploy,不是代码 deploy。当你有企业客户提出具体要求、而销售团队已经承诺平台能处理那些要求时,这个区别至关重要。

入职问题

企业和政府客户的入职流程都很长、很痛苦。但痛法不同。

企业客户想要集成:通过他们已有的身份提供商做 SSO、打入他们的 ATS 或 HRIS 的 webhook、自定义品牌、供他们内部工具使用的 API 访问。他们有开发者。他们能调试 webhook payload。入职问题是可配置性的问题——平台暴露多少个旋钮,文档写得有多好。

政府客户想要合规:数据驻留文档、安全评估、对特定功能的正式审批。他们往往有早于 SaaS 时代的采购流程。他们可能需要本地部署选项或专用基础设施而不是共享云。他们有法务团队,会提出你的标准服务条款没有预料到的问题。

构建一个能同时入职两类客户的平台,是一个无法靠让平台更可配置来解决的产品和架构问题。你需要技术能力之外的组织肌肉——了解采购的人、能出具政府安全评审要求的文档的人、能把”我们需要一份符合当地数据主权法的数据处理协议”翻译成实际平台约束的人。代码是更容易的那部分。

我会做不同的事

移动层——最初是 Ionic,后来是 Flutter——是作为一个独立产品界面添加的,而且看起来也是。移动端的招聘 UX 模式和桌面端真的不同。候选人的移动端体验(申请、追踪申请状态、在手机上做视频面试)有不同于招聘官桌面端体验(审阅数百名候选人、做批量决策、跑招聘轮次)的约束。我们把移动体验构建成 Web 体验的改编版,这意味着它永远不如一个真正 mobile-first 的产品那么好。

如果我今天设计这个,候选人体验和招聘官体验会被当作两个独立的产品处理,有独立的设计约束,只是共用一个后端。API 层会从一开始就考虑两个消费者进行设计,而不是事后为了支持移动端而改造。这是每个人都靠自己学到的标准”API-first”教训。

政府实际上在用 Elevatus。这句话落地的感觉不同,当你经历过让”实际使用”成为现实所需要的采购流程、安全评审、定制谈判和集成工作之后。这不是一个被友好的早期用户使用的 MVP。这是在无法容忍停机的机构里运行关键流程的生产软件。

这就是我构建的东西。