feat: 面试前自我介绍功能 - AI 分析候选人背景动态调整提问#1
Conversation
- 新增 interview_phase (intro/questioning) 和 CandidateProfile 类型 - 面试开始后先收集候选人自我介绍 - AI 自动提取技术栈、经验年限、项目亮点 - 根据候选人背景动态调整面试提问方向和深度 - 更新 landing page 展示新功能(demo、features、flow) - 中英文 i18n 同步更新
|
Blackman99
left a comment
There was a problem hiding this comment.
Code Review: feat/self-introduction
总体评价:功能设计清晰,代码结构合理,但有几个需要修复的问题,主要集中在逻辑正确性和健壮性上。
🔴 必须修复
1. intro→questioning 过渡时 trigger 消息永远不会被注入
文件:src/interviewer.ts
generate_interviewer_turn 里的 trigger 注入依赖 messages.length === 0:
if (messages.length === 0) {
const trigger = interview.interview_phase === 'intro' ? '...' : '...';
messages.push({ role: 'user', content: trigger });
}但在 intro 分支里,handle_candidate_reply 函数开头已经执行了 append_message(interview_id, 'user', candidate_message),所以当 phase 切换到 questioning 后调用 generate_interviewer_turn 时,get_conversation 返回的历史已经包含开场白 + 候选人自我介绍,messages.length 不为 0,questioning 阶段的过渡 trigger 永远不会被注入。
结果:AI 在 questioning 阶段第一轮没有明确的过渡指令,可能直接跳入提问而没有「感谢您的介绍」的过渡语。
建议修复:给 generate_interviewer_turn 增加一个可选的 inject_trigger?: string 参数,在 intro→questioning 过渡时显式传入:
async function generate_interviewer_turn(
interview: Interview,
time_remaining_ms: number | null,
inject_trigger?: string, // 新增
): Promise<string> {
// ...
if (messages.length === 0 || inject_trigger) {
const trigger = inject_trigger ?? (interview.interview_phase === 'intro' ? '...' : '...');
messages.push({ role: 'user', content: trigger });
}
}2. analyze_self_introduction 的 interview_id 参数未使用
文件:src/interviewer.ts:137
async function analyze_self_introduction(interview_id: number, intro_text: string): Promise<CandidateProfile>interview_id 在函数体内完全未使用,是死参数,应删除。
3. DB migration 默认值导致历史数据状态错误
文件:src/db.ts:75
ALTER TABLE interviews ADD COLUMN interview_phase TEXT NOT NULL DEFAULT 'intro'已处于 in_progress 或 completed 状态的历史面试,升级后 interview_phase 会被设为 'intro'。若这些面试还有后续消息处理,会错误进入 intro 分支。
建议将默认值改为 'questioning',或在 migration 后补一条 UPDATE:
UPDATE interviews SET interview_phase = 'questioning'
WHERE status IN ('in_progress', 'completed', 'ready', 'notified')🟡 建议改进
4. analyze_self_introduction 解析失败无日志
文件:src/interviewer.ts:163
JSON 解析失败时静默降级,生产环境难以排查。建议加 console.error 记录原始响应。
5. thinking: { type: 'adaptive' } 与 max_tokens: 1500 可能冲突
文件:src/interviewer.ts:196
Anthropic extended thinking 要求 max_tokens 足够大(thinking tokens + output tokens)。max_tokens: 1500 在启用 adaptive thinking 时可能导致 API 报错或输出被截断。建议提高到 16000 或移除 thinking 参数。
6. candidate_profile 反序列化缺少结构校验
文件:src/db.ts:129
candidate_profile: row.candidate_profile ? JSON.parse(row.candidate_profile as string) : null,直接类型断言,没有校验字段结构。数据库中若存有格式不符的旧数据,会在运行时静默出错。
7. i18n key analyzing 和 intro_received 是死代码
文件:src/i18n/locales/zh-CN.json、en-US.json
两个新增 key 在 interviewer.ts 中均未使用。候选人提交自我介绍后需等待两次 AI 调用,期间没有任何反馈消息。建议在 intro 分支中实际使用这些 key 向用户发送中间状态提示,否则删除。
8. Landing page flow5 描述主语不一致
文件:docs/index.html
中文 flow5 描述「面试官请候选人做自我介绍」与整体「面试官只需完成第一步」的定位矛盾。建议改为「AI 自动请候选人做自我介绍」。
✅ 做得好的地方
CandidateProfile类型定义清晰,字段语义明确- migration 用 try/catch 包裹,兼容已有数据库
analyze_self_introduction有完善的 fallback 默认值- intro 阶段 system prompt 独立构建,职责分离清晰
- i18n 中英文同步更新,无遗漏
- HTML demo 对话和 flow steps 与功能逻辑一致
Blackman99
left a comment
There was a problem hiding this comment.
Code Review: feat/self-introduction
总体评价:功能方向正确,但有几个需要修复的问题才能合并。
严重问题
1. Race condition:interview_phase 未做并发保护(interviewer.ts:108-125)
handle_candidate_reply 先读取 phase,再异步调用 analyze_self_introduction,再写回 phase。如果候选人在分析期间快速发送第二条消息,两个并发调用都会进入 intro 分支,导致:
analyze_self_introduction被调用两次(浪费 API 调用)- 两次
generate_interviewer_turn都用questioning的 trigger,但 conversation history 里只有一条用户消息,第二次调用会产生重复的过渡语
建议:在内存中维护一个 Set<number> 记录「正在处理 intro」的 interview_id,进入前检查并加锁,完成后释放。
2. analyze_self_introduction 参数 interview_id 未使用(interviewer.ts:137)
函数签名接收 interview_id: number 但函数体完全没有用到它。这是死参数,会让读者误以为函数会读取历史记录或做持久化操作。应改为 analyze_self_introduction(intro_text: string)。
中等问题
3. get_candidate_profile 是多余的 DB 往返(db.ts:290-293)
该函数内部调用 get_interview 再取 candidate_profile,但目前代码里没有任何地方调用这个函数,是死代码。建议删除,调用方直接用 interview.candidate_profile。
4. interview_phase migration 默认值语义问题(db.ts:77)
Migration 给已有记录设置 DEFAULT 'intro',但这些记录可能已经是 in_progress 或 completed 状态的面试。给已完成的面试打上 intro phase 语义不正确。
建议:migration 应根据 status 设置合理默认值,或在 SCHEMA_SQL 里直接加上这两列,避免 migration 的歧义。
5. JSON.parse 无类型校验(db.ts:131,interviewer.ts:170)
JSON.parse(row.candidate_profile as string) as CandidateProfile 没有运行时校验。如果 DB 里存了格式不对的数据,会在运行时静默产生类型错误。analyze_self_introduction 里 parse 后直接 cast 同理,没有校验 tech_stack 是否为数组等。
建议:加一个简单的 type guard 函数做运行时校验。
小问题 / 建议
6. analyzing i18n key 已定义但未使用
en-US.json 和 zh-CN.json 都加了 interview.analyzing,但 interviewer.ts 里没有任何地方发送这条消息给候选人。候选人在等待 AI 分析时没有任何反馈。建议在 intro 分支开始时先发送一条「正在分析...」消息。
7. SCHEMA_SQL 未同步新列(db.ts:23-58)
新增的 interview_phase 和 candidate_profile 列只在 migration 里加了,SCHEMA_SQL 里的 CREATE TABLE 没有更新。对全新部署没有影响,但 SCHEMA_SQL 作为文档不准确。建议把这两列加到 CREATE TABLE 里。
8. intro phase 的 system prompt 缺少 questions_text(interviewer.ts:21-36)
intro phase 的 prompt 里没有注入面试题目。如果有意省略,加个注释说明原因。
- 添加 intro 阶段并发处理 race condition 保护 (Set guard) - 修复 DB migration 默认值:已有面试默认 questioning,仅新面试为 intro - SCHEMA_SQL 同步新增 interview_phase/candidate_profile 列 - 添加 CandidateProfile 运行时类型校验 (is_valid_candidate_profile) - safe_parse_candidate_profile 替代裸 JSON.parse - 删除未使用的 get_candidate_profile 死代码 - analyze_self_introduction 移除未使用的 interview_id 参数 - generate_interviewer_turn 新增 inject_trigger 参数解决过渡消息问题 - JSON.parse 失败时增加 console.error 日志 - max_tokens 从 1500 提升到 16000 (adaptive thinking 需要) - 移除未使用的 i18n keys (analyzing, intro_received) - Landing page flow5 描述修正
Blackman99
left a comment
There was a problem hiding this comment.
整体设计思路清晰,intro 阶段的拆分合理,race condition guard 有考虑到。但有几个功能性问题需要修复:
必须修复
analyze_self_introduction只分析最后一条消息,多轮自我介绍会丢失信息(见 interviewer.ts:121)set_candidate_profile+set_interview_phase非原子写入,崩溃后会出现 phase=questioning 但 profile=null 的不一致状态(见 interviewer.ts:122)- 并发 intro 被拦截时静默返回空字符串,调用方无法区分「正常空回复」和「被拦截」(见 interviewer.ts:116)
建议修复
4. String.replace 只替换第一次出现的 INTERVIEW_COMPLETE(见 interviewer.ts:138)
5. 贪婪正则可能匹配到错误范围(见 interviewer.ts:184)
6. parse_interview 中 InterviewPhase 没有合法性校验(见 db.ts:159)
顺带一提(不在本次 diff 内):generate_summary 里 "generated_at": ${Date.now()} 把时间戳硬编码进 prompt,期望 LLM 原样回传,这很脆弱。建议在 extract_summary 解析完成后由代码赋值。
1. Race condition 静默失败 → 添加 warn 日志,返回 skipped 标记 2. 自我介绍只取最后一条 → 收集 intro 阶段所有 user 消息拼接 3. 非原子写入 → 新增 set_phase_and_profile 原子函数 4. String.replace → replaceAll 避免 INTERVIEW_COMPLETE 残留 5. 贪婪正则 → 非贪婪匹配 JSON 6. InterviewPhase 无校验 → 添加合法值校验,非法值 fallback 到 intro
- 功能列表新增「智能自我介绍分析」 - 状态流转图新增 intro 阶段 - 状态表新增 intro 状态说明 - 候选人操作流程新增自我介绍步骤 - types.ts 架构描述更新
- eslint.config.js: flat config, typescript-eslint recommended - .github/workflows/ci.yml: PR 和 push 触发 lint + typecheck - package.json: 新增 lint / lint:fix scripts
- vitest.config.ts: 测试配置 - src/db.test.ts: 25 个数据库层单元测试 - CRUD、状态流转、phase/profile 原子操作 - 异常 JSON 处理、无效 phase fallback - 消息、语言偏好、查询辅助函数 - research notes 和 summary - .github/workflows/ci.yml: 新增 test job - package.json: 新增 test/test:watch scripts
新增测试文件: - parser.test.ts: 15 个测试 — parse_schedule_request 完整覆盖 (JSON 解析、时间校验、duration 范围、@ 去除、嵌入 JSON、边界值) - interviewer.test.ts: 16 个测试 — build_interviewer_system_prompt + extract_summary (intro/questioning phase、candidate_profile、research_notes、时间显示、 JSON 提取、markdown code block、fallback、thinking blocks) - scheduler.test.ts: 4 个测试 — fmt_time 时区格式化 (zh-CN/en-US locale、CST 时区、跨日期) 扩展 db.test.ts (+18 个测试): - is_valid_candidate_profile: 10 个边界用例 (null/undefined/非对象/缺字段/类型错误) - safe_parse_candidate_profile: 8 个用例 (null/undefined/空字符串/无效 JSON/结构错误/数组/类型错误) 导出内部函数用于测试 (@internal): - db.ts: is_valid_candidate_profile, safe_parse_candidate_profile - interviewer.ts: build_interviewer_system_prompt, extract_summary - scheduler.ts: fmt_time
- 移动 src/*.test.ts → tests/*.test.ts - 更新 import 路径 ./xxx.js → ../src/xxx.js - 更新 vi.mock 路径 ./xxx.js → ../src/xxx.js - 新增 tsconfig.test.json (extends tsconfig.json, 包含 src + tests) - 更新 eslint.config.js: projectService.defaultProject + allowDefaultProject - 更新 vitest.config.ts: include tests/**/*.test.ts - 更新 package.json lint 脚本包含 tests/ - 修复 db.test.ts 中 vi.mock factory 内错误嵌入的 describe 块
功能概述
面试开始后,不再直接进入提问环节,而是先请候选人做自我介绍,AI 自动分析其背景后动态调整后续面试方向。
改动内容
核心逻辑 (5 files, +142 lines)
InterviewPhase类型 (intro|questioning) 和CandidateProfile接口interview_phase和candidate_profile列(含 migration),以及读写函数Landing Page (docs/index.html)
CandidateProfile 结构
面试流程变化
TypeScript 编译通过,兼容已有数据库。