This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
多 agent 的 AI 视频面试平台。HR 上传 JD / 岗位要求 / 公司资料,候选人面试前上传 Resume; 系统结合 JD 与 Resume 自动生成面试计划、执行多轮面试与追问、产出结构化评估报告。 最终目标是「招聘端 + 候选人端」双边 AI 面试基础设施。
面试题目按 Question.category 分类,与 Question.type(题目风格)正交:
- KNOWLEDGE:基础知识,由 JD / 岗位要求驱动
- PROJECT_EXPERIENCE:项目/实习深挖,由 Resume 驱动
- SELF_INTRO / SCENARIO:自我介绍 / 场景题(Sprint 5.5 起加入,见下文 stage 化)
面试支持两套 track(JobContext.track):
- campus(校招):self_intro → knowledge(重) → project(结合 intro_text 现场生成) → scenario(轻)
- lateral(社招):self_intro → knowledge(轻) → project(重) → scenario(重)
进度:Sprint 0–5.9 已落(骨架 → 持久化 → API → RAG → 候选人端 → HR Dashboard → track/stage 化 → Assessor → CompletionPolicy → calibration);字母 sprint 系列 (知识管线 / 简历分段 + 图片 OCR / Planner 主题匹配)已落;Sprint 6 视频面试 5/6 已落 (consent 门 / TTS 播报 / 三态 avatar / 语音作答 / 录制归档,Tier A 真口型待定); Sprint 6.5 效果评估 / 6.7 飞书接入 / 6.8 注册+owner 隔离已落; Sprint 8.1–8.5 Agent 能力升级全部已落(注入防御 / 决策 trace+回放 / 校准+信念驱动追问 / CandidateModel 记忆 / rubric 化打分+裁判团(默认关), 提案与评审见 AGENT_UPGRADES.md);8.3.1 sim 冻结回归基线已落; Sprint 9 异步队列 + 高并发承载(RQ / Milvus standalone / 会话锁)已落; Sprint 7(多模态分析) 未开。详见 sprint.md。 完整架构与合规约束见 ARCHITECTURE.md,特别是第 7 节多模态评价 + LLM-as-judge 的硬约束。
- Python 3.11+,pydantic v2
- openai(LLM chat + embedding,单一 provider;Sprint 3 起从 anthropic 切过来 consolidate key/计费)
- SQLAlchemy 2.0 + psycopg3(Postgres,Sprint 1)
- redis-py(Redis 热存储,Sprint 1)
- pymilvus + milvus-lite(向量存储,Sprint 3;dev/eval 单进程);Sprint 9 起部署态用 Milvus standalone(docker,MILVUS_SERVER_URI,多进程安全)
- FastAPI(HTTP API,Sprint 2)
- Next.js 16 + React 19 + Tailwind 4(
web/候选人端 + HR 端,Sprint 4/5) - websockets(Sprint 6:火山流式 ASR 的 WS 客户端);TTS/STT 走 HTTP/WS 自研调用点, 不引厂商 SDK
- WebRTC 仅在 Tier A 真口型数字人(LiveTalking,未开工)接入时引入
source .venv/bin/activate
pip install -e . # 装依赖(pyproject.toml 已声明)
python -m src.main # Sprint 1 起:需要 Redis + Postgres 跑(无 API key 仍会进 stub 分支)
uvicorn api.main:app --reload # Sprint 2 起:HTTP API,热重载;/docs 看 OpenAPI
brew services start postgresql # 本机外部服务(macOS)
brew services start redis
docker compose up -d milvus # Sprint 9: 独立 Milvus (多进程/高并发); 不起则用 milvus-lite
python -m src.jobs.worker # Sprint 9: RQ worker (JOBS_QUEUE_ENABLED=1 时需要)
brew services stop redis # 停 Redis环境变量见 .env.example:OPENAI_API_KEY / OPENAI_CHAT_MODEL /
OPENAI_EMBEDDING_MODEL / OPENAI_BASE_URL / POSTGRES_URL / REDIS_URL /
MILVUS_LITE_URI / MILVUS_SERVER_URI(Sprint 9,配了优先) /
JOBS_QUEUE_ENABLED(Sprint 9,开队列须另起 worker) /
SESSION_TTL_SECONDS / LLM_CACHE_TTL_SECONDS /
EMBEDDING_CACHE_TTL_SECONDS;Sprint 6 媒体(全可选,不配 = 纯文字面试):
TTS_PROVIDER / STT_PROVIDER / VOLC_* / AZURE_SPEECH_* / MEDIA_STORAGE_DIR。
运行时按需读取,缺哪个就退到对应的回退分支。
跑 eval(stdlib unittest,无第三方依赖):
python -m unittest evals.test_skeleton # 全部
python -m unittest evals.test_skeleton.ComplianceInvariantTests # 单类
python -m unittest discover -s evals # discoverevals/ 里所有 TestCase 都被强制走 LLM stub(清掉 OPENAI_API_KEY),保证结构性护栏快、稳、不烧 token。
需要 PG+Redis 的端到端 TestCase 在缺 env 时自动 skip。
坑提醒:pymilvus.settings 在 import 时调 load_dotenv() 自动把 .env 塞回 os.environ。
所以 import 了 vector_store 的 eval(如 test_seed_questions、未来的 Planner RAG eval),
模块顶 pop 太早,必须在 setUp 里 pop 才稳。test_skeleton 不 import pymilvus,模块顶 pop 仍然有效。
src/schemas/— 全部 pydantic 数据契约。Agent 输入输出都是这里的类型。src/llm/— OpenAI Chat Completions 的唯一调用点。complete(system, user)同步入口。src/embeddings/— OpenAI Embeddings 的唯一调用点。embed(text)同步入口。src/agents/{planner,interviewer,assessor,evaluator,analyzer}/— 每个 agent 一个__init__.py, 暴露一个动词函数:plan/next_turn/assess/evaluate/analyze。 Assessor 是 Sprint 5.6 新增的独立模块(单题在线打 sufficiency/confidence/followup_goal), 不要把它揉进 Interviewer 或 Evaluator —— 它的并发模型和延迟预算都不一样。src/orchestrator/— 串联 agent 的唯一入口。Agent 之间绝不互相调用。 也是内部状态的独家写入方(blackboard):信念更新 / CandidateModel 沉淀 / integrity_flags / per-session 锁。src/trace/— 决策 trace 收集(contextvars 旁路) + 确定性回放 + diff (Sprint 8.2)。与 coverage 同级的纯共享模块,agent 可 import 埋点。src/beliefs.py— competency 信念高斯共轭更新(8.3);src/candidate_model.py— 跨 stage 候选人记忆(8.4);src/coverage.py— coverage/richness 计算。 三者都是纯数值/纯规则共享模块,不调 LLM 不碰 DB(candidate_model 的 reflection 是唯一例外,timeout+失败跳过)。src/jobs/— RQ 任务队列(Sprint 9):enqueue 封装 + worker 入口。默认关, 开启后候选人上传的 ingest+plan 走独立 worker;任何不可用退回 BackgroundTasks。src/db/— Postgres 归档:base.pyengine(池容量 env 化)、models.pyORM、repository.pysave/load。src/cache/— Redis:会话/trace 热存储 + LLM/embedding/TTS 缓存 + per-session 锁(locks.py)。src/vector_store/— Milvus:questions 题库 + documents RAG 切片。 双模式(Sprint 9):MILVUS_SERVER_URI(standalone,多进程) 优先, MILVUS_LITE_URI(单文件,dev/eval) 兜底;检索恒 Strong 一致性。src/auth/— JWT + bcrypt + httpOnly cookie(cookie 优先,Bearer 兜底—— 写共享 TestClient 的 eval 必须 cookies.clear() 才能用 Bearer 切身份)。src/connectors/— 飞书 OpenAPI(6.7):wiki/docx 拉取,凭证 env 优先 / DB(Fernet 加密) 兜底。src/tts//src/stt/— 面试官语音合成 / 候选人流式转写的唯一调用点(Sprint 6),TTS_PROVIDER/STT_PROVIDER按 region 路由(volc/azure);未配置一律返 None/False, 前端自动退纯文字。新增媒体调用点保持同款「绝不 raise、降级保底」模式。src/media_store/— 面试录像归档(Sprint 6-5)。只录不判:任何"拿录像做 分析/打分"的调用都属 Sprint 7 且受 §7 约束,不许从这里长出来。src/knowledge_pipeline/+src/derivation/— md 语料解析 + 反向出题。离线管线, 不进面试运行时链路,不要与 agent 体系混用。src/main.py— 写死 JD + Resume + 候选人回答跑通全链路的 demo 入口。api/— FastAPI 层(Sprint 2 起):只做 HTTP 入口 + 校验 + 异常映射,业务下沉到 orchestrator。scripts/— 运维脚本(seed_* / ingest / golden 录制 / frozen 冻结 / 压测 / 归属迁移 / 复核统计等,各脚本头注释即用法)。evals/— stdlib unittest,结构性 + 合规护栏 + API smoke(强制 stub)。sim/— 真 LLM 效果评估(烧 token,只能显式跑):persona 仿真 / 对抗 / 公平性 / LLM-as-judge / 各类校准 / RAG 指标 / 冻结基线。与 evals 物理分离, 纪律见 EVALUATION.md。
后续 web/(Next.js)见 sprint.md。
src/llm/complete() 在 OPENAI_API_KEY 未配置或 SDK 不可用时,返回前缀 [stub] ...
的占位文本。每个调用 LLM 的 agent 都必须 if llm.is_stub(text): return fallback,
让骨架在无 key 环境也产出真实可用的输出(而非占位字符串泄漏到结果)。
同款模式:src/embeddings/embed() 缺 key 时返回全零 stub 向量,is_stub_vector() 让
调用方在入 Milvus 前判断是否跳过(全零进库污染向量空间)。
新增 agent / 新增 LLM / embedding 调用点时保持这个模式。
src/db 和 src/cache 顶层 import 不读环境变量、不建连接。
调用 init_db() / save_session() / get_redis() 等才真正连接;未配置 URL 时抛
DatabaseNotConfigured / RedisNotConfigured。新增持久化能力时保持这个模式。
注:Sprint 0 时 src.main 可以无 PG/Redis 跑通;Sprint 1 之后 orchestrator 强依赖
两者(状态机基于 Redis 读写),src.main 需要 PG+Redis 才能跑。
Orchestrator 三段式 API:start_session → submit_answer*N → finalize,
中途可用 resume_session(session_id) 重发当前待答提示(中断恢复)。Session 与 Plan
都在 Redis 里同 TTL 同生共死(SESSION_TTL_SECONDS);finalize 时把 session+report
归档到 Postgres,并立刻从 Redis 删 session 和 plan。run_interview() 是这三段的便利
封装,保留 Sprint 0 风格的一把跑完。
Agent 模块 import 范围:src.llm / src.schemas + 纯共享模块
(src.coverage / src.trace / src.beliefs / src.candidate_model——
无 I/O、不碰 DB/Cache 的计算与埋点),不互相 import,不接触 DB/Cache。
所有路由(planner → interviewer 循环 ↔ assessor → analyzer → evaluator)以及与 Redis/PG 的
交互都在 orchestrator 内完成。新增 agent 时遵循同款:动词函数 + 输入输出都是 schemas 类型。
stage 挂在 InterviewRound.stage 上(self_intro / knowledge / project /
scenario),session 不单存 stage——当前 stage 由「当前题所在 round」派生。
Interviewer 按 plan 的 round 顺序推进。self_intro 永远 0 追问,
拿到的回答存进 session.intro_text 给后续 stage 用(也回灌 evaluator 作软信号)。
Knowledge / scenario 题在 plan 阶段就生成;project 题用 lazy generation:等候选人 self_intro
答完,进 project stage 时再用 intro_text + Resume RAG + CandidateModel 存疑条目
现场生成项目深挖题,避开"读简历瞎猜项目"的失真。追问配额是每题上限
(FollowUpPolicy.max_followups_per_question),stage 默认表在
FollowUpPolicy.for_stage(self_intro=0 / knowledge=1 / project=2 / scenario=2),
新增 stage 时同步更新该表。
Interviewer 每收到一个回答,先调用 Assessor 拿一份 AnswerAssessment
(sufficiency / confidence / missing_signals / strengths / concerns / followup_goal
/ stop_reason),再决定追问 or 跳到下一题。Assessor 的硬约束:
- 走
gpt-4o-mini+ 10s timeout,LLM 调用失败/超时一律回退到 Sprint 0 的启发式判断 (回答字数 + 含项目关键词触发追问),双路径永远共存,不能拆掉启发式。 AnswerAssessment会落库(InterviewSession.assessments),但绝不暴露给 HR UI—— HR 只看 evaluator 的最终结构化报告 +overall;sufficiency / confidence 这些数字是 LLM-as-judge 的中间产物,校准前不可见,校准后也仅作内部诊断信号。- 上线前必须跑 calibration eval(20–30 条人工标注样本对齐 sufficiency 阈值), evals 跑过才能把 Assessor 接进 production codepath。改 Assessor prompt = 重跑校准。
追问与结束都用配置驱动,避免 if-else 散落各处:
- FollowUpPolicy:每题上限
max_followups_per_question(stage 默认表 self_intro=0 / knowledge=1 / project=2 / scenario=2)+ raw/校准双阈值 + Sprint 8.3 的全局预算(total_followup_budget)与信念门 (min_variance_to_probe+min_established_mean,只作用于校准路径)。 Assessor 给的followup_goal(+ CandidateModel 澄清目标)拼进追问 prompt。 HR 在 UI 配的 raw 阈值会在建 job 时经 Platt 映射折算进校准阈值(两路径同一意图)。 - CompletionPolicy:基于
competency_coverage终止,mandatory 每维度还须 ≥min_assessed_per_mandatory(默认 2)道不同题被评过(F5 防单发幸运分逃逸); 硬max_total_questionscap 防失控;use_belief_lcb灰度开关(默认关)可用 信念置信下界替代裸 max。 绝对不做动态补题——题库由 plan + lazy project gen 一次确定,coverage 不够就让 HR 复核 环节人工兜底,不能让 LLM 在线生成新题再考一遍候选人(结果不可复现 + 公平性塌方)。
除 AnswerAssessment 外,以下同待遇——不进总分、不进 HR UI 明细、不见候选人:
CompetencyBelief(8.3) / CandidateModel+SkillClaim(8.4) /
DecisionTrace(8.2,仅审计导出端点) / integrity_flags(8.1)。
新增任何数字信号默认归入此类,想展示须显式评审。
- golden trace(stub,零 token):3 场典型面试的决策序列 diff,改 prompt/
policy 变红属预期信号,确认合理后
scripts/record_golden_traces重录同 commit。 - frozen 批次(真 LLM):9 persona 录制答案复放,输入全固定,批次间分差
只可能来自评估端。题集变更型改动的 frozen Δ 是"变更检测"不是质量裁决,
质量用生成式批次复核;出题变更后
scripts/freeze_persona_answers重冻结。
EvaluationReport 把 content_scores(内容维度,进总分)与 performance_observations
(表现维度,软信号)分开;overall 只由 content_scores 加权得出,不能依赖软信号。
任何新增表现/软信号永远走 PerformanceObservation,永远不进 DimensionScore。
Sprint 5.6 起,Assessor 的 AnswerAssessment 是第三类——既不进总分也不展示给 HR,只在
orchestrator 内做追问决策 + 落库审计。背景见 ARCHITECTURE.md 第 7 节。
嵌套结构(history / answers / content_scores / performance_observations)走 JSONB,
顶层可查询字段(status / job_id / overall / needs_human_review)提列。
有按嵌套字段查询/聚合的需求时再拆子表,不要一次拆完。
Sprint 1 阶段不引 Alembic,用 Base.metadata.create_all;schema 真的开始演进时再切。
- 所有函数带类型注解;数据契约一律放
src/schemas/,不在 agent 里另起 dataclass - LLM 调用一律走
src/llm/,embedding 调用一律走src/embeddings/,不要在 agent 里直接 importopenai - 每个 agent 暴露一个清晰的入口函数,输入输出都是 schemas 里的类型
- 一次只做 sprint.md 里的一个 task,做完立刻验证并 commit,不批量推进
- 改 prompt 模板前确认对应 eval 存在(eval 尚未引入时先记账,Sprint 1 末补上)
- 改任何 prompt / policy 阈值 = 双门禁(Sprint 8.2 起):跑
python -m unittest evals.test_trace_replay(golden trace 决策序列 diff, 变红属预期信号)+ 对应 calibration eval;确认变化合理后python -m scripts.record_golden_traces重录 golden 并随代码同 commit, golden 更新必须显式出现在 diff 里 - 改 Assessor / FollowUpPolicy / CompletionPolicy 前必须先跑 calibration eval—— 这类改动直接影响候选人体验和公平性,不能凭感觉调阈值
- 新增 LLM 调用必带 timeout + 启发式 fallback;不能引入"LLM 挂了整条链路就挂"的依赖
- 多模态分析(视线/表情/语气打分)是 Sprint 7 且必须先落 ARCHITECTURE.md §7 全部护栏才许实现。媒体层(TTS/STT/avatar/录制)已存在但只是纯适配器—— 录像只录不判,任何"拿录像/音频做打分"的代码都不许写
- 不要让 TTS/STT/avatar/录制成为面试链路的硬依赖——任一媒体环节失败必须降级回 文字问答(文字是永远的保底路径)
- 不要把 API key 写进代码(包括
.env.example),用环境变量 - 不要在没有对应 eval 的情况下改 prompt 模板
- 多模态「眼神/语气」信号只能作为参考证据,绝不可作为自动淘汰的唯一依据 (原因见 ARCHITECTURE.md 第 7 节)
- 不要把
AnswerAssessment的数字(sufficiency / confidence)暴露给 HR UI 或候选人—— 那是 LLM-as-judge 的中间产物,校准前不可信,校准后也只内部诊断用 - 候选人端不返回 EvaluationReport / DimensionScore——候选人不接触自己的报告
- 不做动态补题——任何"LLM 在线生成新题再考一遍"的设计直接拒,破坏可复现性 + 公平性
- 不要拆掉 Sprint 0 的启发式 fallback——Assessor 失败时它就是保底
- MAE 校准门禁未过不许开
EVAL_PANEL_ENABLED(Sprint 8.5)——裁判团 替代公式分之前,必须有报告级人工标注 +sim/calibrate_evaluator通过; rubric 权重(RUBRIC_WEIGHT)调整同样要 frozen 复验 + 区分度不塌