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

42 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
架构重构计划 将现有 Life Echo API 从混合分层结构重构为 feature-first flat 架构,引入 ports/adapters 实现 provider-agnostic 基础设施抽象,补齐 code-first OpenAPI 契约治理,并新增面向回忆录生成的 Memory + RAG 能力,集成 Alembic 做 schema 变更治理,统一日志到 loguru。
id content status
phase0-core Phase 0: 搭建 app/core/ 骨架config, db, logging, errors, security, dependencies, middleware pending
id content status
phase0-txn Phase 0: 重写 get_async_db() 去掉自动 commit定义事务边界规则 pending
id content status
phase0-alembic Phase 0: 初始化 Alembic手写空 baselinestamp head不依赖 autogenerate pending
id content status
phase0-loguru Phase 0: 集成 loguru + InterceptHandler + request_id middleware pending
id content status
phase0-errors Phase 0: 实现全局异常体系 + exception handlers仅错误响应统一成功响应不包装 pending
id content status
phase0-openapi Phase 0: 建立 OpenAPI 契约规范router metadata、custom_openapi、内部接口可见性、WebSocket 协议文档) pending
id content status
phase1-ports Phase 1: 定义 7 个核心 port protocolsms, llm, image_gen, storage, tts, asr, embedding pending
id content status
phase1-embedding Phase 1: 新增 embedding port可选预留 reranker port并完成 DI 装配 pending
id content status
phase1-adapters Phase 1: 封装现有 SDK 代码为 adapter 实现 pending
id content status
phase1-di Phase 1: 在 core/dependencies.py 注册 DI factory pending
id content status
phase2-content Phase 2: 迁移 content featurefaq, legal, home pending
id content status
phase2-plan Phase 2: 迁移 plan feature pending
id content status
phase2-quota Phase 2: 提取 quota 为独立 featureQuotaService + router pending
id content status
phase2-user Phase 2: 迁移 user featureUser model pending
id content status
phase2-auth Phase 2: 迁移 auth featureRefreshToken + SmsVerificationCode models依赖 user + sms port pending
id content status
phase2-memory Phase 2: 新增 memory featuresources/chunks/summaries/facts/timeline + hybrid retrieval pending
id content status
phase2-payment Phase 2: 迁移 payment feature pending
id content status
phase2-memoir Phase 2: 迁移 memoir featurebooks + chapters + memoir_state 合并) pending
id content status
phase2-conversation Phase 2: 迁移 conversation feature + WebSocket 拆为 ws/ 子包6 个文件) pending
id content status
phase3-cleanup Phase 3: 清理旧目录,更新 Docker/CI/测试路径 pending
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/
    ...

目录结构修订说明

修订 1core/db.py 显式列出。 存放 Base = declarative_base()、engine/session factory、get_async_db 生成器。所有 feature models 共享此 Base。

修订 2core/response.py 移除。 统一响应包装只用于错误响应(在 core/errors.py 的 exception handler 中处理)。成功响应直接返回 Pydantic model / FileResponse / 原始 dict不强制 {code, data, message} 包装,避免对 FastAPI schema 自动生成、文件下载、支付回调、WebSocket 造成额外负担。

修订 3auth models 与 user models 分离。 RefreshTokenSmsVerificationCode 归入 features/auth/models.py,因为它们是 identity/session 资产,认证流程 (auth/service.py) 是唯一强依赖方。User 模型留在 features/user/models.pyauth 通过 import user models 引用 User允许的依赖方向auth -> user

修订 4WebSocket 从单个 ws_handler.py 拆为 ws/ 子包。 当前 routers/websocket.py 有 1067 行同时承担连接管理、消息类型定义、音频分段处理、ASR 转写、配额校验、用户资料收集、Agent 编排、DB 落库、Celery 任务提交。仅改名为 ws_handler.py 会原样搬运复杂度。拆分为 6 个文件,每个职责单一。

修订 5quota 提升为独立 feature。 当前 routers/quota.py 包含 PLAN_QUOTAS 配置、get_segment_countget_chapter_countcheck_can_send_messagecheck_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_modelstatus_coderesponsessummarydescriptiontags、必要时的 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、错误消息、状态流转、鉴权、重连与幂等等规则。

修订 8memory 从 agent 占位收敛为独立 feature。 本项目中的 memory 不再按“聊天助手长期记忆”设计,而是定义为“面向回忆录生产的素材检索与事实组织系统”:原始口述素材沉淀 + 结构化事实抽取 + RAG 检索增强生成。它服务于跨会话素材召回、人物/事件/时间线组织、章节生成 grounding 与来源追溯。

修订 9memory 优先做基础设施与内部服务,不把后台编辑 API 作为一期前置条件。 Phase 2 先完成 MemoryService、数据模型、ingest/retrieve 流程、异步任务与 conversation/memoir 接入;管理端/编辑端接口保留在 memory/router.py 作为二期扩展位。


三、事务边界与 Session 管理

3.1 现状问题

当前存在两种矛盾的事务策略:

  • get_async_db() 在请求结束时自动 commitdatabase.py line 76),但 router 内部又有显式 await db.commit()(如 auth.py line 194),导致双重 commit 或语义不清。
  • Celery 任务直接用同步 SessionLocal()memoir_tasks.py line 18),手动管理 session 生命周期,无统一规则。
  • WebSocket handler 用 async for db in get_async_db() 获取独立 sessionwebsocket.py line 276),绕过 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 的同步版本或直接在任务函数中管理事务。

