career-ops calibrate 模式实战:用确定性校准报告验证评分体系是否真的预测了求职结果
2026/9/7 2:42:44 网站建设 项目流程

career-ops calibrate 模式实战:用确定性校准报告验证评分体系是否真的预测了求职结果

【免费下载链接】career-opsOpen-source AI job search: scan job portals, evaluate listings into a structured A-H report with a global 1-5 score, tailor your CV, track applications — runs locally in your AI coding CLI (Claude Code, Codex, OpenCode, Antigravity…)项目地址: https://gitcode.com/GitHub_Trending/ca/career-ops

本篇基于 career-ops 仓库的 calibrate 模式定义 及其核心脚本 calibrate.mjs,讲清这个"学习闭环"的完整工作机制:如何运行node calibrate.mjs --json生成分数段(score band)× 面试率 / offer 率的校准报告,如何解读 separating / flat / inverted / insufficient 四种判定,以及脚本在证据优先级、小样本底线(honest floors)、在途申请剔除等细节上的确定性实现。读完你能独立完成一次自我校准,知道报告的每个数字从哪来、哪些数字被刻意 withheld、以及这个模式为什么"只报告证据、从不改规则"。

一、calibrate 模式在项目中的定位:闭合"评估 → 结果"的学习闭环

career-ops 的整体工作流是:扫描职位 → 生成 A–H 结构化评估报告并给出 1–5 分 → 按分数决定投递 → 记录结果。calibrate 模式对应模式表中的这一行(见 modes/README.md):

calibrate— Advisory report: do your evaluation scores predict your real outcomes? Reads/outcomedata; never changes scoring

