Files
life-echo/docs/plans/backend-architectural-refactor-plan.md

866 lines
42 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.
---
name: 架构重构计划
overview: 将现有 Life Echo API 从混合分层结构重构为 feature-first flat 架构,引入 ports/adapters 实现 provider-agnostic 基础设施抽象,补齐 code-first OpenAPI 契约治理,并新增面向回忆录生成的 Memory + RAG 能力,集成 Alembic 做 schema 变更治理,统一日志到 loguru。
todos:
- id: phase0-core
content: "Phase 0: 搭建 app/core/ 骨架config, db, logging, errors, security, dependencies, middleware"
status: pending
- id: phase0-txn
content: "Phase 0: 重写 get_async_db() 去掉自动 commit定义事务边界规则"
status: pending
- id: phase0-alembic
content: "Phase 0: 初始化 Alembic手写空 baselinestamp head不依赖 autogenerate"
status: pending
- id: phase0-loguru
content: "Phase 0: 集成 loguru + InterceptHandler + request_id middleware"
status: pending
- id: phase0-errors
content: "Phase 0: 实现全局异常体系 + exception handlers仅错误响应统一成功响应不包装"
status: pending
- id: phase0-openapi
content: "Phase 0: 建立 OpenAPI 契约规范router metadata、custom_openapi、内部接口可见性、WebSocket 协议文档)"
status: pending
- id: phase1-ports
content: "Phase 1: 定义 7 个核心 port protocolsms, llm, image_gen, storage, tts, asr, embedding"
status: pending
- id: phase1-embedding
content: "Phase 1: 新增 embedding port可选预留 reranker port并完成 DI 装配"
status: pending
- id: phase1-adapters
content: "Phase 1: 封装现有 SDK 代码为 adapter 实现"
status: pending
- id: phase1-di
content: "Phase 1: 在 core/dependencies.py 注册 DI factory"
status: pending
- id: phase2-content
content: "Phase 2: 迁移 content featurefaq, legal, home"
status: pending
- id: phase2-plan
content: "Phase 2: 迁移 plan feature"
status: pending
- id: phase2-quota
content: "Phase 2: 提取 quota 为独立 featureQuotaService + router"
status: pending
- id: phase2-user
content: "Phase 2: 迁移 user featureUser model"
status: pending
- id: phase2-auth
content: "Phase 2: 迁移 auth featureRefreshToken + SmsVerificationCode models依赖 user + sms port"
status: pending
- id: phase2-memory
content: "Phase 2: 新增 memory featuresources/chunks/summaries/facts/timeline + hybrid retrieval"
status: pending
- id: phase2-payment
content: "Phase 2: 迁移 payment feature"
status: pending
- id: phase2-memoir
content: "Phase 2: 迁移 memoir featurebooks + chapters + memoir_state 合并)"
status: pending
- id: phase2-conversation
content: "Phase 2: 迁移 conversation feature + WebSocket 拆为 ws/ 子包6 个文件)"
status: pending
- id: phase3-cleanup
content: "Phase 3: 清理旧目录,更新 Docker/CI/测试路径"
status: pending
isProject: false
---
# Life Echo API 架构重构计划
## 一、现状诊断
当前项目位于 `api/` 下,采用混合分层结构:
```
api/
main.py
routers/ # 16 个路由文件,部分含大量业务逻辑
services/ # 混合了业务服务和外部 SDK 调用
database/ # 单文件 models.py9 个模型)+ database.py
agents/ # AI agent 层conversation, memory, memoir
middleware/ # 仅 auth.py
payment/ # 唯一较完整的 feature 模块
tasks/ # Celery 后台任务
migrations/ # 手写 SQL无版本管理
```
**核心问题**
- 路由层过胖,直接操作 DB 和调用外部服务
- 外部服务耦合SMS腾讯云、存储腾讯 COS、TTSOpenAI直接写死 SDK
- `memory` 语义不清:当前更像“聊天助手记忆”占位,缺少面向回忆录生产的素材沉淀、事实抽取、检索增强与来源追溯
- 无统一配置类,`os.getenv()` 散落各处
- 无 Alembic手写 SQL 迁移无版本追踪
- 标准库 logging无结构化日志
- 无全局异常处理
- WebSocket 路由 ~1000 行,职责过重
**已有较好抽象的部分**(可复用/迁移):
- `payment/`:已有 service facade + 双 provider
- `services/llm_service.py`:通过 LangChain 已做 provider 切换
- ASR已有 provider 切换机制
---
## 二、目标目录结构
```
api/
app/
main.py # 仅做组装include_router, middleware, lifespan, OpenAPI wiring
core/
config.py # 统一 BaseSettingspydantic-settings
db.py # Base, engine, session factory, get_async_db
logging.py # loguru 配置
errors.py # 全局异常体系 + exception handlers
security.py # JWT 签发/校验、密码哈希
middleware.py # CORS, request_id, logging middleware
dependencies.py # 全局共享依赖get_current_user, get_xxx_service, port DI factory
openapi.py # custom_openapi():全局 schema 增强title/version/logo/external docs
pagination.py # 通用分页
features/
auth/
router.py # 注册、登录、SMS 登录、刷新、登出
schemas.py
service.py # 认证业务逻辑
models.py # RefreshToken, SmsVerificationCode
repo.py
deps.py # feature 特有依赖(如 sms_port 注入)
conversation/
router.py # 对话 CRUD
schemas.py
service.py # 对话编排(调用 agent、quota 检查等)
models.py # Conversation, Segment
repo.py
ws/ # WebSocket 子包(从 1067 行 websocket.py 拆分)
router.py # WebSocket endpoint仅做连接生命周期
connection_manager.py # 连接注册/注销、active_connections 字典
message_types.py # MessageType 枚举 + 消息序列化
pipeline.py # 消息分发管道audio_segment -> ASR -> agent -> TTS -> 响应
profile_collector.py # 资料收集模式:缺失字段检测、提取、回填
quota_guard.py # 配额校验 hook从 routers/quota 独立)
protocol.md # WebSocket 消息契约message/event/error schema + reconnect/idempotency 规则
memoir/
router.py # books, chapters, memoir_state 合并
schemas.py
service.py # 回忆录编排(章节生成、状态流转)
models.py # Book, Chapter, MemoirState
repo.py
processor.py # 从 agents/memoir_processor.py 迁入
memory/
router.py # 可选:后台编辑/管理接口(二期);一期以内部服务为主
schemas.py
service.py # MemoryServiceconversation / memoir 的统一门面
repo.py
models.py # Source, Chunk, Summary, Fact, TimelineEvent, CurationAction
retriever.py # metadata filter + FTS + vector retrieval + score fusion
chunker.py # transcript -> chunks
extractor.py # entity/event/fact extraction
summarizer.py # session / rolling summaries
timeline.py # chronology organization
curation.py # exclude / restore / correct / reject / confirm
deps.py
payment/
router.py
schemas.py
service.py # 统一支付门面(已有良好抽象,迁移即可)
models.py # Order
repo.py
deps.py
user/
router.py # profile, subscription, feedback
schemas.py
service.py
models.py # User
repo.py
quota/
service.py # PLAN_QUOTAS 配置 + check_can_send/check_can_organize
schemas.py # QuotaCheckResponse
router.py # GET /api/quota/check
deps.py # get_quota_service 注入
plan/
router.py
schemas.py
service.py
content/
router.py # faq, legal, home轻量静态内容
schemas.py
agents/
conversation_agent.py
prompts/
conversation_prompts.py
profile_prompts.py
ports/ # 能力接口定义Protocol / ABC
sms.py # SmsSender protocol
llm.py # LLMProvider protocol
image_gen.py # ImageGenerator protocol
storage.py # ObjectStorage protocol
tts.py # TTSProvider protocol
asr.py # ASRProvider protocol
embedding.py # EmbeddingProvider protocol
reranker.py # 可选:二期 cross-encoder rerank
adapters/ # 具体 provider 实现
sms/
tencent.py
llm/
deepseek.py # 基于现有 llm_service.py
image_gen/
liblib.py
storage/
tencent_cos.py
tts/
openai_tts.py
asr/
whisper_local.py
tencent_asr.py
embedding/
openai.py
payment/
wechat.py
alipay.py
tasks/
celery_app.py
memoir_tasks.py
memory_tasks.py
alembic/
env.py
versions/
alembic.ini
tests/
...
```
### 目录结构修订说明
**修订 1`core/db.py` 显式列出。** 存放 `Base = declarative_base()`、engine/session factory、`get_async_db` 生成器。所有 feature models 共享此 Base。
**修订 2`core/response.py` 移除。** 统一响应包装只用于错误响应(在 `core/errors.py` 的 exception handler 中处理)。成功响应直接返回 Pydantic model / `FileResponse` / 原始 dict不强制 `{code, data, message}` 包装,避免对 FastAPI schema 自动生成、文件下载、支付回调、WebSocket 造成额外负担。
**修订 3auth models 与 user models 分离。** `RefreshToken``SmsVerificationCode` 归入 `features/auth/models.py`,因为它们是 identity/session 资产,认证流程 (`auth/service.py`) 是唯一强依赖方。`User` 模型留在 `features/user/models.py`auth 通过 import user models 引用 User允许的依赖方向auth -> user
**修订 4WebSocket 从单个 `ws_handler.py` 拆为 `ws/` 子包。** 当前 `routers/websocket.py` 有 1067 行同时承担连接管理、消息类型定义、音频分段处理、ASR 转写、配额校验、用户资料收集、Agent 编排、DB 落库、Celery 任务提交。仅改名为 `ws_handler.py` 会原样搬运复杂度。拆分为 6 个文件,每个职责单一。
**修订 5`quota` 提升为独立 feature。** 当前 `routers/quota.py` 包含 `PLAN_QUOTAS` 配置、`get_segment_count``get_chapter_count``check_can_send_message``check_can_submit_organize`,被 conversationWebSocket line 628、memoir任务提交前 line 1050、plan 共同依赖。放在任何单个 feature 内都会造成反向依赖。独立为 `features/quota/`,其他 feature 的 service 通过注入 `QuotaService` 调用。
**修订 6router 同时承担 OpenAPI 契约职责。** router 依然保持“薄”,不承载业务逻辑;但所有 HTTP 契约元数据必须定义在 path operation decorator 或 `APIRouter(...)` 上,包括 `response_model``status_code``responses``summary``description``tags`、必要时的 `operation_id` / `openapi_extra`。FastAPI 的 OpenAPI schema 是根据注册到 `app.routes` 的 path operations 自动生成的,因此契约不能下沉到 service。
**修订 7WebSocket 协议独立成文。** WebSocket endpoint 可以继续使用 FastAPI 依赖注入做鉴权与资源装配,但其消息协议不会自然进入 HTTP OpenAPI。`conversation/ws/` 除代码拆分外,必须额外维护 `protocol.md` 或等价文档,显式描述 client -> server、server -> client、错误消息、状态流转、鉴权、重连与幂等等规则。
**修订 8`memory` 从 agent 占位收敛为独立 feature。** 本项目中的 memory 不再按“聊天助手长期记忆”设计,而是定义为“面向回忆录生产的素材检索与事实组织系统”:原始口述素材沉淀 + 结构化事实抽取 + RAG 检索增强生成。它服务于跨会话素材召回、人物/事件/时间线组织、章节生成 grounding 与来源追溯。
**修订 9`memory` 优先做基础设施与内部服务,不把后台编辑 API 作为一期前置条件。** Phase 2 先完成 `MemoryService`、数据模型、ingest/retrieve 流程、异步任务与 conversation/memoir 接入;管理端/编辑端接口保留在 `memory/router.py` 作为二期扩展位。
---
## 三、事务边界与 Session 管理
### 3.1 现状问题
当前存在两种矛盾的事务策略:
- `get_async_db()` 在请求结束时自动 commit[database.py line 76](api/database/database.py)),但 router 内部又有显式 `await db.commit()`(如 [auth.py line 194](api/routers/auth.py)),导致双重 commit 或语义不清。
- Celery 任务直接用同步 `SessionLocal()`[memoir_tasks.py line 18](api/tasks/memoir_tasks.py)),手动管理 session 生命周期,无统一规则。
- WebSocket handler 用 `async for db in get_async_db()` 获取独立 session[websocket.py line 276](api/routers/websocket.py)),绕过 FastAPI 依赖注入。
### 3.2 新规则
**核心原则repo 不提交service 统一管理事务边界。**
- `core/db.py` 中的 `get_async_db()` 改为"干净 session":只负责创建和关闭,不自动 commit/rollback。
- 事务控制权交给 service 层service 方法内显式 `await db.commit()` 或使用 `async with db.begin():` 块。
- `repo.py` 只做查询和 `db.add()` / `db.delete()`;禁止调用 `commit()` / `rollback()``flush()` 不是常规手段,但在需要提前拿主键或尽早触发约束检查时允许受控使用,优先由 service 显式决定是否触发。
- router 层不接触 session通过 `Depends` 注入 serviceservice 内部持有 session
**Celery 任务**:在 `tasks/` 中定义 `get_sync_db()` context manager同样遵循"service commit"规则。任务函数调用 service 的同步版本或直接在任务函数中管理事务。
**WebSocket**`ws/pipeline.py` 中每个消息处理周期获取独立 session通过 `async with AsyncSessionLocal() as session:`),处理完成后由 pipeline 显式 commit。
```python
# core/db.py - 新的 session 生成器
async def get_async_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
# features/auth/service.py - service 管理事务
class AuthService:
def __init__(self, db: AsyncSession, sms: SmsSender):
self._db = db
self._sms = sms
async def register(self, request: RegisterRequest) -> TokenResponse:
user = User(...)
self._db.add(user)
refresh_token = RefreshToken(...)
self._db.add(refresh_token)
await self._db.commit()
await self._db.refresh(user)
return TokenResponse(...)
```
---
## 四、依赖方向规则
用以下有向图描述允许的依赖方向(箭头 = "依赖于"
```mermaid
graph TD
Router["features/*/router.py"] --> Service["features/*/service.py"]
Service --> Repo["features/*/repo.py"]
Service --> Ports["ports/*"]
Service --> QuotaSvc["features/quota/service.py"]
ConversationSvc["conversation/service.py"] --> MemorySvc["memory/service.py"]
MemoirSvc["memoir/service.py"] --> MemorySvc
MemorySvc --> MemoryRepo["memory/repo.py"]
MemorySvc --> EmbeddingPort["ports/embedding.py"]
Repo --> Models["features/*/models.py"]
Adapters["adapters/*"] -.->|"实现"| Ports
Core["core/*"] --> Nothing["(无外部依赖)"]
Router --> Core
Service --> Core
Tasks["tasks/*"] --> Service
Agents["agents/*"] --> Ports
Agents -.->|"只消费 evidence bundle / ports不直接读写 memory 表"| MemorySvc
WsPipeline["conversation/ws/pipeline.py"] --> Service
WsPipeline --> Ports
```
### 4.1 Service 装配规则
`core/dependencies.py` 和各 feature 的 `deps.py` 不只是放 token/user 依赖,也负责把 FastAPI dependency graph 延伸到 service 装配。router 不手工拼装 `db + adapter + settings`,而是统一依赖 `get_xxx_service()`
```python
def get_auth_service(
db: Annotated[AsyncSession, Depends(get_async_db)],
sms: Annotated[SmsSender, Depends(get_sms_sender)],
) -> AuthService:
return AuthService(db=db, sms=sms)
@router.post(
"/login",
response_model=TokenResponse,
status_code=status.HTTP_200_OK,
responses={401: {"model": ErrorResponse}},
)
async def login(
request: LoginRequest,
service: Annotated[AuthService, Depends(get_auth_service)],
) -> TokenResponse:
return await service.login(request)
```
这样可以直接复用 FastAPI 对函数/类依赖、sub-dependencies、`yield` 资源清理和类型驱动文档生成的能力。
**硬性禁止**
- `router.py` 不得直接 import adapter 或操作 DB session
- `service.py` 不得 import 具体 adapter只依赖 port protocol
- `ports/` 不得 import `adapters/`(方向反转)
- feature 之间不得互相 import router跨 feature 调用通过 service 注入
- `repo.py` 不得调用 `commit()` / `rollback()``flush()` 仅允许在明确需要提前拿 ID / 触发约束检查时受控使用
- `conversation` / `memoir` 不得直接读写 memory 表,只能调用 `MemoryService`
- `agents/` 不得直接读写 memory 表,只能消费 `MemoryService` 产出的 evidence bundle 或通过 port 获取能力
---
## 五、OpenAPI 契约与文档治理
### 5.1 Router 是 OpenAPI 主战场
每个 HTTP path operation 至少应在 router decorator 上明确声明以下信息中的适用项:
- `response_model`
- `status_code`
- `responses`
- `summary`
- `description`
- `tags`
如需扩展再使用:
- `operation_id`:只有要做 SDK 生成或需要稳定外部契约名时才手工维护
- `openapi_extra`:仅用于少量自定义扩展字段,避免把业务元数据塞进 service 或 schema 生成后处理
- `include_in_schema=False`:明确隐藏内部接口,而不是靠“没人知道 URL”
router 可以保持“薄”但绝不能失去契约职责service 负责业务router 负责 HTTP 与 OpenAPI 边界。
### 5.2 Router 级共享元数据
每个 feature 的 `APIRouter(...)` 默认应收敛以下共享信息:
- `prefix`
- `tags`
- 共享 `responses`(如 401、403、404、429
- 必要时的通用 `dependencies`
这样 feature boundary 会自然映射为文档 boundary减少 decorator 重复并保证错误响应口径一致。
### 5.3 全局 OpenAPI 只做增强,不替代路由声明
`core/openapi.py` 中实现 `custom_openapi()`,内部通过 `get_openapi(..., routes=app.routes)` 基于已注册的 path operations 生成 schema并只做全局增强例如
- `title` / `version` / `summary` / `description`
- `x-logo`
- `externalDocs`
- tags 描述与排序
不要把正常应该写在 router 上的 `response_model``responses``summary` 等信息挪到 `custom_openapi()` 里“补”出来。
### 5.4 OpenAPI 版本与可见性策略
- 默认接受 FastAPI 生成的 OpenAPI 3.1.0
- 只有某个代码生成器、网关或遗留工具明确不兼容 3.1 时,才考虑覆盖 `openapi_version`
- 内部管理接口、回调探针或调试端点若不应暴露在文档中,使用 `include_in_schema=False`
### 5.5 WebSocket 协议单独治理
FastAPI 支持在 WebSocket endpoint 中继续使用 `Depends``Query``Cookie``Header` 等依赖能力,但 WebSocket 消息协议本身不应指望由 HTTP OpenAPI 自动表达。因此 `features/conversation/ws/` 需要额外维护一份协议文档,至少覆盖:
- client -> server message schema
- server -> client event schema
- 错误消息 schema
- 鉴权方式
- 状态流转
- reconnect / idempotency / sequence 规则
---
## 六、Memory + RAG 增补方案
### 6.1 定位与设计原则
本项目中的 `memory` 不等于 chat history也不等于 persona memory。它的产品定义应收敛为
- 面向回忆录生产的可检索素材资产
- 可持续抽取、修正、确认的结构化事实层
- 为章节生成和追问生成提供 grounding 的 RAG 基础设施
明确不做:
- 通用“记住这个”的终端用户指令式记忆
- 聊天助手式 persona memory
- 面向终端用户的自然语言 `forget that`
一期目标是先打通内部基础设施:`MemoryService`、混合检索、摘要/事实/时间线分层存储、异步抽取与 evidence bundle 供给;管理端编辑 API 作为二期扩展,不作为 Phase 2 前置。
### 6.2 架构选择SQL 事实层 + 摘要层 + Hybrid Retrieval
`memory` 不应做成“只有向量库”的方案,推荐采用:
- 原始素材层:保存 transcript / note / draft 等 truth source
- 摘要层:沉淀 session / rolling summary降低长上下文重复注入成本
- 结构化事实层:人物、事件、关系、地点、里程碑等可确认事实
- 时间线层:面向 memoir 组织与 chronology 视图
- 混合检索层metadata filter + PostgreSQL FTS + pgvector semantic retrieval + score fusion
根据 `pgvector` 文档,向量可直接与业务字段共存在 Postgres 中,并与普通 SQL filter 组合;根据 PostgreSQL 文档FTS 可通过 `tsvector` + `GIN` index + `ts_rank/ts_rank_cd` 获得可控全文检索与排序能力。因此,推荐统一落在 PostgreSQL 中,以 `pgvector` 承担向量召回,以 `tsvector` / `GIN` 承担关键词与短语召回,再由 service 侧做融合与 rerank。
### 6.3 数据模型分层
`memory/models.py` 一期建议至少包含以下表:
- `memory_sources`:原始素材主记录,保存 `source_type``raw_text``storage_key``speaker``captured_at``status`
- `memory_chunks`:检索单元,保存 `content``content_tsv``embedding``chunk_index``speaker``event_year``metadata_json``is_excluded`
- `memory_summaries`:会话摘要 / 滚动摘要 / 主题摘要
- `memory_facts`:候选事实与已确认事实,包含 `fact_type``subject``predicate``object_json``confidence``source_chunk_id``status`
- `timeline_events`:时间线事件视图,包含 `event_year``event_date``person_refs``source_fact_ids`
- `memory_curation_actions`:记录 `exclude` / `restore` / `correct` / `merge` / `confirm` / `reject` 等操作轨迹
设计约束:
- 原始素材、摘要、事实、时间线必须分层存储,不能把所有语义都压进单张向量表
- 每条 summary / fact / timeline event 必须能追溯到 `source``chunk`
- `memory_chunks.embedding` 使用 `pgvector`
- `memory_chunks.content_tsv` 使用 PostgreSQL FTS建议 generated / maintained `tsvector` 列 + `GIN` index
### 6.4 写入路径Ingest
核心写入路径分两类:
- 路径 A对话口述沉淀。对话完成后transcript 落 `memory_sources`,再切块写入 `memory_chunks`,随后异步生成 embedding / summary / facts / timeline
- 路径 B人工导入素材。照片注释、手稿、家谱、旧文档、人工补录文本走同一套 source -> chunk -> enrich 流程
`MemoryService.ingest_transcript()` 建议按阶段编排:
1. 写入 `source`
2. 切块并批量写入 `chunks`
3. 提交主事务
4. 触发异步任务补齐 embedding、summary、fact extraction、timeline build
这样可以避免把重型 AI 处理塞进实时链路,同时保持来源可追溯与失败可重试。
### 6.5 读取路径Retrieve与 Evidence Bundle
章节生成、对话追问、memoir 组织前,统一由 `MemoryService` 执行检索编排:
1. 生成 retrieval query
2. 先做 metadata filter`user_id``book_id``conversation_id``speaker``event_year``status``is_excluded`
3. 并行执行 FTS 检索、向量检索、摘要检索
4. 做 score fusion必要时走可选 reranker
5. 按 token budget 组装 evidence bundle
6. 将结构化上下文交给 `conversation` / `memoir`
传给 LLM 的上下文不建议直接拼成长文本,而是建议显式区分:
- `relevant_memories`
- `relevant_summaries`
- `relevant_facts`
- 可选的 `timeline_hints`
这样更利于控制来源、分数、时间线信息和引用追踪,减少模型遗漏、编造与上下文漂移。
### 6.6 依赖方向与端口设计
依赖边界如下:
- `conversation` 只调用 `MemoryService`
- `memoir` 只调用 `MemoryService`
- `memory/service.py` 可依赖 `EmbeddingProvider`、可选 `Reranker`、以及既有 `LLMProvider`
- `memory/repo.py` 只做数据访问与检索查询,不承载生成逻辑
- `agents/` 不直接读写 memory 表
新增 ports 建议:
- `ports/embedding.py`:必须新增,提供 `embed_text()` / `embed_texts()`
- `ports/reranker.py`:可选新增,留给二期 cross-encoder rerank
### 6.7 事务与异步任务边界
`memory` 遵循既有事务原则repo 不提交service 管事务边界。但 `ingest_transcript()` 允许分阶段提交,避免单事务承载向量生成与抽取全链路。
以下操作建议放到 `tasks/ + Celery` 异步执行:
- embedding 生成
- session / rolling summary 生成
- fact extraction
- timeline build
- reindex / backfill
`conversation` 实时主链路只负责触发 ingest 或请求 retrieval不等待全部 enrich 完成。
---
## 七、Provider-Agnostic Ports 设计
### 7.1 核心可移植契约Core Portable Contract
每个 port 定义为 `typing.Protocol`,仅包含业务真正依赖的最小稳定能力:
**SMS** (`ports/sms.py`)
- `send_verification_code(phone: str, code: str) -> bool`
**LLM** (`ports/llm.py`)
- `complete(messages: list[Message], temperature: float, ...) -> str`
- `stream(messages: list[Message], ...) -> AsyncIterator[str]`
**ImageGen** (`ports/image_gen.py`)
- `generate(prompt: str, size: ImageSize, ...) -> ImageResult`
- `check_status(task_id: str) -> TaskStatus`
**Storage** (`ports/storage.py`)
- `upload(key: str, data: bytes, content_type: str) -> str`
- `get_url(key: str, expires: int) -> str`
- `delete(key: str) -> None`
**TTS** (`ports/tts.py`)
- `synthesize(text: str, voice: str) -> bytes`
**ASR** (`ports/asr.py`)
- `transcribe(audio: bytes, format: str) -> str`
**Embedding** (`ports/embedding.py`)
- `embed_text(text: str) -> list[float]`
- `embed_texts(texts: list[str]) -> list[list[float]]`
**Reranker可选** (`ports/reranker.py`)
- `rerank(query: str, documents: list[str]) -> list[float]`
### 7.2 Provider Extensions 规则
**绝对禁止 service 直接拿 adapter 扩展方法。** 这会打穿 port 边界,让 service 再次耦合到具体 provider。
处理厂商特有能力的正确方式:
- **方案 A首选扩充现有 port。** 如果能力具备跨 provider 通用性(如 image_gen 的 inpaint在 port protocol 上新增方法,各 adapter 按能力实现或抛 `NotImplementedError`
- **方案 B定义第二个 port。** 如果能力确实只属于某个 provider 的特殊功能(如 Liblib 的模板管理),定义一个独立的窄 port`ImageTemplateManager`),只在需要的 feature deps 中注入。service 依赖的仍然是 port protocol而非 adapter 类。
- **方案 Cadapter 内部自行消化。** 厂商特有的配置项(如 Liblib template UUID、COS 生命周期策略)作为 adapter 构造参数传入,不暴露给 service。adapter 在实现 port 方法时自行使用这些配置。
**判断标准**:如果 service 代码中出现了 `from adapters.xxx import ...`,说明边界被打穿了。
### 7.3 DI 装配
`core/dependencies.py` 中根据 `config.py` 的配置选择 adapter
```python
def get_sms_sender() -> SmsSender:
if settings.sms_provider == "tencent":
return TencentSmsSender(settings.tencent_sms)
raise ValueError(f"Unknown SMS provider: {settings.sms_provider}")
```
feature 的 `deps.py` 或 router 通过 `Depends(get_sms_sender)` 注入。
---
## 八、Alembic 集成与 Schema 变更治理
### 8.1 初始化
-`api/` 根目录执行 `alembic init alembic`
- 修改 `alembic/env.py`,导入所有 feature 的 `models.py` 到统一 `Base.metadata`
- `alembic.ini``sqlalchemy.url` 读取 `DATABASE_URL`
### 8.2 初始基线(不依赖 autogenerate
当前 schema 来源是 `Base.metadata.create_all()` + 9 个手写 SQL 迁移文件,两者可能已经出现漂移。直接 autogenerate 大概率产出不准确的 diff。
**正确步骤**
1. 在 staging/dev 环境连接真实数据库,用 `pg_dump --schema-only` 导出当前实际 schema 作为基准参照。
2. 手写一个空的 Alembic revision 作为 baseline`alembic revision -m "baseline_empty"`upgrade/downgrade 均为 pass。
3. 对现有数据库执行 `alembic stamp head`,将当前库标记为已在 head。
4. **此后**才开始用 autogenerate 生成增量迁移,每次 review 时对照 `pg_dump` 确认无遗漏。
5.`migrations/` 目录下的 9 个手写 SQL 归档到 `migrations_legacy/`,不再使用。
### 8.3 变更治理规则
- 所有 schema 变更必须通过 Alembic migration 合入,禁止直接改 DB
- `autogenerate` 只做草稿PR 中必须人工 review
- 破坏性变更(删列、改类型)采用 expand/contract先加新列 -> 双写 -> 迁移数据 -> 切读路径 -> 删旧列
- data migration 和 schema migration 分开写
- 每次部署前校验 `alembic current` == `alembic heads`
- `main.py` 移除 `Base.metadata.create_all()`,改为部署流程执行 `alembic upgrade head`
### 8.4 Model 拆分
`database/models.py` 的 9 个模型拆入对应 feature
- `User` -> `features/user/models.py`
- `RefreshToken`, `SmsVerificationCode` -> `features/auth/models.py`identity/session 资产auth 是唯一强依赖方)
- `Conversation`, `Segment` -> `features/conversation/models.py`
- `Book`, `Chapter`, `MemoirState` -> `features/memoir/models.py`
- `Order` -> `features/payment/models.py`
所有模型共享同一个 `Base = declarative_base()`(放 `core/db.py`Alembic `env.py` 统一 import 所有 feature models。
**跨 feature model 引用规则**auth/models.py 中的 `RefreshToken.user_id` 外键指向 `users.id`(表名级引用),不需要 import User 类。如需 ORM relationship通过字符串引用 `relationship("User", ...)`
---
## 九、日志统一化loguru
### 9.1 配置
`core/logging.py` 中:
- 移除标准库 `logging.basicConfig`
- 配置 loguruJSON 格式输出、按级别分文件、rotation/retention
- 拦截标准库 logging`InterceptHandler`),使 uvicorn / sqlalchemy / celery 日志统一走 loguru
### 9.2 Request Logging Middleware
新增 middleware每个请求注入 `request_id`UUID通过 loguru 的 `contextualize` 绑定到所有日志:
```python
with logger.contextualize(request_id=request_id, user_id=user_id):
response = await call_next(request)
```
### 9.3 Provider 调用日志
在每个 adapter 中统一记录provider 名称、操作、耗时、成功/失败、错误分类。
---
## 十、全局异常处理
`core/errors.py` 中定义异常体系:
```python
class AppError(Exception):
status_code: int
error_code: str
message: str
class NotFoundError(AppError): ...
class AuthenticationError(AppError): ...
class ProviderError(AppError): ...
```
`main.py` 注册全局 exception handler统一返回格式
```json
{"error_code": "PROVIDER_ERROR", "message": "...", "request_id": "..."}
```
---
## 十一、统一配置
将散落的 `os.getenv()` 收归 `core/config.py`,使用 pydantic-settings `BaseSettings`
```python
class Settings(BaseSettings):
database_url: str
redis_url: str
secret_key: str
sms_provider: str = "tencent"
storage_provider: str = "tencent_cos"
image_gen_provider: str = "liblib"
# 嵌套配置
tencent_sms: TencentSmsConfig
tencent_cos: TencentCosConfig
...
model_config = SettingsConfigDict(env_file=".env", env_nested_delimiter="__")
```
**强约束**:环境变量统一通过 `core/config.py` 中的 `Settings` 单点读取。除配置模块外,禁止在业务代码中直接调用 `load_dotenv()``dotenv_values()` 或散落 `os.getenv()`;其他模块只允许 import `settings` 使用配置值。
**依赖收敛约定**Redis 客户端统一使用 `redis` / `redis.asyncio`JWT 统一使用 `PyJWT``api/pyproject.toml``api/uv.lock` 是依赖真相源,`requirements.txt` 如保留仅作为兼容导出产物,不再作为主入口。
---
## 十二、迁移执行策略
### Phase 0 - 基础设施搭建(无功能变更)
1. **迁移到 uv**:以 `api/pyproject.toml``api/uv.lock` 作为依赖真相源;将 `requirements.txt` 的依赖迁入 `pyproject.toml``[project.dependencies]`。完成迁移后,`requirements.txt` 如继续保留,仅作为兼容导出文件,不再作为主入口
2. 创建 `app/core/` 骨架config, db, logging, errors, security, dependencies, middleware, openapi
3. 重写 `get_async_db()` 去掉自动 commit定义 `get_sync_db()` context manager 供 Celery 使用
4. 初始化 Alembic手写空 baseline revision + `stamp head`(不用 autogenerate
5. 集成 loguru配置 InterceptHandler 拦截标准库 logging
6. 添加 request_id middleware
7. 定义全局异常体系(仅统一错误响应格式)
8. 建立 OpenAPI 基线:统一 router metadata 规范、共享错误响应、`custom_openapi()` 全局增强、内部接口可见性策略
9. 预留 memory 基础设施:确认 `pgvector` 扩展、FTS 列与索引策略、`memory` 表族命名与 Alembic 迁移约定
**验证点**`uv sync --dev` 能基于 `api/pyproject.toml``api/uv.lock` 安装全部依赖,应用能以新 core 启动,所有现有 API 不受影响。注意 `get_async_db()` 的 commit 语义变更需要同步修改所有现有 router 中缺少显式 commit 的写操作。
### Phase 1 - Ports 定义 + Adapters 迁移
1. 定义 7 个核心 port protocolsms, llm, image_gen, storage, tts, asr, embedding
2. 将现有 SDK 代码封装为 adapter实现对应 protocol
3. 预留可选 `reranker` port二期再接具体实现
4.`core/dependencies.py` 中注册 DI factory`get_embedding_provider()`
5. payment adapter 从 `payment/` 直接迁移(已有好的抽象)
**验证点**:所有 adapter 通过 port protocol 类型检查,现有功能不变。
### Phase 2 - Feature 模块重构(逐个 feature
按依赖关系从外到内逐个迁移,建议顺序:
1. `content`faq, legal, home— 最简单,验证 feature 骨架
2. `plan` — 轻量,少依赖
3. `quota` — 提升为独立 feature定义 `QuotaService`
4. `user` — 拆出 User model
5. `auth` — 拆出 RefreshToken/SmsVerificationCode models依赖 user models + sms port
6. `memory` — 新增独立 feature打通 source/chunk/summary/fact/timeline 与 hybrid retrieval
7. `payment` — 迁移现有 payment/ 模块
8. `memoir` — 合并 books/chapters/memoir_state迁移 processor并接入 `MemoryService`
9. `conversation` — 最复杂,包含 WebSocket 深度拆分(见下),并接入 transcript ingest / retrieval
**每个 feature 迁移步骤**
- 创建 `models.py`(从全局 models.py 拆出)
- 创建 `repo.py`(从 router 中提取 DB 操作,禁止 commit
- 创建 `service.py`(从 router 中提取业务逻辑,负责事务提交)
- 创建 `schemas.py`(收集 Pydantic schema
- 精简 `router.py`(只保留 HTTP 边界参数解析、响应状态码、OpenAPI 契约声明、调用 service
- 为每个公开接口补齐 `response_model` / `status_code` / `responses` / `summary` / `description` / `tags`
- 更新 `main.py``include_router`
**memory feature 额外要求**
- 建立 `memory_sources` / `memory_chunks` / `memory_summaries` / `memory_facts` / `timeline_events` / `memory_curation_actions`
- `memory_chunks` 同时具备 `embedding``content_tsv`
- 先完成 `MemoryService` 内部接口,再由 `memoir` / `conversation` 接入
- 后台编辑 API 保留为后续扩展,不阻塞一期基础设施落地
**conversation WebSocket 拆分详细步骤**
当前 `routers/websocket.py`1067 行)需拆为 `features/conversation/ws/` 子包:
- `message_types.py`:提取 `MessageType` 枚举(当前 line 32-45+ 消息构造工具函数
- `connection_manager.py`:提取 `ConnectionManager` 类(当前 line 49-120只保留连接注册/注销/发送,移除 agent/runner 实例持有
- `profile_collector.py`:提取 `_get_missing_profile_fields``_get_filled_profile_fields``_apply_extracted_profile`(当前 line 854-898和资料收集模式分支line 917-952
- `quota_guard.py`:封装配额校验逻辑(当前 line 627-637调用 `quota/service.py`
- `pipeline.py`:核心消息处理管道,编排 ASR -> 落库 -> Agent -> TTS -> 响应,依赖上述模块 + portsasr, tts, llm
- `router.py`WebSocket endpoint仅做连接生命周期管理accept, 主循环, disconnect将每条消息委托给 pipeline
- `protocol.md`:补充消息协议、鉴权、错误码、状态流转、重连与幂等等非 HTTP 契约
**验证点**:每迁移一个 feature运行测试确保该 feature API 行为不变。
### Phase 3 - 清理与加固
1. 删除旧 `routers/`, `services/`, `database/models.py`, `middleware/`, `migrations/`
2. 删除 `main.py` 中的 `init_db()` / `create_all()`
3. 更新 Dockerfile改用 uv 安装依赖(`COPY pyproject.toml uv.lock ./` + `uv sync --frozen --no-dev`),或用 `uv export --no-dev > requirements.txt` 导出后继续用 pip
4. 更新 docker-compose、deploy.sh 中的路径和启动命令
5. 更新 Celery worker 启动命令(`uv run celery ...`
6. 更新所有测试 import 路径
7. 补充缺失的测试
---
## 十三、硬性规则(写入项目规范)
1. `**common/` / `utils/` / `shared/` / `helpers/` 禁止创建**:只有 truly cross-cutting 能力进 `core/`,业务 DTO/校验/错误码归 feature
2. **router 是 HTTP + OpenAPI 契约边界**:所有数据访问通过 repo所有业务逻辑通过 service`response_model``status_code``responses``summary``description``tags` 等契约元数据必须定义在 router / `APIRouter`
3. **service 禁止 import adapter**:只依赖 port protocol通过 DI 注入。代码中出现 `from adapters.xxx import ...` 即为违规
4. **feature 间禁止 import router**:跨 feature 调用通过 service 注入
5. **所有 schema 变更走 Alembic**:禁止直接 DDL
6. **新增 provider 必须实现 port protocol**:不得在 feature 中直接调用 SDK
7. **事务边界repo 不提交service 管事务**`repo.py` 只做 `add/delete/query``commit/rollback` 由 service 或 UoW 统一执行。`get_async_db()` 不自动 commit`flush()` 仅在明确需要提前拿主键或触发约束检查时受控使用
8. **port 边界不可打穿**service 需要厂商增强能力时,必须扩充 port 或定义第二个窄 port禁止直接引用 adapter 扩展方法
9. **quota 是独立 feature**conversation、memoir、payment 如需配额检查,通过注入 `QuotaService`,不得直接 import quota 内部函数
10. **成功响应不强制包装**`core/errors.py` 只统一错误响应格式 `{error_code, message, request_id}`,成功响应直接返回 Pydantic model / FileResponse / 原始结构
11. **配置统一通过 `pydantic-settings` 管理**:只允许在 `core/config.py` 中定义 `BaseSettings` / `SettingsConfigDict`;除配置模块外,禁止业务代码直接 `load_dotenv()``dotenv_values()` 或散落 `os.getenv()`
12. **依赖真相源是 `api/pyproject.toml` + `api/uv.lock`**`requirements.txt` 如存在仅作兼容导出,不作为主依赖入口
13. **运行时基础库保持收敛**Redis 客户端统一使用 `redis` / `redis.asyncio`JWT 统一使用 `PyJWT`
14. **service 装配走 FastAPI dependency graph**router 只依赖 `get_xxx_service()`,不得在路由函数里手工 new service 或自行拼装 `db + adapter + settings`
15. **每个 feature router 必须维护共享 tags / responses**:使用 `APIRouter(prefix=..., tags=..., responses=...)` 收敛文档边界与共享错误响应
16. **内部接口显式隐藏**:不对外暴露的 path operation 使用 `include_in_schema=False`,禁止依赖“没人知道 URL”
17. **OpenAPI 全局自定义只做增强**`custom_openapi()` 只做全局 schema 增强,不替代 router 上的正常声明
18. **WebSocket 协议必须独立成文**:实时消息 schema、错误、鉴权、状态流转、重连与幂等规则不得只存在于代码实现中
19. **memory 的定义是素材检索与事实组织,不是聊天 persona 记忆**:不实现通用“记住这个”或面向终端用户的自然语言 `forget`
20. **memory 必须采用混合检索**:至少包含 metadata filter + PostgreSQL FTS + vector retrieval纯向量库不得作为唯一真相源
21. **原始素材、摘要、事实、时间线必须分层存储**:不得把所有语义压进单一向量表
22. **memory 写入必须可追溯**summary / fact / timeline event 必须能追溯到 `source``chunk`
23. **conversation / memoir 只通过 `MemoryService` 调用 memory**:不得直接查询 memory 表
24. **memory feature 不得直接耦合具体 embedding SDK**:只依赖 `EmbeddingProvider`(可选 `Reranker` 同理)
25. **所有 pgvector / FTS schema 变更必须走 Alembic**索引、扩展、generated column、operator class 变更都要纳入 migration
26. **章节生成必须优先使用 evidence bundle**:不得只凭当前会话上下文裸生成