返回博客

构建实录 / 2026-06-28

给自己造一个人在环中的求职系统:用 AST 静态检查守住隐私红线

BOSS 求职工作台桌面界面

BOSS 求职工作台是我给自己造的求职系统:岗位抓取、简历审计、JD 定制、投递记录、公司调研,一条流程;本地优先、隐私安全,所有对招聘平台的写回都要我本人点头。现在后端约 2.3 万行 Python(其中约 1 万行是测试)、前端约 1.28 万行 TypeScript。

这篇讲的不是它多好用,是它的几个架构性决定——为什么不做全自动海投、怎么用静态检查守住隐私红线、怎么把"人在环中"落成代码而不是口号。

一、我没做那个最酷的功能

做求职工具,最诱人的方向是全自动海投:脚本搜岗位、改简历、点投递,一晚上投几百家。听起来很爽。

我最后没做,是刻意的。那条路全是雷:投错岗位、简历被 AI 改乱了还投出去、页面被自动保存、隐私数据在流程里到处流、一堆对招聘平台不可控的动作。省下的那点时间,不值得任何一个后果。

所以工具的原则从一开始就定死:AI 可以帮我决策和生成,但每一次对招聘平台的写回都需要我本人确认。 它不是海投脚本,是一个把人放在流程更关键位置的工作台——不是把人从流程里拿掉。

二、用 DDD 领域语言把"边界"钉死

一个人在环中的系统,最怕的是概念含糊——"简历"到底指本地事实、某个定制版本、还是 BOSS 上的在线简历?含糊一次,就可能拿错东西写回错地方。

所以项目开头没写代码,先写了一份 CONTEXT.md 领域语言,每个概念都配一个"不要叫它什么":

  • 简历事实文档:本地的简历事实源。_不是_原始文本、简历字符串
  • 简历版本:面向特定岗位生成的不可变候选版本,只有用户确认事实后才能进投递。_不是_当前简历、模型结果
  • 定制运行:一次绑定简历修订 + 岗位 + JD + 模型配置的执行。不是 AI 建议批次
  • BOSS 草稿写回:生成字段映射、填入、读回校验的本地证据流程;它永远不点击保存、提交或投递。_不是_自动更新、自动投递
  • 就绪快照:后端根据 SQLite、只读 BOSS 状态和本地产物重算的流程证据,不接受前端自行声明通过。_不是_前端验收状态

这套语言本身就是设计——"简历版本不可变""草稿写回永不点保存""就绪由后端算",这些约束在还没写代码时就已经确定了。

三、隐私红线:用 AST 静态检查强制,而不是靠自觉

简历、JD、投递记录全是隐私。谁投了哪、简历怎么改的,没理由默认发到别人服务器上。后端是 FastAPI + SQLite 全本地,数据就在我自己机器上。

但"本地优先"不够——只要还调云端模型,简历就会进模型的上下文。这里做了一个我最满意的设计:把"模型不许留存我的数据"这条红线,变成一条 AST 静态断言。

所有模型调用必须显式带 store=False(禁止 OpenAI 侧留存),而且必须集中在 openai_executor.py 一个文件里。这两条不是写在文档里靠自觉,是 model_safety.py 用 Python 的 ast 模块静态扫描出来的验收项:

missing_store = _model_calls_missing_store_false(executor_tree)
# 遍历 AST,找每个模型调用的 keyword 参数里有没有 store=False 常量
if missing_store:
    return AcceptanceRunItem(status="pending",
        issue=f"模型调用缺少显式 store=False:{...}")

outside_calls = _model_calls_outside_executor(root, executor_path)
# 扫全项目,确保没有任何模型调用散落在集中执行器之外
if outside_calls:
    return issue="模型调用必须集中在 openai_executor.py"

它做两件事:解析 openai_executor.py 的语法树,检查每个模型调用的 keyword 里是否有 store=False 的常量;再扫全项目,确保没有一个模型调用漏在集中执行器之外。任一条不满足,这个验收项就是 pending——红灯。

这样"隐私红线"就不是一句承诺,是可以被机器验证的状态。我改代码时哪怕手滑漏了 store=False,或者图省事在别处直接调了一次模型,验收就会亮红。把纪律交给静态检查,比交给记性可靠得多。

四、CDP 只读快照:三级递进,最后一步永远是我按

我最小心的步骤是"写回 BOSS 页面"——它动的是真实账号里的在线简历,错一次别人就看见了。

BOSS 相关能力全部通过 Chrome CDP,设计成三级只读递进

  1. 快照capture_snapshot):打开简历页,读一次 document.body.innerText 存到本地,mode 明确标 readonly_cdp_snapshot——不点任何东西,还等 _looks_like_loading 判定页面渲染完才读,避免抓到加载中的空壳。
  2. 字段计划capture_field_plan):扫描页面可见的 input/textarea/select/[contenteditable],从 aria-labellabel[for]、父级 label 里抽出每个字段的名字,生成一份"这个页面有哪些可填字段"的计划——仍然只读,只读 value/innerText,不写。
  3. 草稿写回:真要动手时,最多把内容填进页面草稿,但绝不自动点保存;填之前先备份当前页面文本,填之后再存一张校验快照。

这条链路让工具"能用但不越界"——它能把定制好的简历送到编辑框门口,但最后那下确认,永远是我自己按。

五、就绪判定:后端权威,前端不可信

一个多步骤流程需要"我到哪一步了"的判断。容易偷懒的做法是让前端记状态——但前端状态可以被绕过、可以过时。

