统一性能、token、配额与成本口径,补齐短信登录等缺口任务,并回写脚手架实测暴露的思考模式与峰谷计价约束。 Co-authored-by: Cursor <cursoragent@cursor.com>
1156 lines
83 KiB
Markdown
1156 lines
83 KiB
Markdown
# 「美搭」技术架构文档
|
||
|
||
> 版本:**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 部署与成本模型 |
|