Files
meida/docs/技术架构文档.md
T
xslandCursor f4075f6827 收口文档评审冲突并补齐 Sprint 执行计划
统一性能、token、配额与成本口径,补齐短信登录等缺口任务,并回写脚手架实测暴露的思考模式与峰谷计价约束。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 09:40:50 +08:00

1156 lines
83 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 「美搭」技术架构文档
> 版本:**v1.3** | 更新日期:2026-08-13 | 状态:待技术评审
>
> **本文的定位**:把产品需求翻译成可直接拆解为开发任务的技术方案。读完这份文档,应该能回答三个问题——系统由哪些部分组成、数据长什么样、每条关键链路怎么跑通。
>
> **前置阅读**:《技术选型与调研报告》(所有选型结论与理由,本文只引用编号 `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) | — | — |
### 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`, **`bonus_granted`**, `updated_at`;UNIQUE(`user_id`,`quota_key`,`period_key`) | ✅ | **V1 扣减判定的唯一依据**。周期配额计量(日/月/终身),按 `period_key` 自然重置,**不要写定时清零任务**。`bonus_granted` 物化本周期已获任务奖励,避免每次判定都聚合流水 |
| `bl_quota_rule` | `quota_key`, `period_type`, `base_limit`, `task_bonus_max`, `hard_cap` | ✅ | 配额规则配置化(`FR-BILL-04`),改配置不发版。数值来源:产品方案 §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 语义扩展为点数余额,**同一张表,不改结构** |
> 🔑 **四张表的职责边界见 §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` |
| `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 <access_token>`(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/
├─ 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 配置
├─ docs/ # 本文档集
└─ client-harmony/ # 鸿蒙 ArkTS 工程
```
**包内约定**:每个业务域包统一为 `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-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 部署与成本模型 |