返回博客

构建实录 / 2026-06-28

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

BOSS 求职工作台桌面界面

BOSS 求职工作台是我给自己造的求职系统:岗位抓取、简历审计、JD 定制、投递记录、公司调研,一条流程;本地优先、隐私安全,所有对招聘平台的写回都要我本人点头。后端约 9000 行 Python、全项目 16000 行。

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

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

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

我最后没做,是刻意的。那条路全是雷:投错岗位、简历被 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

技术决策与取舍

决策取舍
不做全自动海投避开投错/改乱/封号;放弃"数量",换"每一步可控"
FastAPI + SQLite 全本地隐私数据不出机器;放弃云端多端同步
store=False 由 AST 强制红线可机器验证;需维护静态检查器
模型调用集中单一执行器隐私面收敛到一处;所有调用绕这一个文件
BOSS 三级只读 + 人工确认能用不越界;牺牲"全自动"的效率
就绪判定后端权威状态不可伪造;前端只能显示

现在的状态

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

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

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