# 零资料新用户 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= 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、源码或文档中。