# 「美搭」技术架构文档 > 版本:**v1.4** | 更新日期:2026-08-14 | 状态:待技术评审 > > **本文的定位**:把产品需求翻译成可直接拆解为开发任务的技术方案。读完这份文档,应该能回答三个问题——系统由哪些部分组成、数据长什么样、每条关键链路怎么跑通。 > > **前置阅读**:《技术选型与调研报告》(所有选型结论与理由,本文只引用编号 `TD-xx`,不重复论证) > **配套阅读**:《需求规格说明》(需求编号 `FR-xx`/`NFR-xx`)、《合规与安全方案》、《开发任务拆解》 --- ## 1. 架构目标与约束 ### 1.1 这个系统真正难的地方 在动手设计之前先明确难点,否则很容易把它当成一个「CRUD + 调几个 AI 接口」的普通应用,那会在三个月后付出代价: 1. **它是一个「长耗时任务为主」的系统,不是请求-响应系统**。报告生成、换装出图都在数十秒量级。这意味着任务中心、状态机、幂等、进度反馈、失败退款、断线恢复不是附加功能,而是**主干链路**。 2. **AI 输出必须被约束,而不是被信任**。报告结论与搭配建议要可解释、可审计、不能违反禁忌规则。因此架构上必须有「检索 → 规则约束 → 生成 → 生成后校验」的闭环,而不是把用户问题直接丢给大模型。 3. **它处理的是敏感个人信息(人脸)**,且产出的全部是 AI 生成内容。合规不是上线前补的文档,而是**架构约束**:原图不落盘、生成物统一打标识、输入输出双向审核。 4. **成本随活跃度线性增长**。GPU 出图与大模型调用都是按量付费。如果没有从第一天就把成本埋进链路并可归因到功能和用户,用户量上来后无法优化,只能凭感觉砍功能。 5. **知识库是持续演进的资产**,它的更新频率远高于代码。所以规则、色板、单品必须是**数据**而不是代码,且有配套的运营入口与回归测试。 ### 1.2 质量属性优先级 按项目当前阶段(V1 单端、效果优先、团队 4~6 人)排序: | 优先级 | 质量属性 | 具体要求 | | --- | --- | --- | | 1 | **正确性与可解释性** | 建议不违反硬性禁忌;每条结论可追溯到规则或知识条目 | | 2 | **合规性** | 人脸处理、AI 标识、内容审核、备案登记均满足现行要求 | | 3 | **可演进性** | 域边界清晰,V2 拆服务/换模型/多端时不需要重写 | | 4 | **可观测性** | 业务漏斗、AI 成本、任务健康三张看板齐备 | | 5 | 性能 | 见 `NFR` 指标;报告与出图允许秒级到分钟级,但**等待体验必须做好** | | 6 | 极致成本优化 | V1 只要求「可归因、可预警」,不要求「最省」 | ### 1.3 架构约束(不可协商) | 约束 | 来源 | | --- | --- | | 客户端为鸿蒙原生 ArkTS,服务端保持端无关 | `TD-01` | | 服务端全 Python(FastAPI) | 项目决策 | | 基础设施以腾讯云为主,服务器部署在中国大陆 | 项目决策 + `TD-15` 手机号获取要求 | | 人脸原图不进入持久化存储 | `TD-20` | | 所有对外生成物必须经过统一合规出口 | `TD-21` | | **V1 不接入任何支付能力**,配额为唯一成本控制手段 | `TD-28` | | V2 起虚拟商品支付必须走华为 IAP | `TD-16` | | V1 不上微服务、不上 K8s、不上专用向量库与图数据库 | `TD-08`/`TD-09`,团队规模 | --- ## 2. 系统上下文 ``` ┌──────────────────────┐ │ 鸿蒙客户端 (ArkTS) │ └──────────┬───────────┘ │ HTTPS / SSE ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ 美搭服务端 (Python / 腾讯云) │ │ │ │ API 层 ──▶ 业务域模块 ──▶ AI 能力层 ──▶ 数据层 │ │ │ │ │ └──▶ 任务队列 ──▶ Worker │ └───┬──────────┬──────────┬──────────┬──────────┬──────────┬───────────────┘ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ ┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐┌──────────────┐ │华为服务 ││大模型 ││腾讯云AI ││自有换装 ││腾讯云 ││腾讯云 ││腾讯云基础设施 │ │账号/推送││DeepSeek││人脸识别 ││发型服务 ││天御审核 ││短信SMS ││COS/CDN/PG/ │ │Push ││智谱GLM ││ ││(GPU) ││ ││ ││Redis/CLS │ └────────┘└────────┘└────────┘└────────┘└────────┘└────────┘└──────────────┘ 注:V1 不接入天气 API(TD-33),Agent 的天气类问题走「反问用户 + 城市月份推断气候区」降级 ``` **外部依赖及其失效影响**(每一个都要有降级方案,见 §13.4): | 外部系统 | 用途 | 失效影响 | 降级策略 | | --- | --- | --- | --- | | 华为 Account Kit | 登录(通道一) | 该通道不可用 | **手机号验证码登录仍可用**(两条通道不得互相依赖);游客模式可继续体验 | | **腾讯云短信 SMS** | **登录(通道二,`TD-32`)** | 该通道不可用 | **华为账号登录仍可用**;登录页给出明确提示而非笼统报错 | | 华为 Push Kit | 任务完成通知 | 通知不到 | 应用内任务中心轮询兜底 | | DeepSeek / GLM | 文案生成、对话 | 报告与对话不可用 | 双供应商自动切换;均失败时报告排队重试,对话返回兜底话术 | | 腾讯云人脸识别 | 面部分析 | 报告无法生成面部维度 | 报告降级为「体型+色彩」子集,标注待补全 | | 自有换装服务 | 出图 | 换装不可用 | 任务转排队,不扣配额,超时退还 | | 腾讯云天御 | 内容审核 | **不可降级** | 审核失败一律按「拦截」处理(fail-close),宁可少发不可漏审 | | 华为 IAP | 支付(**V2 起**) | 无法购买 | 提示稍后再试,不影响已有权益。**V1 不依赖此项** | > 🔑 **登录必须有两条互不依赖的通道**,这是 V1 唯一的单点故障防护。华为账号是唯一通道时,华为侧授权异常会让用户直接进不来产品;而一键登录已推至 V2(`TD-29`),所以手机号验证码是 V1 的必需项,不是可选项。 --- ## 3. 总体架构 ### 3.1 分层视图 ``` ┌───────────────────────────────────────────────────────────────────────┐ │ L1 客户端层 │ │ 鸿蒙 ArkTS:页面 / 状态管理 / 端侧缓存 / 图片压缩 / SSE流式渲染 │ └───────────────────────────────────────────────────────────────────────┘ ▼ ┌───────────────────────────────────────────────────────────────────────┐ │ L2 接入层 │ │ CLB + Nginx:TLS / 限流 / 防刷 / 请求日志 / 静态与图片走 CDN │ └───────────────────────────────────────────────────────────────────────┘ ▼ ┌───────────────────────────────────────────────────────────────────────┐ │ L3 API 层(FastAPI,面向端的编排层) │ │ 认证鉴权 · 参数校验 · 幂等 · 配额校验与预扣 · 统一响应 · OpenAPI │ └───────────────────────────────────────────────────────────────────────┘ ▼ ┌───────────────────────────────────────────────────────────────────────┐ │ L4 业务域层(模块化单体:包内隔离,禁止跨域直接访问对方数据表) │ │ ┌────────┬────────┬────────┬────────┬────────┬────────┬────────┐ │ │ │identity│profile │ report │ agent │ tryon │ kb │billing │ │ │ │账号会话│画像记忆│美学报告│对话Agent│换装发型│ 知识库 │配额计量│ │ │ ├────────┼────────┼────────┼────────┼────────┴────────┴────────┤ │ │ │ task │wardrobe│compliance│ notify │ │ │ │ │任务中心│ 衣橱V2 │合规出口 │ 通知 │ │ │ │ └────────┴────────┴─────────┴────────┴────────────────────────┘ │ └───────────────────────────────────────────────────────────────────────┘ ▼ ┌───────────────────────────────────────────────────────────────────────┐ │ L5 AI 能力层(可独立部署与扩容,对上以内部 API 暴露) │ │ ┌──────────────┬──────────────┬──────────────┬──────────────┐ │ │ │ llm-gateway │ vision-svc │retrieval-svc │ rules-engine │ │ │ │多模型路由 │分割/关键点/ │混合检索+重排 │决策表求值+ │ │ │ │Prompt版本 │色值采样/体型 │ │可解释路径 │ │ │ ├──────────────┼──────────────┼──────────────┴──────────────┤ │ │ │ matting-svc │ tryon-svc │ │ │ │ │批量抠图 │换装/发型(GPU)│ │ │ │ └──────────────┴──────────────┴─────────────────────────────┘ │ └───────────────────────────────────────────────────────────────────────┘ ▼ ┌───────────────────────────────────────────────────────────────────────┐ │ L6 数据层 │ │ PostgreSQL + pgvector(业务/知识库/向量/事件)· Redis(缓存/队列/限流) │ │ COS 四桶(临时/用户内容/知识素材/报告产物)· CLS(日志与审计) │ └───────────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────┐ 横向贯穿: │ Celery Workers(gpu/cpu/io/beat)│ │ 可观测性(OTel/Prom/Grafana) │ │ 配置与密钥(SSM/KMS) │ └──────────────────────────────┘ ``` ### 3.2 为什么是「模块化单体 + 独立 AI 服务」 这是本架构最关键的一个取舍,理由: **业务域做单体**:identity/profile/report/agent/billing 之间事务关联紧密(一次报告生成要扣配额、写画像、写记忆、记成本)。拆成微服务后这些操作就需要分布式事务或最终一致性补偿,对 4~6 人团队是净损失。单进程内一个数据库事务就解决了。 **AI 能力做独立服务**:它们的资源画像与业务代码完全不同——GPU 依赖、模型文件体积、推理并发模型、扩缩容节奏都不一样。混在一个进程里会导致「换装排队把 API 拖慢」。独立部署后可以单独限流、单独扩容、单独重启。 **但单体不等于混乱**。硬性规则: 1. 每个域是一个 Python 包,**只能通过对方暴露的 service 接口调用,禁止直接查对方的表**。 2. 领域逻辑放在 `packages/domain`,不依赖 FastAPI、不依赖数据库会话,可纯单元测试。 3. 跨域的写操作通过**域内事件**(进程内事件总线 + 持久化 outbox)解耦,例如「报告完成」事件被 profile(写画像)、memory(写记忆)、notify(推送)、billing(结算成本)分别消费。 4. 数据库虽是单库,但表按域前缀命名,**为将来拆库预留边界**。 **演进触发条件**:当某个域的写入 QPS 或数据量成为瓶颈,或需要独立发布节奏时,按 §19 的路径拆出。不要提前拆。 --- ## 4. 模块职责与依赖 ### 4.1 业务域模块清单 | 模块 | 职责 | 关键对外能力 | 不负责 | | --- | --- | --- | --- | | `identity` | 账号、登录、会话、设备、游客迁移、同意授权记录 | `login()` `get_current_user()` `merge_guest()` `record_consent()` | 权益判定(归 billing) | | `profile` | 用户画像(诊断结论)+ 分层记忆系统 | `get_profile()` `get_memory_snapshot()` `write_memory()` `resolve_conflict()` | 报告生成流程 | | `report` | 报告触发判定、生成编排、版本管理、分享 | `can_trigger()` `create_report()` `get_report()` `create_share()` | 具体的视觉分析与文案生成(调 AI 层) | | `agent` | 会话管理、意图识别、上下文组装、生成编排 | `chat_stream()` `extract_preference()` | 知识检索实现(调 retrieval) | | `tryon` | 换装/发型的业务编排、素材校验、结果管理 | `create_tryon_task()` `get_result()` | GPU 推理(调 tryon-svc) | | `kb` | 知识实体/关系/规则/素材的读写与发布 | `query_entities()` `get_rules()` `publish()` | 检索算法(调 retrieval) | | `billing` | **V1**:配额账本、流水、周期重置、任务奖励发放。**V2 增**:会员、订单、IAP 校验 | `check_quota()` `reserve()` `commit()` `refund()` `grant()` | 定价策略配置(配置化);V1 无支付相关职责 | | `task` | 统一任务中心:创建、状态机、进度、查询、取消 | `submit()` `get_status()` `list_user_tasks()` | 具体任务实现(Worker) | | `compliance` | **统一合规出口**:审核、水印、元数据标识、内容编号登记、审计 | `moderate()` `publish_asset()` `register_content()` | 业务判断 | | `notify` | 推送、应用内消息、Token 管理 | `push()` `notify_task_done()` | — | | `wardrobe` | 我的衣橱(V2) | — | — | > **域与包不是一一对应。** 上表按产品职责切(`profile` 含记忆);§16.1 的 `packages/` 按发布与依赖切。记忆有独立表、独立生命周期,且会被 agent / report 同时写入,因此代码上是独立的 `packages/memory`,产品职责仍归 profile 域。跨域只许 import `memory` 的 `service`,不许直接读 `pf_memory`。 ### 4.2 依赖规则 ``` identity ← 所有模块(读当前用户) billing ← report / tryon / agent(消耗资源前预扣) task ← report / tryon(长耗时任务统一走它) compliance ← report / tryon / agent / kb(所有产物出口) profile ← report(写入)/ agent(读取+写入) kb ← agent / report(读取) ``` **禁止的依赖**:`compliance` 不得反向依赖任何业务域(它必须能被任何模块安全调用);`identity` 不得依赖其他业务域(避免登录链路被业务拖累)。 ### 4.3 AI 能力层服务 | 服务 | 形态 | 内容 | 资源 | | --- | --- | --- | --- | | `llm-gateway` | 进程内包(V1)→ 独立服务(V2) | 多供应商路由、Prompt 版本、结构化输出约束、流式、缓存前缀优化、成本计量、限流降级 | IO 密集 | | `vision-svc` | 独立服务 | 人像分割(云)、人体关键点(ONNX 自建)、色值采样、体型测算、人脸属性与关键点(云) | CPU(含 ONNX) | | `retrieval-svc` | 进程内包 | 混合检索:硬过滤 → 规则命中 → 向量召回 → BM25 → 重排 → 解释路径 | CPU + PG | | `rules-engine` | 进程内包 | 决策表求值、硬约束校验、命中链输出 | CPU | | `matting-svc` | 离线批处理 | BiRefNet 抠图 + 白底标准化 + 规格校验 | GPU(按需) | | `tryon-svc` | 独立服务(现有服务改造) | 换装三工作流、发型更换/发色 | GPU(常驻或按需) | **`llm-gateway` 为何 V1 先做进程内包**:它没有独立的资源诉求(纯 IO),拆成服务只增加一跳网络。但它的**接口必须按远程调用来设计**(无共享内存假设、明确超时、可序列化的入参出参),这样 V2 拆出去时零成本。 ### 4.4 现有换装服务的改造要求 既然服务自有可控(`TD-00` 前提),必须做以下改造,否则它会成为整个系统的不稳定源: | 改造项 | 现状问题 | 要求 | | --- | --- | --- | | 鉴权 | 可能是开放接口 | 改为内部服务,仅允许内网/网关调用,带服务间令牌 | | 调用模式 | 同步长连接 | 改为**提交任务 + 回调通知**(或提供任务查询接口),避免长连接占用 | | 幂等 | 无 | 接受 `idempotency_key`,重复提交返回同一结果 | | 并发控制 | 无 | 明确最大并发数并对外暴露,让上游队列按容量限流 | | 成本埋点 | 无 | 每次推理返回 `gpu_seconds` 与模型版本,供成本归因 | | 结果存储 | 直传阿里云 OSS | 改为写入腾讯云 COS(统一存储与生命周期治理);历史数据做迁移或双读 | | 输入校验 | 未知 | 前置校验图片规格、人脸存在性、单品白底图规格,尽早失败 | | 版本管理 | 未知 | 模型/工作流版本号随结果返回,便于回溯效果变化 | --- ## 5. 技术栈总览 | 层次 | 选型 | 决策依据 | | --- | --- | --- | | 客户端 | HarmonyOS ArkTS + ArkUI | `TD-01` | | 端侧能力 | Account Kit / Push Kit(V1);IAP Kit / Core Speech Kit(V2) | `TD-15`~`TD-17`、`TD-14`、`TD-28` | | 接入 | 腾讯云 CLB + Nginx + CDN | `TD-03` | | 服务端 | Python 3.12 + FastAPI + Uvicorn/Gunicorn | `TD-02` | | ORM/迁移 | SQLAlchemy 2.0 (async) + Alembic | `TD-02` | | 校验 | Pydantic v2(同时用于 LLM 结构化输出约束) | `TD-02` | | 任务 | Celery + Redis(gpu/cpu/io/beat 四队列) | `TD-11` | | 数据库 | PostgreSQL 16 + pgvector | `TD-08` | | 缓存 | Redis 7 | — | | 存储 | 腾讯云 COS(4 桶)+ CDN | `TD-03` | | 大模型 | DeepSeek(主)+ 智谱 GLM(备/视觉) | `TD-04` | | Embedding | BGE-M3(自建 ONNX) | `TD-22` | | 视觉 | 腾讯云人脸识别 + RTMPose(ONNX) + BiRefNet | `TD-05`~`TD-07` | | 审核 | 腾讯云天御 IMS/TMS | `TD-13` | | 渲染 | Playwright + Chromium(服务端长图/PDF) | `TD-12` | | 可观测 | OpenTelemetry + Prometheus + Grafana + CLS | `TD-24` | | 部署 | Docker + Docker Compose(V1)→ TKE(V2) | `TD-03` | | CI/CD | GitHub Actions 或腾讯云 CODING;ruff + mypy + pytest 卡关 | `TD-02` | --- ## 6. 数据模型 ### 6.1 设计原则 1. **单库多域**,表名以域前缀区分(`id_`/`pf_`/`rp_`/`kb_`/`ag_`/`tr_`/`bl_`/`tk_`/`cp_`)。 2. **主键统一用雪花 ID(bigint)**,不用自增(避免枚举攻击与将来分库冲突),不用 UUID 作主键(索引局部性差)。对外暴露的 ID 做混淆编码。 3. **不存人脸原图,不存可反推生物特征的数据**。诊断结果只存分类标签与色值等派生量(`TD-20`)。 4. **所有涉及配额/金额的表都有幂等键与流水**,任何余额变更必须有对应流水记录,余额可由流水重算校验。V1 虽不涉及金钱,此原则同等适用 —— 用户对"次数被白扣"的敏感度不比扣钱低。 5. **软删除 + 硬删除双轨**:常规软删除;账号注销走**硬删除任务**(合规要求,见《合规与安全方案》)。 6. JSONB 用于「结构会演进但不需要索引全部字段」的场景(诊断结论、规则条件、检索轨迹);需要过滤的字段一律提升为列。 ### 6.2 核心表清单 #### identity 域 | 表 | 关键字段 | 说明 | | --- | --- | --- | | `id_user` | `user_id` PK, `status`('active'/'frozen'/'deleting'/'deleted'), `nickname`, `avatar_key`, `register_source`, `created_at`, `last_active_at`, `deleted_at` | 内部主键,与外部身份解耦。**手机号不存本表**,见下行 | | `id_user_identity` | `user_id`, `provider`(**'huawei'/'phone'**), `union_id`, `open_id`, **`phone_enc`(AES 加密), `phone_hash`(检索用)**, UNIQUE(`provider`,`union_id`) | 一个用户可有多个外部身份。**手机号作为一种 `provider` 存在本表,不在 `id_user` 冗余** —— 它是登录凭证而非用户属性,且便于统一加密与脱敏(`NFR-SEC-03`)。检索用 `phone_hash`,展示用解密后掩码 | | `id_guest` | `guest_id`, `device_fp`, `merged_to_user_id`, `created_at` | 游客数据,登录后合并 | | `id_device` | `device_id`, `user_id`, `push_token`, `os_version`, `last_seen_at` | 推送 Token 管理 | | `id_consent` | `consent_id`, `user_id`, `consent_type`, **`granted` bool**, `doc_version`, `granted_at`, `revoked_at`, `client_ip`, `ua` | **合规留证**:人脸的单独同意必须独立成行。枚举见下表 | | `id_session` | `session_id`, `user_id`, `refresh_token_hash`, `expire_at`, `revoked` | 刷新令牌服务端可撤销 | #### profile 域(画像与记忆) | 表 | 关键字段 | 说明 | | --- | --- | --- | | `pf_profile` | `profile_id`, `user_id`, `label`, `is_default` | **预留多档案**(帮家人诊断的场景),V1 只创建默认档案 | | `pf_diagnosis` | `diagnosis_id`, `profile_id`, `source_report_id`, `dimensions` JSONB, `version`, `overall_confidence`, `created_at` | **32 个**诊断维度的结构化结论(字典见美学报告 §4,为唯一事实源;编号 33/34 已随 `TD-31` 移除且不再复用);按版本追加,不覆盖 | | `pf_memory` | `memory_id`, `user_id`, `layer`(L0~L5), `mem_key`, `mem_value` JSONB, `confidence` numeric, `status`, `source`, `evidence_ref`, `valid_from`, `last_reinforced_at`, `decay_after`, `created_at` | 记忆项主表;索引 (`user_id`,`layer`,`status`)。**`status` 与 `source` 的枚举取值以《用户系统与记忆系统-需求文档》§3.3 为唯一事实源**,见下表 | | `pf_memory_event` | `event_id`, `memory_id`, `action`('create'/'reinforce'/'decay'/'conflict'/'user_edit'/'delete'), `detail` JSONB, `created_at` | 记忆变更审计,支撑「我的记忆」页面的可解释性 | | `pf_memory_snapshot` | `user_id`, `snapshot` JSONB, `token_count`, `content_hash`, `built_at`, `expire_at` | 注入 LLM 的画像摘要缓存,避免每轮重算 | **枚举取值(本轮对齐,此前架构与用户系统文档给了两套不兼容的枚举)** > ⚠️ 原架构表写 `status`=archived/rejected、`source`=explicit/implicit/report/behavior,与用户系统 §3.3 的枚举无法对应。**以用户系统文档为准**,因为它的取值承载了衰减与冲突消解的语义(`decayed`/`overridden` 必须可区分,否则 §3.6 的冲突消解无法实现)。 | 字段 | 取值 | 语义要点 | | --- | --- | --- | | `pf_memory.status` | `active` / `pending_confirm` / `decayed` / `overridden` / `deleted` | `decayed`(置信度衰减至 < 0.3,可恢复)与 `overridden`(被新证据覆盖)**必须区分**,前者可由用户恢复,后者不可 | | `pf_memory.source` | `explicit_user_input` / `user_setting` / `report_vision` / `self_reported` / `behavior_inferred` / `llm_extracted` | 衰减规则只作用于 `behavior_inferred` 与 `llm_extracted`(用户系统 §3.5),因此**不能合并为笼统的 `implicit`** | | `id_consent.consent_type` | `privacy`(隐私政策)/ `face`(**人脸,须独立成行**)/ `personalization`(个性化推荐)/ `push`(推送通知)/ `minor`(未成年人确认) | ⚠️ **`personalization` 不可省略**:用户系统 §3.10 要求记忆写入必须建立在该项授权之上,缺了它记忆系统就没有合规依据 | #### report 域 | 表 | 关键字段 | 说明 | | --- | --- | --- | | `rp_report` | `report_id`, `user_id`, `profile_id`, `tier`('basic'/'full'), `status`, `version`, `result` JSONB, `model_info` JSONB, `content_code`, `rendered_asset_key`, `cost_cents`, `created_at`, `completed_at` | `content_code` 为 AI 标识内容编号(`TD-21`) | | `rp_input` | `input_id`, `report_id`, `questionnaire` JSONB, `photo_meta` JSONB, `derived_features` JSONB | **只存派生特征与图片元信息,不存原图** | | `rp_share` | `share_id`, `report_id`, `share_token`, `expire_at`, `view_count`, `revoked` | 分享链接可失效、可撤回 | #### kb 域(知识库) | 表 | 关键字段 | 说明 | | --- | --- | --- | | `kb_entity` | `entity_id`, `entity_type`(12 类), `name`, `attrs` JSONB, `status`('draft'/'reviewing'/'published'/'archived'), `version`, `source`, `license`, `updated_by` | 12 类实体统一表 + `attrs` 差异化 | | `kb_relation` | `from_id`, `rel_type`, `to_id`, `weight`, `confidence`, `evidence` | 邻接表,递归 CTE 做多跳(`TD-09`) | | `kb_rule` | `rule_id`, `version`, `status`, `priority`, `conditions` JSONB, `effects` JSONB, `is_hard`, `evidence`, `author`, `reviewer`, `published_at` | 决策表(`TD-10`) | | `kb_chunk` | `chunk_id`, `entity_id`, `chunk_type`, `text`, `embedding` vector(1024), `embedding_model`, `embedding_version`, `tsv` tsvector | 向量 + 全文双索引,支撑混合检索 | | `kb_asset` | `asset_id`, `entity_id`, `asset_type`, `cos_key`, `spec_check` JSONB, `tryon_ready` bool, `review_status`, `license` | `tryon_ready` 是换装闭环的关键标志位 | | `kb_color` | `color_id`, `season_code`, `role`('base'/'accent'/'avoid'), `lab_l`,`lab_a`,`lab_b`, `hex`, `name_cn` | 自建色板(`TD-18`) | | `kb_gap` | `gap_id`, `query_text`, `filter_ctx` JSONB, `hit_count`, `first_seen_at` | 知识缺口埋点:无检索结果的查询,驱动内容生产 | #### agent 域 | 表 | 关键字段 | 说明 | | --- | --- | --- | | `ag_conversation` | `conv_id`, `user_id`, `title`, `rolling_summary`, `status`, `msg_count`, `created_at` | 长会话滚动摘要存这里(L4 记忆) | | `ag_message` | `msg_id`, `conv_id`, `role`, `content`, `intent`, `retrieval_trace` JSONB, `rule_hits` JSONB, `model`, `tokens_in/out/cached`, `cost_cents`, `moderation` JSONB, `created_at` | `retrieval_trace` + `rule_hits` 让每个回答可复盘 | | `ag_feedback` | `feedback_id`, `msg_id`, `rating`, `reason`, `created_at` | 驱动隐式偏好推断与质量评估 | #### tryon 域 | 表 | 关键字段 | 说明 | | --- | --- | --- | | `tr_task` | `task_id`, `user_id`, `workflow_type`, `input_refs` JSONB, `result_asset_key`, `gpu_seconds`, `model_version`, `content_code`, `cost_cents` | 与统一任务中心 `tk_task` 一对一关联 | | `tr_favorite` | `fav_id`, `user_id`, `asset_key`, `tags`, `created_at` | 用户收藏的效果图 | #### task 域(统一任务中心) | 表 | 关键字段 | 说明 | | --- | --- | --- | | `tk_task` | `task_id`, `user_id`, `task_type`('report'/'tryon'/'hairstyle'/'matting'/'render'), `status`, `progress`, `stage_text`, `idempotency_key` UNIQUE, `payload` JSONB, `error_code`, `retry_count`, **`quota_reserved`**, `submitted_at`, `started_at`, `finished_at`, `eta_seconds` | **全系统唯一的长耗时任务视图**,前端任务中心直接读它。⚠️ 字段名由 `points_reserved` 改为 **`quota_reserved`** —— V1 是配额不是点数,用 points 命名会在实现期误导 | #### billing 域(V1 为配额与计量,无支付) > **V1 不接支付**(`TD-28`)。本域在 V1 的职责是配额限制与计量。**关键设计:表结构一次做到位,V2 上支付时不需要迁移**。 | 表 | 关键字段 | V1 | 说明 | | --- | --- | :--: | --- | | `bl_quota` | `user_id`, `quota_key`, `period_key`, `used`, **`limit_value`**, **`bonus_granted`**, `updated_at`;UNIQUE(`user_id`,`quota_key`,`period_key`) | ✅ | **V1 扣减判定的唯一依据**。周期配额计量(日/月/终身),按 `period_key` 自然重置,**不要写定时清零任务**。`limit_value` 是本周期创建时从规则拷下的 `base_limit` 快照(规则中途被改,本周期仍按快照判定);`bonus_granted` 物化本周期已获任务奖励,避免每次判定都聚合流水 | | `bl_quota_rule` | `quota_key`, `period_type`, `base_limit`, `task_bonus_max`, `hard_cap` | ✅ | 配额规则配置化(`FR-BILL-04`),改配置不发版。数值与 `quota_key` 映射见下表,**一律引用产品方案 §3.2,禁止在种子里另写一套** | | `bl_ledger` | `tx_id`, `user_id`, `delta`, `tx_type`, `status`, `reason`, `ref_type`, `ref_id`, `balance_after`, `idempotency_key` UNIQUE, `created_at` | ✅ | **流水是真相**,但**不参与 V1 的判定**(判定看 `bl_quota`)。`tx_type` V1 用 `system_grant`/`consume`/`refund` **三种**,V2 增加 `purchase`/`gift` —— 这是 V1→V2 免迁移的关键 | | `bl_wallet` | `user_id` PK, `balance`, `version`(乐观锁) | ✅ | ⚠️ **V1 仅用于「跨周期不清零」的额度**(如首次终身报告);周期类额度一律走 `bl_quota`。V2 语义扩展为点数余额,**同一张表,不改结构** | **`bl_quota_rule.quota_key` 与产品方案 §3.2 的对应**(种子必须与此表一致): | `quota_key` | 对应 §3.2 行 | `period_type` | `base` / `task_bonus_max` / `hard_cap` | | --- | --- | --- | --- | | `agent_chat` | AI 对话 | day | 10 / 10 / 40 | | `tryon` | 换装试穿 | day | 3 / 3 / 8 | | `hairstyle` | 发型 / 发色试戴 | day | 3 / 2 / 6 | | `hd_export` | 高清导出 | day | 1 / 0 / 3 | | `report_basic` | 初级美学报告(首次) | lifetime | 1 / 0 / 1 | | `report_regen` | 报告重新生成 | month | 1 / 1 / 2 | 「每日 ≤ 2」在月额度上限为 2 时自然满足,不必再拆一条日配额。**记忆条数 200 是存储上限,不是消耗型配额,不进本表。** 游客额度按设备维度计量,依赖 `id_guest`;游客合并链路未建之前**不写入 `guest_*` key**,避免种子里出现没人读的第二套口径。 > 🔑 **四张表的职责边界见 §7.4 开头**。这是最容易实现成"两套并行扣减逻辑"的地方,务必先读那一段。 | `bl_membership` | `user_id`, `level`, `valid_from`, `valid_to`, `source_order_id`, `auto_renew` | ❌ V2 | 会员 | | `bl_order` | `order_id`, `user_id`, `product_id`, `product_type`, `price_cents`, `platform`('huawei_iap'), `purchase_token` UNIQUE, `status`, `verified_at`, `raw_receipt` JSONB | ❌ V2 | `purchase_token` 唯一约束防重复发货(`TD-16`) | > **原 `bl_point_tx` 改名为 `bl_ledger`**:V1 阶段它记录的是配额流水而非点数流水,用 `point` 命名会在 V1 造成误解。改用中性名称,同时新增 `tx_type` 字段承载两个阶段的语义差异。 #### compliance 域 | 表 | 关键字段 | 说明 | | --- | --- | --- | | `cp_content_registry` | `content_code` PK(27 位), `content_type`, `ref_type`, `ref_id`, `provider_code`, `model_info` JSONB, `generated_at` | AI 生成内容编号登记(`TD-21`) | | `cp_moderation_log` | `log_id`, `target_type`, `target_id`, `provider`, `suggestion`, `labels` JSONB, `action`, `created_at` | 审核留证 | | `cp_audit_log` | `audit_id`, `actor_type`, `actor_id`, `action`, `target`, `before` JSONB, `after` JSONB, `ip`, `created_at` | 后台操作审计,知识库改动必须留痕 | | `cp_data_request` | `request_id`, `user_id`, `type`('export'/'delete'), `status`, `completed_at` | 个人信息导出/删除请求(法定权利) | #### 观测与成本 | 表 | 关键字段 | 说明 | | --- | --- | --- | | `ob_ai_call` | `call_id`, `user_id`, `scene`, `provider`, `model`, `prompt_id`, `prompt_version`, `tokens_in/out/cached`, `tokens_reasoning`, `latency_ms`, `cost_cents`, `rate_tier`, `success`, `error_code`, `ref_type`, `ref_id`, `created_at` | **成本归因的唯一数据源**,按月分区。`prompt_id`/`prompt_version` 使每次调用可复现(§8.1 要求 Prompt 版本化,缺这两列就无法回答「上周的报告是哪版 Prompt 生成的」);`tokens_reasoning` 单独记思考模式 token(按输出价计费,混进 `tokens_out` 会掩盖开关思考模式的成本差异);`rate_tier` 记录该次调用适用的峰谷档位,否则事后无法复核 `cost_cents`。**保留口径**:明细至少保留到 V2 定价完成(`T-P6-16`);满 12 个月的分区可迁冷存储,**不可按常规日志周期删除** —— 删了就失去定价依据 | | `ob_event` | `event_id`, `user_id`, `event_name`, `props` JSONB, `client_ts`, `server_ts` | 埋点事件,按月分区(`TD-23`) | ### 6.3 诊断维度的存储形态 **32 个**诊断维度(字典见《美学报告-需求文档》§4,为唯一事实源)用 `pf_diagnosis.dimensions` JSONB 存储,但**每个维度都是结构化三元组**,不是自由文本: ```json { "color_season": { "value": "浅暖柔和型", "confidence": 0.82, "evidence": ["skin_lab:[68.2,12.4,18.7]", "hair_lab:[24.1,6.2,9.8]", "contrast:low"], "source": "vision+rule", "rule_hits": ["R-COLOR-0021", "R-COLOR-0034"] }, "body_shape": { "value": "X型", "confidence": 0.76, "evidence": ["shoulder_hip_ratio:1.02", "waist_hip_ratio:0.71"], "source": "vision+rule", "rule_hits": ["R-BODY-0007"] } } ``` **为什么必须带 `confidence` 和 `evidence`**:低置信度维度要在报告中降级表述(「你的体型偏向 X 型」而不是断言),并且要能在用户质疑时说明依据。这也是记忆系统冲突消解的输入——用户显式反馈会覆盖低置信度的机器推断。 --- ## 7. 关键链路设计 ### 7.1 美学报告生成(最复杂的链路) ``` 客户端 API task/Worker AI 能力层 │ │ │ │ │ 1.POST /reports │ │ │ │ (idempotency_key) │ │ │ ├───────────────────────▶│ │ │ │ │ 2.触发资格校验 │ │ │ │ (已有报告?配额?) │ │ │ │ 3.配额预扣(reserve) │ │ │ │ 4.创建 tk_task │ │ │◀── task_id ────────────┤ │ │ │ │ 5.入队(cpu队列) │ │ │ ├───────────────────▶│ │ │ 6.订阅进度(轮询/Push) │ │ 7.拉取临时区原图 │ │ │ ├───────────────────▶│ │ │ │ 8.内容审核(图片) │ │ │ │ 9.人像分割 │ │ │ │ 10.人体关键点(ONNX) │ │ │ │ 11.人脸关键点+属性 │ │ │ │ 12.色值采样(Lab) │ │ │ │ 13.体型/季型/脸型测算│ │ │ │◀── 结构化诊断 ──────┤ │ │ │ 14.**立即删除原图** │ │ │ │ 15.规则引擎求值 │ │ │ │ (建议/禁忌/依据) │ │ │ │ 16.知识库检索 │ │ │ │ (配色/单品/发型) │ │ │ │ 17.LLM 生成文案 │ │ │ │ (结构化输出约束) │ │ │ │ 18.生成后校验 │ │ │ │ (禁忌违反=0?色值在板内?)│ │ │ │ 19.文本审核 │ │ │ │ 20.渲染长图(Playwright)│ │ │ │ 21.合规出口: │ │ │ │ 水印+元数据+编号登记│ │ │ │ 22.写画像/记忆/成本 │ │ │ │ 23.配额 commit │ │ │ │ 24.Push 通知 │ │◀──── 报告就绪 ─────────┴────────────────────┤ │ ``` **关键工程要求**: | 要求 | 做法 | | --- | --- | | **原图生命周期** | 上传直传 COS 临时桶(预签名 URL,不经过应用服务器);Worker 拉取后处理,第 14 步显式删除;桶级 2 小时生命周期作为兜底 | | **失败即退款** | 任何步骤失败,`tk_task` 转 failed 并触发 `refund()`;用户看到明确原因与「重试」入口 | | **分步可恢复** | 第 7~13 步的视觉分析结果落库为中间态,LLM 或渲染失败时**不需要重跑视觉分析**(也就不需要用户重新上传) | | **生成后校验不可跳过** | 校验失败最多重试 2 次(提高约束强度),仍失败则降级为「规则直出模板文案」,绝不放行违规内容 | | **等待体验** | 阶段文案随进度更新;预估时间基于历史 p50;超过阈值转为「后台生成 + 完成推送」,允许用户离开页面 | | **幂等** | 相同 `idempotency_key` 重复提交返回同一 `task_id`,**不重复扣减配额** | ### 7.2 Agent 对话(含检索与画像注入) ``` 用户提问 │ ▼ ① 输入审核(TMS + 备案关键词库)───拦截──▶ 返回安全提示,记录日志 │ 通过 ▼ ② 意图识别(轻量模型 / 规则前置) ├─ 场景咨询 / 单品搭配 / 风格建议 / 闲聊 / 越界(医疗、身材评判等) ▼ ③ 上下文组装(token 预算严格分配) ├─ 系统提示词(固定前缀,放最前以命中缓存) ├─ 画像摘要(L0/L1/L2,来自 pf_memory_snapshot) ├─ 会话滚动摘要(L4) └─ 检索结果(见 ④) ▼ ④ 混合检索(retrieval-svc) ├─ a. 硬过滤:SQL WHERE(季型/体型禁忌/场合/季节) ├─ b. 规则命中:rules-engine 求值,取建议与禁忌 ├─ c. 向量召回:pgvector 相似度 Top-K ├─ d. 关键词召回:tsvector BM25 ├─ e. 融合重排:RRF + 规则权重 + 新鲜度 └─ f. 输出:知识片段 + 命中规则 ID + 图谱路径(供解释链) ▼ ⑤ LLM 生成(llm-gateway,流式 SSE) └─ 约束:只能使用检索到的知识;硬禁忌不得出现;输出结构化搭配卡片 ▼ ⑥ 生成后校验:禁忌违反检查 + 色值在色板内检查 ▼ ⑦ 输出审核(TMS)───拦截──▶ 重新生成或返回兜底 ▼ ⑧ 落库:message + retrieval_trace + rule_hits + 成本 ▼ ⑨ 异步:偏好抽取 → 写入 L2 记忆(显式)/ 生成待确认候选(隐式) ``` **Token 预算分配(必须硬性限制,否则成本会失控)** > 🔒 **本表是 token 预算的唯一定义位置**。`FR-MEM-04`、`FR-AGT-03`、产品方案 §2.4.2、用户系统 §3.8、穿搭 Agent §3.5.2/§10.2、`T-P3-17` **一律引用本表,不得复述数值**。 > > ⚠️ **此前存在两套互相矛盾的口径,本轮已查明原因并修正**。原架构表写「会话摘要 ≤ 400、总输入 ≤ 3700」,用户系统 §3.8 写「会话上下文 ≤ 800、记忆注入合计 ≤ 1400」。**错在架构表**:用户系统的 800 是「**最近 6 轮原文 + 更早轮次的滚动摘要**」,而架构表只算了滚动摘要、漏掉了近轮原文。因此**以用户系统 §3.8 的分项为准,并修正总输入合计**。 | 层级 | 部分 | 预算 | 说明 | | --- | --- | --- | --- | | 总输入 | 系统提示词 | ≤ 800 | 固定不变,放最前以最大化缓存命中 | | 总输入 | **记忆注入(小计)** | **≤ 1400** | 展开见下四行;这是 `FR-MEM-04` 的验收口径 | | └ 记忆注入 | 画像摘要 L0/L1 | ≤ 250 | 模板化压缩,非 LLM 生成 | | └ 记忆注入 | 偏好记忆 L2 | ≤ 150 | `confidence ≥ 0.5` 按置信度取 Top 15,**硬禁忌优先保留** | | └ 记忆注入 | 事件记忆 L3(向量召回) | ≤ 200 | **V2 能力,V1 预算预留但不使用** | | └ 记忆注入 | 会话上下文 L4 | ≤ 800 | = 最近 6 轮原文 ≤ 400 + 更早轮次滚动摘要 ≤ 400 | | 总输入 | 检索知识 | ≤ 1500 | 重排后取前 N 条,超出则截断而非全塞 | | 总输入 | 用户当前输入 | ≤ 500 | 超长提示精简 | | **总输入合计** | | **≤ 4200** | `FR-AGT-03` 的验收口径;原 3700 因漏算近轮原文而偏低 | | 输出 | | ≤ 800 | 超长回答不利于阅读,也贵 | **V1 实际用量**:L3 不启用,故记忆注入 ≤ 1200、总输入 ≤ 4000,留有余量。 **裁剪优先级(预算超限时按此顺序丢弃,不得报错)**:事件记忆 L3 → 滚动摘要尾部 → 检索知识低分项 → 偏好记忆低置信项 → 近轮原文最早的几轮。**硬禁忌与安全约束永不裁剪** —— 裁掉它们会导致推荐用户明确拒绝的品类,属 P0 缺陷(产品方案 §8.3.3 要求禁忌违反率为 0)。 **缓存前缀纪律**:系统提示词与稳定知识片段必须放在 prompt 头部且**不含时间戳、随机 ID、用户昵称等变量**,否则缓存永远不命中,输入成本翻数倍(`TD-04`)。 ### 7.3 换装任务 ``` ① 前置校验:单品是否 tryon_ready?用户照片是否合规且质量达标? ② 配额:check_quota → reserve ③ 创建 tk_task(type=tryon) → 入 gpu 队列 ④ Worker:调 tryon-svc(带 idempotency_key),拿到 gpu_seconds ⑤ 结果图审核(IMS) ⑥ 合规出口:水印「AI 生成」+ 写入元数据与内容编号 → 存 COS 用户内容桶 ⑦ commit 配额、记录成本、Push 通知 ⑧ 失败:区分可重试(网络/排队超时)与不可重试(输入非法/审核拒绝),后者立即退还配额 ``` **队列即容量**:`gpu` 队列的并发上限直接由 GPU 容量决定,并把「当前排队位次 + 预估等待」返回给用户。**不要让请求无限积压**——超过阈值直接拒绝新任务并提示稍后再试,这比让用户等 10 分钟体验更好。 ### 7.4 配额消耗(V1)与 IAP 支付(V2) #### V1:四张表的职责边界(**必须先看这段,否则会实现成两套并行的扣减逻辑**) `billing` 域有四张表,此前文档没写清它们的关系,导致 `bl_quota.used` 与 `bl_wallet.balance` 看起来像两套重复机制。实际分工是: | 表 | 在 V1 的角色 | 是否参与 V1 的扣减判定 | | --- | --- | --- | | `bl_quota_rule` | **规则**:每个 `quota_key` 的周期类型、基础额度、任务奖励上限、防刷硬上限 | 提供判定用的阈值,配置化,改配置不发版 | | `bl_quota` | **周期计数器**:`(user_id, quota_key, period_key)` 上的 `used` 与本周期已获得的 `bonus_granted` | ✅ **V1 的判定就在这张表上** | | `bl_ledger` | **流水(真相)**:每一次发放、消耗、退还都留痕,供对账与 V2 定价分析 | ❌ 不参与判定,但**每次判定通过后必须写** | | `bl_wallet` | **余额缓存**:V1 语义为「跨周期不清零的可累积额度」 | ⚠️ **V1 仅"首次终身"类额度用它**(如首次报告),周期类额度不用 | **一句话概括**:**V1 的判定看 `bl_quota`(周期计数器),V2 的判定才看 `bl_wallet`(点数余额)。`bl_ledger` 在两个版本里都只是流水。** 之所以 V1 就把 `bl_wallet`/`bl_ledger` 建起来,是为了 V2 上支付时不迁移数据(§6.2),不是因为 V1 需要它们做判定。 **判定公式中 `task_bonus` 的来源已明确**:不是从 `bl_ledger` 实时聚合(那样每次判定都要扫流水,且并发下不可靠),而是**在 `bl_quota` 上物化为 `bonus_granted` 字段**,发放任务奖励时同时递增它。判定公式因此是纯单行读: ``` 有效上限 = MIN( bl_quota_rule.base_limit + bl_quota.bonus_granted, -- 基础额度 + 本周期已获奖励 bl_quota_rule.hard_cap -- 防刷硬上限,任何情况不可超越 ) 放行条件 = bl_quota.used < 有效上限 ``` #### V1:配额消耗三段式 ``` ① 熔断检查:全局日成本水位是否已达 100%?(NFR-COST-02) ↓ 已熔断 → 直接拒绝,且【不得预扣】,返回"今日服务繁忙" ② 校验:SELECT ... FOR UPDATE 读 bl_quota 单行 → 按上方公式判定 ↓ (服务端硬校验,客户端提示仅为体验,FR-BILL-06) ③ 预扣 reserve → bl_quota.used += N(同一事务内) bl_ledger 写 delta=-N, tx_type=consume, status=reserved ↓ ④ 任务执行(Celery,长耗时) ↓ 成功:commit → 流水 status=committed(bl_quota.used 不变,已在③计入) 失败:refund → bl_quota.used -= N bl_ledger 写反向流水 delta=+N, tx_type=refund(带 idempotency_key) 发放:任务奖励 → bl_quota.bonus_granted += N(受 task_bonus_max 约束) bl_ledger 写 delta=+N, tx_type=system_grant ↓ (同一任务同一周期幂等,FR-BILL-03) ``` **三处最容易写错的地方**: 1. **熔断必须在预扣之前**(步骤①在③之前)。顺序颠倒会导致平台自身限流却扣掉用户额度 —— 这是 `NFR-COST-02` 里「配额净扣减 = 0」那条验收标准要防的情况。 2. **`bl_quota.used` 的增减必须与 `bl_ledger` 写入在同一个数据库事务里**,否则对账永远不平。 3. **周期重置靠 `period_key` 自然切换,不要写定时清零任务**。`period_key` 形如 `2026-08-13`(日)/ `2026-08`(月)/ `lifetime`(终身),新周期首次访问时插入新行即可。定时清零在跨时区、补偿执行、任务漏跑时都会出错。 **为什么预扣而不是直接扣**:长耗时任务可能失败,直接扣会产生「扣了额度没拿到结果」的客诉。先冻结再确认,失败自动释放。V1 虽然不涉及金钱,这个问题依然存在 —— **用户对"次数被白扣"的敏感度不比扣钱低**,而且 V1 没有客服退款兜底手段,只能靠机制保证。 **对账**:所有变更都写流水,余额可由流水重算校验(每日定时,不一致则告警)。V1 就要做,否则 V2 上支付时对账逻辑得从零开发且无历史数据验证。 #### V2:IAP 支付(V1 不实现) ``` 购买:客户端 IAP 下单 → 支付成功拿 purchase_token ↓ POST /orders/verify (purchase_token) ↓ 服务端调华为 IAP 校验接口(**必须服务端校验**,不可信客户端结果) ↓ UNIQUE(purchase_token) 保证只发货一次 ↓ 发放点数/会员 → 写 bl_ledger(tx_type=purchase,带 idempotency_key) ``` **V1→V2 的衔接成本被刻意压到最低**:消耗侧逻辑(reserve/commit/refund)完全复用,只需新增「购买」这一条发放路径。这是 §6.2 中坚持让 V1 与 V2 共用 `bl_wallet` / `bl_ledger` 的收益。 ### 7.5 统一合规出口 **所有对外生成物必须经过 `compliance.publish_asset()`,没有例外。** ```python async def publish_asset( raw: bytes | str, content_type: Literal["image", "text", "pdf"], ref: ContentRef, model_info: ModelInfo, ) -> PublishedAsset: # 1. 内容审核(fail-close:审核服务不可用则拒绝发布) # 2. 生成 27 位内容编号并登记 cp_content_registry # 3. 显式标识:图片加可见「AI 生成」水印;文本加提示语 # 4. 隐式标识:写入文件元数据(属性信息 + 服务提供者编码 + 内容编号) # 5. 上传目标 COS 桶,返回带签名的访问地址 ``` **必须专门测试的点**:图片经过 CDN、缩略图处理、客户端保存与再次分享后,**元数据是否仍然存在**。很多图片处理链路会剥离 EXIF/XMP,一旦剥离就等于没做隐式标识(`TD-21`)。 --- ## 8. AI 能力层设计 ### 8.1 LLM 网关 ```python class LLMGateway: async def complete( self, scene: Scene, # 场景决定模型档位、超时、预算 messages: list[Message], schema: type[BaseModel] | None, # 需要结构化输出时传 Pydantic 模型 stream: bool = False, user_id: int | None = None, ) -> Completion | AsyncIterator[Chunk]: ... ``` **内置能力**: | 能力 | 实现要点 | | --- | --- | | 场景化路由 | `scene → (provider, model, temperature, max_tokens, timeout)` 全部配置化,改模型不改代码 | | 故障转移 | 主供应商超时/5xx/限流 → 自动切备用;连续失败触发熔断,冷却后半开探测 | | 结构化输出 | 传入 Pydantic Schema,网关负责 JSON Schema 约束 + 解析失败重试 + 校验 | | Prompt 版本 | Prompt 存文件(随代码版本化)+ 场景绑定版本号;A/B 时按用户哈希分流 | | 成本计量 | 每次调用写 `ob_ai_call`,含缓存命中 token 数 | | 预算保护 | 场景级日预算 + 用户级频控;超限降级到轻量模型或拒绝 | | 审核联动 | 输入输出审核在网关内统一完成,业务方无需关心 | | 可复现 | 记录 prompt 版本 + 模型版本 + 参数,便于回溯「为什么当时这么答」 | ### 8.2 视觉服务 ``` POST /internal/vision/analyze ├─ segment_portrait → 腾讯云 SegmentPortraitPic → 前景掩膜 ├─ body_keypoints → RTMPose ONNX → 关键点(肩/髋/踝定高) ├─ face_landmarks → 腾讯云 AnalyzeFace(90点) / AnalyzeDenseLandmarks(888点,仅全面报告) ├─ face_attributes → 腾讯云 DetectFaceAttributes(Skin/Hair/Shape/Eye/...) ├─ color_sampling → 自研:按关键点定位采样区,转 Lab,稳健统计(去高光/阴影) └─ body_measure → 自研:掩膜宽度剖面法(TD-06) 返回:结构化特征 + 每项置信度 + 质量问题标记(模糊/遮挡/多人/姿态异常) ``` **质量门禁前置**:图片质量不达标(多人、遮挡、侧脸过大、光线过暗)应在分析前就返回明确的重拍指引,而不是硬算出一个低置信度结果。**用户宁愿重拍一次,也不愿收到一份错的报告。** ### 8.3 混合检索的实现 核心是一条 SQL 完成硬过滤 + 向量召回(这正是选 pgvector 的收益): ```sql WITH filtered AS ( SELECT c.chunk_id, c.entity_id, c.text, c.embedding, e.attrs FROM kb_chunk c JOIN kb_entity e ON e.entity_id = c.entity_id WHERE e.status = 'published' AND e.entity_type = ANY(:entity_types) AND (:occasion IS NULL OR e.attrs->'occasions' ? :occasion) AND NOT (e.attrs->'avoid_body_shapes' ? :body_shape) -- 硬禁忌直接排除 ), vec AS ( SELECT chunk_id, 1 - (embedding <=> :query_vec) AS vec_score FROM filtered ORDER BY embedding <=> :query_vec LIMIT 50 ), kw AS ( SELECT chunk_id, ts_rank(tsv, plainto_tsquery('simple', :query_text)) AS kw_score FROM kb_chunk WHERE tsv @@ plainto_tsquery('simple', :query_text) LIMIT 50 ) SELECT ... -- RRF 融合,再叠加规则权重与新鲜度 ``` **重排权重**(可配置,需用黄金集调参):`最终分 = 0.45×向量 + 0.2×关键词 + 0.25×规则权重 + 0.1×新鲜度`。规则命中的条目要有明显加权——**专业规则的可信度高于语义相似度**,这是自建知识库的意义所在。 ### 8.4 记忆系统的读写 **读(注入)**: ``` get_memory_snapshot(user_id, budget_tokens) → 优先级排序:L0 身份 > L1 画像 > L2 显式偏好 > L5 衣橱 > L3 事件 → 过滤:status='active' AND confidence >= 阈值 → 裁剪到预算内 → 缓存(hash 变更才重建) ``` **写**(置信度初值的唯一事实源是《用户系统与记忆系统-需求文档》§3.5,本处原写 0.95 与之冲突,已更正为 0.90): ``` 用户确认 / 设置页配置 → confidence=1.00 对话中明确陈述 → confidence=0.90,立即生效,回执告知用户「已记住」 报告产出(机器诊断) → confidence=诊断置信度(高置信维度 0.85 / 争议维度 0.65),写 L1 自评问卷 → confidence=0.60 行为推断(隐式) → confidence=0.50,累积证据达阈值生成 status='pending_confirm' → 在合适时机征询确认,确认后才转 active(且 confidence 置 1.0) ``` **衰减与冲突**:定时任务按 `decay_after` 降低未被强化的记忆置信度;同 key 冲突时**显式 > 报告 > 隐式**,时间新 > 时间旧,并写 `pf_memory_event` 留痕。用户在「我的记忆」页面的编辑拥有最高优先级且不可被自动覆盖。 --- ## 9. API 设计规范 ### 9.1 基本约定 | 项 | 约定 | | --- | --- | | 基础路径 | **规范写法为 `/api/v1`**,大版本进路径。`/v1/*` 作为等价别名由中间件重写到 `/api/v1/*`,两种写法返回完全一致的响应;文档、客户端与新代码一律写 `/api/v1`(别名只为兼容早期任务描述中的 `/v1`,不要在新代码里使用) | | 风格 | 面向资源的 REST;流式对话用 SSE(`text/event-stream`) | | 认证 | `Authorization: Bearer `(JWT,15 分钟)+ 刷新令牌(服务端可撤销) | | 幂等 | 写操作支持 `Idempotency-Key` 头,服务端 24 小时内保证同键同结果 | | 分页 | 游标分页(`cursor` + `limit`),不用 offset | | 时间 | 全部 ISO 8601 UTC,客户端负责本地化 | | 文档 | FastAPI 自动生成 OpenAPI,**契约先行**:先定 schema 再写实现 | ### 9.2 统一响应 ```json { "code": 0, "message": "ok", "data": { }, "request_id": "..." } ``` ```json { "code": 40301, "message": "今日额度已用完", "data": { "required": 1, "remaining": 0, "reset_at": "..." }, "request_id": "...", "hint": "明天恢复,或完成任务多换 3 次" } ``` **错误码分段**:`4xxxx` 客户端错误、`5xxxx` 服务端错误、`6xxxx` 外部依赖错误(便于监控区分「我们的问题」和「厂商的问题」)。每个错误码都要有**用户可读的 message** 和**可选的 hint(告诉用户怎么办)**——AI 产品的失败场景多,错误文案是体验的一部分。 ### 9.3 主要接口清单 | 域 | 方法与路径 | 说明 | | --- | --- | --- | | 认证 | `POST /auth/huawei` `POST /auth/refresh` `POST /auth/guest` `POST /auth/merge` | 登录、刷新、游客、游客合并 | | 用户 | `GET/PATCH /me` `POST /me/consents` `DELETE /me`(注销) | 含同意记录与注销 | | 上传 | `POST /uploads/presign` | 返回 COS 临时桶预签名 URL,**图片不经应用服务器** | | 报告 | `POST /reports` `GET /reports/{id}` `GET /reports` `POST /reports/{id}/share` `POST /reports/{id}/regenerate` | | | 画像 | `GET /profile` `GET /profile/memories` `PATCH /profile/memories/{id}` `DELETE /profile/memories/{id}` | 「我的记忆」可见可控 | | Agent | `POST /agent/conversations` `POST /agent/conversations/{id}/messages`(SSE) `POST /agent/messages/{id}/feedback` | | | 换装 | `POST /tryon/tasks` `GET /tryon/tasks/{id}` `POST /tryon/results/{id}/favorite` | | | 发型 | `GET /hairstyles` `POST /hairstyles/tasks` | | | 任务 | `GET /tasks` `GET /tasks/{id}` `POST /tasks/{id}/cancel` | 任务中心 | | 知识 | `GET /kb/items` `GET /kb/items/{id}` | 只读对外 | | 配额 | `GET /quota` `GET /quota/ledger` `POST /quota/tasks/{task_key}/claim` | V1;`hint` 文案不得含价格字样(`FR-BILL-07`) | | 支付 | ~~`GET /products` `POST /orders/verify`~~ | **V2 起** | | 埋点 | `POST /events/batch` | 客户端批量上报 | --- ## 10. 缓存策略 | 数据 | 位置 | TTL | 失效方式 | | --- | --- | --- | --- | | 记忆快照 | Redis | 1 小时 | 记忆写入时按 `user_id` 主动失效 | | 知识检索结果 | Redis | 10 分钟 | 键含 query hash + 过滤条件 + 知识库版本号 | | 规则集(已发布) | 进程内 + Redis | 5 分钟 | 发布时广播失效 | | 色板 | 进程内 | 长期 | 版本号变更时重载 | | 报告长图 | COS + CDN | 长期 | 报告版本变更生成新对象(不覆盖,便于回溯) | | LLM 结果 | 不缓存业务层 | — | 依赖供应商 prompt 缓存降本,不做业务层结果缓存(个性化内容缓存意义不大) | | 用户配额 | Redis 计数器 + PG 落账 | 周期 | 周期切换重置 | **知识库版本号是关键设计**:所有检索缓存键都带上全局 `kb_version`,知识库发布时版本号 +1,**所有旧缓存自然失效**,不需要精确追踪哪些缓存受影响。 --- ## 11. 安全设计 ### 11.1 认证与授权 - JWT 短期 access token(15 分钟)+ 服务端可撤销的 refresh token;登出/注销时撤销全部会话。 - 所有资源访问必须校验归属(`report.user_id == current_user.id`),**不能只靠 ID 不可猜**。越权是这类应用最常见的漏洞。 - 后台采用独立账号体系与独立域名/网段,不与用户端共用鉴权。 ### 11.2 数据安全 | 数据 | 措施 | | --- | --- | | 人脸原图 | 临时桶、服务端加密、2 小时生命周期、处理后立即删除、访问全程走预签名 URL | | 诊断结论 | 属敏感个人信息衍生数据,数据库加密存储敏感字段,日志中禁止打印 | | 结果图 | 私有桶 + CDN 签名 URL(短期有效)+ 防盗链,禁止公开可读 | | 密钥 | 全部走 SSM/KMS,禁止进代码、镜像、环境变量明文 | | 日志 | 脱敏中间件:手机号、UnionID、图片 URL、诊断结论一律脱敏或不记录 | | 备份 | PG 自动备份 + 定期恢复演练(**没演练过的备份等于没有备份**) | ### 11.3 防刷与滥用 | 风险 | 措施 | | --- | --- | | 免费额度被批量刷取 | 设备指纹 + 华为账号绑定;同设备多账号限制;异常注册风控 | | 换装接口被外部盗用 | 内网隔离 + 服务间令牌;对外接口强制登录 + 配额消耗 | | 恶意图片攻击 | 上传前校验类型/尺寸/像素上限;解码走安全库;审核前置 | | Prompt 注入 | 用户输入与系统提示词严格分区;输出侧校验;禁止用户内容影响工具调用决策 | | 爬取知识库 | 对外接口只暴露必要字段,加频控与签名 | ### 11.4 降级与熔断 每个外部依赖都要有明确的降级行为(见 §2 表格)。**唯一不允许降级的是内容审核**:审核服务不可用时按拦截处理(fail-close)。其他能力遵循 fail-soft,保证核心体验可用。 --- ## 12. 性能与容量 ### 12.1 性能目标(NFR) | 链路 | 指标 | 目标 | | --- | --- | --- | | 普通 API | p95 响应 | < 300ms | | Agent 首字 | p95 | < 2s | | Agent 整轮 | p95 | < 8s | | 初级报告生成 | p50 / p95 | < 45s / < 90s | | 全面报告生成 | p95 | < 180s(允许后台生成 + 推送) | | 换装出图 | p95 | < 40s(受 GPU 容量约束,排队时间单独统计) | | 知识检索 | p95 | < 200ms | | 长图渲染 | p95 | < 8s | | 可用性 | 月度 | ≥ 99.5%(V1) | **排队时间与执行时间必须分开统计**。混在一起看会得出错误结论:明明是 GPU 容量不足(该扩容),却被误判为推理慢(去优化模型)。 ### 12.2 容量假设与压力点 **基线取 1 万 DAU**。与产品方案 §8.1.1 的口径关系需说明清楚,否则会被误读为架构容量低于业务目标: - 产品方案的 V1 业务目标是 **MAU 15 万**,且**明确不以 DAU 作为考核指标**(形象诊断是低频决策型工具,改用季度回访率)。 - 低频工具的 DAU/MAU 通常在 5~10%,因此 MAU 15 万对应 DAU 约 **0.75~1.5 万** —— **1 万 DAU 基线与业务目标是吻合的**,不是保守估计。 - 本基线的作用是确认架构不会在早期撞墙;实际压测值由 `T-P5-06` 确定,Agent 并发上限亦由此推导(穿搭 Agent §7.1)。 | 资源 | 估算 | 压力点 | | --- | --- | --- | | API QPS | 峰值约 100~200 | 单机 FastAPI 多 worker 足够 | | 报告生成 | 首日集中,后续每日新增用户量级 | CPU 队列 + 视觉分析,可水平扩 worker | | 换装出图 | **最大瓶颈** | GPU 数量决定吞吐,靠队列削峰 + 配额限制需求 | | PG 连接 | 需连接池(asyncpg pool + 上限保护) | 长事务与 Worker 争用,必须分离读写池 | | 向量检索 | 十万级向量 | pgvector HNSW 足够,索引重建走低峰 | --- ## 13. 成本模型 **架构上的硬性要求:每一次付费调用都必须落 `ob_ai_call`,并可归因到用户与功能。** 没有这张表,成本优化只能靠猜。 > ⚠️ **V1 不收费让这张表的地位更高,不是更低**(`TD-28`)。V1 没有收入,因此成本是唯一的财务变量;同时 `ob_ai_call` 是 **V2 定价的唯一数据来源** —— 单次真实成本与人均使用频次都从这里出。埋点缺失的功能,在 V2 定价时就是一个必须靠猜的空洞。**这一项不允许"先上线后补埋点"。** ### 13.1 三个必须分开的成本口径 > ⚠️ **本节原有一处错误论证,已更正**。原文称「月度成本上限 = MAU × 单用户配额上限 × 单次成本,是可事前算准的确定值」。这不成立:**按人头发配额约束的是「单用户能用多少」,而总成本 = MAU × 人均,MAU 本身没有上限。** 配额防的是单用户刷爆,防不了总量涨爆。 | 口径 | 公式 | MAU 15 万时 | 用途 | | --- | --- | --- | --- | | **理论最坏** | `MAU × 防刷硬上限成本(¥236/月)` | ≈ ¥3,540 万/月 | 只说明"配额不是预算工具",无实际预算意义 | | **预期成本** | `Σ(各功能月用量推算 × 单次成本)` | **¥7~12 万/月** | **唯一的预算口径**,含使用率假设 | | **真实封顶** | 全局日成本熔断阈值 × 30 | **¥15 万/月** | **唯一与用户规模无关的硬闸门** | 预期成本的完整算式与其隐含的使用率假设见产品方案 §5.3.1;单用户三档成本见产品方案 §5.2.1。**单一事实源在产品方案,本节不重复数值。** ### 13.2 单位成本建模 > ⚠️ **LLM 单价自 2026-08-17 起分峰谷两档,本节的所有 `C_llm` 必须按调用时刻取价。** > DeepSeek 高峰时段(北京时间 09:00-12:00、14:00-18:00)单价为空闲时段的 **2 倍**, > 且新价目相对旧价目**输出侧涨约 2.25~4.5 倍**。两个直接后果: > > 1. **`ob_ai_call.cost_cents` 必须在调用当时算好落库**,不能事后按「当前单价」回算 —— > 否则同一条记录在不同时间重算会得到不同结果,V2 定价的依据就不可复现。 > 实现上由两套价目表 + 时段判定完成(`T-P1-25c`),并用 `rate_tier` 记录当次档位。 > 2. **§13.1 的预期成本与熔断阈值需按新单价重算一次**。原数值基于旧单价推得, > 而报告生成集中在白天高峰 —— 不重算会让熔断阈值落在错误的位置。 > > **可控杠杆有两个**:一是缓存前缀纪律(固定系统前缀命中缓存后输入侧大幅降价,已实测 > 单次命中 128/240 输入 token);二是把非实时的文案生成挪到空闲时段,直接省一半。 单位成本核算公式(数值需在 Phase 0 验证任务中实测填入,此处为建模框架): | 功能 | 成本构成 | 建模公式 | | --- | --- | --- | | 初级报告 | 人脸 API + 分割 API + 自建推理 + LLM + 审核 + 渲染 | `≈ C_face + C_seg + C_llm(in≈8k, out≈3k) + C_mod×n + C_render` | | 全面报告 | 初级 + 稠密关键点 + 更长文案 | `≈ 初级 × 3~4` | | Agent 单轮 | LLM(缓存命中后输入成本大幅下降)+ 检索 + 双向审核 | `≈ C_llm(in≈4.2k 多数缓存, out≈0.8k) + C_mod×2`。**思考模式须显式关闭**(`T-P1-24a`):开启后思考 token 按输出价计费,实测同一问答耗时从 1.8s 涨到 13.5s | | 换装单次 | GPU 秒数 + 审核 + 存储 + CDN | `≈ gpu_seconds × 单价 + C_mod + C_storage` | | 抠图单件 | GPU 秒数(离线批量摊薄) | 批量跑,按批次成本 / 件数 | | **短信验证码** | 单条约 ¥0.03~0.045(`TD-32`) | `发送条数 × 单价`。**量级上可忽略,但必须计量并告警** —— 被刷时它是直接的现金损失,且不像 GPU 成本那样有配额兜着 | ### 13.3 三道成本闸门(按拦截范围从大到小) | # | 闸门 | 拦截范围 | 触发后行为 | 需求编号 | | --- | --- | --- | --- | --- | | 1 | **全局日成本熔断** | 平台总量 | **自动降级,不是仅告警** | `NFR-COST-02` | | 2 | 场景级日预算 | 单个功能 | 该功能切轻量模型或暂停 | `NFR-COST-02` | | 3 | 用户级配额 | 单个用户 | 服务端拒绝 | `FR-BILL-06` | **第 1 道是新增的,也是唯一真正封顶的一道**(原设计只有"预警",见 §13.1 的更正)。实现要求: - **计数**:Redis 日累计成本键(`cost:daily:{yyyymmdd}`),由 LLM 网关与 GPU 任务在**成本产生后立即累加**,日切自动过期。落库对账以 `ob_ai_call` 为准,Redis 只做实时判定。 - **分级降级顺序**(按"用户感知损失最小"排序,阈值百分比可配置): | 日成本水位 | 动作 | 用户可感知的变化 | | --- | --- | --- | | 60% | 告警,不影响用户 | 无 | | 80% | 停高清导出、停报告重新生成 | 两个低频次要功能不可用 | | 90% | 停发型试戴;换装强制降画质 + 排队 | 次要生成能力不可用 | | 100% | 仅保留**在途任务**与纯文本对话;新的生成类请求全部拒绝 | 「今日服务繁忙,请明天再试」 | - **在途任务必须放行**:成本已经付出,中断只会浪费掉且让用户白等。熔断只拦**新请求**。 - **关键场景需显式豁免**:少数调用一旦失败会让整个链路无法继续(例如意图识别 —— 拿不到意图就无法路由, 用户看到的是功能完全坏掉而不是降级)。这类场景在场景配置上标记为 `critical`,**不受熔断拒绝**, 但仍照常计入日成本。豁免清单必须极短且逐个论证,否则熔断会被绕空。 - **必须与配额解耦**:熔断拒绝的请求**不得扣减用户配额**(走 `refund` 流水,`FR-BILL-05`),否则用户会因平台原因损失额度。这是最容易写错的一处。 - **必须可人工干预**:运营后台需有熔断状态查看与手动解除开关(`FR-ADM-*`),不能只能等日切。 --- ## 14. 部署架构 ### 14.1 V1 部署(单可用区,Docker Compose) ``` 腾讯云 VPC ├─ CLB(公网入口,TLS 终止) ├─ CVM-app(2~4 核) │ ├─ nginx │ ├─ api (FastAPI × N workers) │ ├─ admin (极简运营后台) │ ├─ worker-cpu (Celery: cpu 队列) │ ├─ worker-io (Celery: io 队列) │ ├─ beat (定时任务,单实例) │ └─ vision-svc (ONNX 推理) ├─ CVM-gpu(按需启停) │ ├─ tryon-svc (换装/发型推理) │ ├─ worker-gpu (Celery: gpu 队列) │ └─ matting (批量抠图,仅素材生产期运行) ├─ 云数据库 PostgreSQL(含 pgvector) ├─ 云数据库 Redis ├─ COS × 4 桶 + CDN └─ CLS / 监控告警 ``` **GPU 实例按需启停**是 V1 最有效的成本手段:换装量小的时候没必要 7×24 常驻。实现方式是队列有积压时拉起、空闲一段时间后释放(配合任务中心的排队体验,用户可接受冷启动的额外等待)。 ### 14.2 环境 | 环境 | 用途 | 数据 | | --- | --- | --- | | local | 本地开发 | Docker Compose 起 PG/Redis/COS 模拟;AI 能力走 mock 或少量真实调用 | | dev | 联调 | 独立库;外部 API 用测试账号与低配额 | | staging | 预发/验收 | 结构与生产一致,**保留可供监管抽查的测试通道与虚拟测试账号**(备案已完成,但变更备案与日常抽查仍需,`TD-19`) | | prod | 生产 | — | ### 14.3 发布 - 容器镜像 + 标签化版本;数据库迁移在发布流程中作为独立步骤执行(先迁移后发布,保证向后兼容)。 - **迁移必须向后兼容**(加列不删列、先双写后切读),避免回滚时数据不兼容。 - 灰度:按用户哈希分流;报告与 Agent 这类核心链路的 Prompt/规则变更必须先灰度 + 看黄金集回归。 --- ## 15. 可观测性 | 类型 | 工具 | 关键内容 | | --- | --- | --- | | 链路追踪 | OpenTelemetry | 一次报告生成的完整 span 树(含各外部调用耗时),排障必需 | | 指标 | Prometheus + Grafana | 三张看板:业务漏斗、AI 成本、任务健康(`TD-24`) | | 日志 | CLS | 结构化 JSON,带 `request_id`/`user_id`/`task_id` 贯穿 | | 审计 | `cp_audit_log` | 后台操作、知识库变更、权限变更 | | 告警 | 云监控 + 企业微信/飞书 | 分级:P0(服务不可用/审核失效)、P1(任务失败率超阈值/成本突增)、P2(延迟劣化) | **必须能一键回答的三个问题**(这是可观测性的验收标准): 1. 「某用户说他的报告生成失败了」→ 用 `user_id` 找到 task,看到失败在哪一步、错误码、**配额是否已退还**。 2. 「昨天成本涨了 3 倍」→ 按功能/模型分组的成本对比,定位到具体场景。 3. 「换装最近变慢了」→ 区分是排队变长(扩容)还是推理变慢(模型/资源)。 --- ## 16. 工程规范 ### 16.1 仓库结构 ``` meida_backend/ # 后端仓库。文档在独立仓库 meida,鸿蒙端尚未建库 ├─ apps/ │ ├─ api/ # FastAPI 应用(路由、依赖注入、中间件) │ ├─ worker/ # Celery 任务定义 │ └─ admin/ # 极简运营后台 ├─ services/ │ ├─ vision/ # 视觉推理服务 │ ├─ matting/ # 抠图批处理 │ └─ tryon/ # 换装服务(改造纳管) ├─ packages/ │ ├─ domain/ # 领域模型与业务规则(纯逻辑,无 IO、无框架,可纯单测) │ ├─ shared/ # 配置、日志、错误、DB、COS、可观测 │ ├─ llm/ # LLM 网关 │ │ # ── 以下为 §4.1 的 11 个业务域,每域一个包 ── │ ├─ identity/ # 账号与会话 │ ├─ billing/ # 配额与计量 │ ├─ task/ # 任务中心 │ ├─ profile/ # 用户画像 │ ├─ memory/ # 记忆系统 │ ├─ kb/ # 知识库 │ ├─ retrieval/ # 混合检索 │ ├─ rules/ # 规则引擎 │ ├─ report/ # 美学报告 │ ├─ agent/ # 穿搭 Agent │ ├─ tryon/ # 换装与发型编排 │ ├─ wardrobe/ # 衣橱 │ ├─ notify/ # 通知推送 │ └─ compliance/ # 审核 / 水印 / 元数据标识 ├─ prompts/ # Prompt 模板(版本化,随代码走) ├─ migrations/ # Alembic ├─ tests/ │ ├─ unit/ # 领域逻辑 │ ├─ integration/ # 带 DB/Redis(testcontainers) │ └─ golden/ # 黄金集回归(检索/规则/报告) ├─ deploy/ # Dockerfile / compose / CI 配置 └─ scripts/ ``` 文档集在 `meida` 仓库(`docs/`),不进本仓库。鸿蒙客户端独立建库,不预置空目录。 **包内约定**:每个业务域包统一为 `service.py`(用例编排,对外唯一入口)+ `repo.py`(该域自己的表访问) + 可选 `schemas.py`。§4.2 的「禁止直接查对方的表」因此落为一条可机检规则: **跨域只允许 import 对方的 `service`,不允许 import 对方的 `repo`。** **三层的判别标准**(决定新代码该放哪一层): | 层 | 判别标准 | 反例 | | --- | --- | --- | | `packages/domain` | 纯函数与不变量,**不 import 任何框架、不做 IO** | 把 SQLAlchemy 模型放进来 | | `packages/<域>` | 该域的用例编排与自己的表访问 | 在 `agent/` 里直接读 `bl_quota` 表 | | `apps/` | HTTP / Celery 的适配,**不含业务判断** | 在路由函数里写配额判定公式 | > 这三条边界由 import-linter 契约强制,不靠 code review。分层规则一旦只靠人守,第一次赶工就会破。 ### 16.2 AI 辅助开发下的质量防线 既然主要由 AI 编码,人工 review 的带宽会成为瓶颈,因此**必须把规范变成机器可执行的检查**: | 防线 | 内容 | 卡关方式 | | --- | --- | --- | | 契约先行 | 接口先写 Pydantic schema 与 OpenAPI,再写实现 | 客户端类型由 OpenAPI 生成,schema 变更即可见 | | 静态检查 | ruff(lint+format)+ mypy strict(覆盖 `packages/domain`、`packages/llm`) | CI 失败即阻断合并 | | 分层依赖检查 | 用 import-linter 之类的工具声明层间依赖规则 | 跨域直接访问对方表 → CI 报错 | | 黄金集回归 | 检索准确率、规则命中正确性、报告结构完整性、禁忌违反数=0 | 每次 PR 跑,指标退化即阻断 | | 迁移检查 | Alembic 迁移必须可逆或明确标注不可逆 | Review 检查清单 | | 敏感信息扫描 | 密钥/Token 扫描 | pre-commit + CI | **黄金集是最重要的一道**:AI 改代码时最容易破坏的是那些「没有断言保护的隐性行为」。检索链路和规则引擎必须有一套固定输入 → 期望输出的样本集,这既是质量防线,也是《美学知识库-建设方案》中质量保障章节的落地形式。 --- ## 17. 架构演进路线 | 阶段 | 架构形态 | 触发条件 | | --- | --- | --- | | V1 | 模块化单体 + 独立 AI 服务;单机 Compose;PG 单库 | — | | V1.5 | 读写分离;Redis 集群;GPU 自动伸缩 | 报告/换装量上升,PG 读压力显现 | | V2 | 拆出 `llm-gateway`、`retrieval-svc` 为独立服务;引入 TKE | 需要独立发布节奏或独立扩容 | | V2 | 多端(iOS/Android)→ 重新评估跨端框架 | 明确的多端商业需求(`TD-01`) | | V3 | 向量迁 Qdrant/Milvus;事件流引入消息队列;数仓引入 ClickHouse | 向量 > 500 万 / 跨域事件复杂度上升 / 需要即席分析 | **演进纪律**:每一次架构升级都必须先出现**可测量的瓶颈**,再动手。提前架构化是这个阶段最大的浪费。 --- ## 18. 架构决策记录(ADR 索引) | ADR | 决策 | 详见 | | --- | --- | --- | | ADR-01 | 客户端用鸿蒙原生 ArkTS,不用跨端框架 | `TD-01` | | ADR-02 | 业务域用模块化单体,AI 能力独立服务 | §3.2 | | ADR-03 | 向量检索用 pgvector,不引入专用向量库 | `TD-08` | | ADR-04 | 图谱用 PG 邻接表 + 递归 CTE | `TD-09` | | ADR-05 | 规则引擎自研数据驱动决策表 | `TD-10` | | ADR-06 | 人体关键点自建(腾讯云已下线) | `TD-05` | | ADR-07 | 体型测算用掩膜宽度剖面法 | `TD-06` | | ADR-08 | 抠图用 BiRefNet,禁用 RMBG-2.0 | `TD-07` | | ADR-09 | 自建 LLM 网关,双供应商路由 | `TD-04` | | ADR-10 | 人脸原图不落盘,只存派生特征 | `TD-20` | | ADR-11 | 统一合规出口,所有生成物必经 | `TD-21` | | ADR-12 | 报告长图服务端渲染 | `TD-12` | | ADR-13 | 配额预扣-确认-退还三段式;V1 与 V2 共用账本表 | §7.4、`TD-28` | | ADR-14 | 统一任务中心承载全部长耗时任务 | §7 | --- ## 19. 技术风险登记 | 风险 | 影响 | 概率 | 应对 | | --- | --- | --- | --- | | ~~备案登记周期拖延上线~~ | ~~最高~~ | **已消除** | ✅ **备案已完成**,此风险关闭(`TD-19`) | | **备案范围与实际调用的模型不一致** | 高:超范围提供服务 | 中 | 核实备案覆盖的模型清单;LLM 网关已做多路由抽象,必要时 V1 先单路由(`TD-19` 残留项) | | **素材自采产能不足** | 高:`tryon_ready` < 300 件则「建议→试穿」闭环不成立 | **中高** | 备案解除后这是最长工序(`TD-26`)。按周产能倒排,未达标则功能降级为「加入搭配」 | | **全职顾问到岗延迟** | 中:规则库启动即延后,M2 顺延 | 中 | Phase 0 最早启动招聘(`TD-27`、`T-P0-20`) | | 团队缺乏 ArkTS 经验 | 前端进度不确定 | 中 | 第一个任务就打通端到端骨架,尽早暴露问题 | | 体型测算准确率不达标 | 报告核心结论不可信 | 中 | Phase 0 验证任务(≥80% 一致率);不达标则降级表述 + 引入人工校准 | | 换装成功率依赖白底图素材 | 核心闭环卡住 | 高 | 素材生产列为 Phase 1 前置任务,先小批量走通再规模化 | | GPU 成本随活跃度线性增长 | 单位经济恶化 | **低(V1)** | V1 不收费,配额为硬上限,**月度成本上限 = MAU × 单用户配额成本,可事前算准**。V2 引入付费后此风险重新升级 | | 大模型调价或能力变动 | 成本与质量波动 | 中 | 网关抽象 + 双供应商 + 场景配置化 | | 云 API 下线(已发生一次) | 能力缺口 | 中 | 核心能力(关键点、抠图)自建;对云能力保留替换预案 | | AI 标识元数据在链路中被剥离 | 合规不达标 | 中 | 专项测试覆盖 CDN/压缩/客户端保存全路径 | | 知识库内容生产跟不上 | 建议质量低、可解释性弱 | 高 | 知识缺口埋点驱动优先级;顾问资源尽早接洽(`TD-27`) | --- ## 20. 与需求文档的映射 | 本文章节 | 对应需求文档 | | --- | --- | | §7.1 报告生成链路 | 《美学报告-需求文档》§6 生成流程 | | §7.2 Agent 链路、§8.3 混合检索 | 《穿搭Agent-需求文档》§3.4、《美学知识库-建设方案》§6 | | §6.2 profile 域、§8.4 记忆读写 | 《用户系统与记忆系统-需求文档》§3 | | §6.2 kb 域、§8.3 | 《美学知识库-建设方案》§2~§6 | | §7.4 配额与支付 | 《用户系统与记忆系统-需求文档》§2.4/2.5、《需求规格说明》§2.9 | | §7.5、§11 | 《合规与安全方案》全文 | | §13 成本模型 | 《产品方案》§5.2 成本结构 | --- ## 相关文档 - [文档索引](./README.md) - [需求规格说明](./需求规格说明.md)(本文实现的需求编号来源) - [技术选型与调研报告](./技术选型与调研报告.md)(本文每项技术选择的理由、备选与切换条件) - [合规与安全方案](./合规与安全方案.md)(§7.5 统一合规出口与 §11 安全设计的义务来源) - [开发任务拆解](./开发任务拆解.md)(按本文的模块与链路切分的 WBS) - [美搭 · 智能穿搭美学助手 产品方案](./鸿蒙美搭-智能穿搭美学助手-产品方案.md) --- ## 变更记录 | 日期 | 版本 | 变更 | | --- | --- | --- | | 2026-08-14 | v1.4 | **文档 ↔ 代码交叉评审回写**。① §16.1 根目录 `meida/` → `meida_backend`,并去掉本仓库并不存在的 `docs/`、`client-harmony/`。② §4.1 补「域与包不是一一对应」:产品职责上记忆归 profile,代码上独立 `packages/memory`。③ `bl_quota` 列清单补 `limit_value`(本周期 base_limit 快照),并给出 `quota_key` 与产品方案 §3.2 的一一映射;游客 key 与记忆条数明确不进种子。④ `ob_ai_call` 补保留口径(定价完成前不可按日志周期删除) | | 2026-08-13 | v1.3 | **脚手架落地后按实现回写,五处修正**。① **§16.1 仓库结构补全 11 个业务域包** —— 原树只列了 7 个 `packages/`,§4.1 定义的 11 个业务域在树里无处安放,实现时只能猜;现补全并加「包内约定」(每域 `service.py` + `repo.py`,跨域只许 import 对方 `service`)与三层判别标准表。② **§6.2 `ob_ai_call` 补 `prompt_id`/`prompt_version`/`tokens_reasoning`/`rate_tier` 四列** —— §8.1 要求 Prompt 版本化可复现,但表里没有版本列,等于要求做不到的事;思考 token 混进 `tokens_out` 会掩盖开关思考模式的成本差异。③ **§9.1 澄清基础路径** —— 正文写 `/api/v1`、任务描述写 `/v1`,实现两者都要能用;现明确 `/api/v1` 为规范写法、`/v1/*` 仅作兼容别名。④ **§13.2 新增峰谷计价约束** —— DeepSeek 自 8-17 起高峰单价翻倍且新价目输出侧涨 2.25~4.5 倍,成本须在调用当时算好落库(不可事后回算),§13.1 的预算与熔断阈值需按新价重算;Agent 单轮成本注明思考模式须显式关闭。⑤ **§13.3 补「关键场景显式豁免」** —— 意图识别这类调用失败会让链路整体不可用,须标记 `critical` 不受熔断拒绝,但仍计入成本;豁免清单必须极短,否则熔断被绕空 | | 2026-08-13 | v1.2 | **文档一致性评审修订,其中三处是会直接卡住开发的实质缺陷**。① **§7.4 补齐 `billing` 域四张表的职责边界** —— `bl_quota` 与 `bl_wallet` 此前看起来像两套重复机制,`task_bonus` 的来源也没定义;现明确「V1 判定看 `bl_quota`、V2 判定看 `bl_wallet`、`bl_ledger` 两版都只是流水」,并把 `task_bonus` 物化为 `bl_quota.bonus_granted` 字段(避免每次判定聚合流水),补充三处最易写错的地方(熔断须在预扣之前、`used` 增减须与流水同事务、周期重置靠 `period_key` 而非定时清零)。② **§13 成本模型重写** —— 推翻「月度成本上限 = MAU × 单用户配额上限」这个错误论证(配额约束单用户,总成本 = MAU × 人均,MAU 无上限),改为「理论最坏 / 预期成本 / 真实封顶」三口径,并新增 **§13.3 四档熔断降级**(含在途任务放行、被熔断请求不得扣配额、可人工解除)。③ **§7.2 token 预算改为嵌套表**,合计 3700→**4200**(原表漏算会话近轮原文 400),并明确硬禁忌永不裁剪。另:`pf_memory.status`/`source`、`id_consent.consent_type` 枚举与用户系统对齐(`personalization` 不可省略,否则记忆写入无合规依据);手机号存储位置明确为 `id_user_identity`;诊断维度 34→**32**;`points_reserved`→**`quota_reserved`**;外部依赖表补入腾讯云短信并说明登录双通道不得互相依赖;§12.2 说明 1 万 DAU 基线与 MAU 15 万目标的口径关系 | | 2026-08-13 | v1.1 | 按业务决策收口调整。**billing 域降级为「配额与计量」**:V1 移除支付、订单、会员、IAP;`bl_point_tx` 改名 `bl_ledger` 并新增 `tx_type` 字段,使 V1 配额账本与 V2 点数账本共用同一张表(V2 免数据迁移);新增 `bl_quota_rule` 配额规则表。§7.4 拆为「V1 配额三段式」与「V2 IAP 支付」。§13 成本模型补充「V1 成本上限可事前算准」的公式,并强调 `ob_ai_call` 是 V2 定价的唯一数据来源、不允许后补。§19 风险登记:备案风险关闭,新增备案范围一致性、素材自采产能、顾问到岗三项风险 | | 2026-08-13 | v1.0 | 首次输出。确立「模块化单体 + 独立 AI 服务」架构、十个业务域数据模型、五条关键链路、统一合规出口、V1 单机 Docker Compose 部署与成本模型 |