# 职途 AI 架构说明

> 本文记录当前模块化实现及业务不变量。部署拓扑、Cloudflare、PostgreSQL、R2
> 与任务 worker 见[生产架构](PRODUCTION_ARCHITECTURE.md)。

## 1. 设计目标

职途 AI 是本地单用户求职工作台，不是通用聊天机器人。架构优先保证四件事：业务数据一致、Agent 行为可控、刷新或重启后可继续、没有模型 Key 也能诚实运行。

```text
React 19 / TypeScript Web UI
  |  用户输入 + resume_id / opportunity_id / module
  v
FastAPI（协议、身份与 owner 边界）
  |---------------------> CareerService / InterviewService
  |                              |
  v                              v
AgentService              SQLAlchemy UoW + 领域事件
  |
  +-> ContextBuilder -> 分层记忆 + 实时业务快照
  +-> Orchestrator -> ToolRegistry -> 只读工具/公开网页工具
  +-> ActionProposalService -> 预览 -> 用户确认 -> 领域服务
```

## 2. 模块职责

| 层 | 主要文件 | 职责 |
| --- | --- | --- |
| Web/API | `frontend/src/`、`backend/api/` | React 页面组合、输入校验、薄 FastAPI 适配 |
| 领域服务 | `utils/domain/` | 职业档案、机会、简历版本、行动项、准备度、面试会话、领域事件 |
| Agent 运行时 | `utils/agent_runtime/` | 会话、记忆、上下文、工具注册、编排、提案和回执 |
| 模型网关 | `utils/ai_client.py` | 供应商配置、原生 tool calls、错误分类和本地降级 |
| 持久化 | `backend/adapters/persistence/sqlalchemy/` | SQLite/PostgreSQL 业务事实、会话、审计和回执 |
| 文件/任务 | `backend/adapters/storage/`、`backend/adapters/jobs/` | Local/R2 文件与可恢复后台任务 |

FastAPI router 不复制业务写入逻辑。新增行为进入 application/domain module，并在同一
UoW 中写入业务事实与领域事件。

## 3. 数据模型与迁移

Alembic 是当前 schema 权威。`utils/domain/database.py` 仅保留旧 SQLite 离线纳管兼容；
旧投递状态仍映射到统一阶段，无法识别的历史值保留并归入“待确认”。

核心聚合：

- `career_profiles`：确认过的求职目标和偏好。
- `resumes`：简历资产、父版本和机会关联。
- `job_applications`：机会聚合，包含 JD、阶段、关联简历和跟进信息。
- `interview_sessions`：可恢复的面试状态；完成结果幂等落入训练记录。
- `action_items`、`domain_events`、`career_reports`：行动、业务时间线和阶段报告。
- `agent_*` 表：会话、消息、记忆、运行审计、动作提案与回执。

业务事实始终从领域表读取，不把整份简历或完整 JD 复制成长时记忆。

## 4. Agent 运行循环

每轮 Agent 请求由服务端固定本地用户，并校验页面提交的模块和实体 ID。上下文构建器组合：

- 最近消息与滚动摘要；
- 用户明确表达且带来源的画像事实；
- 已完成任务的情景记忆；
- 未完成任务槽位；
- 当前机会、关联简历、行动项、阻塞项、近期质量结果和时间线事件。

远程模型优先使用供应商原生 `tool_calls`；兼容模式只接受严格 JSON。无 Key 时，`LocalPolicy` 通过意图分类选择确定性计划，例如求职诊断或“带我开始”会依次读取看板、职业档案、行动项和训练摘要，再根据结构化结果生成行动优先级和受白名单约束的页面导航；简历任务会持久化“选择简历 → 诊断或草稿 → 待确认版本”的任务状态，草稿只做事实保真处理，原简历不被覆盖。编排器有轮数、总时长、工具超时和重复调用预算，达到边界后使用已取得的可靠结果结束，不无限自循环。