它解决的核心问题在 modes/calibrate.md 的标题里就直接写明了:评分是否预测了你自己的结果(Does the scoring predict YOUR outcomes?)。此前评分只负责"事前排序",但从没有回头核对"高分投递是否真的转化得更好"。calibrate 闭合的正是这条学习闭环(issue #1724):由 /outcome 模式 记录到data/outcomes/的结果日志会被读回来,与当初投递前的评估分数做对照分析。

与 /outcome 模式 的分工是明确的:outcome 是写入端(对话式记录结果类型、阶段、逐字反馈,并把档案归档到data/outcomes/{num}_{company_slug}_{role_slug}/);calibrate 是读取端(聚合这些日志与 tracker,输出校准统计)。写入端支持的结果类型与 CLI 参数(interview_progressoffer_receivedhiredoffer_declinedrejectedno_responseinterview_only等)在 outcome 模式文档中有完整定义,calibrate 报告中的每一个计数都以这些数据为原材料。

二、三条不可协商的约束(Non-negotiables)

modes/calibrate.md 把模式的边界浓缩为三条硬性约束,这三条贯穿了脚本的全部实现:

  1. Advisory only(仅建议):这个模式永不编辑评分规则、阈值、modes/_shared.md或任何配置。它报告证据,用户自行决定如何使用。calibrate.mjs 的文件头注释同样声明:"Strictly advisory. This script never edits scoring rules, thresholds, modes/_shared.md, or any user file — it reads, aggregates, and reports."从源码看,整个脚本只调用了readFileSync/readdirSync/existsSync,没有任何写文件操作。
  2. Deterministic(确定性):所有数字来自calibrate.mjs的纯本地解析——无网络、无 API key、无 LLM 计算。Agent 呈现报告时不得重新计算、调整或"优化"脚本打印的任何比率。
  3. Honest floors(诚实底线):脚本把某个比率标记为(n too small)时,就按原样呈现。绝不允许把"3 个里成了 2 个"的轶事包装成百分比。

三、运行方式与完整参数

模式文档给出的标准调用是:

node calibrate.mjs --json

结合 calibrate.mjs 文件头的 Usage 说明与 CLI 入口实现,完整的运行形态如下:

命令行为
node calibrate.mjs输出人类可读报告(renderHuman
node calibrate.mjs --json向 stdout 输出完整 JSON,供 Agent/模式流程消费
node calibrate.mjs --min-band-n 5调整每段的样本量下限(floor),默认 5,必须是 ≥1 的整数,否则以退出码 2 报错
node calibrate.mjs --self-test运行内置自测(覆盖聚合逻辑的各分支,不触碰文件系统)

CLI 入口的执行顺序(见 calibrate.mjs 末尾的 main guard 部分):

  1. 若带--self-test,先跑自测;
  2. 解析--min-band-n,非法值直接报错退出;
  3. 通过resolveTrackerPath()定位 tracker(data/applications.md);
  4. 若 tracker 不存在,打印No tracker found at ... — nothing to calibrate yet.并退出——模式文档要求此时如实告知用户,并指向/outcome:闭环需要先有记录的结果才谈得上任何结论,不要用猜测去填补空白
  5. 依次加载 tracker 行、结果日志(journals)、阶段台账(ledger),调用核心聚合computeCalibration(),最后按--json与否输出。

四、报告结构与呈现顺序

模式文档规定呈现必须按以下顺序(这也是renderHuman()的实际输出顺序):

  1. 判定句(Verdict),逐字呈现——它是报告的主标题。四种判定:
    • separating:评分对你是有区分力的——高分段面试率显著高于低分段,"相信分数正在得到回报";
    • flat:尚无明显区分,样本积累后通常会朝某个方向收敛;
    • inverted信号反转——低分段转化得比高分段更好,此时不应继续盲信下一个 4.5 分,而应先回读高分被拒报告的共性;
    • insufficient:已解决的结果不足以比较分数段,提示"继续用/outcome记录并重跑"。
  2. 分数段表(score band × n × 面试率 × offer 率),按脚本打印的原样呈现;
  3. 在途(in-flight)计数:仍处于 Applied/Responded 且未记录结果的申请——这正是总数与 tracker 行数对不上的原因;
  4. 已记录的反馈信号(如有):把这些反馈当作关于你这次求职的数据来引用;若多条反馈出现同一模式(例如同一个技能缺口被两次点名),用一句话指出它。

一个renderHuman生成的报告骨架如下(数字为示意):

📐 Calibration — your scores vs your real outcomes Resolved outcomes: 12 · in flight (not counted): 5 · unscored: 2 | Score band | n | Interviews | Offers | Interview rate | Offer rate | |---|---|---|---|---|---| | <3.5 | 3 | 0 | 0 | (n too small) | | | 3.5-3.9 | 5 | 2 | 0 | 40% | 0% | | 4.0-4.4 | 4 | 2 | 1 | 50% | 25% | | >=4.5 | 0 | 0 | 0 | (n too small) | | Verdict: No meaningful separation yet: ... Signals from recorded feedback (latest 10): - #7 Acme: "Strong system design, gaps in Go depth" This report is advisory. It never changes scoring rules — that conversation belongs to you.

五、源码纵深:分数段、样本底线与判定阈值

5.1 四个固定分数段

分数段的划分硬编码在 calibrate.mjs 中:

const BANDS = [ { key: '<3.5', min: -Infinity, max: 3.5 }, { key: '3.5-3.9', min: 3.5, max: 4.0 }, { key: '4.0-4.4', min: 4.0, max: 4.5 }, { key: '>=4.5', min: 4.5, max: Infinity }, ];

对应 career-ops 全局 1–5 分制评估报告的分数输出(4.4/5这类单元格形式会被parseFloat正确解析——它在斜杠处停止;空白或文本分数变成null而不是 0,避免一行"无分"申请被静默落进最低分段)。

5.2 样本底线:(n too small)的实现

computeCalibration()对每个分数段的处理(calibrate.mjs):

  • 段内样本数n ≥ minBandN(默认 5)时,输出四舍五入的百分比比率;
  • 不足时,计数仍会显示(读者仍能看到原始轶事),但比率被置为null,渲染为(n too small)

这就是"Honest floors"约束的代码形态:小样本下拒绝制造伪比率。

5.3 判定逻辑:±10 个百分点的判定带

判定只比较通过样本底线的最高段与最低段(calibrate.mjs):

  • 不足两个可比段 →insufficient("这不是比较,就明说");
  • 差值gap = 高段面试率 − 低段面试率
    • gap ≥ 10separating
    • gap ≤ −10inverted
    • 其余 →flat

--min-band-n因此是"灵敏度旋钮":样本少时调低它能看到更多比率,代价是离诚实底线更近——这正是模式文档要求"脚本 withheld 就照实呈现"的原因。

六、源码纵深:三源证据的优先级

这是 calibrate 最有信息量的部分。每一行申请的结果可能来自三个来源,优先级固定(calibrate.mjs):

  1. 结果日志(journal)——data/outcomes/{...}/outcome.md,由outcome.mjs写入,记录"实际发生了什么",优先级最高;
  2. 阶段台账(ledger)——data/status-log.tsv(由set-status.mjs写入的状态迁移流水),记录一行经过过哪些阶段。因此一个已拒 offer 现在停在 Discarded 的行仍计为"达到过 offer";一次以拒绝告终的面试仍计为"达到过面试";
  3. tracker 当前状态——只是工作流快照,会丢失历史,因此优先级最低。

6.1 结果类型的两层语义

日志侧的语义表JOURNAL_OUTCOMES(calibrate.mjs)把七种规范结果映射到两个累积层级

结果类型reachedInterview(一级)reachedOffer(二级)terminal
hired/offer_received/offer_declined
interview_progress
interview_only
rejected/no_response

注意两个容易误读的点:拒掉 offer 仍算二级达成(offer_declined 证明评分选中了一个能匹配的角色);interview_only(面完无果)仍算一级达成。tracker 侧的兜底表TRACKER_TERMINAL同理:只有interview/offer/hired/rejected能独立定论,applied/responded判定为在途,evaluated/discarded等无阶段历史的行则完全在校准人群之外

6.2 日志解析:"最后一条 entry 才是真相"

parseOutcomeJournal()(calibrate.mjs)按## Entry:切分 append-only 日志,关键契约是最后一条 entry 代表当前真相——一个先interview_progressrejected的申请必须读作 rejected,而不是"它历史上最幸福的时刻"。实现上,只要存在 Outcome Type 字段就无条件覆盖latestType(包括解析为 null 的情况),防止最后一条无法识别的 entry 让上一条"幸存";无法识别时回落到 tracker 状态,这才是诚实答案。反馈只收集>引用的逐字内容,跳过None recorded空标记。

6.3 在途剔除:为什么总数对不上 tracker 行数

applied/responded且无日志结果的行被推入inFlight并从所有比率中排除。源码注释解释得很直白:把它算作失败会惩罚最近的申请,算作成功会美化一切——它既不是数据点,也不该消失,所以单独计数呈现。这正对应模式文档中"呈现 in-flight count"的固定步骤。

6.4 日志目录的分裂与合并

日志目录形如{num}_{company_slug}_{role_slug}。在早期版本中,目录名由 tracker 行当时的文本派生:两次记录之间编辑了 Role 单元格(比如把 "Senior Backend Engineer" 规范成 "Sr. Backend Engineer"),第二条 entry 就会落到新目录,一个申请被劈成两个半份日志。现在由 lib/outcome-dir.mjs 的outcomeDirsFor()按行号前缀收集全部候选目录,按 outcome.md 的 mtime 最新者优先排序,calibrate.mjs读取dirs[0];若确实存在分裂(dirs.length > 1),会在结果中标记splitAcross报告而非静默修复——修复意味着移动用户档案,一个只读报告不该做这件事。回归测试 tests/outcome-journal-fork.test.mjs 用真实 CLI 复现了"两次记录之间编辑 Role"的场景,断言目录数始终为 1 且 append-only 历史完整保留。

七、单一结果词汇表:防止读写两端漂移

outcome.mjs接受的结果类型会原样写入日志。任何读取日志的一方必须接受写入端接受的全部拼写,否则记录的结果对它就是隐形的。lib/outcome-types.mjs 是这一词汇表的唯一事实源:

  • OUTCOME_MAP14 种接受拼写 → 各自的 tracker 状态与默认备注;
  • CANONICAL_OUTCOMES是其中7 种规范名outcome.mjs的 USAGE 行宣称的集合);
  • 其余 7 种是别名(offeroffer_receiveddeclinedoffer_declinedghostedno_responseacceptedhiredrejectionrejectedinterview/stage_reachedinterview_progress),由canonicalOutcome()统一解析(小写、-归一为_,所以手改日志里写的Offer-Declined也能解析)。

这个设计有一个血泪背景,lib/outcome-types.mjs 的注释与 tests/outcome-vocabulary-drift.test.mjs 都记录了:calibrate 曾携带一份私有的 7 项词汇副本,导致declinedghosted在校准中彻底消失——而"拒掉 offer"恰恰是评分预测良好最有力的证据之一。现在该模块在加载时做覆盖率检查(OUTCOME_MAP 里出现的新拼写若无规范含义会直接 throw),漂移测试则双向断言:写入端接受的每种拼写都必须能解析到 calibrate 的JOURNAL_OUTCOMES中的一个键,且JOURNAL_OUTCOMES恰好等于规范集合,不多不少。

八、Tracker 列映射:一个曾导致"全体静默掉出人群"的坑

loadTrackerRows()依赖 tracker-parse.mjs 的resolveColumns()定位表头。该函数期望整行数组(自己在其中找表头);早期版本误传单个表头字符串,导致逐字符迭代找不到表头,静默回落到无 Via 列的 9 列旧版列映射。后果在带可选 Via 列(位于 Company 与 Role 之间,#1596)的 tracker 上最典型:Role 起所有字段左移一格读取,status读到的是分数单元格("4.5/5"),没有任何状态能匹配,每一行都静默掉出人群——报告呈现"0 resolved / 0 in-flight"和一条虚假的"数据不足"判定,用户却以为自己只是"历史还不够"。回归测试 tests/calibrate-tracker-colmap.test.mjs 同时覆盖带 Via 的 10 列 tracker 与旧版 9 列 tracker,断言 status、score、company 均读到正确列。

九、这个模式永远不做的事(Must-never 清单)

modes/calibrate.md 末尾的禁止清单值得逐条内化,因为它定义了 Agent 与校准数据的正确相处方式:

  • 不建议编辑modes/_shared.md或任何评分规则。如果用户问"那我是不是该改评分?",诚实的回答是:全局评分体系保持不变——证据支持的是调整你自己的投递阈值与目标组合,这是用户的决定。
  • 不把校准结果自动回灌到评估中。career-ops 中任何地方都没有 auto-tuning,这个模式也不会引入它。

同时,inverted判定下的正确后续动作也写得很清楚:读一读高分被拒报告的共性——这是一段对话(可以逐份与用户过那些具体报告),而不是自动重打分。

十、验证与回归保障

围绕 calibrate 的确定性契约,仓库提供了三层验证:

  1. 内置自测node calibrate.mjs --self-test直接对内存数据驱动parseOutcomeJournal/computeCalibration,覆盖"最后一条 entry 胜出"、separating/inverted/insufficient 判定、小样本 withheld、journal 优先于 tracker、在途与未投递行排除、ledger 证据解析等分支(calibrate.mjs)。
  2. 列映射回归:tests/calibrate-tracker-colmap.test.mjs 锁定loadTrackerRows对两种 tracker 形态的解析。
  3. 词汇漂移防护:tests/outcome-vocabulary-drift.test.mjs 从OUTCOME_MAP派生全量拼写普查,新别名加入写入端的当天就会被覆盖到。

CLI 入口还有一个工程细节:--self-test放在 main guard(lib/is-main-module.mjs 的isMainModule检查)之内——否则任何导入本模块做单测的测试进程都会对真实 tracker 跑一遍完整 CLI。

十一、实操建议:什么时候跑 calibrate,怎么用它

结合文档与源码,实用的使用节奏是:

  • 前提:tracker(data/applications.md)存在,且已用/outcome记录了若干结果。每段需 ≥5(默认 floor)个已解决的结果,两个分数段达标才有可比性——也就是说,积累到约 10 个已闭环申请后,报告才开始有判定价值。
  • 每次收到新结果后重跑node calibrate.mjs。判定从insufficientflatseparating/inverted的演进本身就是求职数据的累积过程。
  • separating:维持现有策略,分数可作为投递优先级信号;
  • flat:继续记录,不做任何规则改动;
  • inverted:最值得花时间——按模式文档的建议,把高分被拒的报告拉出来与用户一起过,找出高分假设与真实结果的系统性偏差(如目标公司类型错配),随后调整的是个人投递阈值与目标组合,而非全局评分。

需要牢记的是这个模式的性质:它输出的是"评分体系在你身上表现如何"的证据快照,是本地、确定、可复算的(同一份 tracker + 日志 + 台账,任何时候重跑结果一致)。career-ops 用"读取、聚合、报告、绝不回写"这一设计,把校准从一条可能自我强化的自动化回路,变成了一个人主导的反思工具。

【免费下载链接】career-opsOpen-source AI job search: scan job portals, evaluate listings into a structured A-H report with a global 1-5 score, tailor your CV, track applications — runs locally in your AI coding CLI (Claude Code, Codex, OpenCode, Antigravity…)项目地址: https://gitcode.com/GitHub_Trending/ca/career-ops

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询