WebSocketws/pipeline.py 中每个消息处理周期获取独立 session通过 async with AsyncSessionLocal() as session:),处理完成后由 pipeline 显式 commit。

# 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(...)

四、依赖方向规则

用以下有向图描述允许的依赖方向(箭头 = "依赖于"

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()

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_modelresponsessummary 等信息挪到 custom_openapi() 里“补”出来。

5.4 OpenAPI 版本与可见性策略

  • 默认接受 FastAPI 生成的 OpenAPI 3.1.0
  • 只有某个代码生成器、网关或遗留工具明确不兼容 3.1 时,才考虑覆盖 openapi_version
  • 内部管理接口、回调探针或调试端点若不应暴露在文档中,使用 include_in_schema=False

5.5 WebSocket 协议单独治理

FastAPI 支持在 WebSocket endpoint 中继续使用 DependsQueryCookieHeader 等依赖能力,但 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_typeraw_textstorage_keyspeakercaptured_atstatus
  • memory_chunks:检索单元,保存 contentcontent_tsvembeddingchunk_indexspeakerevent_yearmetadata_jsonis_excluded
  • memory_summaries:会话摘要 / 滚动摘要 / 主题摘要
  • memory_facts:候选事实与已确认事实,包含 fact_typesubjectpredicateobject_jsonconfidencesource_chunk_idstatus
  • timeline_events:时间线事件视图,包含 event_yearevent_dateperson_refssource_fact_ids
  • memory_curation_actions:记录 exclude / restore / correct / merge / confirm / reject 等操作轨迹

设计约束:

  • 原始素材、摘要、事实、时间线必须分层存储,不能把所有语义都压进单张向量表
  • 每条 summary / fact / timeline event 必须能追溯到 sourcechunk
  • 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 filteruser_idbook_idconversation_idspeakerevent_yearstatusis_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 的模板管理),定义一个独立的窄 portImageTemplateManager),只在需要的 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

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.inisqlalchemy.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 作为 baselinealembic 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.pyidentity/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.pyAlembic 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
  • 拦截标准库 loggingInterceptHandler),使 uvicorn / sqlalchemy / celery 日志统一走 loguru

9.2 Request Logging Middleware

新增 middleware每个请求注入 request_idUUID通过 loguru 的 contextualize 绑定到所有日志:

with logger.contextualize(request_id=request_id, user_id=user_id):
    response = await call_next(request)

9.3 Provider 调用日志

在每个 adapter 中统一记录provider 名称、操作、耗时、成功/失败、错误分类。


十、全局异常处理

core/errors.py 中定义异常体系:

class AppError(Exception):
    status_code: int
    error_code: str
    message: str

class NotFoundError(AppError): ...
class AuthenticationError(AppError): ...
class ProviderError(AppError): ...

main.py 注册全局 exception handler统一返回格式

{"error_code": "PROVIDER_ERROR", "message": "...", "request_id": "..."}

十一、统一配置

将散落的 os.getenv() 收归 core/config.py,使用 pydantic-settings BaseSettings

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.asyncioJWT 统一使用 PyJWTapi/pyproject.tomlapi/uv.lock 是依赖真相源,requirements.txt 如保留仅作为兼容导出产物,不再作为主入口。


十二、迁移执行策略

Phase 0 - 基础设施搭建(无功能变更)

  1. 迁移到 uv:以 api/pyproject.tomlapi/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.tomlapi/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 factoryget_embedding_provider()
  5. payment adapter 从 payment/ 直接迁移(已有好的抽象)

验证点:所有 adapter 通过 port protocol 类型检查,现有功能不变。

Phase 2 - Feature 模块重构(逐个 feature

按依赖关系从外到内逐个迁移,建议顺序:

  1. contentfaq, 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.pyinclude_router

memory feature 额外要求

  • 建立 memory_sources / memory_chunks / memory_summaries / memory_facts / timeline_events / memory_curation_actions
  • memory_chunks 同时具备 embeddingcontent_tsv
  • 先完成 MemoryService 内部接口,再由 memoir / conversation 接入
  • 后台编辑 API 保留为后续扩展,不阻塞一期基础设施落地

conversation WebSocket 拆分详细步骤

当前 routers/websocket.py1067 行)需拆为 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.pyWebSocket 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所有业务逻辑通过 serviceresponse_modelstatus_coderesponsessummarydescriptiontags 等契约元数据必须定义在 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/querycommit/rollback 由 service 或 UoW 统一执行。get_async_db() 不自动 commitflush() 仅在明确需要提前拿主键或触发约束检查时受控使用
  8. port 边界不可打穿service 需要厂商增强能力时,必须扩充 port 或定义第二个窄 port禁止直接引用 adapter 扩展方法
  9. quota 是独立 featureconversation、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.lockrequirements.txt 如存在仅作兼容导出,不作为主依赖入口
  13. 运行时基础库保持收敛Redis 客户端统一使用 redis / redis.asyncioJWT 统一使用 PyJWT
  14. service 装配走 FastAPI dependency graphrouter 只依赖 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 必须能追溯到 sourcechunk
  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:不得只凭当前会话上下文裸生成