当前注册 22 个工具。参数由 JSON Schema 校验；读取工具通过领域服务或受限查询返回数据，联网搜索和网页抓取仅访问公开网络并拒绝回环、内网地址和超大响应。模型看不到可写的 `user_id`。

## 5. 写操作确认边界

Agent 工具不能直接修改业务表。写入意图必须经过：

```text
标准化参数 -> 持久化提案 -> 脱敏预览 -> 用户编辑/确认/取消
-> 服务端重新加载提案 -> 领域服务执行 -> 幂等回执 -> 时间线反馈
```

浏览器确认时不能重新提交动作参数，避免前端篡改或陈旧状态。提案有所有者、状态、过期时间和幂等键；重复确认返回已有结果。实体关联 ID 在编辑过程中保持不变，防止把 A 机会的内容写入 B 机会。

## 6. 分层记忆

记忆不是把所有历史拼进 Prompt：

| 层 | 内容 | 生命周期 |
| --- | --- | --- |
| 工作记忆 | 最近消息、当前页面和实体 ID | 当前对话 |
| 摘要记忆 | 已压缩的长对话结论和待办 | 跨刷新/重启 |
| 语义记忆 | 用户明确确认的岗位、城市、薪资等偏好 | 可长期复用 |
| 情景记忆 | 已完成任务的输入摘要和结果引用 | 用于相似任务 |
| 任务记忆 | 缺少的槽位与待继续步骤 | 直到完成或清除 |
| 业务记忆 | 机会、简历、面试、行动和事件 | 始终实时查询领域表 |

语义/情景检索优先使用 SQLite FTS5；环境不支持时退回有上限的结构化和子串排序，避免全库无界扫描。

## 7. 为什么暂不使用 LangChain/LangGraph

当前不引入是有意取舍，不是缺少 Agent 设计：

- 运行循环短且边界明确，核心复杂度在业务事务、身份隔离、提案确认和恢复语义。
- 当前模块化单体已覆盖持久化、任务恢复和审计，不需要为短流程再叠加通用 Agent 框架。
- 现有工具协议、持久化和审计都需要贴合本项目数据模型，套用通用抽象不会自动提高智能度。
- 直接实现更容易验证“模型不能写库”“确认只执行持久化参数”等安全不变量。

当出现多 Agent 并行、跨服务长流程、人工审批节点图、需要可视化状态机或大量第三方集成时，可以评估 LangGraph。引入前应先定义适配层，保留现有领域服务、动作提案和记忆表，不让框架成为业务事实来源。

## 8. 浏览器与媒体策略

前端在运行时检测 Web Speech、`getUserMedia`、MediaRecorder 和 MIME 支持。录音优先选择浏览器实际支持的 WebM/Ogg 格式；任何语音能力缺失都保留文字和上传路径。机会深链接使用 History API，并区分 404/403 与网络/服务器错误：只有实体确定不存在或无权访问时才清理 URL。

## 9. 本地安全与限制

- 默认监听 `127.0.0.1`，CORS 仅允许当前端口的 `localhost` 和 `127.0.0.1`。
- 当前固定单一用户，没有认证或多人隔离，不应直接部署到公网。
- 页面提交的模型 Key 不写入 SQLite 或浏览器存储，但会保存到 Git 忽略的本机 `output/runtime/ai-config.json` 以支持重启后复用；该文件包含明文 Key，应视作本地敏感数据。环境变量 Key 由启动进程读取。
- 上传文件、数据库、导出和日志均属于本地敏感数据，发布门禁禁止跟踪它们。
- 远程模型和联网工具会产生外部数据传输；界面与文档不把本地模式描述成远程 AI 成功。

## 10. 扩展原则

新增功能应遵循：先领域契约和测试，再 API，再 Agent 工具，最后界面；写工具只创建提案；事件携带不可歧义的关联 ID；测试覆盖服务重启、重复提交、跨用户访问、陈旧页面和无模型 Key 场景。
