家庭回忆录AI助手架构复盘:从单文件原型到分层架构的技术选型变更
家庭回忆录AI助手架构复盘:从单文件原型到分层架构的技术选型变更
一、从纸上原型到线上系统:一个家庭场景AI产品的真实演进
家庭回忆录AI助手的出发点是一个朴素的需求:帮助年长家庭成员整理散落在手机相册、微信聊天记录和手写便签中的零散记忆,生成结构化的家庭故事。
原型阶段采用单文件Python脚本实现:FastAPI直接调用OpenAI接口,SQLite存储对话历史,ChromaDB做向量检索。这个"能用就行"的阶段持续了约两周,期间产出了12篇实测回忆录。但随着用户上传的照片超过800张、对话历史突破3000条,问题开始集中爆发:单次请求响应时间从3秒恶化到28秒,内存占用突破2GB,导出PDF功能在并发场景产生文件覆盖错误。
以下是一次典型故障的调用链:用户上传50张家庭聚会照片请求生成回忆录 → ChromaDB加载全量向量到内存触发OOM → FastAPI进程被系统Kill → 前端收到504超时无任何上下文。问题根源并非硬件不足,而是架构层面的耦合:检索、生成、对话管理三个职责被塞进了同一个无状态接口。
生产环境的教训表明,生活类AI产品的架构挑战与TOB系统有本质差异:用户不是管理员,没有"重试"的概念,一次故障就意味着信任破产。这驱动了从原型到生产架构的全面重构。
二、分层架构的设计推导:从职责混沌到清晰边界
重构的核心思路是将单体拆分为三层:接入层负责请求路由与鉴权,服务层承载业务编排,数据层管理向量与结构化存储。分层依据不是教科书的"高内聚低耦合",而是每个家庭场景的故障隔离需求——当生成回忆录的服务挂了,照片浏览功能不应受影响。
分层后每个服务独立部署在Docker容器中,通过Redis Stream进行异步通信。以回忆录生成为例,核心流程从同步变为异步:用户提交请求 → 返回taskId → 后台依次执行"照片筛选→文本检索→章节生成→PDF排版" → WebSocket推送进度。这种设计将最长阻塞时间从28秒降到800ms的taskId返回时间。
三、核心代码实践:任务编排器的生产级实现
以下代码展示了回忆录生成服务的任务编排器。设计意图是:每个生成阶段独立封装,支持失败重试与部分回滚,避免全量重新生成。
import asyncio from enum import Enum from dataclasses import dataclass, field from typing import Callable, Optional import logging logger = logging.getLogger(__name__) class TaskStatus(Enum): """任务状态枚举,区分创建、各阶段、完成与失败""" PENDING = "pending" PHOTO_FILTERING = "photo_filtering" TEXT_RETRIEVING = "text_retrieving" CHAPTER_GENERATING = "chapter_generating" PDF_COMPOSING = "pdf_composing" COMPLETED = "completed" FAILED = "failed" @dataclass class StageContext: """每个阶段的任务上下文,携带该阶段的输入输出与重试信息""" task_id: str stage_name: str input_data: dict output_data: Optional[dict] = None retry_count: int = 0 max_retries: int = 3 errors: list[str] = field(default_factory=list) class RecallWorkflowOrchestrator: """回忆录生成工作流编排器 设计意图: 1. 每个阶段通过 stage_map 动态注册,新增阶段无需修改编排逻辑 2. 失败时仅回滚当前阶段,已完成阶段结果保留 3. 通过 semaphore 控制并发,避免后端模型 API 过载 """ def __init__(self, max_concurrency: int = 5): self.stage_map: dict[str, Callable] = {} # 信号量控制并发,防止大模型API因并发过高触发限流 self._semaphore = asyncio.Semaphore(max_concurrency) def register_stage(self, stage_name: str, handler: Callable): """注册处理函数,对应TaskStatus中的阶段名""" self.stage_map[stage_name] = handler logger.info("注册阶段处理器: %s", stage_name) async def execute(self, task_id: str, stages: list[str], initial_input: dict) -> dict: """按顺序执行指定阶段,任一阶段失败则终止并返回部分结果""" context = StageContext( task_id=task_id, stage_name=stages[0] if stages else "", input_data=initial_input, ) for stage in stages: context.stage_name = stage context.output_data = None try: async with self._semaphore: result = await self._execute_with_retry( stage, context ) context.output_data = result # 当前阶段的输出作为下一阶段的输入 context.input_data = result except Exception as exc: logger.error( "任务 %s 在阶段 %s 失败,已重试 %d 次", task_id, stage, context.retry_count ) # 保留已完成阶段的输出,供前端展示部分结果 return { "task_id": task_id, "completed_stages": [ s for s in stages if stages.index(s) < stages.index(stage) ], "failed_stage": stage, "last_output": context.output_data, "error": str(exc), } return {"task_id": task_id, "output": context.output_data} async def _execute_with_retry( self, stage: str, context: StageContext ) -> dict: """带重试的阶段执行,指数退避策略""" handler = self.stage_map.get(stage) if handler is None: raise ValueError(f"未注册的阶段处理器: {stage}") last_error: Optional[Exception] = None for attempt in range(context.max_retries + 1): try: return await handler(context) except Exception as exc: last_error = exc context.retry_count = attempt + 1 context.errors.append(str(exc)) if attempt < context.max_retries: # 指数退避:2^attempt 秒后重试 wait_seconds = 2 ** attempt logger.warning( "阶段 %s 第 %d 次尝试失败,%d 秒后重试", stage, attempt + 1, wait_seconds ) await asyncio.sleep(wait_seconds) raise last_error # type: ignore[misc]与之配合的还有每个阶段的具体处理函数(略),它们在编排器中被注册调用,确保关注点分离。
四、分层架构的边界与妥协:哪些场景不适合
分层架构带来了清晰的职责划分,但也付出了代价。第一,跨层调用增加了序列化开销:服务间通过Redis Stream通信时,大型照片元数据需要序列化和反序列化,500+照片时额外增加约200ms延迟。对此的缓解策略是在批量任务中合并批次,减少消息数。
第二,运维复杂度显著上升。从单文件到6个Docker容器(API Gateway、回忆录服务、照片服务、对话服务、PostgreSQL、MinIO),部署脚本从3行bash变为docker-compose.yml+健康检查+日志收集。如果团队不具备容器化运维能力,分层架构的收益会被运维成本抵消。
第三,并非所有家庭场景都需要分层。以下场景建议保持单体:用户量小于100的单人工具、数据量不足1000条的场景、要求极低延迟(<500ms)且无并发需求的实时交互。判断标准不是"架构是否高级",而是"当前瓶颈是否源于架构耦合"。
另外,向量存储选型也值得单独复盘。原型阶段的ChromaDB切换为pgvector的决策有其权衡:pgvector维护成本高(需要数据库运维),但避免了Chroma在800+照片场景的内存黑洞问题。如果场景规模不超过Chroma的单机承载上限(约10万向量),Chroma的一键部署特性更具吸引力。
五、总结
本次项目复盘提炼以下关键结论:
架构分层应以故障隔离为出发点:生活类AI工具的分层依据是用户场景的独立可用性需求,而非教科书式的高内聚低耦合。
异步任务编排是提升可感知性能的核心手段:从28秒同步阻塞到800ms异步响应的转变,本质是将用户等待时间从生成耗时转换为提交耗时。
编排器的设计关键:阶段动态注册、失败部分回滚、指数退避重试、并发信号量控制,四个特性构成生产级编排的最小可行集。
技术选型决策链:原型阶段选择最简方案验证想法 → 瓶颈出现后分析根因 → 根据根因选择对应方案(而非盲目升级)。ChromaDB→pgvector的切换是因为内存瓶颈,而非因为pgvector更"高级"。
分层不是终点:对于小规模场景保持单体,在运维能力和业务需求的交叉点做出理性判断。