Merge branch 'refactor/backend-architecture' into development
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: 架构重构计划
|
||||
overview: 将现有 Life Echo API 从混合分层结构重构为 feature-first flat 架构,引入 ports/adapters 实现 provider-agnostic 基础设施抽象,集成 Alembic 做 schema 变更治理,统一日志到 loguru。
|
||||
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)"
|
||||
@@ -17,8 +17,14 @@ todos:
|
||||
- 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: 定义 6 个 port protocol(sms, llm, image_gen, storage, tts, asr)"
|
||||
content: "Phase 1: 定义 7 个核心 port protocol(sms, 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 实现"
|
||||
@@ -41,6 +47,9 @@ todos:
|
||||
- id: phase2-auth
|
||||
content: "Phase 2: 迁移 auth feature(RefreshToken + SmsVerificationCode models,依赖 user + sms port)"
|
||||
status: pending
|
||||
- id: phase2-memory
|
||||
content: "Phase 2: 新增 memory feature(sources/chunks/summaries/facts/timeline + hybrid retrieval)"
|
||||
status: pending
|
||||
- id: phase2-payment
|
||||
content: "Phase 2: 迁移 payment feature"
|
||||
status: pending
|
||||
@@ -79,6 +88,7 @@ api/
|
||||
|
||||
- 路由层过胖,直接操作 DB 和调用外部服务
|
||||
- 外部服务耦合:SMS(腾讯云)、存储(腾讯 COS)、TTS(OpenAI)直接写死 SDK
|
||||
- `memory` 语义不清:当前更像“聊天助手记忆”占位,缺少面向回忆录生产的素材沉淀、事实抽取、检索增强与来源追溯
|
||||
- 无统一配置类,`os.getenv()` 散落各处
|
||||
- 无 Alembic,手写 SQL 迁移无版本追踪
|
||||
- 标准库 logging,无结构化日志
|
||||
@@ -98,7 +108,7 @@ api/
|
||||
```
|
||||
api/
|
||||
app/
|
||||
main.py # 仅做组装:include_router, middleware, lifespan
|
||||
main.py # 仅做组装:include_router, middleware, lifespan, OpenAPI wiring
|
||||
core/
|
||||
config.py # 统一 BaseSettings(pydantic-settings)
|
||||
db.py # Base, engine, session factory, get_async_db
|
||||
@@ -106,7 +116,8 @@ api/
|
||||
errors.py # 全局异常体系 + exception handlers
|
||||
security.py # JWT 签发/校验、密码哈希
|
||||
middleware.py # CORS, request_id, logging middleware
|
||||
dependencies.py # 全局共享依赖(get_current_user, port DI factory)
|
||||
dependencies.py # 全局共享依赖(get_current_user, get_xxx_service, port DI factory)
|
||||
openapi.py # custom_openapi():全局 schema 增强(title/version/logo/external docs)
|
||||
pagination.py # 通用分页
|
||||
|
||||
features/
|
||||
@@ -131,6 +142,7 @@ api/
|
||||
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 合并
|
||||
@@ -140,6 +152,20 @@ api/
|
||||
repo.py
|
||||
processor.py # 从 agents/memoir_processor.py 迁入
|
||||
|
||||
memory/
|
||||
router.py # 可选:后台编辑/管理接口(二期);一期以内部服务为主
|
||||
schemas.py
|
||||
service.py # MemoryService:conversation / 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
|
||||
@@ -172,10 +198,8 @@ api/
|
||||
|
||||
agents/
|
||||
conversation_agent.py
|
||||
memory_agent.py
|
||||
prompts/
|
||||
conversation_prompts.py
|
||||
memory_prompts.py
|
||||
profile_prompts.py
|
||||
|
||||
ports/ # 能力接口定义(Protocol / ABC)
|
||||
@@ -185,6 +209,8 @@ api/
|
||||
storage.py # ObjectStorage protocol
|
||||
tts.py # TTSProvider protocol
|
||||
asr.py # ASRProvider protocol
|
||||
embedding.py # EmbeddingProvider protocol
|
||||
reranker.py # 可选:二期 cross-encoder rerank
|
||||
|
||||
adapters/ # 具体 provider 实现
|
||||
sms/
|
||||
@@ -200,6 +226,8 @@ api/
|
||||
asr/
|
||||
whisper_local.py
|
||||
tencent_asr.py
|
||||
embedding/
|
||||
openai.py
|
||||
payment/
|
||||
wechat.py
|
||||
alipay.py
|
||||
@@ -207,6 +235,7 @@ api/
|
||||
tasks/
|
||||
celery_app.py
|
||||
memoir_tasks.py
|
||||
memory_tasks.py
|
||||
|
||||
alembic/
|
||||
env.py
|
||||
@@ -229,6 +258,14 @@ api/
|
||||
|
||||
**修订 5:`quota` 提升为独立 feature。** 当前 `routers/quota.py` 包含 `PLAN_QUOTAS` 配置、`get_segment_count`、`get_chapter_count`、`check_can_send_message`、`check_can_submit_organize`,被 conversation(WebSocket line 628)、memoir(任务提交前 line 1050)、plan 共同依赖。放在任何单个 feature 内都会造成反向依赖。独立为 `features/quota/`,其他 feature 的 service 通过注入 `QuotaService` 调用。
|
||||
|
||||
**修订 6:router 同时承担 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。
|
||||
|
||||
**修订 7:WebSocket 协议独立成文。** 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 管理
|
||||
@@ -247,7 +284,7 @@ api/
|
||||
|
||||
- `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()` 或 `flush()`。
|
||||
- `repo.py` 只做查询和 `db.add()` / `db.delete()`;禁止调用 `commit()` / `rollback()`。`flush()` 不是常规手段,但在需要提前拿主键或尽早触发约束检查时允许受控使用,优先由 service 显式决定是否触发。
|
||||
- router 层不接触 session,通过 `Depends` 注入 service(service 内部持有 session)。
|
||||
|
||||
**Celery 任务**:在 `tasks/` 中定义 `get_sync_db()` context manager,同样遵循"service commit"规则。任务函数调用 service 的同步版本或直接在任务函数中管理事务。
|
||||
@@ -291,6 +328,10 @@ graph TD
|
||||
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["(无外部依赖)"]
|
||||
@@ -298,25 +339,232 @@ graph TD
|
||||
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()`
|
||||
- `repo.py` 不得调用 `commit()` / `rollback()`;`flush()` 仅允许在明确需要提前拿 ID / 触发约束检查时受控使用
|
||||
- `conversation` / `memoir` 不得直接读写 memory 表,只能调用 `MemoryService`
|
||||
- `agents/` 不得直接读写 memory 表,只能消费 `MemoryService` 产出的 evidence bundle 或通过 port 获取能力
|
||||
|
||||
---
|
||||
|
||||
## 四、Provider-Agnostic Ports 设计
|
||||
## 五、OpenAPI 契约与文档治理
|
||||
|
||||
### 4.1 核心可移植契约(Core Portable Contract)
|
||||
### 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`,仅包含业务真正依赖的最小稳定能力:
|
||||
|
||||
@@ -348,7 +596,16 @@ graph TD
|
||||
|
||||
- `transcribe(audio: bytes, format: str) -> str`
|
||||
|
||||
### 5.2 Provider Extensions 规则
|
||||
**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。
|
||||
|
||||
@@ -360,7 +617,7 @@ graph TD
|
||||
|
||||
**判断标准**:如果 service 代码中出现了 `from adapters.xxx import ...`,说明边界被打穿了。
|
||||
|
||||
### 4.3 DI 装配
|
||||
### 7.3 DI 装配
|
||||
|
||||
在 `core/dependencies.py` 中根据 `config.py` 的配置选择 adapter:
|
||||
|
||||
@@ -375,15 +632,15 @@ feature 的 `deps.py` 或 router 通过 `Depends(get_sms_sender)` 注入。
|
||||
|
||||
---
|
||||
|
||||
## 五、Alembic 集成与 Schema 变更治理
|
||||
## 八、Alembic 集成与 Schema 变更治理
|
||||
|
||||
### 5.1 初始化
|
||||
### 8.1 初始化
|
||||
|
||||
- 在 `api/` 根目录执行 `alembic init alembic`
|
||||
- 修改 `alembic/env.py`,导入所有 feature 的 `models.py` 到统一 `Base.metadata`
|
||||
- `alembic.ini` 中 `sqlalchemy.url` 读取 `DATABASE_URL`
|
||||
|
||||
### 6.2 初始基线(不依赖 autogenerate)
|
||||
### 8.2 初始基线(不依赖 autogenerate)
|
||||
|
||||
当前 schema 来源是 `Base.metadata.create_all()` + 9 个手写 SQL 迁移文件,两者可能已经出现漂移。直接 autogenerate 大概率产出不准确的 diff。
|
||||
|
||||
@@ -395,7 +652,7 @@ feature 的 `deps.py` 或 router 通过 `Depends(get_sms_sender)` 注入。
|
||||
4. **此后**才开始用 autogenerate 生成增量迁移,每次 review 时对照 `pg_dump` 确认无遗漏。
|
||||
5. 将 `migrations/` 目录下的 9 个手写 SQL 归档到 `migrations_legacy/`,不再使用。
|
||||
|
||||
### 5.3 变更治理规则
|
||||
### 8.3 变更治理规则
|
||||
|
||||
- 所有 schema 变更必须通过 Alembic migration 合入,禁止直接改 DB
|
||||
- `autogenerate` 只做草稿,PR 中必须人工 review
|
||||
@@ -404,7 +661,7 @@ feature 的 `deps.py` 或 router 通过 `Depends(get_sms_sender)` 注入。
|
||||
- 每次部署前校验 `alembic current` == `alembic heads`
|
||||
- `main.py` 移除 `Base.metadata.create_all()`,改为部署流程执行 `alembic upgrade head`
|
||||
|
||||
### 6.4 Model 拆分
|
||||
### 8.4 Model 拆分
|
||||
|
||||
将 `database/models.py` 的 9 个模型拆入对应 feature:
|
||||
|
||||
@@ -420,9 +677,9 @@ feature 的 `deps.py` 或 router 通过 `Depends(get_sms_sender)` 注入。
|
||||
|
||||
---
|
||||
|
||||
## 六、日志统一化(loguru)
|
||||
## 九、日志统一化(loguru)
|
||||
|
||||
### 6.1 配置
|
||||
### 9.1 配置
|
||||
|
||||
在 `core/logging.py` 中:
|
||||
|
||||
@@ -430,7 +687,7 @@ feature 的 `deps.py` 或 router 通过 `Depends(get_sms_sender)` 注入。
|
||||
- 配置 loguru:JSON 格式输出、按级别分文件、rotation/retention
|
||||
- 拦截标准库 logging(`InterceptHandler`),使 uvicorn / sqlalchemy / celery 日志统一走 loguru
|
||||
|
||||
### 6.2 Request Logging Middleware
|
||||
### 9.2 Request Logging Middleware
|
||||
|
||||
新增 middleware,每个请求注入 `request_id`(UUID),通过 loguru 的 `contextualize` 绑定到所有日志:
|
||||
|
||||
@@ -439,13 +696,13 @@ with logger.contextualize(request_id=request_id, user_id=user_id):
|
||||
response = await call_next(request)
|
||||
```
|
||||
|
||||
### 6.3 Provider 调用日志
|
||||
### 9.3 Provider 调用日志
|
||||
|
||||
在每个 adapter 中统一记录:provider 名称、操作、耗时、成功/失败、错误分类。
|
||||
|
||||
---
|
||||
|
||||
## 七、全局异常处理
|
||||
## 十、全局异常处理
|
||||
|
||||
在 `core/errors.py` 中定义异常体系:
|
||||
|
||||
@@ -468,7 +725,7 @@ class ProviderError(AppError): ...
|
||||
|
||||
---
|
||||
|
||||
## 八、统一配置
|
||||
## 十一、统一配置
|
||||
|
||||
将散落的 `os.getenv()` 收归 `core/config.py`,使用 pydantic-settings `BaseSettings`:
|
||||
|
||||
@@ -488,28 +745,35 @@ class Settings(BaseSettings):
|
||||
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**:`uv init`,将 `requirements.txt` 的依赖迁入 `pyproject.toml` 的 `[project.dependencies]`,删除 `requirements.txt`,生成 `uv.lock` 并纳入版本控制
|
||||
2. 创建 `app/core/` 骨架:config, db, logging, errors, security, dependencies, middleware
|
||||
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` 能安装全部依赖,应用能以新 core 启动,所有现有 API 不受影响。注意 `get_async_db()` 的 commit 语义变更需要同步修改所有现有 router 中缺少显式 commit 的写操作。
|
||||
**验证点**:`uv sync --dev` 能基于 `api/pyproject.toml` 和 `api/uv.lock` 安装全部依赖,应用能以新 core 启动,所有现有 API 不受影响。注意 `get_async_db()` 的 commit 语义变更需要同步修改所有现有 router 中缺少显式 commit 的写操作。
|
||||
|
||||
### Phase 1 - Ports 定义 + Adapters 迁移
|
||||
|
||||
1. 定义 6 个 port protocol(sms, llm, image_gen, storage, tts, asr)
|
||||
1. 定义 7 个核心 port protocol(sms, llm, image_gen, storage, tts, asr, embedding)
|
||||
2. 将现有 SDK 代码封装为 adapter,实现对应 protocol
|
||||
3. 在 `core/dependencies.py` 中注册 DI factory
|
||||
4. payment adapter 从 `payment/` 直接迁移(已有好的抽象)
|
||||
3. 预留可选 `reranker` port(二期再接具体实现)
|
||||
4. 在 `core/dependencies.py` 中注册 DI factory(含 `get_embedding_provider()`)
|
||||
5. payment adapter 从 `payment/` 直接迁移(已有好的抽象)
|
||||
|
||||
**验证点**:所有 adapter 通过 port protocol 类型检查,现有功能不变。
|
||||
|
||||
@@ -522,9 +786,10 @@ class Settings(BaseSettings):
|
||||
3. `quota` — 提升为独立 feature,定义 `QuotaService`
|
||||
4. `user` — 拆出 User model
|
||||
5. `auth` — 拆出 RefreshToken/SmsVerificationCode models,依赖 user models + sms port
|
||||
6. `payment` — 迁移现有 payment/ 模块
|
||||
7. `memoir` — 合并 books/chapters/memoir_state,迁移 processor
|
||||
8. `conversation` — 最复杂,包含 WebSocket 深度拆分(见下)
|
||||
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 迁移步骤**:
|
||||
|
||||
@@ -532,9 +797,17 @@ class Settings(BaseSettings):
|
||||
- 创建 `repo.py`(从 router 中提取 DB 操作,禁止 commit)
|
||||
- 创建 `service.py`(从 router 中提取业务逻辑,负责事务提交)
|
||||
- 创建 `schemas.py`(收集 Pydantic schema)
|
||||
- 精简 `router.py`(只保留 HTTP 边界:参数解析、响应状态码、调用 service)
|
||||
- 精简 `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/` 子包:
|
||||
@@ -545,6 +818,7 @@ class Settings(BaseSettings):
|
||||
- `quota_guard.py`:封装配额校验逻辑(当前 line 627-637),调用 `quota/service.py`
|
||||
- `pipeline.py`:核心消息处理管道,编排 ASR -> 落库 -> Agent -> TTS -> 响应,依赖上述模块 + ports(asr, tts, llm)
|
||||
- `router.py`:WebSocket endpoint,仅做连接生命周期管理(accept, 主循环, disconnect),将每条消息委托给 pipeline
|
||||
- `protocol.md`:补充消息协议、鉴权、错误码、状态流转、重连与幂等等非 HTTP 契约
|
||||
|
||||
**验证点**:每迁移一个 feature,运行测试确保该 feature API 行为不变。
|
||||
|
||||
@@ -560,16 +834,32 @@ class Settings(BaseSettings):
|
||||
|
||||
---
|
||||
|
||||
## 十一、硬性规则(写入项目规范)
|
||||
## 十三、硬性规则(写入项目规范)
|
||||
|
||||
1. `**common/` / `utils/` / `shared/` / `helpers/` 禁止创建**:只有 truly cross-cutting 能力进 `core/`,业务 DTO/校验/错误码归 feature
|
||||
2. **router 禁止操作 DB**:所有数据访问通过 repo,所有业务逻辑通过 service
|
||||
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
|
||||
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**:不得只凭当前会话上下文裸生成
|
||||
|
||||
|
||||
Reference in New Issue
Block a user