这里的规则是:就绪快照由后端重算,不接受前端声明。 后端读 SQLite 的真实数据、读只读的 BOSS 状态、读本地产物,重新算一遍"现在这一步的证据够不够",返回红绿灯。前端只是显示,判断权在后端。同理,公司调研部分如果搜索后端不可用,系统只生成"检索计划"并明确标记,绝不假装查过——宁可承认"这是推断不是事实",也不编造。求职材料里编造一句,面试时崩的是自己。

六、技能编排:把求职拆成一串可定位的段

求职拆开是一串任务:搜岗位、审当前简历、按 JD 定制、生成投递脚本、记录投递、调研公司。我没把它们塞进一个巨型 prompt,而是串起几个各管一段的服务——resume_quality(审简历)、job_description/job_match(JD 定制与匹配)、boss/scraper(抓岗位)、research(调研检索)、interview_prep(面试准备)。每段输入输出清晰,出错好定位,也给人留了确认的落点。

后端提供两类能力:一类只生成任务包(不需要 API key,把技能规则、输入和产物契约写成文件),一类真正跑模型(需要 key,产物落库)。所有模型调用都走那个被 AST 盯着的 openai_executor.py

七、把"我验过了"变成可追溯的记录

真用起来之后,最先崩的不是功能,是我自己的记忆。这个岗位的简历定制到底确认过没有?上次投递前那份风险提醒我看了吗?改完就绪判定之后,全链路重跑过一遍吗?

靠脑子记这些,等于没有验收。所以加了一套验收系统:后端 acceptance.py(router + service + 测试),前端一个验收看板。

它做三件事。验收运行会被持久化,每次跑完留下记录,可以标注、可以展开看细节、可以在结果之间跳转到对应的功能区。验收快照把某个时点的后端状态固定下来,用于对比"改之前"和"改之后"。手工验收证据允许我把"这一步我人工确认过"记进去——因为这个系统的设计前提就是人在环中,那么人做过什么也必须留痕,否则人在环中就变成了口头承诺。

看板上还有一个"下一步该验什么"的提示。这个小功能的价值超出预期:它把验收从"想起来才做的事"变成了一条有指向的路径。

八、简历不再是一个文件,而是有版本的资产

早期版本里简历就是一份数据,改了就是改了。这在真实求职里很危险——针对 A 公司改完的版本,投 B 公司时可能已经面目全非,而原始版本找不回来了。

现在的处理:支持从 PDF 导入简历(导入前先备份当前版本,导入后先给我审阅再决定应用),提供结构化的模块编辑器,手动草稿可以存成版本,恢复某个版本之前同样先备份,未保存的改动会被拦下来提醒。

还有一条我加得比较狠的规则:版本事实确认。批准一个简历版本之前,系统会要求我确认里面的事实陈述——因为定制过程中模型可能润色出一些"听起来更好但不完全准确"的表述。这道确认不是走过场,它就是我在这个项目里最在意的那条线:AI 可以帮我组织语言,但不能替我承担事实责任。

九、调研这块的诚实设计

投递前的公司调研包接了 Exa 作为检索后端,同时保留了一条公开检索的兜底路径。

比功能本身更重要的是呈现方式:调研结果会显示来源片段抓取时间戳,报告里每条来源还标了它来自哪条检索路径;界面在走兜底时会说明原因。可以复用最近的调研报告,避免重复消耗。

这么做的原因很简单——调研内容是要被我拿去跟 HR 聊的。如果我不知道某条信息是什么时候抓的、从哪儿抓的,那它就是个风险而不是助力。能追到来源和抓取时间的信息才叫情报,追不到的叫印象。(注意这里是抓取时间不是内容发布时间——后者目前还没落到字段里,属于已知缺口。)

技术决策与取舍

决策取舍
不做全自动海投避开投错/改乱/封号;放弃"数量",换"每一步可控"
FastAPI + SQLite 全本地隐私数据不出机器;放弃云端多端同步
store=False 由 AST 强制红线可机器验证;需维护静态检查器
模型调用集中单一执行器隐私面收敛到一处;所有调用绕这一个文件
BOSS 三级只读 + 人工确认能用不越界;牺牲"全自动"的效率
就绪判定后端权威状态不可伪造;前端只能显示
验收记录持久化人工确认也留痕;多一层维护成本
简历版本化 + 事实确认定制不丢原件、润色不夹带事实错误;多两次确认操作
调研显示来源与时间戳情报可核实;界面信息密度变高

现在的状态

核心链路已跑通:岗位搜索抓取、简历审计、JD 定制、本地简历编辑与预览(编辑器形态借鉴 Magic Resume,做成本地轻量版)、岗位匹配报告、投递任务包(30 秒自我介绍、BOSS 开场白、HR 话术、面试追问、面试准备检索词、投递前风险提醒)、BOSS 只读快照与写回计划、投递记录和公司调研包。

后来又长出一批:验收看板与验收快照、简历 PDF 导入与结构化模块编辑器、简历版本与备份回填、岗位定向面试题预测、岗位匹配 rubric 升级与 BOSS JD 措辞解码、带抓取时间戳的调研包,以及一份中文 OpenAPI 接口文档。规模上后端约 2.3 万行 Python(其中约 1 万行是测试),前端约 1.28 万行 TypeScript。

它对我是双重身份:一个我真在用的求职工具,也是一个能拿来讲 AI 应用工程的项目——多技能编排、Chrome CDP 集成、人在环中的隐私设计,正是我想让人看到的东西。

我最喜欢的一点是:它没把人从求职里拿掉。它接管了体力活——抓取、审阅、生成,把判断和那下确认的按键留给了我。而"AI 不许留存我的简历"这条我最在意的红线,不是靠我记着,是靠一段 AST 检查每次验收都替我盯着。