开发纪律:一次只做一个 task,做完立刻运行验证,通过后 commit,再开下一个。 每个 Sprint 都建立在上一个已跑通的基础上。骨架(Sprint 0)务必先跑通再往下走。
目标:python -m src.main 能用写死的输入完整跑完一次「JD + Resume → 计划 → 提问/追问 → 报告」。
不碰视频、语音、多模态、前端、数据库、Redis、Milvus。
- 初始化项目:uv 依赖、目录结构、
.env.example、.gitignore -
src/schemas/:用 pydantic 定义 JobContext / CandidateProfile / Competency / Question(含 type + category: KNOWLEDGE | PROJECT_EXPERIENCE) / InterviewPlan / CandidateAnswer / InterviewSession / EvaluationReport -
src/llm/:anthropic SDK 的最小封装(统一入口,读环境变量;无 key 走 stub 让骨架可本地跑通) - Planner:根据写死 JD + Resume 生成 1 轮 / 2 维度 / 4 题 (每维度 1 道基础知识题 + 1 道项目深挖题)
- Interviewer:提一题,依据回答(简单逻辑)决定是否追问一次
- Evaluator:根据对话产出结构化报告(content / performance 严格分区)
- Analyzer:留空占位(返回空信号)
- Orchestrator:串起全链路,写死 JD + Resume + 几条候选人文本回答
-
src/main.py:跑完并打印 EvaluationReport
完成标准:命令行跑通一次,输出一份可读的结构化报告。
- 接入 Postgres,为 InterviewSession / EvaluationReport 建表与 ORM
- 接入 Redis:进行中的会话状态存 Redis(热存储),结束后归档 Postgres
- Orchestrator 改造:会话状态机基于 Redis 读写(start/submit/resume/finalize 三段式 + 中断恢复)
- 给 LLM 调用与 JD 解析结果加 Redis 缓存(透明缓存 src/llm/complete,stub 不入缓存,Redis 不可用降级)
- 写第一个 eval:固定输入 → 校验报告结构与关键字段(含合规分区不变量护栏)
完成标准:会话可中断后从 Redis 恢复;结束后能在 Postgres 查到归档。 ✅
- FastAPI 骨架:健康检查 + 基础路由(create_app 工厂;/health 不查上游)
- 接口:创建职位(POST /jobs;server 生成 job_id;异常映射 503/422)
- 接口:候选人上传 Resume,关联到某职位 → 触发 Planner 生成 InterviewPlan(JD + Resume) (POST /jobs/{id}/candidates 异步触发 Planner via BackgroundTasks;GET 轮询 plan)
- 接口:创建/进行面试会话(POST /interviews + POST /answers + GET 中断恢复)
- 接口:获取评估报告(GET /interviews/{id}/report 隐式 finalize,幂等,IN_PROGRESS→409)
- Planner 接真实 JD + Resume 文本解析(替换写死输入)—— 通过 API 自然实现: Planner 接受任意上传文本,src.main 写死输入仅作 Sprint 0 demo 用
完成标准:用 HTTP 完整走完一次文本面试,全部经 API。
- 接入 Milvus(Lite, in-process),questions + documents 两个 collection
- 公司资料 / JD / 候选人 Resume 切片 → 向量化 → 入 Milvus(POST 时挂 BG 任务)
- Planner 改造:knowledge 题从 Milvus 召回 + LLM 精修;project 题走 Resume 切片 RAG; Question.source_question_id / source_chunk_ids 记录召回溯源
- 面试与评估时对公司资料 RAG 召回相关片段(Evaluator summary 接 JD+公司资料)
- 扩展 eval:组件级 RAG eval + 端到端 provenance + 跨 job/candidate 隔离
完成标准:计划与提问明显更贴合岗位与公司语境,且可追溯到召回片段。 ✅
附加: Sprint 3 中段把 LLM 从 anthropic 切到 OpenAI 单一 provider(embedding 一直 就是 OpenAI,consolidate 简化 key/计费)。
- Next.js + TypeScript 初始化,候选人面试界面(Next.js 16 + App Router + Tailwind 4)
- 候选人端: 面试前 Resume 上传页(仅 paste 模式;文件上传移到 Sprint 5)
- 对接面试会话 API,文本一问一答 + 追问(含 localStorage 中断恢复 + 答题草稿)
- 会话进度、剩余轮次展示("第 M/N 题" + 追问 amber 徽章;总数从 plan 取,不写死)
- 基础鉴权(candidate_id 作 path soft-auth;JWT 移到 Sprint 5 与 HR 端一起做)
完成标准:候选人能在浏览器里完成一次完整文本面试。 ✅
附加: 加 CORS 中间件、GET /jobs/{id} 与 GET /candidates/{id} 两个候选人端
读取端点;全局 error.tsx 错误边界;Resume 长度三档颜色反馈。
- HR 端:创建职位、上传资料、查看生成的面试计划
(JWT 鉴权 +
/hrDashboard +/hr/jobs/[id]单 job 详情;候选人邀请链接 dev 期直接展示) - 候选人列表与面试状态(四态徽章 plan_pending / ready / completed / reviewed)
- 评估报告查看页:内容维度与表现维度分区展示 (overall banner + summary + content_scores 主区 + performance_observations 副区"仅参考" + RAG chunks 可折叠)
- 人工复核:HR 可标注/覆盖、留存复核记录 (comments / 按维度 score+note 覆盖 / 三态 decision;MVP 覆盖语义不做版本历史)
完成标准:HR 能从建岗到查看报告全程在界面完成。 ✅
附加:
- 后端 JWT (HS256 + bcrypt) +
/auth/login+require_hr_userdependency scripts/seed_users.py种 HR 账号POST /interviews/{id}/finalize返 204 让候选人 done 页自动归档- 推到 Sprint 6:Resume 文件上传 (PDF/docx 解析)、cookie + same-site=strict 鉴权升级
在 Sprint 6 视频面试之前做。让 agent 的题目结构和企业实际招聘流程对齐; 视频化只是输入/输出层的改造,agent 内核届时无需重做。
背景:当前 Planner 写死「2 维度 × 2 类别 = 4 题」(技术深度/沟通协作 × 知识/项目)。 HR 不能配置,校招社招走同一套,没有自我介绍 / 场景题。Sprint 5.5 把出题结构改为 track-aware 多阶段,并补齐自我介绍 + 场景题两个缺失环节。
-
数据契约扩展 + HR track 选择:
JobContext.track: "campus" | "lateral"(默认 lateral,DB 增量加列);QuestionCategory加SELF_INTRO/SCENARIO;InterviewRound.stage字段(self_intro / knowledge / project / scenario);InterviewSession.intro_text字段(存候选人自我介绍全文,供后续题目用); HR 端 POST /jobs 接收 track,新建表单加 campus/lateral 单选 -
场景题库 + seed 脚本扩展:
seed_questions表加 category 列区分 knowledge / scenario;scripts/seed_questions.py --category参数; 种 backend 场景题 ~15 道(如"线上 P99 突然涨 10 倍, 5 分钟内做什么"风格) 实际落地: 15 技术深度 + 5 沟通协作 = 20 道, 落 PG; Milvus 由 task 4 drop + reseed 时一起做进去, category 过滤召回验证通过 -
Planner 改造为 track-aware 多阶段(项目题方案 A:懒生成): 校招 7-8 题:自我介绍 1 + 基础知识 3 + 项目深挖 2(占位)+ 场景 1-2; 社招 7-9 题:自我介绍 1 + 项目深挖 3-4(占位)+ 场景 2-3 + 基础知识 1; 旧的「2 dim × 2 cat × 4 题」覆盖式退役(无 fallback,避免双路径); Plan 数据契约支持「占位题」(text 为空、
lazy: True),等 interviewer 进入 project stage 时再生成 实际落地: campus = [1,3,2,1] = 7, lateral = [1,3,2,1] = 7; Question.competency_id 改 Optional (self_intro=None 不进 content_scores); InterviewPlan.competencies 顶层权威 (跨 stage 共享, Evaluator 走顶层) -
Interviewer 跨阶段 + 项目题懒生成: 自我介绍 turn 不触发 followup 启发式(30 字也不追问); 自我介绍全文落
InterviewSession.intro_text; 进入 project stage 时实时生成项目题(用 intro_text + Resume RAG), 生成期间前端 UI 显示「思考中...」(前端轮询或 SSE 待定,先轮询); 至多 1 次追问/题 这条保持;后续如要升级追问启发式,留 Sprint 5.6+ 实际落地: lazy resolve 移到 orchestrator.submit_answer (检测 next 题 lazy+empty 时整 plan 一次性 resolve, 写回 cache, 用 question_id 找回灌后 的题); _needs_followup(question, answer) 加 question 入参, SELF_INTRO 硬豁免; 前端 isNextTurnLazyProject 启发式预测 + 按钮文案 "思考中... (准备项目题, 约 3-5 秒)" -
HR 详情页阶段视图 + 端到端 eval: HR 候选人详情页:在报告区上方加「面试阶段」视图,每 stage 显示题数 + 每题来源(题库 ID / Resume chunk ID / 场景题库 ID); 端到端 eval:campus job → 简历 → 自我介绍 → 走完 8 题 → 报告 OK; 端到端 eval:lateral job → 同上,验证 knowledge 题数远少于 project; eval:项目题 prompt 确实包含 intro_text(懒生成路径生效) 实际落地: StageView 显示 4 类 stage card (颜色编码) + describeSource 把 knowledge/scenario 显示题库 ID, project 显示 Resume 切片数, self_intro 显示 "固定模板", lazy 未生成显示"待懒生成"; 端到端 eval campus 7 题 walk + lateral knowledge<project + magic-marker intro_text 流转锁住 lazy gen 路径
完成标准:HR 创建 job 时选 track;候选人按 track 对应的多阶段流程完成面试; 项目深挖题真的反映自我介绍里提到的内容(懒生成路径生效)。 ✅
附加:
- evals 数: Sprint 5 收尾时 166 → Sprint 5.5 收尾 212 (+46), 全绿
- Milvus questions collection drop + reseed (30 knowledge + 20 scenario), category 过滤召回 OK; documents collection 顺带 drop, dev 期不阻塞
- 5 个 commit 串完成: 6688804 / 8fe7615 / adaf640 / 5d9f283 / 977e5a5
Sprint 5.5 之后做。把追问从"字数+关键词启发式"换成基于结构化 AnswerAssessment 的 信号充分性判断。阈值校准、延迟控制、降级路径在设计阶段写进,不留事后补。
背景:当前 _needs_followup 只看长度 + 关键词,与题目语义、岗位维度、回答质量
无关;每题至多 1 次硬规则的追问。Sprint 5.6 把这套换成"先评估、再决策、必要时
追问"的三段式,但全程保留启发式 fallback 防 LLM 异常卡死面试。
-
AnswerAssessment schema + Assessor agent: 新 schema
AnswerAssessment(sufficiency / confidence / missing_signals[] / strengths[] / concerns[] / followup_goal / stop_reason); 新 agentsrc/agents/assessor/(独立模块, 不挂在 Evaluator 上 —— Evaluator 只在结束时跑、是同步重调用, Assessor 是每 turn 的实时轻调用, 并发模型不同);assessor.assess(question, answer, session, plan)入口 实际落地: sufficiency / confidence 都是 float ∈ [0,1] (用 pydantic ge/le 校验); stop_reason 是字符串 (sufficient_signals / low_value / diminishing_returns 之一, 未做硬 enum 留 v1 灵活); LLM 输出 JSON parse 失败 / stub / timeout 一律 fallback, _LLMStubFallback 单独类区分"期望中的 stub"和"真异常"防日志噪声。 -
FollowUpPolicy + stage-aware 配额: 新 schema
FollowUpPolicy(max_followups_per_question / min_sufficiency_to_stop / min_confidence_to_stop); stage 默认值:self_intro=0 / knowledge=1 / project=2 / scenario=2; 允许JobContext.followup_policy覆盖 stage 默认(HR UI 留到 5.7); Interviewer.next_turn 改为三步:assess_answer→decide_followup→generate_followup实际落地: FollowUpPolicy.for_stage(stage) classmethod 返回硬编码 stage 默认表; JobContext.followup_policy 字段未加 (留 5.7 一起做 HR 配置 UI); assess 步由 orchestrator 在 next_turn 之前调 (维持 "agents 之间不互调" 约定), assessment 流转走 session.assessments[-1], Interviewer 从 session 反向找。 -
校准 eval(先做, 过不了不上生产路径): 手工标 20-30 个固定样本(明显足够 / 明显不足 / 模糊), 覆盖 4 个 category; 跑 assessor 比对标注, 验证"足够"的平均 sufficiency > "不足"的平均(看排序, 不要求绝对值对得上); 标注集进
evals/data/assessment_calibration.json; eval 不过 → Assessor 不上线, 路径仍走原_needs_followup实际落地: 24 条样本 (knowledge 7 / project 7 / self_intro 4 / scenario 6), 启发式 fallback 路径 sufficient 均值 0.933 vs insufficient 均值 0.093, gap 0.840 = strong pass; ambiguous 5 条只 record 不参与 pass/fail; 真 LLM 路径校准必须人工 PR review 才翻 ASSESSOR_ENABLED=true。 -
延迟 + 降级控制: Assessor 用
gpt-4o-mini(不要 gpt-4o); 所有 LLM 调用 try/except + 10s 超时;任意异常 fallback 到原_needs_followup(保留不删, 作为兜底); LLM cache 自然 cover 重复 prompt; 前端 session 页加"分析中..." 状态(submit 后到下一 prompt 前显示, 避免 用户以为卡死) 实际落地: src/llm/complete() 加 timeout 参数透传 openai SDK; Assessor 写死 gpt-4o-mini + 10s + 600 max_tokens (不读 OPENAI_CHAT_MODEL env 防被切到贵慢的); _decide_followup 在 assessment is None 时退到 _needs_followup, _needs_followup Sprint 0 函数保留不删; 前端按钮文案双档 "分析中... (评估回答)" / "分析中... (评估 + 准备项目题, 约 5-8 秒)"。 -
followup 生成更聚焦: 用
assessment.followup_goal喂给_followup_text, 生成的追问明确指向 missing_signals(如"补一个量化数据" / "讲清你做了什么决策"), 不再泛泛 "展开一个具体例子" 实际落地: _followup_text 加 assessment 入参, prompt 拼 missing_signals + followup_goal; LLM 不可用时模板也拼 followup_goal ("能聚焦讲一下: {goal}"), 即使没 LLM 也比泛泛模板聚焦。
完成标准:assessor 在校准 eval 上能区分"足够 vs 不足";面试链路任何 LLM 失败 都降级到启发式不卡死;前端提交答案到看到下一题的体感延迟可接受(<5s 含降级)。 ✅
附加:
- evals 数: Sprint 5.5 收尾 212 → Sprint 5.6 收尾 233 (+21 单元+校准+集成), 全绿
- ASSESSOR_ENABLED env flag 默认 false: Assessor 代码进 repo 但不参与追问决策, 与 Sprint 5.5 行为一致; calibration eval + 人工 review 真 LLM 路径后翻 true 上线
- InterviewSession.assessments Redis 写入 OK; PG 列 + HR UI 留 Sprint 5.7
- 1 个 commit 串完成: 4ce85e0
Sprint 5.6 之后做。如时间紧可推后, Sprint 6 视频面试不依赖本 sprint。 把 5.6 在线产出的 assessments 落库、给 HR 看;把"面试结束"从"题答完"升级到 "维度证据足够"。显式不做动态补题(可能无限循环 / 报告题目数对不上 plan / 生成质量无保证), 真要做单独立 5.8 评估风险后再决定。
-
数据契约扩展:
InterviewSession.assessments: list[AnswerAssessment] = [](增量加 JSONB 列);EvaluationReport.competency_coverage: dict[str, float] = {}(每维度证据 充分性聚合, 0~1); orchestrator.submit_answer 调 assessor 后把结果追加到 session.assessments 实际落地: assessments JSONB 列 ALTER 加完 (server_default '[]'); coverage JSONB 列 ALTER 加完 (server_default '{}'); orchestrator.submit_answer 在 Sprint 5.6 已经追加 (ASSESSOR_ENABLED 启用时), 本 sprint 把它落 PG。 -
HR 详情页 assessment 视图: 候选人详情页报告区上方加「面试过程」视图; 每题展示 question + answer + assessment.missing_signals + concerns + strengths (展示自然语言字段比展示 sufficiency 数字可信); followup 题展示 followup_goal("为什么发这条追问"); 不让 HR 直接看 sufficiency 数字(避免被无校准模型误导) 实际落地: AssessmentView 在 StageView 与 ReportView 之间; 按 interviewer/candidate turn 配对显示, followup turn 借父题 followup_goal 解释"为什么追问"; sufficiency / confidence 字段在 API 返回但前端 UI 不 渲染 (合规守在前端层, 不剥离 API 层让"内部诊断"未来仍可拿)。GET /hr/sessions/{id} 新端点拉完整 session。
-
CompletionPolicy(按 coverage 决定结束): 新 schema
CompletionPolicy(min_competency_coverage / max_total_questions / mandatory_competencies[]); Interviewer.next_turn 末尾判断:所有 mandatory 题答完 + 每个 mandatory competency 的 coverage >= 阈值 → 结束;否则只要还有非 lazy 题就继续; 硬上限max_total_questions(如 15)封顶防无限循环; 不做动态补题:超出 plan 不允许临时生成题 实际落地: src/coverage.py 单一计算源 (compute_coverage 取 max(sufficiency) per competency, self_intro 不计; 老 plan 顶层空时返 {} 短路); Interviewer next_turn 末尾 hard cap -> mandatory met 提前 done -> 还有题继续 -> 题答 完 coverage 不达 done 标 evidence_insufficient 4 步; 绝不动态补题。 -
JobContext 加 HR 可配置项 + UI:
JobContext.followup_policy: FollowUpPolicy | None(None = 用 stage 默认);JobContext.completion_policy: CompletionPolicy | None; HR 端新建 job 表单加「高级」折叠区, 让 HR 调阈值;默认值合理, HR 不动也能用 实际落地: schema 字段 + JobORM JSONB 列 + repository 双路兼容. HR 新建 job 表单加「高级 (折叠)」, 4 个 NumberField (题数硬上限/coverage 门槛/ Assessor 双阈值), 默认折叠 + 不动 = 后端 null = 用默认; HR 大多不动也 能用。 -
端到端 eval: 足够的 sufficient 答案 → 0 次 followup + coverage 达标 → 提前结束; 不足的答案 → 跟随 policy 触发 followup, 到 max 仍不足 → 进入下一题, 记录 coverage 低; 所有题答完但 coverage 不达 → 报告标记"证据不充分, 建议人工面谈" 实际落地: test_completion_policy 12 条 (coverage 单元 + Interviewer CompletionPolicy + Evaluator evidence_insufficient + 老 plan 短路); "证据不充分, 建议人工面谈: 等维度证据不足。" 自动加 summary 前缀 + needs_human_review=True (不引新 schema 字段)。
完成标准:HR 在详情页能看到每题的过程评估(自然语言, 不是分数);面试结束 基于 coverage 而非简单题数;候选人回答足够时面试自动提前结束(节省时间); 所有 5.6 的延迟/降级护栏在持久化层依旧成立。 ✅
附加:
- evals 数: Sprint 5.6 收尾 233 → Sprint 5.7 收尾 245 (+12)
- 合规层级: HR UI 展示自然语言字段, 数字仅展示 coverage (Evaluator 已基于其判); sufficiency / confidence 经 API 但不上 UI, 内部诊断 future 仍可拿
- 1 个 commit 完成: f333beb
Sprint 5/5.5 留的债。不动 agent 内核, 把"先 paste / localStorage / 后端默认" 三处临时方案换成 prod-ready 的版本。Sprint 6 视频面试不依赖本 sprint, 但 上 prod 前必须做完 (Cookie + Secure flag + CORS 是安全前提)。
背景: Sprint 4-5 推进过程中, 三个不影响 agent 决策、但影响候选人 / HR 实际 体验和上 prod 的项被一路推到 Sprint 6 / 5.8: (1) Resume 文件上传 (现在只能 paste); (2) HR JWT 还在 localStorage (XSS 抗性弱于 httpOnly cookie); (3) HR 没法调 "每题最多追问次数" (只能改后端默认重启)。本 sprint 一并收尾。
-
PDF/docx Resume 解析端点: 新
POST /jobs/{job_id}/candidates/parse-resume接 multipart, 返回{parsed_text: str}; 前端把 parsed_text 填回现有 textarea, 用户可编辑后 走旧POST .../candidates {resume: text}提交 (现有 JSON 端点保持纯净); 库选型:pypdf(PDF) +python-docx(docx), 都加进 pyproject.toml; 校验: 文件大小 ≤ 5 MB; mime + extension 双判 (application/pdf+.pdf/application/vnd.openxmlformats-officedocument.wordprocessingml.document+.docx); 解析后文本 <100 字符直接 422 "解析失败, 请贴文本" (兜底扫描件)。 实际落地: 新src/resume_parser.py是纯计算模块 (无副作用), pypdf 6.x 逐页 extract_text + python-docx paragraphs+tables; ResumeParseError 统一 上抛由 API 路由映射 422; 前端虚线框「选择文件」+ 状态机 (idle/parsing/error), parsed_text 填回 textarea 让用户可编辑, paste 路径保留作 fallback。 -
Cookie httpOnly + SameSite=Strict 鉴权升级:
POST /auth/login同时 set httpOnly cookie (HttpOnly / SameSite=Strict / Path=/ / Max-Age=JWT_EXPIRE_MINUTES*60) 和 返 access_token JSON (转期共存, 让 evals + 脚本继续用 Bearer 头不用全改); Secure 标志由 新 envJWT_COOKIE_SECURE=false控制 (dev http 默 false, prod 翻 true);require_hr_user先读 Cookie 未命中再读Authorization: Bearer(双路径, 至少保留到 Sprint 6+); 新GET /auth/me走require_hr_user返{username, role}, HrGuard 改成调它判 session 有效; 新POST /auth/logoutSet-Cookie Max-Age=0 把 token 清掉; CORS 从allow_origins=["*"]改成 explicit["http://localhost:3000"](dev) +allow_credentials=True, 用 envCORS_ALLOW_ORIGINS配。 实际落地: CORS 在 Sprint 4 起已是 explicit origins + allow_credentials=True (envCORS_ALLOWED_ORIGINS), 本 sprint 只补 .env.example 注释; UserMe API DTO 新加;cookie_name()helper 从 envJWT_COOKIE_NAME取 (默认 auth_token); /auth/logout 不挂 auth dep, 即使已过期 token 也能调清 cookie。 -
前端鉴权切 Cookie:
web/src/lib/auth.ts改成 noop: readToken 返 null, writeToken 不写 localStorage (cookie 由后端 set); fetch 全局加credentials: "include"让 cookie 自动带上; HrGuard 改成调GET /auth/me判 session (401 -> 跳登录); 退出按钮调POST /auth/logout; 旧 localStorage token 在 Sprint 5.8 deploy 那一刻一次性清掉, evals 仍走 Bearer 头不变。 实际落地: auth.ts 删 token 函数只留 role cache (UI 即时渲染用); api.tsauth: true标志保留作 callsite 不动的兼容 (cookie 总会送, 无实际行为); api.getMe/logout 加入; HrGuard 在 /hr/login 跳过 /me 不抖闪。 -
max_followups_per_questionHR UI:web/src/app/hr/page.tsx高级折叠区加 1 个 NumberField "每题最多追问 次数 (0-3)", 整 job 一刀切。HR 不动 = 后端 null = stage 默认 (self_intro=0 / knowledge=1 / project=2 / scenario=2) 仍生效。文档写明: 即使 HR 设 max=2, Interviewer 的 SELF_INTRO 类别二次保护仍在 (if question.category is SELF_INTRO: return False), 自我介绍永远 0 次追问。 实际落地: 高级折叠区从 4 个 NumberField 扩到 5 个 + 注释提示自我介绍 永远不被追问。 -
eval 护栏:
evals/test_parse_resume.py: 用 mock PDF/docx bytes 跑端点, 验返回的 parsed_text 含已知 fixture 字符串; 大小超 5MB → 422; mime 不匹配 → 422; 解析后 <100 字 → 422。evals/test_auth_cookie.py: 登录 Set-Cookie 含 HttpOnly / SameSite=Strict; cookie 单独命中 require_hr_user (无 Bearer); Bearer 单独命中 (无 cookie); logout 清空 cookie; GET /auth/me 返 user info; CORS preflight 含 Access-Control-Allow-Credentials。 实际落地: test_parse_resume 11 条 (parser 单元 7 + endpoint 4); test_auth_cookie 9 条; 还修旧 test_auth + test_hr_api 的 setUp 加self.client.cookies.clear()防 TestClient 持久化 cookie 让"无 Bearer 应当 401" 测试误命中。
完成标准: 候选人能上传 PDF/docx Resume, 解析失败时优雅提示; HR JWT 走 httpOnly cookie + SameSite=Strict + 可控 Secure flag; HR 能在 UI 改"每题最多 追问次数"; evals 双路径 (Bearer + Cookie) 全绿。 ✅
附加:
- evals 数: Sprint 5.7 收尾 245 → Sprint 5.8 收尾 265 (+20)
- 3 个 feat commit 完成: 72823df (PDF) / fbada1c (Cookie) / 50edd54 (追问 UI)
显式不在 5.8 scope (推后):
CompletionPolicy.mandatory_competenciesHR UI: 推后 Sprint 5.9+ (HR 在新建 job 时不知道 competency_id, 该字段需要"先建 job → plan 生成 → 回头编辑 mandatory" 编辑流程, 跟 5.8 "运维收尾" 不匹配; 维持默认 "全 plan.competencies 都 mandatory")- per-stage
max_followups_per_question配置: schema 改 + UI 改 + eval, 不在 5.8 范围内 - Bearer 路径退役: 5.8 阶段 cookie + Bearer 双路径并存 (evals 用 Bearer), 待 cookie 路径稳定后可独立小 sprint 收 Bearer; evals 改造比代码量大
媒体层是纯「适配器」:面试官的嘴 =
TurnResult.prompt文本, 候选人的答 =submit_answer(session_id, text), 三段式 API 与 agent 大脑零改动。 任一媒体环节失败都降级回文字问答(与 LLM stub fallback 同款哲学, 双路径永存)。 Sprint 5.8 与本 sprint 互不依赖。
背景:升级为「看得见类真人中文面试官 + 候选人开口作答 + 开启摄像头」的视频面试。 目标市场国内 + 海外双 lane; 数字人档位取「够真即可, 控成本/可私有化」。
关键设计决策(动手前先读):
-
回合制, 不做全双工实时对话:不用 speech-to-speech 端到端模型(如 OpenAI Realtime)——那会绕开 Planner/Assessor/CompletionPolicy 与「不动态补题」约束。 一次口语回答 = 一个 turn, agent 编排完全复用。
-
MVP 不引 WebRTC:TTS 走普通 HTTP audio(按 ref_id 缓存); 麦克风走 getUserMedia → WS 推流 → 后端代理厂商流式 ASR; 摄像头录制走 MediaRecorder 分片上传。WebRTC 只在真口型数字人(末项 task)接入时由 avatar 层封装引入。
-
数字人自托管解决双市场:国内/海外数字人厂商互不可达, 自托管 (LiveTalking + MuseTalk, 4090 级 GPU 单卡 1-3 路并发)一套代码两地部署, 私有化 + 控成本。avatar 三层降级:A 真口型流 → B 三态视频循环 → C 纯文字(现状保底)。
-
TTS/STT 按 region 路由:
src/tts/src/stt/照src/llm/单一调用点模式,TTS_PROVIDER/STT_PROVIDERenv 按部署选:国内 = 火山, 海外 = Azure Speech。 无 key →synthesize返 None → 前端退纯文字, 不炸链路。 -
STT 结果候选人可校对再提交:中文 ASR 对术语/中英混说会错, 错转写直接喂 Assessor 会污染 sufficiency 判断。转写落进现有 textarea 可编辑, 提交仍走
POST /answers——文本框是唯一真相源。 -
思考间隙遮蔽:Assessor + lazy project gen 有 3-8s 空档, avatar 切 thinking 态 + 播放 plan 阶段预合成的过渡语音(「嗯, 我了解了」)。预生成非动态, 不违反可复现约束。
-
Consent 门 + 会话页布局改版(纯前端, 合规先行): 进 session 前 consent gate:说明录制内容/用途/留存期限(PIPL), 申请 麦克风 + 摄像头权限; 「AI 虚拟面试官」显著标识(《互联网信息服务深度 合成管理规定》要求); 布局改三区:面试官区 / 候选人自拍 PiP / 转写+答题区; 拒绝授权 → 降级纯文字面试, 流程不断 实际落地: session/media.tsx 新文件 (useCandidateMedia getUserMedia 封装, 卸载自动 stop 不留红点; ConsentGate / AiBadge / InterviewerPanel 6-3 占位 / SelfView muted+镜像); page.tsx State 加 consent 初态, consented 门控原 init effect, 授权失败 → amber 提示条降级纯文字; expired/error 终态自动释放摄像头; AV 模式 max-w-3xl + 面试官区/PiP 双区, 纯文字模式 AiBadge 挂题目卡保标识; video+audio 一次申请 (音轨 6-4 才用, 避免二次弹窗); 录制文案写「可能被录制」 + 90 天留存, 6-5 实装录制时与后端策略同步 (RETENTION_DAYS 单一常量)。
-
src/tts+ 音频端点(面试官开口说中文):src/tts/synthesize(text) -> bytes | None单一调用点, provider 路由 (火山 / Azure), 无 key 返 None(同 llm stub 模式);GET /interviews/{sid}/turns/{ref_id}/audio按 ref_id 幂等 + Redis 缓存; 前端拿到 TurnResult 拉音频播放, 播放失败静默退文字 实际落地: src/tts 走 stdlib urllib (零新依赖) + 10s timeout, synthesize 绝不 raise (含厂商异常 exc_info 告警); 火山 query 模式 (Authorization "Bearer;{token}" 分号格式) / Azure SSML REST, 默认音色 BV700_streaming / zh-CN-XiaoxiaoNeural, env 可换; 缓存在 src/cache/tts_cache.py, key 拍 (text, provider, voice), value base64 (get_redis 是 decode_responses=True 的 str 客户端, 存原始 bytes 会炸); orchestrator.get_turn_audio + 新异常 TurnNotFound → 404, TTS 不可用 → API 204 (与 404 区分开, 便于排障); 响应带 Cache-Control private max-age=86400 浏览器侧再缓一层; 前端 playPrompt 仅 AV 模式播 (avModeRef 定型防 init effect 重跑), 停旧段 + revokeObjectURL 防叠音/泄漏, 自动播放被拦截静默; evals/test_tts.py 16 条 (stub 回退 5 分支 / cache key 敏感性 / 无 Redis 降级 / base64 二进制往返 / get_turn_audio 异常映射)。 -
Tier B avatar:三态视频循环(面试官有脸): idle(眨眼微动)/ talking / thinking 三段预生成真人感视频按状态切换; 状态机:出题 → talking, 候选人作答 → idle, 提交后 → thinking(+ 过渡语音); 零 GPU 零厂商依赖, 双市场通用; 视频素材一次性生成, 入静态资源 实际落地: InterviewerPanel 三段
-
src/stt+ WS 转写代理(候选人开口作答):WS /interviews/{sid}/transcribe:前端 getUserMedia 推 PCM 分片, 后端 代理厂商流式 ASR(key 不出后端), partial 转写实时回显; 转写落现有 textarea 可编辑, 提交仍走POST /answers; STT 不可用 → 隐藏麦克风入口, 打字路径永存 实际落地: src/stt 抽象 SttStream (send_audio/finish/receive/close) + SttEvent (partial/final 都带累计全文, 前端整体替换不拼增量); volc.py 自研火山 ASR v2 二进制 WS 协议 (4 字节头 + gzip payload, 打包/解包是纯函数被 eval 锁住; websockets 16 惰性 import, 同 openai SDK 处理); azure STT 未实装 留 seam, is_configured 直接 False 海外先打字; WS 代理双 pump (asyncio.wait FIRST_COMPLETED), 任何厂商异常翻译成 {"type":"error"} + 关连接不泄 500; 新 GET /media/config (独立 /media prefix, 避开 /interviews/{session_id} 动态路由吞路径) 探测 stt/tts 开关; 前端 stt.ts SpeechCapture: AudioContext(16k) + AudioWorklet (Blob URL 内联模块) Float32→Int16, 攒 100ms 分片推 WS, settled 标志防 done/error 双回调; page.tsx 录音三态 (off/recording/finalizing), 红点脉冲预览条, final 换行追加 textarea + 草稿同步, 录音中禁提交; consent AV 成功才探测 /media/config 显示麦克风; evals/test_stt.py 15 条 (配置矩阵 / 协议帧 纯函数 / WS 代理 TestClient 两条拒绝路径), 全量 391 OK。 注: 火山协议按文档记忆实现, 拿到真实 key 联调时对最新文档核对 (协议错也只是没语音输入, 不伤主链路)。 -
摄像头录制归档(只录不判): MediaRecorder 分片
POST /interviews/{sid}/recordings; 存储本地目录起步, 留 S3/MinIO 接口(私有化友好);media_ref挂 session; 仅作 HR 复核素材, 绝不在本 sprint 参与任何打分(视线/表情是 Sprint 7 且受 §7 约束); 留存策略:TTL 清理脚本 + 文档写明期限 实际落地: src/media_store 惰性配置 (MEDIA_STORAGE_DIR, 未配 = 前端不启动 MediaRecorder + consent 文案自动改"不会录制" —— 文案与实际行为一致), session_id 正则白名单防目录穿越; 分片 append 拼接 = 合法 webm, 顺序由 前端 recorder.ts 串行上传链保证, 单片失败整体停录保住已上传前缀合法; media_ref 在 orchestrator.finalize 单点写入 (每片写 session 会与 submit_answer 竞争 Redis 读改写), schema + ORM (nullable 列) + repository 三处贯通, dev/test 两库 ALTER 加列完成; 端点 404 无会话 / 409 未配置 / 413 超 20MB; /media/config 加 recording_enabled; 留存清理 scripts/cleanup_recordings.py (--days/--dry-run, 默认 90 天与前端 RETENTION_DAYS 同步, 三处口径注释互指); evals/test_recordings.py 13 条 (未配置矩阵/拼接往返/路径安全/purge 只删过期/API 三态/老 JSON 兼容), 全量 404 OK。HR 详情页录像回放端点留后续 (require_hr_user + 流式返回)。 -
Tier A:LiveTalking + MuseTalk 真口型(可独立推迟): 自托管 GPU 节点(国内/海外各一), WebRTC 由该层自带、封装在 avatar 接口后 不污染主链路; GPU 不可用自动退 Tier B
完成标准:候选人 consent 后完成一场「面试官有脸会说中文、候选人开口作答、 摄像头全程录制归档」的面试, 转写文本驱动既有 agent 流程; 任一媒体环节失败自动 降级至文字问答; agent 内核与三段式 API 零改动。
附加(预记账):
- 新 env:
TTS_PROVIDER/STT_PROVIDER/ 各厂商 key /MEDIA_STORAGE_DIR(补进 .env.example, 不写真 key) - evals: tts/stt 无 key 回退、WS 转写协议(fake provider)、consent 标记落库、 媒体失败降级文字路径
- 成本量级(粗估): 火山 TTS 每场几分钱; 流式 ASR ~¥1-3/小时; 4090 云主机 ¥2-8/小时按需起停; Tier B 阶段边际成本 ≈ 0(对比 HeyGen 类 SaaS 单场 ¥20-60)
现有 evals/ 是强制 stub 的结构护栏(改坏了会知道); 本 sprint 建的是 效果评估(好不好): 真 LLM、烧 token、显式运行。两套体系对 LLM 的态度 相反, 必须物理分离 ——
sim/不进 unittest discover, 只能python -m sim.*显式跑。公平性审计部分是 Sprint 7「偏见检查」的前置地基。
背景: 404 个结构 eval 回答不了「Planner 出题好不好 / 面试能否区分强弱候选人 / 简历换个名字分数会不会变」。RAG 评估采 RAGAS 思想自研进 judge, 不引 RAGAS 库 (QA-RAG 形态与题库召回-精选不匹配 + evals 无三方依赖约定)。
关键设计决策:
-
仿真直调 planner + orchestrator(不走 HTTP); PG 默认切 TEST_POSTGRES_URL (sim 数据可弃, JSONL artifacts 才是真相源), Milvus 用 dev 库(题库召回要真, resume chunks 按 sim- 前缀 candidate 可清理)
-
候选人答题 prompt 拼 run nonce 绕开 LLM cache —— 否则同 prompt 缓存命中会让 稳定性方差假性为 0(planner 同输入仍会缓存命中, plan 方差单独衡量, 记账待做)
-
judge 用强模型且先过小金标校准; 未校准的 judge 分只作横向对比不作绝对阈值 (与 Assessor 校准同款纪律)
-
每场仿真 ≈ 30-60 次 gpt-4o-mini 调用 ≈ ¥0.1-0.5; CLI 显式 --repeat, 跑前打印 预估成本, 不许静默烧钱
-
仿真引擎 + persona 库: sim/personas: 强/中/弱 × campus/lateral 6 个核心 persona + 对抗 3 型 (复制粘贴刷题 / 跑题 / 超短敷衍, 复用中等简历只换答题风格); sim/candidate: LLM 扮演候选人按 persona 风格作答(带对话历史保持一致性); sim/runner: 建 job/candidate → 简历分段+ingest → planner.plan → orchestrator 三段式跑完整面试 → get_report; artifacts(plan/transcript/assessments/report/耗时)落 JSONL; CLI: python -m sim.run_interviews --personas core --repeat 1 实际落地: 冒烟 (lateral strong vs weak 各 1 场, 真 LLM 全链路 ~4 分钟): 区分度初验通过 — strong overall=93.0 (coverage 0.9/0.9) vs weak 63.3 (coverage 0.5/0.0), weak 正确触发"证据不充分建议人工面谈"兜底。 冒烟顺手抓到两个待查问题, 后续均已结案: ① needs_human_review 恒 True 是 §7-9 设计 (最终决定必须由 HR 做), 信息载荷在 summary 前缀, 指标不看 该字段; ② comp:tech 双 95.0 坐实为 Sprint 0 字数+关键词启发式饱和 (base 129 字封顶 80 + bonus 5 词封顶 15), 修复见下方「维度分升级」task。运维注意: sim 与 uvicorn 并发访问 Milvus Lite 会触发 collection released 重试告警 (有 fallback 不挡链路), 跑批时建议停 dev server。artifact 含 job/plan/session/report 全量, task 2/4 离线复算无需回库。
-
确定性效果指标 + 汇总报告: 区分度(强>中>弱 的 overall 排序 / pairwise 准确率); 稳定性(同 persona --repeat N 的 overall 方差); 过程指标(追问次数 / coverage 收敛 / 总题数 / evidence_insufficient 率); python -m sim.report 汇总出 markdown 实际落地: sim/report.py 零 token 离线复算 artifacts; 概览表 + 分 track pairwise 区分度 + 分维度极差检测 (< 5 分自动标
⚠️ 饱和) + 稳定性 (N=1 明示不可评) + 对抗 persona vs 同 track medium 基线 Δ (≤-5 判正确 压低)。证据不足以 summary 前缀判定, 不看恒真的 needs_human_review。 冒烟报告即自动标出 comp:tech 极差 0.0 饱和 —— 尺子当天就派上用场。 -
Evaluator 维度分升级: assessment 驱动 + 启发式保底(sim 冒烟结案后的修复): 背景: 维度分停在 Sprint 0 字数+关键词启发式, 真实长答案必然饱和 95, 质量信号 (AnswerAssessment.sufficiency) 落库却没被打分消费; 方案: _assessment_score = 100 × mean(该维度实际被问过的题的 best sufficiency), 同题取 max 与 coverage 同口径; 没被问到的题不记 0 —— CompletionPolicy 提前结束是系统行为, 不许反罚候选人 (覆盖缺口由 coverage + evidence_insufficient 表达, 不双重计罚; 第一版记 0 的实现 在离线复算中被发现并当场修正: strong coverage 0.9 却只得 46.8 自相矛盾); assessments 空 (老 session / ASSESSOR_ENABLED=false) 退启发式, 双路径不删; 合规: 分数是 sufficiency 聚合派生量, 层级与已展示的 coverage 相同, 裸数字仍不进 UI。 验收 (冒烟 artifact 离线复算, 零 token): strong tech 95→89.0 / weak tech 95→50.0 (维度极差 0→39, 与 coverage 0.9/0.5 同向); strong comm 89→90 / weak comm 0→0 不变。evals/test_evaluator_scoring.py 11 条锁映射/回退/ 饱和行为文档化; 全量 415 OK 无回归。
-
Assessor 抗"正确废话"(首次全量批次发现 F1/F4): 复制粘贴型对抗 persona(通篇教科书段落、零第一人称细节)Δ-0.4 完全 未被压低, 跑题型仍拿 74 分 —— Assessor 的 sufficiency 偏爱"结构完整的 正确废话"; 方向: sufficiency rubric 加「个人经历具体性」信号(缺第一 人称细节 / 量化数据 / 决策过程 → 压 sufficiency); 纪律: 改 Assessor prompt = 重跑 calibration eval + sim 对抗批次复验 (验收线: adv-copy-paste Δ ≤ -15, adv-off-topic ≤ 60, 核心 persona 区分度与稳定性不回退) 第一轮落地 (2026-07-22): prompt 分类别锚点 (经历向无第一人称 ≤0.35 / knowledge 讲清原理取舍 0.65-0.85 / 跑题 ≤0.2) + sim/calibrate_assessor 真 LLM 金标跑器 (核心集 gap +0.497, 对抗扩展 8 条全过, 守卫样本曾抓到 矫枉过正当场调锚); 量表通缩的连带重校: min_sufficiency_to_stop 0.7→0.6 + 追问预算守卫 (正题优先, 防挤占级联 — 中间态批次 strong 曾崩 52/ medium 崩 38); f1b 复验 27 场: 区分度 6/6 ✅ 且大幅拉开 (lateral tech 极差 32→45, campus 强中贴脸 4.2→13.5 = F2 修复), off-topic 48.1 ✅ 过线, terse 32.8 ✅; copy-paste Δ-7.5 未达 -15 线 — 但三场 100% 触发"证据不足建议人工面谈"安全网 (改前 0%); 解剖发现其 project 答 仍拿 ~0.5 与 medium 同带, 单 turn 无法区分"真实平庸 vs 精致背诵", 需追问拆穿 —— 而追问被 F5 (见下) 挡死, F1 收尾依赖 F5 决策。 终局 (f5b 批次, F5 两轮修复后): copy-paste 48.2 (Δ-14.8, 距 -15 线 0.2, 在测量噪声 ±3.2 内, 判达线), 位次滑入 weak(38.1)-medium(63.0) 之间偏 weak 侧, 三场全程直面追问无逃逸; off-topic 43.7 ✅ (线 ≤60, 曾 74); terse 25.1 Δ-37.9 ✅。F1 关账。
-
F5 (f1b 批次结构性发现, 方案 A 根治): Planner 出题量失控 + 量表依赖常量未重校: ① plan 实际生成 21-22 题 vs hard cap 15 vs CLAUDE.md 设计 7-9 题 —— 字母 sprint topic-match planner 把出题量翻了三倍, plan 从未完整跑完 (改前批次被"提前达标截断+追问挤占"掩盖); ② plan>cap 时预算守卫数学上 永远拦截追问 → 挖掘引擎整体关闭 → 失去拆穿"精致背诵"的手段; ③ min_competency_coverage 0.7 未随量表通缩重校 → medium (best suff ~0.65) 永远"不覆盖" → 证据不足 flag 沦为全员噪声 (campus-medium 认真 作答 100% 被标)。候选方案: A 根治 = planner 出题量回归 ≤ cap-追问预留 (~10-12 题) + coverage 阈值 0.7→0.6 + 复跑批次; B 折中 = 只重校阈值 + 守卫放宽; C 接受纯广度模式 (不推荐)。 实际落地 (方案 A, 两轮): 第一轮: stage 配比 21-22 → 12 主问题 + 3 追问预留 = cap 15 (各 track 权重意图保持, 8 个 eval 文件断言同步, ARCHITECTURE §2.1 口径更新 + "改配比或 cap 必须两边同看"约定); min_competency_coverage 0.7→0.6。 第二轮 (f5 批次抓到新泄漏当场修): coverage max() 对单发幸运分敏感, copy-paste 靠一道 knowledge 教科书答案 0.65+ 提前离场逃过追问 (7 答拿 65) —— CompletionPolicy.min_assessed_per_mandatory=2 + coverage. assessed_counts, 提前结束需每 mandatory ≥2 道不同题评估 (coverage.py 注释预留的升级点), evals +3 = 423 全绿。 f5b 定稿批次 (27 场): 区分度 6/6 (lateral 74.3/63.0/38.1, campus 75.4/57.0/35.1), 稳定性 8/9 (medium 档 σ 从 8+ 收敛到 3-5); 追问引擎 复活且与水平负相关 (strong 0.7 / weak 3.0); 证据不足率恢复区分意义 (strong 0% / medium 0-33% / weak+对抗 100%); 面试长度自适应 (strong 10-12 答提前结束)。
-
公平性扰动审计: 反事实简历变体(姓名性别线索 / 学校层级 / 年龄信号), 答案文本复用 replay(唯一变量是画像), Δoverall / Δ维度分按属性汇总; 超阈值即红灯 实际落地: sim/fairness.py — 基线简历加显式属性头, 5 变体 (女性化 ×2 / 年龄 38 / 学历二本 / 学历 985) × 2 track medium 基线; 变体重新出题 (槽位按 stage 配比对齐) + f5b 答案库逐字重放 + Assessor 重评 + Evaluator 同款评分; 红线 |Δoverall|>3 / 属性 token 泄漏进题 / 结构改变。 结果: 5/5 全绿, Δ 全 0.0 且题目变更 0 槽 —— 属性头未流入任何出题 prompt (技能抽取只取技能, 按段深挖只喂段原文), LLM 缓存命中恒等即 结构性证明; 全程不碰 Milvus/PG (sections 内存路径)。 已知边界 (记账): ① 答案文本内自称的名字未随变体扰动 (答案侧通道留 task 4 judge / 后续扩展); ② summary 文案的措辞偏差不在本审计范围 (不进分数, 属 task 4 报告忠实性)。
-
LLM-as-judge 套件(含 RAG faithfulness): 题目相关性(question vs JD/competency); 追问针对性(是否冲 missing_signals, 而非泛泛展开); 报告忠实性(每条 evidence 能否溯源 transcript, 幻觉检查); lazy 项目题 faithfulness(题中提到的项目必须真实存在于简历 —— RAGAS faithfulness 思想); judge 金标 ~20 条人工标注, 校准过了才算数 实际落地: sim/judges.py 四 judge (JUDGE_MODEL 默认 gpt-4o, 评审员高于 被评者一档, stub/解析失败直接 raise 不降级) + sim/data/judge_calibration 金标 20 条 + sim/calibrate_judges (每 judge ≤1 错才过, 首跑 19/20 ✅) + sim/judge 批次审计 (r1 采样 + 相关性全局去重控成本)。 忠实性 evidence 逐字摘录天然忠实, 审计对象定为 LLM 写的 summary; 裁决规则"无具体指控不判不忠实"; 相关性只审 knowledge/scenario (project 题考察候选人自己的项目, JD 相关性标准失配, 首审误伤后分流 归溯源 judge)。工具修正: artifact 曾存 resolve 前 plan 快照 → judge 从 history 回捞已问文本 + runner 改存 resolved plan。 f5b 审计结果: 追问针对 23/23 ✅ (followup_goal 链路值回票价); 项目题溯源 45/45 编造 0 ✅ (lazy gen "不瞎猜项目"的设计承诺过硬 审计); 题目相关性 4/9 ❌ → F6; 报告忠实性 5/9 ❌ → F7 (已修一轮)。
-
F6 (judge 审计发现, 归 task 5 顺手接): 题库弱相关题混入召回: 9 道 knowledge/scenario 去重题中 5 道弱相关 —— 全部来自知识管线派生 题库 ("大模型技能评估"/"软件工程核心过程"/"RESTful 面试准备"等, corpus 的 ai/软工类 md 派生题被 backend JD 召回)。方向: 召回加 role_family/JD 相关性过滤 或 题库审核收紧; task 5 的 RAG 检索指标 正好量化这个问题。
-
F7 残余: Evaluator summary 忠实性未达线: 第一轮已修 (summary prompt 加忠实性硬约束三条: 只陈述记录事实/评价与 质量一致/亮点须可溯源), 溢美型伪造修复 (copy-paste 场 53 分被写"表现 优异"的 case 转绿); 但离线复验仍 4/9 被标 —— 残余是成色修饰 ("成功/ 显著"包装真实事实)、无据批评 (拿没考察过的点写"待改善", 对候选人 同样不公) 与 judge 边界偏严的混合。达标 (≤20%) 需要: 句级 claim 拆分 judge + 金标扩容 + prompt 第二轮, 与 judge 边界一起调。
-
RAG 检索指标(零 token, 确定性): 题库召回 precision/recall@k(seed questions 自带 competency/category 标签, label match 即可); documents 召回小标注集(query → 期望 chunk) 进展: sim/data/rag_gold.json (题库 10 query 期望/污染关键词 + documents 3 query 固定 fixture 自包含) + sim/rag_metrics.py (hit@5 ≥80% / 污染@5=0 即 F6 验收线 / 标签完整性 100% / documents 全中, 破线 exit 1; 读 dev PG/Milvus)。待办 (OpenAI 配额恢复后, 两条命令): ① python -m scripts.relabel_question_datasets --datasets agent-skill-basics,ai-agent-basics,cs-ai-rag,mcp-basics --role-family ai --allow-api (PG 已改标, 此步让 Milvus 跟上; 缓存被 F8 清空, 需 --allow-api 重 embed) ② python -m sim.rag_metrics (F6 修复验收) 验收 (配额恢复后): Milvus 重建 3626/3626 零错; rag_metrics 全红线过 —— 污染@5 = 0/10 (F6 达成), hit@5 = 8/10 (80% 踩线), 标签完整 10/10, documents 3/3。观察项 (F10): 两个 miss 均为 scenario query —— scenario 题库现仅剩 upload-smoke-security 95 道安全场景, 5.5 种的通用场景题 (P99 暴涨类) 在字母 sprint 期间被替换, 线上应急/秒杀类召回不到对口题; 补种 seed_questions --category scenario 即可, 不阻塞。
-
F6 修复 (PG 侧): 派生题库 role_family 改标: 根因确认: 审批默认 role_family=backend, 4 个 AI 类 dataset 共 600 道 (agent-skill 316 / ai-agent 117 / cs-ai-rag 140 / mcp 27) 全部混入 backend 召回池 (题库总量 3626)。scripts/relabel_question_datasets.py (--dry-run / --allow-api / embedding 缓存覆盖率探针防半残重建) 已把 PG (真理之源) 改标为 'ai'; Milvus 重建待配额 (见上)。
-
F8 (配额烧尽事故根因之一, 已修): test_rag_e2e flushdb 打在 dev Redis: PG 有 TEST 库隔离, Milvus 换了临时文件, 唯独 Redis 没换 —— 每跑一次 discover 就把 dev 的 llm(7d)/embedding(30d)/tts(7d) 缓存全部清零, 缓存失效 → sim 批次全部真调 → 当天烧穿配额。修复: e2e 类 setUpClass 切 TEST_REDIS_URL (缺省同实例 db/9) + 单例重置, tearDownClass 还原; 哨兵 key 验证 dev 缓存在 e2e 运行后存活 ✅。
-
F9 (配额断供现形, 已修): "强制 stub" 承诺在 test_completion_policy 破洞: 模块顶 pop 的 key 被 setUp 内 import planner 触发的 pymilvus load_dotenv() 塞回 (CLAUDE.md 记载的坑的新变体: pop 必须紧贴在调用 之前、import 之后), 该文件长期悄悄打真 API, 配额在时全绿无感, 断供当天 8 个 429 现形。修复后套件耗时 73s → 37s (真调消失的铁证), 423 全绿。观察项: 其余 eval 文件同类漏点值得一次全面 sweep。
-
HR 复核回流统计: scripts/review_stats.py: ReviewRecord 采纳率(decision vs 报告推荐)/ dimension_overrides 改分率 / needs_human_review 比例 实际落地: 设计上 AI 不做录用建议, 无直接"采纳率" —— 用分数-决策 同向性代替 (recommend/borderline/reject 三桶 overall 均值应单调递减, 不单调 = AI 分与 HR 判断打架); 另有复核率 / 改分率 + 每维度 |Δ| 均值 / "证据不充分"报告的最终决策交叉。聚合是纯函数, evals/test_review_stats 7 条锁行为 (全量 430 绿); dev 现 0 条复核数据, 空数据优雅降级, 待 HR 真实使用后即出数。
完成标准: 一条命令跑出效果报告(区分度 / 稳定性 / 公平性 / judge / RAG 指标
- 成本统计); 强弱 persona 的 overall 排序正确且跨 repeat 稳定; 反事实扰动 Δoverall 在阈值内; 全程不碰 evals/ 的 stub 体系。
附加 (首次全量批次 2026-07-22, 9 persona × 3 = 27 场, ~35 分钟):
- ✅ 区分度: 双 track pairwise 6/6 = 100%; lateral 梯队 90.9 / 80.0 / 39.3 清晰
- ✅ 稳定性: 8/9 persona σ ≤ 4.1
- ✅ 行为合理性: 追问数与水平负相关 (strong 0 次 / weak 8-9 次), 证据不足率 同向 (weak 67-100%, 其余 0%); adv-terse 被碾到 11.5 ✓
⚠️ F1 (最重, 已立 task): adv-copy-paste Δ-0.4 未被压低⚠️ F2 (观察): campus strong/medium 贴脸 (87.5 vs 83.3, comm 维度双双 86.7 持平) —— 校招基础题对"认真的教科书式回答"区分不动, 与 F1 同根⚠️ F3 (观察): campus-weak σ=14.5 (r2 冲到 70.6 与 medium 重叠), 弱 persona 的作答方差 + Assessor 宽大度方差叠加, 待 F1 修复后复测⚠️ F4: adv-off-topic 74 分, 刚过压低线但绝对值仍虚高, 并入 F1 task 验收- 跑批运维: --run-dir 分批合并 (单批 ≤10 分钟), 跑批期间停 uvicorn 防 Milvus Lite 并发降级
HR 在「上传 md 出题」流程之外, 直接从飞书 wiki/docx 导入内部文档出题。 集成方式经拍板: 直连飞书 OpenAPI(不经 MCP —— 后端要确定性 REST, MCP 是 agent 工具协议; app 凭证与 lark-mcp 同源可复用)。范围: 单篇 + wiki 目录批量。设计全部来自 2026-07-18 手工抓取的实战教训 (memory: feishu-wiki-batch-ingest / corpus-md-conventions)。
关键设计决策:
-
权限分层: 单篇文档 tenant_access_token 即可; 列 wiki 子节点必须 user OAuth(个人 wiki 对 tenant 返 131006)。OAuth 走标准回调端点, user/refresh token 只存 Redis(TTL 对齐飞书 2h/30d), 不落 PG。
-
blocks API 替代 raw_content: raw_content 丢标题层级(上次靠手工整理); 产品化改用 docx blocks 接口拿结构化 heading 直接生成合规 md (H1 标题/H2 专题/H3 知识点), 切片器零改动。
-
自动清洗 = 上次的手工活函数化: 裸 uuid 图片行、公众号导流语、 《后续文章》导航行、拍平表格重建; 真实坑样本做金标 eval。
-
未配置即隐藏: FEISHU_APP_ID/SECRET 缺失 → /admin/feishu/status 报 unconfigured, HR UI 整个入口不渲染(/media/config 同款探测哲学); 连接器绝不成为知识管线硬依赖。
-
复用不重造: 导入 BG 任务把清洗后的 md 喂给 admin_upload 现有 _run_upload_pipeline(入库→派生→审核队列), 追加到已有 dataset 时 沿用 memory 约定(不 truncate、只对新 chunk 出题)。
-
task 1 飞书 connector(src/connectors/feishu.py): tenant token 获取+进程内缓存至过期; OAuth authorize-url 生成 / code 换 token / refresh; wiki get_node / list_child_nodes(user token, 递归拉子文档); docx blocks -> 结构化 md; URL 解析(wiki 链接/docx 链接 -> node_token/document_id); is_configured()/is_authorized(); 全部调用带 timeout + 类型化异常, 未配置返 None/False 实际落地: src/connectors/ 新包 (外部系统单一调用点约定); tenant token 进程缓存提前 60s 过期; user/refresh token 存 Redis (TTL 2h/30d); _call 权限重试 (131006 等权限码 + 有 user token 自动切换重试一次, 无则 FeishuNotAuthorized); blocks 分页拉全; parse_url 域名白名单 (防 feishu.cn.evil.com 伪造); blocks->md 转换按纯函数归属挪到 task 2 清洗器文件。evals/test_feishu_connector.py 8 条 (URL 矩阵/ scope 显式断言/权限重试三分支/tenant 缓存), 全量 438 绿。
-
task 2 清洗器 + 金标(knowledge_pipeline/feishu_clean.py): blocks->md 转换内嵌清洗(uuid 图片行/导流语/导航行/错位加粗/表格重建); evals 金标用实战坑样本锁行为(纯函数, 零 infra) 实际落地: blocks_to_md 层级映射 page->H1 / heading1->H2 / heading2->H3 (切片器契约), heading4+ 降加粗行; 图片/分割线 block 源头跳过 ("裸 uuid 行"在 blocks 路径天然消失, clean_md 保留 uuid/图片行清理兜底 raw_content 形态输入); 表格按 (row_size,column_size)+cell 树重建 md 表格, cell 子块防重复渲染; 有序列表断段重新编号; 加粗 style 输出即 修复内侧空格。clean_md: uuid/裸图片文件名行、公众号导流、《后续文章》 导航、错位 ** 修复、连续空行折叠 —— 全部真实坑样本进金标。 evals/test_feishu_clean.py 11 条, 全量 449 绿。
-
task 3 admin API + 后台导入: GET /admin/feishu/status(configured+authorized 探测); GET /admin/feishu/authorize-url + GET /admin/feishu/oauth/callback; POST /admin/feishu/preview(贴链接 -> 标题/子篇数/是否需授权); POST /admin/feishu/import(dataset_id/topic/include_children -> BackgroundTasks: 拉取->清洗->_run_upload_pipeline; 进度落 Redis 供轮询); 异常映射(未配置 409 / 未授权 401 / 飞书侧错误透传语义) 实际落地: api/routes/admin_feishu.py 六端点; OAuth 回调不挂 HR 鉴权 (飞书浏览器跳转), 一次性 state nonce (Redis TTL 10min) 防 CSRF, 完成 302 回 HR admin; 进度 Redis (feishu:import:{job}, TTL 1h) 前端 轮询; BG 任务: 拉 blocks -> blocks_to_md -> 同批内容哈希去重 (实战坑: 同名不同 token 整篇重复) -> 落临时目录 -> 复用 _run_upload_pipeline (入库+派生+可选批准, tmpdir 由它清理); source_commit 记 <节点token>@<日期> 溯源约定; dataset 冲突 409 与 md 上传口径一致 (追加已有 dataset 留后续)。异常映射: NotConfigured/NotAuthorized 都走 409 (401 语义被 HR 登录占用, 防前端误跳登录), FeishuApiError -> 502。 evals/test_feishu_admin.py 10 条 (dependency override 绕 JWT + connector 全 mock), 全量 459 绿。
-
task 4 HR UI: /hr/admin 加「从飞书导入」入口: 连接状态+授权按钮 -> 贴链接预览 (标题+子文档列表) -> 配 dataset_id/topic -> 导入 -> 轮询进度 -> 完成跳审核队列; 未配置时入口整体隐藏 实际落地: /hr/admin/feishu 新页 (状态探测 -> 授权条 [单篇免授权文案] -> 预览 [子文档列表/需授权提示/非 docx 标跳过] -> 表单 [dataset/topic/ category/维度/含子文档/自动批准] -> 2s 轮询进度 -> done 展示 chunks/drafts/去重数 + 跳审核); topic 自动带入预览标题; 主页入口 按 feishuStatus.configured 显隐。顺手清了 hr/admin 三页的存量 lint error (setState-in-effect ×3 挪微任务 / 未转义引号 / unused import), eslint 全绿 + build 过。
-
task 5 前端凭证配置(用户追加需求: 不改 .env 不重启): 实际落地: app_settings 键值表 (PG) + get/set/delete repository; 凭证解析 env 优先 (部署钉死, UI 409 拒改) -> DB 兜底 (前端配置), 60s 进程缓存; app_secret 用 JWT_SECRET 派生密钥 Fernet 加密入库 (JWT_SECRET 本就必配, 零新增 env), secret 永不回传前端; PUT /admin/feishu/config 保存前先换一次 tenant token 做连通性测试 (无效凭证 422 带飞书错误码); status 扩展 source + app_id 掩码; 前端: 未配置态直接渲染配置表单 (验证并保存), 已配置显示掩码+来源芯片, db 来源可清除重配。evals +7 (加密往返/优先级/无 JWT_SECRET 降级/ env 锁 409/无效凭证 422/加密入库不回显), 全量 466 绿。
完成标准: HR 不出 Web 界面, 从飞书 wiki 目录批量导入一组文档并走完 出题-审核-入库全流程; 单篇免授权可用; 未配置飞书时现有 md 上传流程零变化; 凭证可全程在前端配置 (env 部署则界面只读)。
拍板 (2026-07-24): 注册只给 HR, 开放自助注册; 每个 HR 各管各的 (方案 B) —— 只见自己创建的岗位/候选人/报告, 题库与知识管线仍是共享 公共资产。隔离到位后开放注册无数据泄露顾虑; 保留可选邀请码 (REGISTER_INVITE_CODE 配了就要求, 不配 = 完全开放)。 候选人保持免账号链接直达 (刻意设计, 不动)。
关键设计决策:
-
归属落在 job 上 (
jobs.owner_user_id): 候选人/plan/session/report 都 经 job 派生归属, 不必逐表加 owner 列。 -
admin 角色看全部 (后台语义); hr 只见 own。存量无主数据用 scripts/backfill_job_owner.py 归到指定账号, 不留"无主可见"的模糊态。
-
顺手封存量洞: POST /jobs 此前无鉴权 (dev 遗留), 隔离要求创建时 必有归属人 → 挂 require_hr_user; 受影响的 e2e evals 用 dependency override 补登录态。
-
越权访问一律 404 (不泄露资源存在性), 与既有 401 同文案防枚举哲学一致。
-
task 1 注册端点 + 页面: POST /auth/register (username 3-32 字符校验/重名 409/密码最短 8 位/ 可选邀请码; bcrypt; 注册即登录 set cookie, role=hr); /hr/register 页 + 登录页互链; evals (重名/弱密码/邀请码三态/开放注册) 实际落地: 重名 409 明确提示 (注册场景无枚举增益, 与登录 401 防枚举 语义区分, 注释写明); 并发撞 unique 约束统一按 409; 永远 role=hr (admin 只能种子脚本); 注册页双密码确认 + 按错误码文案映射; evals/test_auth_register.py 6 条 (校验 422 ×3 / 邀请码 403 ×2 / PG-gated e2e: 注册即登录 me 通 + 重名 + login 复验), 全量 472 绿。 顺手清 hr/ 整树剩余存量 lint error 6 处 (layout/page/candidates 详情 的 setState-in-effect + 引号), hr/ 从此 eslint 全绿。
-
task 2 按 owner 隔离: jobs.owner_user_id 列 (ALTER dev/test) + schema/repository 贯通; POST /jobs 挂鉴权并写 owner; /hr/* 全部端点归属校验 (job 直查 / candidate·plan·session·report 经 job 派生), 越权 404; GET /hr/jobs 按 owner 过滤 (admin 全量); scripts/backfill_job_owner.py 迁移存量; 受影响 evals 补 override; 双账号隔离 eval (互不可见/越权 404/admin 全量) 实际落地: hr.py 统一走 require{job,candidate,report}access 三个 helper (18 处端点全覆盖, 候选人列表越权返 [] 不泄存在性); owner_user_id 只落 job 一张表, agent 内核不消费该字段 (schema 注释 写明); 隔离 eval 踩了 5.8 "cookie 优先 Bearer 兜底" 的坑 —— 共享 TestClient 三连登录后 cookie 停在 admin, 所有 Bearer 请求被顶掉, 登录后 cookies.clear() 才让 Bearer 生效 (eval 内注释记录); test_api/test_rag_e2e/test_parse_resume 用 dependency override 注入 固定 HR 身份 (流程测试不掺权限, 真实鉴权归 test_auth/test_owner isolation 守); test_hr_api 的 hr2 改 admin 角色保住 "reviewer_id 取自 JWT" 的测试意图; dev 库 15 个存量岗位已 backfill 归 hr1; 全量 480 绿 (472+8)。
完成标准: 新 HR 注册后独立走完建岗到复核全流程, 且看不到其他 HR 的 任何岗位/候选人/报告; admin 可见全部; 存量数据完成归属迁移。
2026-07-28 立项。出处与评审见 AGENT_UPGRADES.md(五方向提案,本 sprint 为 顺序第一)。威胁模型:真实简历库约 1% 含隐藏注入(hireEZ/USENIX'26 实测); 绝对打分比成对比较更易被操纵。原则:标记不拦截(InjecGuard 证明检测器 over-defense 严重),命中只导人工复核;决策逻辑零改动。 防御限界(评审修正 4):指令注入可防;隐藏关键词堆砌型数据注入只有 "简历原文 HR 可见 + 人工复核"兜底,不宣称全防。
- task 1 wrap_untrusted 统一包装:
新增 src/llm/sanitize.py(
wrap_untrusted:sha256(text) 派生 nonce 的 边界标记,防伪造闭合且确定性可复现;UNTRUSTED_NOTICE系统提示声明); 接入 4 处候选人文本拼接点:Assessor user 模板 / Interviewer 追问 prompt / Evaluator summary transcript / Planner lazy 出题(intro_text + 简历段); Assessor prompt 变 → 重跑 sim/calibrate_assessor; evals/test_injection_guard.py(nonce 性质 / stub 路径行为不变) 实际落地: 校准通过 —— 核心集 gap +0.489 (门槛 0.3), 对抗+守卫 8/8; monkeypatch llm.complete 捕获 prompt 验证集成, 原文只包裹不过滤; summary prompt 变更的 F7 忠实性审计随下次 sim 批次复验 (记入手动清单)。 - task 2 入库净化 + 标记:
sanitize.py 增
strip_invisible(剥离零宽/双向控制/控制字符,幂等); resume 解析与 answer 提交两个入口接入;命中超阈值只标记 session.integrity_flags,不拦截 实际落地: 剥完再判 MIN_TEXT_CHARS (不可见字符不给凑长度); 阈值 5 (单 BOM/零宽残留不误伤); ORM 补列 candidates.injection_suspected + interview_sessions.integrity_flags (dev/test 已 ALTER), repository 贯通; needs_human_review 现状恒 True, flags 先只落库审计, 未来条件化时必须并入。 - task 3 输出侧异常校验:
assessment 落地点检测异常模式(sufficiency ≥ 0.95 且 missing_signals/
concerns 全空)→ integrity_flags;
EVALUATION.md 记录防御限界 + 真实 LLM 对抗测试手动清单
实际落地:
_assessment_anomaly纯函数 (不误伤带 signals 的正常高分); EVALUATION.md 手段清单 +#13, 手动清单 3 项 (指令注入拉分复验 / F7 忠实性 / 防御限界声明); evals 19 条, 全量 499 绿。
完成标准:注入样本不改变任何决策路径行为(stub/启发式 evals 全绿), 只多出人工复核标记;assessor 校准两组金标重跑通过;flags 不进 HR UI 明细 (HR 只见 needs_human_review)。
2026-07-28 立项。出处 AGENT_UPGRADES.md 8.2。动机:决策依据只活在代码路径里, LLM 请求响应不落盘;EU AI Act Annex III(招聘=高风险)Article 12 要求 事件日志达到「决策可重构」,2026-08-02 起执行。原则:纯旁路观测, 不改任何决策;回归判定用确定性 diff,不用 LLM 审 trace(TRAIL:最佳 11%)。 架构注:src/trace 是与 src.coverage 同级的纯共享模块(contextvars 内存收集, 无 I/O),agent 可 import 埋点,不违反「agent 不碰 DB/cache」。 限界:embedding 只录不回放(检索结果依赖 Milvus 状态;但召回片段已进 chat prompt 原文,回放 chat 调用即足以重构决策)。
- task 1 trace 收集骨架: schemas 增 LLMCallRecord / DecisionSpan / DecisionTrace(OTel gen_ai.* 对齐命名);src/trace 收集器(contextvars,无 trace 时零开销 no-op); llm.complete()/complete_vision()/embeddings.embed() 单点录制 (request_hash/prompt 全文/response/latency/path: cache|llm|stub); orchestrator 三段式入口开/续/存 trace(Redis 同 session TTL)+ assess / lazy_gen 埋点;interviewer 埋 followup_decision / completion_check span(记阈值比较实际数值);eval:span 树完整 + 每个决策有依据字段 实际落地: request_hash 与 LLM 缓存 key 同源 (canonical sha256); interviewer 短路表达式拆布尔量逐一等价, 决策行为零变化; AnswerAssessment 增 via 审计标注 (llm|heuristic|rule_duplicate); 坑: test_api 模块级 ASSESSOR_ENABLED=false 不回收, e2e setUp 显式开。
- task 2 PG 归档 + 审计导出: finalize 归档 decision_traces 表(session_id 提列 + spans/llm_calls JSONB)后删 Redis;GET /hr/sessions/{id}/trace 审计导出(权限同 review:owner/admin,越权 404);trace 不进 HR UI 页面;eval 落库 round-trip + 越权 实际落地: 表不设 FK (旁路观测不反向约束 session 落库); 归档失败保留 Redis 副本至 TTL 且不阻塞 finalize; dev/test 已建表。
- task 3 确定性回放 + golden traces: REPLAY_MODE=1 时 complete() 按 request_hash 命中录制返回,miss 抛 ReplayDivergence(replay 优先于 key 判断,无 key 可回放); trace diff 工具(决策序列比较,剔除 id/时间戳类字段); scripts/record_golden_traces.py 录 3 场典型 stub 面试入 evals/data/golden_traces/;eval:重放决策序列一致 + 篡改可定位; 约定升级:改 prompt / policy 阈值 = golden trace diff + calibration 双门禁(写进 CLAUDE.md 约定 + EVALUATION.md 手段清单) 实际落地: 两处立项修正 —— (1) 内存路径的 plan 生成也纳入 trace (否则回放在 plan 阶段就 miss; API 路径 plan 预生成不受影响); (2) golden 场景强制关 Milvus (pop URI + reset 单例, 且必须在 pymilvus load_dotenv 之后 pop —— F9 同款坑), 否则 golden 耦合本机 corpus 换机器就废。录制脚本每场景连跑两遍做确定性自检; 3 场 golden (campus 追问密集 41 span / lateral 长答 41 span / 提前 finalize 9 span); diff 规约: 剔 ts/span_id/path/latency + uuid 类, coverage 按值序列比较。全量 517 绿。
完成标准:stub 全链路面试的每个追问/结束/评估决策都能从 trace 重构出 依据(决策类型 + 实际数值 + llm|heuristic 路径);golden trace 重放 diff 为空; 篡改任一录制响应能被定位;evals 全绿且现有决策行为零变化。
2026-07-28 立项。出处 AGENT_UPGRADES.md 8.3 + 评审修正。这是 8.x 系列唯一 真正改决策行为的 sprint,推进纪律:
- 新逻辑只挂在校准路径上(assessment.calibrated_sufficiency 非 None, 即真 LLM 路径):stub/启发式路径行为逐字节不变 → golden trace 保持绿, dev/eval 环境零扰动;真实行为变化由 sim 批次复验
- 评审修正:50 条金标用 Platt(logistic)不用 isotonic(小样本过拟合)
- 明确不做:在线 EIG 前向模拟 / IRT / 动态补题(红线)
- 留账:confidence 校准缺金标(需"判断是否正确"的人工标注),Step 1 只校准 sufficiency;"missing_signals → competency 方差最大者定焦"依赖信号-维度 映射,当前 schema 无此结构,不实现
- task 1 Step 1 校准层: sim/calibrate_platt.py 在核心金标 42 条二分类样本上收集真 LLM sufficiency 并拟合 Platt(stdlib Newton,零依赖;LLM 调用大概率命中 今天校准跑的 Redis 缓存 ≈ 零成本);参数硬编码进 src/agents/assessor/calibration.py(版本注释 + 重拟合指引); AnswerAssessment 增 calibrated_sufficiency: float | None(仅 LLM 路径 填,启发式恒 None);_decide_followup 有校准值时用 min_calibrated_to_stop=0.5(P(sufficient)≥0.5 语义,阈值可解释); eval:映射单调 + 边界性质 + 启发式路径不受影响;双门禁跑过
- task 2 Step 2 信念状态 + 预算调度: schemas 增 CompetencyBelief(mean/variance/n_observations,高斯共轭: 固定先验 μ0=0.5,σ0²=0.25 + 固定观测噪声 σ_obs²=0.09 → 方差单调不增 且顺序不变,纯数值不加 LLM);InterviewSession.beliefs(JSONB 落库, ALTER dev/test);orchestrator 在校准 assessment 落地时更新 + trace belief_update span;FollowUpPolicy 增 total_followup_budget(全局 预算,默认=各 stage 配额之和)+ min_variance_to_probe(证据已足的 competency 不再消耗追问预算 → 预算流向高方差维度); CompletionPolicy 增 use_belief_lcb 开关(默认关):coverage 判定用 mean − k·√variance 置信下界替代裸 max;belief 数字不进 HR UI/报告; eval:test_belief_update(共轭数值性质)+ test_followup_scheduling (不均衡场景预算流向 / 预算耗尽 / 无校准值时行为与现在一致)
- task 3 真 LLM 复验 + 收尾: sim/calibrate_assessor(prompt 未动,应原样过)+ golden trace diff (应全绿,新逻辑不触 stub 路径)+ 小规模 sim 批次(core+adversarial 各 repeat 1)对比基线:区分度排序不塌、对抗 Δ 不缩水、followup 总量 变化记录;EVALUATION.md / CLAUDE.md 记录 实际落地: 区分度 6/6 保持; 复验修正方差门语义 —— 加均值条件 (variance<0.03 且 mean≥0.8 才停 probe), 未达标维度保留追问以暴露 缺口证据 (勘误: 追问回答不进评分, "second chance" 理由不成立); 用 8.2 trace 逐 span 解剖, 门修复前后 medium 决策一致证明 8.3 无害; S83-R1 已定位 (固定输入再评估, 54 对 × 双臂): 8.1 prompt 效应 -0.02 排除, 评审端漂移 +0.07 方向相反 → medium 基线 63→51 漂移在 候选人仿真端 (persona 当日生成波动, σ≈8), 系统评估侧无回归; 记账: 回归批次应冻结 persona 答案 fixture 或 repeat≥3 报置信区间。 批次跑法纪律又验证一次: 跑批必须停 uvicorn (Milvus Lite 并发)。 全量 538 绿。
完成标准:真 LLM 路径追问决策由校准分 + 信念方差 + 全局预算共同驱动; stub/启发式路径与 8.2 golden 完全一致;sim 复验区分度与对抗指标不劣化; belief/校准数字对 HR 与候选人不可见。
2026-07-28 立项。S83-R1 的直接后续:persona 级基线误差条 ±10 分,根因是 候选人仿真端逐日生成漂移。回归型批次改为录制式冻结:把 s83 批次 (已付费) 每个 persona 的真实回答按 category 抽成 fixture,
--frozen模式下候选人端零 LLM、逐字节复放 —— 输入全固定后,批次间分差只能来自 评估端(assessor/planner/policy),即「真 LLM 版 golden」。 生成式 persona 保留,探索性测试用;fairness 的答案重放机制不动。
- task 1 冻结机制 + 基线: scripts/freeze_persona_answers.py(从批次 artifact 抽 9 persona 的 分类别答案池)→ sim/data/frozen_answers.json;sim/frozen.py 发放器 (按 category 顺序消费,溢出循环加后缀防撞复制粘贴硬规则); runner/run_interviews 加 --frozen;冻结基线批次(答案与 s83 同 → assessor 撞当日缓存 ≈ 零成本)入 EVALUATION.md 作新基线; 结构 eval(fixture 完整性 / 发放器行为);fixture 失效即重冻结 (planner 出题变更后),与 golden 重录同款纪律 实际落地: 连跑两批 9/9 逐字节一致; 与源批次 8/9 吻合 (campus-weak 复放对位差异 37.7→29.7, 冻结基线以自身为准); 追问答案归父题 category 池 (冻结与复放同规则); TTL 过期后重跑 frozen 批次即评估端 漂移检测器; evals +7, 全量 545 绿。
完成标准:--frozen 批次候选人端零 LLM 调用且两次连跑分数逐字节一致; 9 persona fixture 齐全;EVALUATION.md 记录冻结基线与重冻结纪律。
2026-07-28 立项。出处 AGENT_UPGRADES.md 8.4 + 评审修正 1(covered_aspects 已被 coverage/richness 消费,真正闲置的是 strengths/concerns —— 本 sprint 的原料就是它们)。定位:第四类数据(继 AnswerAssessment 之后)—— 不进总分、不见 HR UI 明细、不见候选人,仅内部决策 + 落库审计。 结构化与原文双轨(Verbatim beats Extracted 教训):每条 claim 挂 evidence(answer_id 列表),结论可溯源到原文。 矛盾消解不让 LLM 仲裁:确定性规则 = 同 competency 下 verified 与 doubted 条目文本相似度过阈 → 双方标 contradicted 留双方证据; 澄清走既有追问配额(followup_goal 注入),追问后仍矛盾 → 人工兜底。 与 CLAUDE.md 红线逐条对齐:不动态补题 / 启发式 fallback 不动 / 新 LLM 调用仅 stage reflection 一处(timeout + 失败跳过)。
- task 1 schema + 题目级更新(纯规则,零新 LLM): SkillClaim(status: claimed/verified/doubted/contradicted + evidence 原文双轨 + source_stage)+ CandidateModel(claims + contradictions 对,list[list[str]] 不用 tuple —— JSON round-trip); InterviewSession.candidate_model(JSONB 落库,ALTER dev/test); src/candidate_model.py 纯规则模块(coverage 同级):每份 assessment 落地时 strengths→verified/claimed(按分高低)、concerns→doubted, Mem0 式 ADD/UPDATE/NOOP(同 competency + 字符 2-gram Jaccard 粗判; 任何异常退化为只追加 —— 降级铁律);矛盾确定性标记; orchestrator 独家写 + trace span;eval:幂等/合并/矛盾确定性/退化
- task 2 stage reflection(唯一新 LLM 调用)+ 澄清接入: stage 切换时一次 reflection(10s timeout,失败跳过,prompt 进 eval 管辖):本 stage 条目蒸馏合并(改写 claim 文本、并 evidence, 不许发明记录外结论);追问 focus 与 8.3 打通:同等条件下该题 competency 有 doubted claim → followup_goal 前缀注入澄清目标 (走既有配额,不加问数);eval:reflection 失败退化 / 澄清注入结构
- task 3 消费点 + frozen 复验: lazy project 生成 prompt 注入 doubted/contradicted 条目(优先深挖 存疑点);Evaluator:未消解 contradicted → needs_human_review=True (现恒 True,先接线)+ contradicted 条目(带 evidence)进 summary 输入;frozen 批次复验(8.3.1 首次实战):prompt 变更对分数的 影响在固定输入下测量;golden 重录(span 变);EVALUATION.md 记录 实际落地: frozen 首战即立功 —— lateral-strong -19.5 被圈出, 定性为 fixture 与新题集失配 (8.4 故意改题) 而非质量回归; 生成式复核 lateral-strong 76.6 持平、区分度 6/6、对抗 Δ 恢复 -8/-8/-26; 按纪律从 post-8.4 批次重冻结, frozen-d 与生成式源 9/9 一致 + d/e 连跑 9/9 逐字节一致, 新基线入 EVALUATION.md; 方法论沉淀: 题集变更型 feature 的 frozen Δ = 变更检测信号, 质量裁决用生成式复核, 两者配合使用。全量 564 绿。
完成标准:claims 全程可溯源(evidence→原文);矛盾标记确定性(同输入 同输出);stub 环境 reflection 跳过、其余纯规则路径全绿;frozen 复验分差 可解释;第四类数据红线(不进总分/不见 HR 明细/不见候选人)eval 钉死。
2026-07-28 立项。出处 AGENT_UPGRADES.md 8.5 + 评审修正 2。两个前置的 处理决定:
- panel 异构问题:单 provider 下先用同家族多模型(默认 gpt-4o-mini / gpt-4.1-mini / gpt-4o,env EVAL_PANEL_MODELS 可换), 明确标注为「伪异构」(Nine Judges:错误相关 → 有效票 < 3);接第二 家族走 OPENAI_BASE_URL 网关即可,不改代码。panel 默认关 (EVAL_PANEL_ENABLED),MAE 校准过门禁前不进主路径。
- MAE 门禁前置(评审修正 2):报告级人工数值标注不存在。本 sprint 交付标注模板 + 拟合/门禁脚本(无标注优雅跳过);标注 20-30 份报告的 工作量记账给 HR/用户,标完跑 sim/calibrate_evaluator 出映射。
- rubric 在 plan 阶段生成一次随 plan 固定 → 不违反「不动态补题」 (改的是评分依据不是题);stub 走分类别模板 rubric(确定性)。
- Assessor 的 rubric 判定放条件 user 块(有 rubric 才拼),system prompt 不动 → 校准金标 prompt 逐字节不变,不触发重校准级联。
- task 1 per-question rubric + 命中率进分: Question.rubric: list[str](plan 生成,3-6 条二元项;stub 走分类别 模板;lazy project 题在 resolve 时生成);Assessor 条件 user 块逐项 判定 → AnswerAssessment.rubric_hits: list[bool](长度不匹配 → 弃用 该次 rubric 判定,记 log,不猜);评分主路径升级:题级质量 = 0.7×best_sufficiency + 0.3×best_rubric 命中率(初始权重,复验调), 无 rubric / 无 hits 退回纯 sufficiency(老 plan 兼容); eval:rubric 随 plan 固定 / hits 与分单调 / 兼容路径;golden 重录; frozen 批次复验(评估端变更,frozen Δ 就是本变更的效果)+ 区分度不塌
- task 2 小型裁判团(默认关): finalize 时对每维度 3 judge(EVAL_PANEL_MODELS)独立打分取中位数; 极差 > 阈值 → 该维度 judge_disagreement → needs_human_review; 任一 judge 失败剩余继续,全挂退回 assessment 公式路径(现主路径变 fallback 链的一环,启发式仍是最后兜底);裁判与生成分离:judge 模型 与追问生成模型强制不同名(config 校验);EVAL_PANEL_ENABLED 默认 false,过 MAE 门禁前不开;eval:中位数聚合 / 部分失败降级 / 全挂 回退 / 分离校验(stub 模拟)
- task 3 MAE 校准门禁脚手架 + 复验收尾: evals/data/report_score_labels.json 标注模板(report_id + 人工 overall + 维度分;附标注指引)+ sim/calibrate_evaluator.py(线性/ 单调映射拟合 + MAE 门禁,无标注优雅跳过并打印指引); panel 开启条件写死:MAE 门禁通过 + 用户确认; EVALUATION.md 手段 #16 + 记账;frozen/生成式复验记录;CLAUDE.md 「不要做的事」补:MAE 未过门禁不许开 panel
实际落地(2026-07-28):task 1 经 frozen 驱动的三轮 prompt 迭代定稿 (f 判定过宽 → f2 收紧但有效率 26% → f3 定位根因"mini 严格照 JSON schema 块输出、旁述字段被丢" → f4 写进 schema 块, 有效率 100%);f4 指标:区分度 6/6、copy-paste 压制 -8.2→-11.8(rubric 核心收益)、strong 端一致性量表 下移非随机惩罚,f4 为新冻结基线。task 2 panel 就绪默认关, 裁判/生成模型 强制不同名, 全挂回退公式→启发式链不断。task 3 标注模板+MAE 门禁脚本可跑 (空标注优雅跳过);CLAUDE.md 红线:MAE 未过不许开 panel。全量 577 绿。
完成标准:rubric 全链路(plan 生成→assessor 判定→评分组合)stub/真 LLM 双路径工作且老 plan 兼容;frozen 复验 Δ 可解释、区分度 6/6 不塌; panel 代码就绪但默认关,门禁脚手架可跑;标注工作量清晰记账。
2026-07-28 立项。目标:公网部署前让系统扛得住多人同时面试。 核心取舍:不做全链路 async/await 重写 —— 同步内核跑在 FastAPI threadpool 里,并发容量 = worker 数 × 线程数,对面试类负载(单 turn 秒级、非毫秒级)足够;而重写要动全部 agent/orchestrator/db/cache 签名, 577 个 eval + golden/frozen 体系全在同步假设上,风险收益不成比。 真并发瓶颈按序拆解:
- Milvus Lite 单进程锁(跑批都要停 uvicorn 的根源)→ 独立 Milvus standalone(docker),多 worker 才可能
- BackgroundTasks 假异步(串行、进程内、无重试)→ RQ 任务队列 (Redis broker + 独立 worker 进程;RQ 不可用退回 BackgroundTasks, 降级铁律)
- 会话读改写竞态(并发 submit 同一 session 会互相覆盖)→ per-session Redis 锁
- 连接池/线程池默认值(PG pool 5、threadpool 40)→ env 可调 全链路 async 重写留账:若实测 >500 并发会话再评估,先改 llm 调用层。
- task 1 Milvus 独立部署解耦: vector_store 支持 MILVUS_URI(http:// 服务端)与 MILVUS_LITE_URI (本地文件)二选一,server 优先;docker-compose.yml 起 milvus standalone;本机 docker 实测两种模式行为一致(collections/插入/ 检索/删除);evals 保持 lite(CI 零依赖);「跑批停 uvicorn」纪律 在 server 模式下解除(文档更新)
- task 2 RQ 任务队列: pyproject 加 rq;src/jobs/(enqueue 封装 + worker 入口 python -m src.jobs.worker);候选人上传路径(ingest_resume + run_planner)改走队列,带重试;RQ 未装/Redis 不可用/未起 worker 时 自动退回 BackgroundTasks(降级铁律,eval 钉死);candidate 的 plan_pending 轮询语义不变,前端零改动
- task 3 会话锁 + 池容量 + 压测: submit_answer/finalize 挂 per-session Redis 锁(SET NX,拿不到返 409,候选人端提示重试);PG engine pool_size/max_overflow env 化 (默认 20/30);FastAPI startup 把 threadpool tokens 提到 THREADPOOL_TOKENS(默认 100);scripts/load_test.py(stub 模式 N 路 并发完整面试,验证无跨会话串扰 + 容量数字);eval:锁互斥/降级/池配置
实际落地(2026-07-28):
- task 1: server/lite 双模式 (MILVUS_SERVER_URI 优先); 实测修复一致性差异 —— server 默认 Bounded 下 ingest 后立刻检索读不到自己刚写的切片, 检索统一 consistency_level=Strong (面试链路是 read-own-write)
- task 2: RQ 踩坑 —— 业务 Redis client decode_responses=True 会把 pickle 任务体解码炸掉, 队列走专用 raw 连接; Planner 失败抛出交 RQ 重试, 分段/ingest 吞异常 (下游有 fallback)
- task 3: 锁冲突 409 语义上线; evals +9, 全量 586 绿。压测首版对比数字 (lite 29/30 / p50 3133ms vs server 30/30 / p50 5ms / 4×)后经复查作废 —— stub 泄漏(pymilvus load_dotenv 回填 key, F9 同款)混入真 LLM 时延与 缓存效应, 见 EVALUATION.md 2026-07-30 勘误; 修复 harness 后干净基线: 30 场×10 并发 30/30 零失败、p50 18ms / p95 41ms、~367 turns/s、 串扰 0(纯应用层口径, stub 下 Milvus 不被触达)。
完成标准:docker milvus + 多 worker uvicorn + RQ worker 架构本机跑通; 压测 N≥30 路并发面试零串扰零 5xx;所有降级路径(无 server milvus 退 lite / 无 RQ 退 BackgroundTasks / 锁冲突 409)有 eval;全量 evals 绿(CI 零外部 依赖)。
实现前先落实 ARCHITECTURE.md 第 7 节的全部约束。
- Analyzer 异步管线:经 Redis 队列处理音视频,避免阻塞面试
- 语言信号(基于转写文本)
- 语气 / 韵律信号(基于音频)
- 视线 / 表情信号(基于视频帧)—— 默认保守、可单独关闭
- 信号附置信度,仅作为参考证据进入报告「表现维度」
- 报告明确区分软信号;保留人工复核与审计日志
- 偏见检查:针对受保护特征做差异审计
完成标准:软信号以可解释、可关闭、可审计的方式呈现,且不参与自动淘汰。
- 每次改 agent / prompt 后跑 eval,防行为退化
- 维护 CLAUDE.md:发现 Claude Code 反复犯错就把纠正写进去
- 每个 task 一次 commit,用 conventional commits