🤵 Alfred

上传工作流设计 · V2
V1 重新设计定稿 · 2026-08-03
本可视化已更新至 V1 重新设计(V2,2026-08-03)。 旧版(v0.1 基线:company/position/job_family/tag.namespace='skill'/coffee_chat/LaTeX 简历)已全面作废。 权威文本见本站顶层:数据模型 V2术语表ADR 0009–0021。 旧草稿保留于 archive/v0.1-baseline(仅供追溯,请勿照此实现)。

0这份文档在说什么

只讲一件事:东西是怎么从「你随手发一段话/一张截图」变成「库里干净、能查、能复用、可主动提醒你的数据」的。

你发东西 原样存档
永不丢
AI 拆解 你过目确认 正式入库
行为记 event
状态物化 fact
随时调用
含主动提醒
最关键的两步:①「你过目确认」挡住脏数据;②「双真相落库」——行为进 event(append-only 不可变),状态进 fact(带 valid_from→valid_to),两者经 fact.source_event_id 双向可追,谁都赖不掉。

已经拍板的四个核心定论

议题结论(V2)为什么
真相怎么存双真相event(行为)+ fact(状态)"发生了什么"和"现在是什么状态"是两类东西,混在一起没法查时间线也没法纠错
技能库怎么建共享词表 skill + 我的投影 user_skill + JD 需求 position_requirement"Excel 全库只定义一次";三种"技能"严格拆开,AI 技能包不算技能
个人经历放哪单独建表 experience,仅存口语稿 raw_input_md经历是"简历素材"要反复挑用;AI 优化稿按岗位存入简历,不污染源稿
和 Alfred 的对话双方都存,带时间戳;chat_thread/chat_segment/chat_message你要的是一条像微信一样能回看、且能调出原话的时间线

1核心范式变化(v0.1 → V2)

这一节是"最新版到底是什么"的锚点。下面每一行都是已被 ADR 接受的决定。

旧(v0.1,已作废)新(V2,当前权威)ADR
positionjob_posting0009
position.job_familyoccupation(独立职位大类表)0009
companyorganization(带 kind 枚举,泛化机构)0017
tag.namespace='skill'skill 表 + user_skill + position_requirement00100011
event(带 kind 的时间轴表)event(行为真相,append-only)+ 强类型 interaction/interview0010
coffee_chat 概念interaction(kind 枚举:coffee_chat/meal/hangout/call/networking_event/info_session/referral_talk)0010
position.location(字符串)location 实体 + location_aliasjob_posting.location_id 外键0015
experience_skill 关联表experience_source(来源)+ user_skill + document_version_fact0010
document_version 存 LaTeX 源码结构化 Markdown 主信息 + template_ref + checksum0014
编排:Action 原语直写库LangGraph 管状态/中断,写库仍走 Action 原语0012
一句话记住 V2:行为用 event、状态用 fact;公司叫 organization、岗位叫 job_posting、职位大类叫 occupation;技能是共享词表 + 我的投影 + JD 需求三件套;简历是 Markdown 套模板;咖啡聊天泛化成 interaction;系统会主动 nudge 你。

2名词对照表

口语说法 → 系统里的叫法(V2)。带 新建 的是 V1 重新设计新增的。

你平时说的系统里的名字(V2)一句话解释
公司 / 学校 / 机构organization 改名泛化机构,一条一个,带 kind
岗位 / 职位job_posting 改名某机构 + 某 occupation + 某 location 的一次招聘
职位大类occupation 新建software engineer / psychologist,跨公司可比
地点location + location_alias 新建结构化地点 + 别名(HK / 香港 → 同一行)
技能(词表)skill 新建共享受控词表,"Excel 全库只定义一次"
我的技能user_skill 新建skill 的私有投影,带 proficiency 1–5
岗位要求position_requirement 已有JD 每条要求一行,kind 区分技能/资质/经历
我的经历experience + experience_source 新建仅存口语稿;多段语音挂到同一条
简历 / 求职信document_asset + document_version 已有Markdown 主信息 + 模板引用 + checksum
简历里的 bulletdocument_version_fact 新建bullet → fact 软引用,支撑溯源 footnote
投递application + application_evidence 新建多模态凭证(截图/确认邮件)挂这里
面试interview 已有每轮一条
咖啡聊天/约饭/宣讲会interaction 新建泛化接触记录,kind 区分
我和 Alfred 的聊天chat_thread/chat_segment/chat_message 新建双方消息,带时间戳、可分段
我的记忆 / 偏好memory 新建收容强类型表之外的残余
系统主动提醒nudge_rule + nudge 新建与手设 reminder 分表
行为日志event 语义变更append-only 行为真相,不再是时间轴表
当前状态快照fact 新建从 event + 规则物化,带有效期

