Files
resume-agent/PRD.md
T
2026-07-20 14:48:41 +08:00

716 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 零资料新用户 AI 简历创建 Agent PRD
## 1. 文档信息
| 项目 | 内容 |
| --- | --- |
| 产品名称 | AI 创建第一份简历 |
| 文档版本 | MVP v1.0 |
| 适用用户 | 没有现成个人资料或简历的新用户 |
| 前端 | Vue |
| 后端 | Python + FastAPI |
| 核心编排 | Python 显式有限状态机(FSM |
| 模型接入 | OpenAI Python SDK 兼容网关 |
| 当前实现目录 | \`resume-agent-mvp/\` |
本文档描述产品目标、交互规则、内容门禁、模型边界、技术方案和验收标准。它以“零资料新用户”为前提,不从已有个人资料或历史简历开始。
## 2. 产品定义
用户进入“AI 创建第一份简历”后,在一个连续的对话时间线中完成:隐私授权、简历手机号、姓名、求职阶段、第一段必要经历采集和确认。结构化信息使用对话消息中的可操作组件,自由描述使用聊天输入框。
产品的核心不是让模型自由规划流程,而是让模型处理事实,让确定性的状态机持续约束流程和内容门禁。
### 2.1 MVP 目标
1. 新用户可以从 0 资料开始创建一份业务简历。
2. 结构化步骤不依赖用户自由输入,全部以对话组件呈现。
3. Agent 能从自然语言中抽取经历事实,并只追问当前最高优先级缺口。
4. 模型不能自行决定阶段、组件、门禁或数据库写入。
5. 业务简历创建不被可选字段阻塞;创建后继续丰富。
6. AI 改写必须经过用户确认后才写入正式简历内容。
### 2.2 非目标
- V1 不加载或合并用户已有个人资料、旧简历或历史会话。
- V1 不做简历投递、职位匹配、JD 定制和人工审核。
- V1 不引入 RAG、向量数据库、多 Agent 或长期语义记忆。
- V1 不让模型直接操作业务数据库。
## 3. 已有业务字段
业务库已有字段如下。姓名、手机号为必填;其余字段按门禁规则分为首段必要字段和创建后的可选字段。
### 3.1 个人信息
- 姓名
- 手机号
- 微信
- 邮箱
- 所在城市
- 个人作品集链接
### 3.2 经历和标签
- 教育经历(多段):学校名、专业、学历、是否全日制、就读开始时间、就读结束时间、学习经历描述
- 工作经历(多段):公司名、职位、工作开始时间、工作结束时间、工作经历描述
- 实习经历(多段):公司名、职位、实习开始时间、实习结束时间、实习经历描述
- 项目经历(多段):项目名、项目角色、项目开始时间、项目结束时间、项目经历描述
- 竞赛(多段):竞赛名、获奖名称、获奖时间、竞赛经历描述
- 证书(多标签):正式名
- 技能(多标签):技能名
## 4. 用户流程
### 4.1 首次创建路径
~~~text
进入“AI 创建第一份简历”
→ 隐私授权组件
→ 手机号来源组件
→ (选择其他号码时)手动手机号组件
→ 姓名输入组件
→ 求职类型组件
→ (其他类型时)首段经历类型组件
→ 开放式首段经历问题
→ LLM 抽取 + 缺口追问
→ 首段经历确认组件
→ 最低门禁达成
→ 用户点击创建简历
→ 创建后丰富
~~~
### 4.2 对话时间线原则
所有消息都保留在同一条时间线中,不跳转独立表单页面。已提交组件变为只读,用户可以回顾之前的选择。
示例:
~~~text
Agent 隐私授权卡
→ 用户点击同意
→ Agent 手机号选择卡
→ 用户选择使用其他号码
→ Agent 手机号输入卡
→ 用户提交号码
→ Agent 姓名输入卡
→ 用户提交姓名
→ Agent 求职类型卡
→ 用户选择校招
→ Agent 开放式经历问题
→ 用户自然语言描述
→ Agent 日期补充卡
→ 用户选择日期
→ Agent 经历确认卡
→ 用户确认
→ Agent 创建简历卡
~~~
### 4.3 语气定义
Agent 使用“温和、明确、正式但不僵硬”的中文语气:
- 先确认已理解的事实,再说明还缺什么。
- 每轮只追问一个最高优先级缺口。
- 不责备用户信息不完整,不使用压迫表达。
- 对未知事实明确说“暂时不知道”,不替用户猜测。
- 正式化时使用职业化表达,但保留用户原始事实和数字。
- 结构化组件前用一句短说明解释用途,避免连续输出长段落。
推荐表达:
> 我已经记录了公司和职位。为了确认这段经历,还需要补充开始和结束时间。
不推荐表达:
> 你的信息不完整,请重新输入全部内容。
## 5. 结构化对话协议
### 5.1 Agent 消息模式
| 模式 | 自由输入框 | 适用场景 |
| --- | --- | --- |
| \`ui_only\` | 关闭 | 隐私、手机号、姓名、求职类型、确认、创建 |
| \`chat\` | 开启 | 开放式经历描述、事实补充 |
| \`hybrid\` | 按组件配置 | 日期补充、创建后丰富、确认后的继续对话 |
### 5.2 ConversationTurn
~~~text
ConversationTurn
- message_id
- sender: assistant | user | system
- stage
- input_mode: ui_only | chat | hybrid
- blocks[]
- composer
- created_at
~~~
\`blocks\` 支持:
~~~text
TextBlock
ComponentBlock
ResumePatchBlock
StatusBlock
ErrorBlock
~~~
组件块:
~~~text
ComponentBlock
- component_id
- component_name
- component_version
- props
- status: active | submitted | expired | replaced | confirmed
- allowed_actions
- validation_schema
~~~
输入框:
~~~text
composer:
enabled
placeholder
accepted_input: text | none
max_length
~~~
### 5.3 组件生命周期
1. Python 状态机返回当前阶段。
2. 渲染层根据阶段输出唯一主要活动组件。
3. 用户提交组件事件。
4. 服务端校验组件 ID、事件、会话版本和 payload。
5. 原组件变为 \`submitted\` 或 \`confirmed\`,不可重复生效。
6. 时间线追加结构化用户回复摘要和下一条 Agent 消息。
7. 修改关键字段时,依赖它的确认状态失效并重新计算门禁。
同一时间只能有一个主要 \`active\` 组件。点击过期组件返回 \`COMPONENT_EXPIRED\`,并引导用户回到当前有效组件。
## 6. 阶段与组件映射
阶段由 Python FSM 生成,LLM 不得输出阶段或组件名称。
| Stage | 主要组件 | 输入模式 |
| --- | --- | --- |
| \`PRIVACY_CONSENT\` | \`PrivacyConsentCard\` | \`ui_only\` |
| \`PHONE_SELECTION\` | \`ResumePhoneSelector\` | \`ui_only\` |
| \`MANUAL_PHONE_INPUT\` | \`ResumePhoneInput\` | \`ui_only\` |
| \`NAME_CAPTURE\` | \`ResumeNameInput\` | \`ui_only\` |
| \`JOB_TYPE_SELECT\` | \`JobTypeCards\` | \`ui_only\` |
| \`ANCHOR_TYPE_SELECT\` | \`AnchorTypeCards\` | \`ui_only\` |
| \`ANCHOR_COLLECTING\` | 文本问题、短文本、学历、日期组件 | \`chat\` / \`hybrid\` |
| \`CONTENT_DISAMBIGUATION\` | \`ChoiceChips\` 或补充提示 | \`ui_only\` / \`chat\` |
| \`ANCHOR_CONFIRM\` | \`ExperienceConfirmCard\` | \`ui_only\` |
| \`MINIMUM_READY\` | \`CreateResumeCard\` | \`ui_only\` |
| \`RESUME_CREATING\` | \`CreatingStatusCard\` | \`ui_only\` |
| \`RESUME_ENRICHING\` | 文本、进度卡、字段组件 | \`chat\` / \`hybrid\` |
| \`CONTENT_READY\` | \`ExperienceConfirmCard\`、\`ContentReadyCard\` | \`ui_only\` / \`hybrid\` |
| \`CREATE_FAILED\` | \`CreateRetryCard\` | \`ui_only\` |
### 6.1 隐私卡
隐私文案由产品预设,不由模型生成,至少包含:
- 数据处理目的。
- 将收集的信息类型。
- AI 如何处理用户输入。
- 不会自动公开、投递或发送给第三方。
- 用户可以退出并删除草稿。
- 完整隐私政策链接和版本号。
- “同意并继续”和“暂不使用”按钮。
### 6.2 手机号
登录手机号存在时,展示脱敏号码和两个选项:
- 使用此号码。
- 使用其他号码。
手动号码使用前端和后端双重格式校验:
~~~text
^1[3-9]\d{9}$
~~~
不发送验证码,不验证号码归属,只提示“仅完成格式校验”。完整手机号不得进入 LLM prompt、模型上下文或 trace。
### 6.3 姓名和求职类型
姓名使用短文本组件,过滤纯空格和明显非法内容。求职类型提供:
- 校招
- 社招
- 其他
求职类型用于选择首段经历路径和创建后丰富优先级,不写入已有简历字段。
## 7. 内容门禁
### 7.1 两级门禁
| 门禁 | 作用 | 条件 |
| --- | --- | --- |
| 业务创建门禁 | 允许创建业务简历实体 | 核心资料、求职类型、首段必要经历已确认 |
| 正式内容门禁 | 判断已有正式可用内容 | 业务简历已创建,至少一段 AI 改写已被用户确认,无冲突 |
正式内容门禁不能反向删除或隐藏已经创建的业务简历。
### 7.2 首段必要经历映射
| 求职类型 | 默认锚点 | 创建所需字段 |
| --- | --- | --- |
| 校招 | 教育经历 | 学校、专业、学历、开始时间、结束时间或至今 |
| 社招 | 工作经历 | 公司、职位、开始时间、结束时间或至今 |
| 其他 | 用户选择教育、工作、实习或项目 | 对应记录的核心字段 |
实习核心字段为公司、职位、开始时间、结束时间或至今;项目核心字段为项目名、角色、开始时间、结束时间或至今。
以下信息不阻塞业务简历创建:
- 经历描述和成果。
- 是否全日制。
- 第二段经历。
- 微信、邮箱、城市、作品集。
- 技能、证书和竞赛。
社招用户没有正式工作经历时,提供:使用实习、使用项目、修改求职类型、保存草稿并退出。
### 7.3 门禁公式
~~~text
can_create_resume =
privacy_accepted
AND resume_phone_format_valid
AND name_confirmed
AND job_type_confirmed
AND anchor_type_allowed
AND anchor_required_fields_complete
AND anchor_dates_valid
AND anchor_confirmed
AND unresolved_conflicts == 0
~~~
达到门禁后才展示 \`CreateResumeCard\`。用户点击主按钮后,服务端才写入业务简历。
### 7.4 AI 改写确认门禁
创建后的经历先生成提议内容:
~~~text
用户事实
→ LLM 抽取
→ LLM 正式化
→ ExperienceConfirmCard 展示提议
→ 用户确认
→ 更新正式简历 revision
~~~
用户选择“需要调整”时,提议内容不得写入正式简历,回到 \`RESUME_ENRICHING\` 继续对话。
## 8. 首段经历采集 Loop
### 8.1 开放式首问
校招:
> 请介绍当前或最高的一段教育经历,包括学校、专业、学历和就读时间。你可以像平时聊天一样描述。
社招:
> 请介绍一段最近或最有代表性的工作,包括公司、职位和大致任职时间。
实习:
> 请介绍一段实习经历,包括公司、职位和实习时间。
项目:
> 请介绍一个代表性项目,包括项目名、你的角色和项目时间。
### 8.2 处理链路
~~~text
自然语言输入
→ LLM 抽取候选字段
→ Pydantic Schema 校验
→ 确定性格式与时间校验
→ 更新临时经历
→ 计算缺失字段
→ 输出一个最高优先级问题或组件
→ 字段齐全后输出确认卡
→ 用户确认
→ MINIMUM_READY
~~~
LLM 输出可以包含 \`field_updates\`、\`evidence_spans\`、\`ambiguities\` 和 \`extra_records\`,但不能输出当前 Stage、Vue 组件、门禁结果或写库指令。
### 8.3 缺口优先级
工作和实习:
1. 公司与职位。
2. 开始和结束时间。
3. 工作/实习归属歧义。
4. 经历确认。
5. 创建后追问职责、行动、方法和成果。
教育:
1. 学校。
2. 专业与学历。
3. 开始和结束时间。
4. 经历确认。
5. 创建后追问全日制和学习亮点。
项目:
1. 项目名与角色。
2. 开始和结束时间。
3. 经历确认。
4. 创建后追问行动、方法和成果。
一轮只处理一个最高优先级缺口,但同一条消息可以抽取多个字段。
### 8.4 缺口组件策略
| 缺失内容 | 组件或交互 |
| --- | --- |
| 公司、职位、学校、专业、项目名 | 短文本组件或针对性追问 |
| 学历 | \`DegreeSelector\` |
| 开始和结束时间 | \`DateRangeSelector\` |
| 工作、实习、项目归属 | \`ChoiceChips\` |
| 职责、行动、方法、成果 | 自然语言问题 |
| 整段经历确认 | \`ExperienceConfirmCard\` |
结构化事实优先使用组件;需要回忆、组织和解释的内容优先使用自然语言。
## 9. 复杂交互规则
### 9.1 一次提供多段经历
- 拆分成多个临时记录。
- 选择符合求职类型的一段完成业务创建门禁。
- 其他记录保留到创建后的丰富阶段。
- 未确认的其他记录不得阻止业务简历创建。
### 9.2 内容归属不明确
显示:
> 这段内容更接近哪种经历?
选项:工作、实习、项目。选择前不得写入具体业务模块。
### 9.3 时间处理
- “至今”是合法结束状态。
- 结束时间早于开始时间必须要求修正。
- 模型不能自行推断“至今”。
- 未知月份不能自动填成一月。
- 年月统一使用 \`YYYY-MM\`。
### 9.4 修改和幂等
- 修改姓名会同步更新简历名称。
- 修改首段关键字段会使确认状态失效。
- 修改求职类型会重新计算必要锚点,但保留已采集事实。
- 已提交组件重复提交不能产生第二次状态变更。
- 创建简历按 session 幂等,重试不能产生重复业务简历。
### 9.5 解析失败
1. Structured Output 校验失败自动重试一次。
2. 再失败时不推进阶段。
3. 保留原始用户消息,输出针对性的组件或追问。
4. 网关不可用时按配置选择规则降级或安全报错。
## 10. 创建后丰富
### 10.1 创建成功消息
包含:
- 创建成功状态。
- 简历 ID 或名称。
- 当前完成度。
- “继续完善”和“稍后再说”。
- 下一条高价值问题。
“稍后再说”是正常结束,不计为技术失败。
### 10.2 丰富优先级
社招:首段工作职责 → 行动/方法/成果 → 更多工作经历 → 项目 → 教育/技能/证书 → 可选联系方式。
校招:实习或项目 → 竞赛 → 教育亮点 → 技能和证书 → 可选联系方式。
其他:当前锚点描述和成果 → 下一段核心经历 → 技能、证书和可选信息。
## 11. Agent 与 LLM 边界
### 11.1 Python FSM 负责
- 当前阶段和合法转移。
- 活动组件和输入模式。
- 门禁计算。
- 字段格式和时间校验。
- 组件生命周期和幂等。
- 草稿、业务简历写入。
- 错误码和恢复策略。
### 11.2 LLM 负责
- 从用户自然语言抽取明确事实。
- 标注证据片段和歧义。
- 将已确认事实改写为正式简历表达。
### 11.3 LLM 禁止负责
- 决定 Stage。
- 决定使用哪个 Vue 组件。
- 宣布门禁通过。
- 自行补全缺失事实。
- 覆盖用户已确认字段。
- 直接写数据库。
- 处理完整手机号、登录标识或会话元数据。
## 12. 技术架构
### 12.1 V1 选型
- Vue 对话组件系统。
- Python FastAPI。
- Python 显式 FSM。
- Pydantic 状态、事件和模型输出 Schema。
- SQLite 会话、时间线和 MVP 业务简历存储。
- OpenAI Python SDK 兼容网关。
- 前端开发代理和 JSON API。
V1 暂不使用 LangChain、LangGraph、AgentExecutor、多 Agent、RAG 或向量数据库。当前流程是明确枚举的单主线状态机,显式实现更容易测试非法事件、回退、幂等和门禁。
当出现上传简历解析、旧简历合并、JD 定制、并行模型节点或人工审核时,再评估迁移 LangGraph。
### 12.2 服务接口
~~~python
transition(
state: ResumeDraftState,
event: ResumeEvent,
) -> TransitionResult
~~~
模型接口:
~~~text
ExperienceExtractor
ResumeRewriter
~~~
渲染层根据 \`TransitionResult\` 生成 \`ConversationTurn\`。
### 12.3 OpenAI SDK 配置
~~~dotenv
RESUME_AGENT_LLM_PROVIDER=openai
OPENAI_API_KEY=<server-side-secret>
OPENAI_BASE_URL=https://re.94xy.cn
OPENAI_MODEL=gpt-5.5
OPENAI_STRUCTURED_OUTPUT_MODE=json_schema
OPENAI_TIMEOUT_SECONDS=30
OPENAI_MAX_RETRIES=2
OPENAI_STRUCTURED_OUTPUT_RETRIES=1
RESUME_AGENT_LLM_FALLBACK_TO_RULES=true
~~~
代码使用官方 SDK 的兼容形式:
~~~python
client = OpenAI(
api_key=settings.openai_api_key,
base_url=settings.openai_base_url,
timeout=settings.openai_timeout_seconds,
max_retries=settings.openai_max_retries,
)
client.chat.completions.create(
model=settings.openai_model,
messages=messages,
response_format=response_format,
)
~~~
如果网关不支持 \`json_schema\`,切换为 \`json_object\`,并继续由 Pydantic 校验结果。
## 13. API 设计
~~~text
POST /ai-api/resume-agent/sessions
GET /ai-api/resume-agent/sessions/{id}/timeline
POST /ai-api/resume-agent/sessions/{id}/component-events
POST /ai-api/resume-agent/sessions/{id}/messages
POST /ai-api/resume-agent/sessions/{id}/create
DELETE /ai-api/resume-agent/sessions/{id}
~~~
组件事件:
~~~json
{
"component_id": "block_xxx",
"event": "submit",
"payload": {},
"revision": 3,
"idempotency_key": "event_xxx"
}
~~~
Agent 响应至少包含:
~~~text
session_id
revision
stage
turn / turns
missing_fields
gate.core_ready
gate.anchor_ready
gate.can_create
gate.formal_content_ready
gate.blockers
draft_id
resume_id
trace_id
~~~
手机号和登录标识只在服务端内部状态及脱敏视图中处理,不进入模型请求和客户端日志。
## 14. 隐私与安全
- API Key 只放服务端环境变量,不进入 Vue、Git、README、测试或模型输出。
- \`.env\` 文件必须被 Git 忽略。
- 手机号、邮箱、微信号在 SDK 边界二次脱敏。
- 模型请求使用 allow-list DTO,不传完整 profile、metadata、姓名或登录手机号。
- trace 只使用随机 ID,不携带 prompt、响应体或用户内容。
- 网关错误对客户端返回安全错误摘要,不返回响应体和凭证。
- 用户自由文本可持久化到会话时间线;生产环境需关闭反向代理和 APM 的请求体采集,或增加日志脱敏。
- 用户可以删除草稿和整个创建会话。
- 当前使用的网关密钥应使用专用、可撤销凭证;明文暴露后应轮换。
## 15. 验收指标
| 指标 | MVP 目标 |
| --- | ---: |
| Stage 与组件映射正确率 | 100% |
| UI-only 阶段错误开放文本输入 | 0 |
| 已提交组件重复生效 | 0 |
| 过期组件修改当前状态 | 0 |
| 刷新后活动组件恢复正确率 | ≥99.5% |
| 已确认字段被重复追问 | ≤1% |
| 内容门禁误放率 | ≤1%,关键集为 0 |
| 内容门禁误阻率 | ≤3% |
| 可选字段阻止业务创建 | 0 |
| 第一段必要经历完成率 | ≥75% |
| 首段经历提问轮数 P50 | ≤4 |
| 首段经历提问轮数 P90 | ≤7 |
| 门禁后一次确认进入创建 | ≥95% |
| 业务简历写库成功率 | ≥99% |
| 重复创建简历 | 0 |
| 创建后同会话继续丰富率 | ≥35% |
| 字段抽取 micro-F1 | ≥95% |
| AI 改写关键事实虚构 | 0 |
| 完整手机号进入 LLM 或 trace | 0 |
## 16. Eval 方案
### 16.1 离线数据集
建立脱敏样本集,覆盖:
- 只提供公司和职位。
- 一条消息同时提供多字段。
- 中文和英文公司/职位。
- 至今、缺月份、倒序日期。
- 多段经历混写。
- 工作/实习/项目归属歧义。
- 提示注入、虚构数字、敏感信息。
- 用户修改已确认字段。
### 16.2 自动评测
- 字段级 precision、recall、micro-F1。
- 日期合法率和倒序拦截率。
- 门禁误放、误阻率。
- 组件映射和输入模式准确率。
- 过期事件、重复事件、幂等测试。
- 改写数字和事实 grounding 检查。
- 手机号、邮箱、微信号泄露扫描。
### 16.3 人工评测
每条改写按事实保持、正式程度、可读性、成果表达和语气分别评分。任何新增关键数字、公司、职位、技术栈或成果都判定为严重错误。
## 17. 测试计划
### 17.1 后端
- FSM 单元测试。
- Pydantic Schema 和模型解析测试。
- Fake OpenAI SDK 测试。
- API 全流程测试:校招、社招、其他类型。
- 组件生命周期和过期事件测试。
- 手机格式和隐私泄露测试。
- 业务简历创建幂等测试。
- AI 改写确认后才更新 revision 的测试。
### 17.2 前端
- Vue 类型检查。
- Vite 生产构建。
- Playwright E2E:隐私 → 手机号 → 姓名 → 求职类型 → 首段经历 → 创建 → 丰富。
- UI-only 阶段输入框关闭测试。
- 刷新后活动组件恢复测试。
- 错误、重试、修改和移动端布局测试。
### 17.3 真实网关 Smoke Test
使用 \`backend/scripts/smoke_llm.py\` 发起一次抽取请求,确认:
- API Key 从环境变量读取。
- Base URL 和模型 ID 正确。
- 网关支持当前结构化输出模式。
- 返回内容能通过 Pydantic 校验。
- 请求中不含完整手机号或其他受保护字段。
## 18. MVP 当前实现与后续迭代
当前 MVP 已实现单一对话时间线、Python FSM、SQLite 会话、结构化组件、首段经历门禁、创建后丰富、OpenAI SDK 适配、规则 fallback 和 AI 改写确认。
后续迭代优先级:
1. 接入正式业务简历服务和认证用户上下文。
2. 完整支持教育、工作、实习、项目、竞赛、证书和技能字段的创建后组件。
3. 增加 SSE 流式输出和更细粒度的进度状态。
4. 增加 Playwright E2E 测试及 CI。
5. 增加人工审核和事实冲突处理。
6. 评估上传简历解析、旧简历合并和 JD 定制。
## 19. Definition of Done
满足以下条件才可称为 MVP 完成:
- 新用户可以从零资料进入完整首段经历流程。
- 所有结构化步骤均在对话时间线中完成。
- 未满足门禁时不会展示可创建按钮或允许创建接口成功。
- 满足门禁后由用户点击创建,且创建按 session 幂等。
- AI 改写在用户确认前不更新正式简历。
- LLM 失败时状态机不被推进到非法阶段。
- 测试、类型检查和生产构建通过。
- API Key 不出现在前端、日志、trace、源码或文档中。