42 KiB
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。 |
|
false |
Life Echo API 架构重构计划
一、现状诊断
当前项目位于 api/ 下,采用混合分层结构:
api/
main.py
routers/ # 16 个路由文件,部分含大量业务逻辑
services/ # 混合了业务服务和外部 SDK 调用
database/ # 单文件 models.py(9 个模型)+ database.py
agents/ # AI agent 层(conversation, memory, memoir)
middleware/ # 仅 auth.py
payment/ # 唯一较完整的 feature 模块
tasks/ # Celery 后台任务
migrations/ # 手写 SQL,无版本管理
核心问题:
- 路由层过胖,直接操作 DB 和调用外部服务
- 外部服务耦合:SMS(腾讯云)、存储(腾讯 COS)、TTS(OpenAI)直接写死 SDK
memory语义不清:当前更像“聊天助手记忆”占位,缺少面向回忆录生产的素材沉淀、事实抽取、检索增强与来源追溯- 无统一配置类,
os.getenv()散落各处 - 无 Alembic,手写 SQL 迁移无版本追踪
- 标准库 logging,无结构化日志
- 无全局异常处理
- WebSocket 路由 ~1000 行,职责过重
已有较好抽象的部分(可复用/迁移):
payment/:已有 service facade + 双 providerservices/llm_service.py:通过 LangChain 已做 provider 切换- ASR:已有 provider 切换机制
二、目标目录结构
api/
app/
main.py # 仅做组装:include_router, middleware, lifespan, OpenAPI wiring
core/
config.py # 统一 BaseSettings(pydantic-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 # 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
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 造成额外负担。
修订 3:auth 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)。
修订 4:WebSocket 从单个 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,被 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 管理
3.1 现状问题
当前存在两种矛盾的事务策略:
get_async_db()在请求结束时自动 commit(database.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()获取独立 session(websocket.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注入 service(service 内部持有 session)。
Celery 任务:在 tasks/ 中定义 get_sync_db() context manager,同样遵循"service commit"规则。任务函数调用 service 的同步版本或直接在任务函数中管理事务。
WebSocket:ws/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 sessionservice.py不得 import 具体 adapter,只依赖 port protocolports/不得 importadapters/(方向反转)- feature 之间不得互相 import router;跨 feature 调用通过 service 注入
repo.py不得调用commit()/rollback();flush()仅允许在明确需要提前拿 ID / 触发约束检查时受控使用conversation/memoir不得直接读写 memory 表,只能调用MemoryServiceagents/不得直接读写 memory 表,只能消费MemoryService产出的 evidence bundle 或通过 port 获取能力
五、OpenAPI 契约与文档治理
5.1 Router 是 OpenAPI 主战场
每个 HTTP path operation 至少应在 router decorator 上明确声明以下信息中的适用项:
response_modelstatus_coderesponsessummarydescriptiontags
如需扩展再使用:
operation_id:只有要做 SDK 生成或需要稳定外部契约名时才手工维护openapi_extra:仅用于少量自定义扩展字段,避免把业务元数据塞进 service 或 schema 生成后处理include_in_schema=False:明确隐藏内部接口,而不是靠“没人知道 URL”
router 可以保持“薄”,但绝不能失去契约职责;service 负责业务,router 负责 HTTP 与 OpenAPI 边界。
5.2 Router 级共享元数据
每个 feature 的 APIRouter(...) 默认应收敛以下共享信息:
prefixtags- 共享
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/descriptionx-logoexternalDocs- 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、statusmemory_chunks:检索单元,保存content、content_tsv、embedding、chunk_index、speaker、event_year、metadata_json、is_excludedmemory_summaries:会话摘要 / 滚动摘要 / 主题摘要memory_facts:候选事实与已确认事实,包含fact_type、subject、predicate、object_json、confidence、source_chunk_id、statustimeline_events:时间线事件视图,包含event_year、event_date、person_refs、source_fact_idsmemory_curation_actions:记录exclude/restore/correct/merge/confirm/reject等操作轨迹
设计约束:
- 原始素材、摘要、事实、时间线必须分层存储,不能把所有语义都压进单张向量表
- 每条 summary / fact / timeline event 必须能追溯到
source或chunk memory_chunks.embedding使用pgvectormemory_chunks.content_tsv使用 PostgreSQL FTS(建议 generated / maintainedtsvector列 +GINindex)
6.4 写入路径(Ingest)
核心写入路径分两类:
- 路径 A:对话口述沉淀。对话完成后,transcript 落
memory_sources,再切块写入memory_chunks,随后异步生成 embedding / summary / facts / timeline - 路径 B:人工导入素材。照片注释、手稿、家谱、旧文档、人工补录文本走同一套 source -> chunk -> enrich 流程
MemoryService.ingest_transcript() 建议按阶段编排:
- 写入
source - 切块并批量写入
chunks - 提交主事务
- 触发异步任务补齐 embedding、summary、fact extraction、timeline build
这样可以避免把重型 AI 处理塞进实时链路,同时保持来源可追溯与失败可重试。
6.5 读取路径(Retrieve)与 Evidence Bundle
章节生成、对话追问、memoir 组织前,统一由 MemoryService 执行检索编排:
- 生成 retrieval query
- 先做 metadata filter(如
user_id、book_id、conversation_id、speaker、event_year、status、is_excluded) - 并行执行 FTS 检索、向量检索、摘要检索
- 做 score fusion,必要时走可选 reranker
- 按 token budget 组装 evidence bundle
- 将结构化上下文交给
conversation/memoir
传给 LLM 的上下文不建议直接拼成长文本,而是建议显式区分:
relevant_memoriesrelevant_summariesrelevant_facts- 可选的
timeline_hints
这样更利于控制来源、分数、时间线信息和引用追踪,减少模型遗漏、编造与上下文漂移。
6.6 依赖方向与端口设计
依赖边界如下:
conversation只调用MemoryServicememoir只调用MemoryServicememory/service.py可依赖EmbeddingProvider、可选Reranker、以及既有LLMProvidermemory/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, ...) -> strstream(messages: list[Message], ...) -> AsyncIterator[str]
ImageGen (ports/image_gen.py):
generate(prompt: str, size: ImageSize, ...) -> ImageResultcheck_status(task_id: str) -> TaskStatus
Storage (ports/storage.py):
upload(key: str, data: bytes, content_type: str) -> strget_url(key: str, expires: int) -> strdelete(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 类。 - 方案 C:adapter 内部自行消化。 厂商特有的配置项(如 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.ini中sqlalchemy.url读取DATABASE_URL
8.2 初始基线(不依赖 autogenerate)
当前 schema 来源是 Base.metadata.create_all() + 9 个手写 SQL 迁移文件,两者可能已经出现漂移。直接 autogenerate 大概率产出不准确的 diff。
正确步骤:
- 在 staging/dev 环境连接真实数据库,用
pg_dump --schema-only导出当前实际 schema 作为基准参照。 - 手写一个空的 Alembic revision 作为 baseline:
alembic revision -m "baseline_empty",upgrade/downgrade 均为 pass。 - 对现有数据库执行
alembic stamp head,将当前库标记为已在 head。 - 此后才开始用 autogenerate 生成增量迁移,每次 review 时对照
pg_dump确认无遗漏。 - 将
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.pyRefreshToken,SmsVerificationCode->features/auth/models.py(identity/session 资产,auth 是唯一强依赖方)Conversation,Segment->features/conversation/models.pyBook,Chapter,MemoirState->features/memoir/models.pyOrder->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 - 配置 loguru:JSON 格式输出、按级别分文件、rotation/retention
- 拦截标准库 logging(
InterceptHandler),使 uvicorn / sqlalchemy / celery 日志统一走 loguru
9.2 Request Logging Middleware
新增 middleware,每个请求注入 request_id(UUID),通过 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.asyncio;JWT 统一使用 PyJWT。api/pyproject.toml 与 api/uv.lock 是依赖真相源,requirements.txt 如保留仅作为兼容导出产物,不再作为主入口。
十二、迁移执行策略
Phase 0 - 基础设施搭建(无功能变更)
- 迁移到 uv:以
api/pyproject.toml和api/uv.lock作为依赖真相源;将requirements.txt的依赖迁入pyproject.toml的[project.dependencies]。完成迁移后,requirements.txt如继续保留,仅作为兼容导出文件,不再作为主入口 - 创建
app/core/骨架:config, db, logging, errors, security, dependencies, middleware, openapi - 重写
get_async_db()去掉自动 commit,定义get_sync_db()context manager 供 Celery 使用 - 初始化 Alembic:手写空 baseline revision +
stamp head(不用 autogenerate) - 集成 loguru,配置 InterceptHandler 拦截标准库 logging
- 添加 request_id middleware
- 定义全局异常体系(仅统一错误响应格式)
- 建立 OpenAPI 基线:统一 router metadata 规范、共享错误响应、
custom_openapi()全局增强、内部接口可见性策略 - 预留 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 迁移
- 定义 7 个核心 port protocol(sms, llm, image_gen, storage, tts, asr, embedding)
- 将现有 SDK 代码封装为 adapter,实现对应 protocol
- 预留可选
rerankerport(二期再接具体实现) - 在
core/dependencies.py中注册 DI factory(含get_embedding_provider()) - payment adapter 从
payment/直接迁移(已有好的抽象)
验证点:所有 adapter 通过 port protocol 类型检查,现有功能不变。
Phase 2 - Feature 模块重构(逐个 feature)
按依赖关系从外到内逐个迁移,建议顺序:
content(faq, legal, home)— 最简单,验证 feature 骨架plan— 轻量,少依赖quota— 提升为独立 feature,定义QuotaServiceuser— 拆出 User modelauth— 拆出 RefreshToken/SmsVerificationCode models,依赖 user models + sms portmemory— 新增独立 feature,打通 source/chunk/summary/fact/timeline 与 hybrid retrievalpayment— 迁移现有 payment/ 模块memoir— 合并 books/chapters/memoir_state,迁移 processor,并接入MemoryServiceconversation— 最复杂,包含 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.pypipeline.py:核心消息处理管道,编排 ASR -> 落库 -> Agent -> TTS -> 响应,依赖上述模块 + ports(asr, tts, llm)router.py:WebSocket endpoint,仅做连接生命周期管理(accept, 主循环, disconnect),将每条消息委托给 pipelineprotocol.md:补充消息协议、鉴权、错误码、状态流转、重连与幂等等非 HTTP 契约
验证点:每迁移一个 feature,运行测试确保该 feature API 行为不变。
Phase 3 - 清理与加固
- 删除旧
routers/,services/,database/models.py,middleware/,migrations/ - 删除
main.py中的init_db()/create_all() - 更新 Dockerfile:改用 uv 安装依赖(
COPY pyproject.toml uv.lock ./+uv sync --frozen --no-dev),或用uv export --no-dev > requirements.txt导出后继续用 pip - 更新 docker-compose、deploy.sh 中的路径和启动命令
- 更新 Celery worker 启动命令(
uv run celery ...) - 更新所有测试 import 路径
- 补充缺失的测试
十三、硬性规则(写入项目规范)
**common//utils//shared//helpers/禁止创建**:只有 truly cross-cutting 能力进core/,业务 DTO/校验/错误码归 feature- router 是 HTTP + OpenAPI 契约边界:所有数据访问通过 repo,所有业务逻辑通过 service;但
response_model、status_code、responses、summary、description、tags等契约元数据必须定义在 router /APIRouter上 - service 禁止 import adapter:只依赖 port protocol,通过 DI 注入。代码中出现
from adapters.xxx import ...即为违规 - feature 间禁止 import router:跨 feature 调用通过 service 注入
- 所有 schema 变更走 Alembic:禁止直接 DDL
- 新增 provider 必须实现 port protocol:不得在 feature 中直接调用 SDK
- 事务边界:repo 不提交,service 管事务:
repo.py只做add/delete/query,commit/rollback由 service 或 UoW 统一执行。get_async_db()不自动 commit;flush()仅在明确需要提前拿主键或触发约束检查时受控使用 - port 边界不可打穿:service 需要厂商增强能力时,必须扩充 port 或定义第二个窄 port,禁止直接引用 adapter 扩展方法
- quota 是独立 feature:conversation、memoir、payment 如需配额检查,通过注入
QuotaService,不得直接 import quota 内部函数 - 成功响应不强制包装:
core/errors.py只统一错误响应格式{error_code, message, request_id},成功响应直接返回 Pydantic model / FileResponse / 原始结构 - 配置统一通过
pydantic-settings管理:只允许在core/config.py中定义BaseSettings/SettingsConfigDict;除配置模块外,禁止业务代码直接load_dotenv()、dotenv_values()或散落os.getenv() - 依赖真相源是
api/pyproject.toml+api/uv.lock:requirements.txt如存在仅作兼容导出,不作为主依赖入口 - 运行时基础库保持收敛:Redis 客户端统一使用
redis/redis.asyncio,JWT 统一使用PyJWT - service 装配走 FastAPI dependency graph:router 只依赖
get_xxx_service(),不得在路由函数里手工 new service 或自行拼装db + adapter + settings - 每个 feature router 必须维护共享 tags / responses:使用
APIRouter(prefix=..., tags=..., responses=...)收敛文档边界与共享错误响应 - 内部接口显式隐藏:不对外暴露的 path operation 使用
include_in_schema=False,禁止依赖“没人知道 URL” - OpenAPI 全局自定义只做增强:
custom_openapi()只做全局 schema 增强,不替代 router 上的正常声明 - WebSocket 协议必须独立成文:实时消息 schema、错误、鉴权、状态流转、重连与幂等规则不得只存在于代码实现中
- memory 的定义是素材检索与事实组织,不是聊天 persona 记忆:不实现通用“记住这个”或面向终端用户的自然语言
forget - memory 必须采用混合检索:至少包含 metadata filter + PostgreSQL FTS + vector retrieval;纯向量库不得作为唯一真相源
- 原始素材、摘要、事实、时间线必须分层存储:不得把所有语义压进单一向量表
- memory 写入必须可追溯:summary / fact / timeline event 必须能追溯到
source或chunk - conversation / memoir 只通过
MemoryService调用 memory:不得直接查询 memory 表 - memory feature 不得直接耦合具体 embedding SDK:只依赖
EmbeddingProvider(可选Reranker同理) - 所有 pgvector / FTS schema 变更必须走 Alembic:索引、扩展、generated column、operator class 变更都要纳入 migration
- 章节生成必须优先使用 evidence bundle:不得只凭当前会话上下文裸生成