3核心实体与唯一标识

每个实体靠什么"认出自己",撞车了怎么办。

实体唯一键长什么样冲突了怎么办
机构slugbytedance先查 slug,再按别名模糊匹配;命中就复用
岗位fingerprinthash(机构id + 标准职位名 + 地点)同岗从两个渠道来 → 只留一条,来源追加
职业大类slugsoftware-engineer共享词表,跨用户可比
技能slug + skill_aliasexcel / 别名 excel-数据处理归一化后仍不命中 → 建 proposed 等你确认
岗位要求position_id + 原文哈希重复解析只留一条
经历slughku-psy-ra-2025你自己命名,重名拒绝
简历版本(asset_id, version_no) + checksumcv-clinical-psy #7内容没变就不新建版本(全局复用,不限最新版)
投递id(软约束:岗位+投递日)同岗二投合法,但会提醒"3 月投过"
聊天消息id + sent_at重发用幂等键挡掉
附件sha256同截图传两次只存一份

三个"两层结构"在 V2 怎么处理

① 职位大类
独立表 · occupation

大类 = occupation,细分 = job_posting.title
职业大类跨公司可比,"所有要 SWE 的岗位"必须能一键查出来,所以独立成表。

② 技能
共享词表 + 别名

不再用 parent_id 两级树。技能是平的共享词表;相似说法靠 skill_alias 反查合并("商务写作"→excel 这种跨类不强行层级)。

③ 经历
一层就够

经历表本身是平的,仅存口语稿;"这段经历证明了什么能力"靠 user_skill 与简历 document_version_fact 体现,不污染源稿。

4技能三义分离 + 共享词表

整个设计里最容易废掉的一点 —— "技能"这个词在本项目里有三种完全不同的意思,必须拆开。

三种"技能"落点说明
AI 技能包(Skill 目录)文件系统里的 skills/*/Alfred 会做的一件事(拆职位、记笔记…),不是简历里的能力
共享技能词表skill 新建"Excel"全库只定义一次;skill_alias 做同义合并
我的技能user_skill 新建skill 的私有投影,带 proficiency 1–5
JD 所需技能position_requirement(kind=skill) 已有挂在具体岗位下,可溯源到 JD 原句
为什么必须拆:"AI 技能包"是代码概念,"我会 Excel"是私人事实,"这个岗要 Excel"是岗位需求——三者 JOIN 才能回答"我能投哪些要 Excel 的岗""我的技能还差什么",混在一起谁都查不了。

AI 抽 JD 技能的四步法

  1. 抽取 把 JD 句子压成技能短语:
    "熟练使用 Excel 进行数据透视分析" → Excel
  2. 归一 套同义词表 + skill_alias
    "Microsoft Excel" / "excel" / "EXCEL 技能" → excel
  3. 匹配 拿结果去 skill 表比对(slug + 别名 + 模糊):
    命中 → 直接复用(99% 的情况)
  4. 提议 不命中才新建 proposed 技能,等你确认 → approved / merged / rejected
关键约束:proposed 状态的技能不参与任何统计和筛选,它只是候选。即使 AI 提了一堆垃圾,也污染不了你的正式技能库。

JD 要求分三类,全进 position_requirement,用 kind 区分

kind含义例子挂 skill_id
skill能力英语商务写作、Excel 数据透视、CBT 咨询技术✅ 挂
qualification学历 / 证书 / 专业背景心理学硕士、注册心理师执照❌ 不挂
experience经历 / 年限2 年以上咨询实习经验❌ 不挂

三大匹配查询(靠 JOIN 实现)

① 哪些岗位要我的技能 X
user_skill ⋈ skill ⋈ position_requirement
② 我凭技能 X 能投哪些岗
同上反向过滤 job_posting
③ 我的技能 vs 岗位要求
matched / gap 缺口分析
技能确认流程
flowchart LR
    P(["AI 提议的新技能"]) --> Q{"你怎么看"}
    Q -->|"确实是新技能"| A["approved
进共享词表"] Q -->|"跟已有的重复"| M["merged
并入已有技能
说法进 skill_alias"] Q -->|"这不算技能"| R["rejected
下次不再提"] Q -->|"先放着"| P A --> USE(["可用于筛选/匹配/统计"]) M --> USE

5经历库与简历生成

  1. 你说:给字节的数据分析岗改简历,用经历 1、3、5
  2. Alfred 定位机构 + 岗位(没有就先建)
  3. experience 取出你点名的那几条(仅存口语稿 raw_input_md
  4. 缺口比对:岗位要求 vs 我的 user_skill →「这岗要 SPSS,你选的三条没体现,要不要加经历 4」
  5. 生成 Markdown 草稿(结构化主信息,编译时套 resume_asset.base_template
  6. 你确认 / 让它改
  7. 定稿 → 存 document_version(Markdown + template_ref + checksum
  8. 编译 PDF(复用模板,不必存源码)
  9. AI 优化稿按岗位存入 document_version_experience.rendered_bullets,并可软引用 fact

关键设计:记住「这版用了哪几条经历」+ bullet 可溯源

document_version_experience
document_version_id哪一版简历
experience_id用了哪条经历
order_no在简历里排第几
rendered_bullets这版实际怎么措辞的(同一段经历,投研究岗和投咨询岗写法不一样)
简历可解释(ADR-0014):每条 bullet 经 document_version_fact 软引用到 fact。渲染时拼 footnote 标注出处;校验器阻断"无源 bullet"——简历里出现任何无法溯源到经历/事实的句子都会被拦下。

版本管理规则(基于全局 checksum 复用)

不可变
每次定稿 = 一个新版本,老版本永远不改
checksum 复用
生成新简历时算主信息 checksum,与同 asset 下任意历史版本比对,命中则直接复用该版(不限于最新版);否则新建
based_on_id
指向用户显式选择的父版本(非自动,避免误继承)
必填摘要
每版必须有「这版改了什么」,AI 自动生成,你可以改
延后编译
只在你确认后才编译 PDF,草稿阶段不浪费时间
经历 ≠ bullet。experience 只存你的口语原话("港大做 RA,跟量表评估相关那段");AI 把它优化成的简历措辞,存在简历版本的 rendered_bullets 里,不污染源稿、随岗位不同。

6投递 · 面试 · 接触

投递(多模态凭证)

"我投了"不只是语音——截图(投递成功页/确认邮件)、文字、转发的确认邮件同样受理。截图是投递凭证,不是普通附件。

记录什么存哪(V2)
投了哪家哪个岗application.job_posting_id
用的哪份简历/求职信resume_version_id / cover_letter_version_id
投递渠道、时间channel / submitted_at
网页表单填了什么submission_detail_md
内推人是谁referrer_person_id
投递凭证截图/确认邮件application_evidence(application_id, attachment_id, role) 新建
投递"行为"本身同时写一条 event(APPLICATION_SUBMITTED)
凭证时间优先取截图内的时间戳;截图是证据而非附件,所以走专门的 application_evidence 关系表,不和普通 attachment 混。

面试(每轮一条,强类型)

记录什么存哪
轮次、类型、时间interview
问了什么、我怎么答的interview.questions
面试官是谁interview_interviewer 新建
面试感受feedback_md + feeling
"发生面试"行为event(INTERVIEW_SCHEDULED)

接触记录(泛化为 interaction

coffee_chat 太窄(约饭/出去玩/线上通话/宣讲会都不是 coffee chat)。改为 interaction(kind=coffee_chat|meal|hangout|call|networking_event|info_session|referral_talk),多人场景用 interaction_person 关系表。其"发生"行为写 event(INTERACTION_HAPPENED)
投递状态流转
stateDiagram-v2
    [*] --> draft
    draft --> submitted
    submitted --> ack_received
    submitted --> ghosted : 长期无回应
    ack_received --> oa_pending
    oa_pending --> oa_done
    ack_received --> interviewing
    oa_done --> interviewing
    interviewing --> offer
    interviewing --> rejected
    offer --> accepted
    offer --> declined
    ghosted --> interviewing : 突然回复

7聊天 · 记忆 · 主动提醒

聊天时间线 chat_thread / chat_segment / chat_message

chat_message 新建
sent_at
时间戳,时间线排序靠它
role
user(你发的) / assistant(Alfred 回的)
media_kind
text / voice / image / link / mixed
content_md
显示文本(语音就是转写、图片就是描述)
segment_id
所属语义段落(AI 检测主题漂移自动开新段)
thread_id
所属对话线程
分层结构
chat_thread
每用户一个常驻对话分组/归档
chat_segment
段内语义聚合;可重命名/合并/归档;存 summary_md
chat_message
双方全量原话 + sequence + segment_id 外键化;保留软丢弃
为什么分三层
长对话能按主题检索与回收上下文,而不是一整坨线性流
有意的冗余:chat_message.content_md 把语音转写冗余存一份,时间线是读得最频繁的东西,多存一份文本换秒开,划算。

记忆 memory + 双真相

强类型表为真相,memory 仅收容残余。无法归入强类型表的非结构化内容(kind = fact/preference/intent/context)才进 memory。 而你说的每句话原话都进 chat_message,日后能调出;AI 提炼的 fact/memory 是可重算的派生物,错了对原话无损。

主动提醒:reminder vs nudge

类型来源例子
reminder你手设的提醒"周五提醒我改简历"
nudge_rule + nudge系统算出来的提醒截止日临近未投、投递后静默 14 天、面试后 1 天未发感谢信、技能缺口、简历陈旧、命中率低、经历空洞、follow-up 逾期、offer 期限、每周复盘
分表不混:你手动设的 reminder 和系统主动生成的 nudge 是两套机制。nudge 必须有去重与冷却cooldown_days / dismissed_at),同一件事不重复烦你。主推送通道 = 后端邮件 + 每日聚合。

8数据流图 · Data Flow Diagram

数据从哪来、经过谁、存到哪去(V2 命名)。

Level 0 · 整体
flowchart LR
    U(["你"])
    subgraph ALFRED["Alfred 系统"]
        direction TB
        A1["接收与存档"]
        A2["AI 拆解(路由→抽取→归一)"]
        A3["预览确认与入库"]
        A4["调用与生成"]
    end
    LLM(["AI 模型"])
    WEB(["外部网页 / 搜索"])
    DB[("数据库")]

    U -- "文字/语音/图片/链接" --> A1
    A1 -- "原始内容 + event 行为" --> DB
    A1 -- "归一化文本" --> A2
    A2 <-- "问答" --> LLM
    A2 -- "抓正文/查背景" --> WEB
    A2 -- "预览卡片" --> U
    U -- "改一改 + 确认" --> A3
    A3 -- "正式写入(fact 物化)" --> DB
    U -- "给这个岗改简历" --> A4
    DB -- "经历/技能/岗位要求" --> A4
    A4 -- "简历草稿" --> U
Level 1 · 上传 / 拆解 的内部数据流
flowchart TB
    IN(["你发的内容"])
    N1["归一化
链接抓正文 / 截图OCR / 语音转写"] S1[("attachment
原件+转写")] S2[("ingest_request
原始输入")] S3[("chat_message
你说的这句")] R1["路由:这是什么类型"] E1["抽取:拆成结构化字段"] K1["技能归一:匹配共享词表 skill"] P1{"预览卡片
等你确认"} W1["写入实体(event + fact)"] S4[("organization / job_posting")] S5[("position_requirement")] S6[("skill 提议区")] S7[("experience / note / memory")] S8[("application / interaction")] S9[("chat_message
Alfred 的回复")] S10[("event 行为日志")] BG["背景搜集
只看不存"] IN --> N1 IN --> S2 IN --> S3 N1 --> S1 N1 --> R1 R1 --> E1 E1 --> K1 K1 --> P1 E1 --> BG BG -.->|"只展示"| P1 P1 -->|"确认"| W1 P1 -->|"丢弃"| X(["不入库"]) W1 --> S4 W1 --> S5 W1 --> S6 W1 --> S7 W1 --> S8 W1 --> S9 W1 --> S10
Level 1 · 简历生成的数据流
flowchart LR
    Q(["给X机构Y岗改简历
用经历1/3/5"]) D1[("position_requirement
这个岗要什么")] D2[("experience
我的经历素材")] D3[("user_skill
我会什么")] M["缺口比对
要求 vs 我的技能"] G["Markdown 生成 + 套模板"] P{"你确认"} V[("document_version
Markdown + checksum")] L[("document_version_experience
用了哪几条经历")] F[("document_version_fact
bullet 溯源 fact")] PDF[("PDF 文件")] Q --> M D1 --> M D2 --> M D3 --> M M -->|"提示缺口"| Q M --> G G --> P P -->|"再改改"| G P -->|"定稿"| V V --> L V --> F V --> PDF

9流程图 · Flow Diagram

上传 / 拆解 主流程(含所有分支)
flowchart TD
    START(["你发了链接/截图/语音"]) --> SAVE["立即存档原件
此刻起内容不会丢"] SAVE --> NORM["归一成纯文本"] NORM --> OK1{"文本拿到了吗"} OK1 -->|"否"| FAIL1["标记 partial
告诉你抓取失败
可手动补内容"] OK1 -->|"是"| ROUTE["判断类型"] ROUTE --> ISJOB{"是岗位信息吗"} ISJOB -->|"否"| OTHER["走别的技能
笔记 / 经历 / 接触 / 记忆"] ISJOB -->|"是"| EXTRACT["AI 拆解字段"] EXTRACT --> CO{"识别到机构了吗"} CO -->|"否"| PLACE["建占位机构
标记待补"] CO -->|"是"| MATCH["按 slug + 别名匹配已有 organization"] PLACE --> FP MATCH --> FP["算岗位指纹"] FP --> DUP{"这个岗已经存过了吗"} DUP -->|"是"| MERGE["复用已有岗位
追加新来源"] DUP -->|"否"| NEW["准备新建岗位"] MERGE --> SK NEW --> SK["技能归一与匹配"] SK --> SKHIT{"技能库里有吗"} SKHIT -->|"有"| REUSE["直接复用"] SKHIT -->|"没有"| PROPOSE["建成待确认技能"] REUSE --> CARD PROPOSE --> CARD["生成预览卡片"] CARD --> BG["顺手搜背景信息
只展示不入库"] BG --> REVIEW{"你看一眼"} REVIEW -->|"改字段"| CARD REVIEW -->|"丢弃"| DROP(["不入库
原始内容仍留档"]) REVIEW -->|"确认"| WRITE["写入数据库(event + fact)"] WRITE --> REM{"有截止日期吗"} REM -->|"有"| MKREM["自动建提前提醒 nudge/reminder"] REM -->|"没有"| REPLY MKREM --> REPLY["Alfred 回话
写进聊天时间线"] REPLY --> END(["完成"])
简历生成流程(Markdown)
flowchart TD
    S(["给某机构某岗改简历"]) --> PICK{"指定用哪几条经历了吗"}
    PICK -->|"指定了"| GET["取出指定经历"]
    PICK -->|"没指定"| REC["按岗位要求自动推荐经历"]
    REC --> CONFIRM1{"这几条行吗"}
    CONFIRM1 -->|"换一批"| REC
    CONFIRM1 -->|"可以"| GET
    GET --> GAP["比对岗位要求
找出没覆盖到的能力"] GAP --> WARN{"有明显缺口吗"} WARN -->|"有"| TELL["提示你
要求SPSS但选的经历没体现"] WARN -->|"没有"| GEN TELL --> GEN["生成 Markdown 草稿 + 套模板"] GEN --> LOOK{"你看草稿"} LOOK -->|"改措辞/换经历"| GEN LOOK -->|"定稿"| SAVEV["存新版本
记录用了哪几条经历"] SAVEV --> CK{"checksum 和历史版一样吗"} CK -->|"一样"| REUSEV["复用已有版本"] CK -->|"不一样"| PDF["编译 PDF"] PDF --> FACT["bullet 软引用 fact 建索引"] FACT --> DONE(["可用于投递"])
从上传到投递的完整闭环
flowchart LR
    A(["发现岗位"]) --> B(["解析入库"])
    B --> C(["写评价与准备思路"])
    C --> D(["生成定制简历"])
    D --> E(["投递 + 凭证 evidence"])
    E --> F(["跟踪状态"])
    F --> G(["面试记录"])
    G --> H(["复盘写进记忆"])
    H -.->|"经验反哺"| C
    G -.->|"新技能被问到"| I(["补进技能词表"])
    I -.-> D

10ER 图 · 实体关系(V2)

拆成三张看,避免一张图糊成一团。

10.1 摄入 · 对话 · 双真相
erDiagram
    CHAT_THREAD ||--o{ CHAT_SEGMENT : "分段"
    CHAT_THREAD ||--o{ CHAT_MESSAGE : "含"
    CHAT_MESSAGE ||--o{ ATTACHMENT : "含(方向化)"
    CHAT_MESSAGE ||--o{ EVENT : "触发行为"
    EVENT ||--o| FACT : "物化为状态"

    CHAT_THREAD {
        string id PK
        string user_id FK "私有"
        string title
        string status "active archived"
    }
    CHAT_SEGMENT {
        string id PK
        string thread_id FK
        string title
        string status "active archived merged"
        text summary_md
        string user_id FK
    }
    CHAT_MESSAGE {
        string id PK
        string thread_id FK
        string segment_id FK
        timestamp sent_at "时间戳"
        string role "user 或 assistant"
        string media_kind "text voice image link"
        text content_md "显示文本"
        json attachment_ids
        string reply_to_id FK
        string status "sent processing done failed"
        bool discarded "软丢弃"
    }
    ATTACHMENT {
        string id PK
        string media_kind
        string sha256 UK "同文件不重复存"
        string storage_uri
        text transcription "语音转写"
        text vision_caption "图片描述"
    }
    EVENT {
        string id PK
        string type "FACT_DECLARED APPLICATION_SUBMITTED INTERACTION_HAPPENED SKILL_EXTRACTED INTERVIEW_SCHEDULED RESUME_GENERATED CUSTOM"
        string actor_user_id FK "私有"
        timestamp occurred_at
        string source_type "conversation import api"
        string source_ref_id
        json payload
    }
    FACT {
        string id PK
        string kind "skill_declaration relationship attribute custom"
        timestamp valid_from
        timestamp valid_to NULL "纠错时封口"
        string subject_ref FK
        string object_ref FK
        string proficiency NULL
        string source_event_id FK "双向追溯"
        string owner_user_id FK "私有"
    }
10.2 机构 · 岗位 · 职业 · 地点 · 技能
erDiagram
    ORGANIZATION ||--o{ JOB_POSTING : "发布"
    OCCUPATION ||--o{ JOB_POSTING : "属于"
    LOCATION ||--o{ JOB_POSTING : "位于"
    LOCATION ||--o{ LOCATION_ALIAS : "别名"
    SKILL ||--o{ SKILL_ALIAS : "别名"
    OCCUPATION }o--o{ USER_SKILL : "跨用户可比"
    SKILL ||--o{ USER_SKILL : "技能定义"
    SKILL ||--o{ POSITION_REQUIREMENT : "JD所需"
    JOB_POSTING ||--o{ POSITION_REQUIREMENT : "要求"

    ORGANIZATION {
        string id PK
        string name
        string slug UK "唯一标识"
        string kind "company school club ngo government fund open_source_office other"
        json aliases "别名用于去重"
    }
    OCCUPATION {
        string id PK
        string slug UK
        string name "software-engineer 等"
    }
    LOCATION {
        string id PK
        string country
        string city
        string district
        string admin_code
    }
    LOCATION_ALIAS {
        string id PK
        string location_id FK
        string alias_slug "香港 HK HongKong → 同行"
    }
    SKILL {
        string id PK
        string slug UK "excel"
        string name "显示名"
        string kind "hard soft language tool domain"
        string status "proposed approved merged rejected"
        string merged_into_id FK
    }
    SKILL_ALIAS {
        string id PK
        string canonical_skill_id FK
        string alias_slug "反查合并键"
        string source "agent manual"
    }
    POSITION_REQUIREMENT {
        string id PK
        string position_id FK
        string kind "skill qualification experience"
        string skill_id FK "仅能力类才有"
        text text "要求原文(可溯源 JD)"
        string importance "must nice"
        json source_sentences
    }
    USER_SKILL {
        string id PK
        string user_id FK "私有"
        string skill_id FK
        int proficiency "1到5"
    }
    JOB_POSTING {
        string id PK
        string organization_id FK
        string fingerprint UK "去重指纹"
        string occupation_id FK "职位大类"
        string location_id FK "替代 location 字符串"
        string title "细分职位名"
        timestamp posted_at
        timestamp deadline_at "投递截止→驱动提醒"
        string apply_url
        json required_documents "要交的材料"
        string source_kind
        int interest_level
        text personal_comment_md
    }
10.3 经历 · 文档 · 投递 · 面试 · 接触
erDiagram
    EXPERIENCE ||--o{ EXPERIENCE_SOURCE : "来源"
    EXPERIENCE ||--o{ DOCUMENT_VERSION_EXPERIENCE : "被引用"
    DOCUMENT_ASSET ||--o{ DOCUMENT_VERSION : "版本链"
    DOCUMENT_VERSION ||--o{ DOCUMENT_VERSION_EXPERIENCE : "用了哪几条"
    DOCUMENT_VERSION ||--o{ DOCUMENT_VERSION_FACT : "溯源fact"
    FACT ||--o{ DOCUMENT_VERSION_FACT : "被引用"
    JOB_POSTING ||--o{ APPLICATION : "投递"
    APPLICATION ||--o{ INTERVIEW : "轮次"
    APPLICATION ||--o{ APPLICATION_EVIDENCE : "凭证"
    ATTACHMENT ||--o{ APPLICATION_EVIDENCE : "截图/确认邮件"
    PERSON ||--o{ INTERVIEW_INTERVIEWER : "面试官"
    INTERVIEW ||--o{ INTERVIEW_INTERVIEWER : ""
    PERSON ||--o{ INTERACTION_PERSON : "参与"
    INTERACTION ||--o{ INTERACTION_PERSON : "多人"

    EXPERIENCE {
        string id PK
        string user_id FK "私有"
        string slug UK
        string title
        string kind "internship project research award"
        string org_name
        date started_on
        date ended_on
        text raw_input_md "仅存口语稿,不存优化稿"
    }
    EXPERIENCE_SOURCE {
        string id PK
        string experience_id FK
        string source_type "voice text screenshot"
        string source_ref_id "指向 attachment / chat_message"
    }
    DOCUMENT_ASSET {
        string id PK
        string slug UK
        string kind "resume 或 cover_letter"
        string name
        string base_template "模板引用"
    }
    DOCUMENT_VERSION {
        string id PK
        string asset_id FK
        int version_no
        string checksum "内容没变就不新建"
        text markdown_main "结构化Markdown主信息"
        string template_ref "套用模板"
        string based_on_id FK "用户显式选父版"
        text change_summary_md "这版改了什么"
        string compiled_pdf_url
    }
    DOCUMENT_VERSION_EXPERIENCE {
        string document_version_id FK
        string experience_id FK
        int order_no
        json rendered_bullets "实际措辞(按岗不同)"
    }
    DOCUMENT_VERSION_FACT {
        string id PK
        string document_version_id FK
        string fact_id FK
        text bullet_text "软引用 fact,支撑 footnote"
    }
    APPLICATION {
        string id PK
        string user_id FK
        string job_posting_id FK
        string status "投递状态机"
        string resume_version_id FK
        string cover_letter_version_id FK
        timestamp submitted_at
        text submission_detail_md
        string referrer_person_id FK
    }
    APPLICATION_EVIDENCE {
        string id PK
        string application_id FK
        string attachment_id FK
        string role "screenshot confirmation_email"
    }
    INTERVIEW {
        string id PK
        string application_id FK
        int round_no
        string kind
        timestamp scheduled_at
        json questions "问了什么我怎么答"
        text feedback_md
    }
    INTERACTION {
        string id PK
        string user_id FK
        string kind "coffee_chat meal hangout call networking_event info_session referral_talk"
        timestamp occurred_at
        text summary_md
    }
V2 已移除旧 entity_link「万能连线」。强关系一律用外键强类型表(上面 ER 即全部);弱关联走受控关系词 + 专门关系表(如 interaction_personinterview_interviewerapplication_evidence),不再有任意两实体自由连线的一张总表。

11潜在问题 Brainstorm

先替你踩坑。按风险高低排。

高风险 · 不处理一定出事

① 技能库爆炸 / 语义碎片
三个月后技能表 400 条,其中 300 条是同义重复;或"Excel/商务写作"被拆成两个毫无关系的技能
对策 AI 只能提议不能直接入库;proposed 不参与筛选;skill_alias 反查合并,合并时旧说法自动进别名;共享词表全库唯一
② 截图 / OCR 认错关键字段
公司名认成"字节眺动",截止日期认成去年
对策 强制走预览卡片;AI 推算出来的字段("本周五"→ 具体日期)在卡片上高亮标注
③ 经历和笔记 / 记忆混为一谈
想往简历里加东西时,翻遍笔记也找不到那段实习怎么写的
对策 经历单表只存口语稿;AI 优化稿在简历版本里;非结构残留进 memory。判断标准一句话——能写进简历的是经历,其余是笔记/记忆
④ 简历版本 / bullet 无法溯源
收到面试通知,不知道当初投的是哪版;简历里出现 AI 瞎编、无来源的能力
对策 resume_version_id 硬关联 + 记录用了哪几条经历 + 每版必填改动摘要;document_version_fact 软引用 fact,校验器阻断无源 bullet

中风险 · 会不舒服但不致命

问题对策
⑤ 同岗位重复入库指纹先过中英归一表;不确定时卡片上问"这是不是和已有的 XX 同一个岗"
⑥ 聊天时间线越滚越长chat_segment 按主题分段、可归档;分页加载;纯闲聊可标记归档
⑦ 你的评价被塞进 JD 描述里prompt 已明确"主观评价只进 personal_comment";再加校验——描述字段出现第一人称就打回
⑧ 背景搜集的信息想留下来默认不存,卡片上给「存为笔记/记忆」按钮,你点了才存
⑨ 系统提醒烦人nudge 带 cooldown_days / dismissed_at 去重冷却;与手设 reminder 分表
⑩ 投递凭证 vs 普通附件混了专用 application_evidence 关系表挂凭证,时间优先取截图内时间戳

低风险 · 记着就行

⑪ 材料清单没人管
required_documents + 生成 checklist 提醒
⑫ 同岗二次投递
不阻止,但提示"你 3 月投过这个岗"
⑬ 文件越存越多
PDF 本身不大;真占地方的是截图,sha256 去重已解决大半
⑭ 旧 event 数据迁移
event.kind 内容迁到 interaction/interview 强类型表;event 改行为真相

12实现清单(与现有代码的差距)

要新建的表(按依赖顺序)

作用优先级
occupation职位大类,跨公司可比P0
job_posting替代 position(加 occupation_id / location_id / required_documents / posted_at)P0
skill + skill_alias共享技能词表 + 别名合并P0
position_requirement替代 position.requirements JSON 字段P0
user_skill我的技能投影(proficiency)P0
experience + experience_source经历库(仅口语稿)+ 来源P0
fact + event双真相(行为 + 状态)P0
chat_thread / chat_segment / chat_message分层对话时间线P1
document_version_experience + document_version_fact简历用了哪几条经历 + bullet 溯源P1
interaction + interaction_person泛化接触记录P1
application_evidence投递多模态凭证P1
nudge_rule + nudge系统主动提醒P1
memory残余记忆收容P1

要改 / 语义变更的地方

位置改什么
models/tables.pyposition→job_postingcompany→organization;加 occupation_id/location_iddocument_version 改 Markdown 主信息 + template_ref + checksum
models/enums.pySkillStatus/SkillKind/RequirementKind/ExperienceKind/MemoryKind/ThreadKind/OccupationStatus
skills/extract_job/skill.yamlskills 升级为带 evidence/importance 对象;requirementskindadd_tags 换成 upsert_skill + link_requirement
services/executor.py新增 upsert_skill/link_requirement/upsert_experience/record_application/create_memory/upsert_user_skill
编排LangGraph 管状态/中断(ADR-0012),写库仍走 Action 原语(dry_run + commit)
接口层技能确认/合并、经历 CRUD、聊天时间线分页、记忆检索、技能匹配查询、投递/面试分别提交
docs/DATA_MODEL.md(v0.1)把技能表述为「namespace='skill' 的 tag」。已被 DATA_MODEL_V2.md 取代——tag 是自由标签,扛不住"共享词表 + 提议确认 + 别名合并"这三件事。旧草稿保留于 archive/v0.1-baseline/,请勿照此实现。

建议的落地顺序

  1. skill + skill_alias + position_requirement + user_skill,改 extract_job 技能 ← 上传链路立刻变干净
  2. 加预览确认机制(dry_run + 确认接口) ← 挡住脏数据
  3. 双真相落地:event + fact 写入路径 ← 可审计、可纠错
  4. experience + experience_source 表 + 录入你现有的经历 ← 为简历生成铺路
  5. 简历生成链路(Markdown + document_version_experience + document_version_fact
  6. chat_thread/chat_segment/chat_message 时间线 + memory
  7. interaction + application_evidence + nudge_rule/nudge(主动提醒引擎)

13还需要你后续拍板的问题

不影响现在动工,但迟早要定。

技能要不要多级
V2 用平的共享词表 + skill_alias 反查,放弃强制两级树。若日后真要层级,可加 parent_id 自引用,不破坏现有模型。
我的整体熟练度
现在 user_skill.proficiency 1–5 已能表达。要不要再分"自评/岗位要求"两套,先不加。
背景搜集完全不存会后悔吗
已给折中:默认不存 + 一键存为笔记/记忆。
CL 和 PS 要分开吗
document_asset.kind 加枚举值即可。
经历的中英双语措辞
现在 AI 优化稿在 document_version_experience.rendered_bullets 里,按岗位生成,天然支持双语。
nudge 推送通道
V2 先定后端邮件 + 每日聚合;小程序/微信通道留 channel 字段未来扩展。