Isolate cloud model access before enabling product RAG workflows
Some checks failed
verify / verify (push) Has been cancelled

The API and ingestion tools now use a fixed internal model gateway while
governed profiles, embedding cache assignments, traceable citations, and
stable API errors establish the boundaries required by later workflows.

Constraint: The current Alibaba Cloud workspace rejects all three live model calls with authentication failures
Rejected: Give the API or seed tools the Bailian key and direct egress | combines database access, cloud credentials, and public network access
Rejected: Mix offline and Bailian vectors in one demo namespace | makes profile activation and retrieval ambiguous
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep Bailian credentials and egress exclusive to model-gateway and create a new immutable profile hash for any embedding identity change
Tested: make verify; 121 backend tests; 14 frontend tests; fresh and populated Alembic upgrade-downgrade-upgrade; two idempotent offline seeds; Docker health and HTTP retrieval; isolated provider smoke
Not-tested: Successful live Bailian responses because the supplied workspace credential currently fails authentication
This commit is contained in:
2026-07-13 04:09:06 +08:00
parent 99b7df64ea
commit 75592af33a
28 changed files with 3932 additions and 254 deletions

View File

@@ -4,9 +4,9 @@
|---|---|
| 课题 | 基于 RAG 的地质找矿知识问答系统构建与应用 |
| 学科方向 | 大数据分析 |
| 文档版本 | v1.0-design |
| 状态 | 设计基线,待实现验证 |
| 更新日期 | 2026-07-11 |
| 文档版本 | v1.1-implementation-sync |
| 状态 | 设计基线;安全运行骨架已部分实现,产品主链路未完成 |
| 更新日期 | 2026-07-13 |
| 后端 | Python + FastAPI |
| 前端 | React + TypeScript |
| 向量存储 | PostgreSQL + pgvector |
@@ -22,7 +22,7 @@
1. 仓库、提交历史、镜像层、前端包、测试数据和日志都不能出现真实 Key。
2. 开发环境从未提交的 Docker Secret 文件读取;生产环境从 Secret Manager 或编排平台读取。
3. 前端永远不接触 Key所有百炼请求由后端发起。
3. 前端、入口 Gateway、业务 API、Worker、seed 和 smoke 工具永远不接触百炼 Key所有百炼请求只由独立 `model-gateway` 发起。
4. 日志不得记录 `Authorization` 请求头或完整模型请求体。
5. 工作空间真实域名也只进入本地部署配置;文档统一使用 `<workspace-id>`
6. 每次提交前执行 `make verify`;仓库已配置本地钩子和 Gitea Actions 双重检查Actions 需以远端 runner 首次成功记录作为生效证据。
@@ -33,11 +33,15 @@
```text
React/Nginx
-> 无 Secret 入口 Gateway
-> FastAPI API
-> PostgreSQL + pgvector
-> 阿里云百炼 Embedding / Rerank / Chat
-> FastAPI Worker与 API 共用同一 Python 代码镜像)
-> 内部 model-gateway client
-> Python Worker与 API 共用同一 Python 代码镜像)
-> 文档解析、分块、向量化和持久化任务
-> 内部 model-gateway client
model-gateway唯一持有百炼 Key 和公网出口)
-> 阿里云百炼 Embedding / Rerank / Chat
```
默认模型链:
@@ -51,7 +55,7 @@ deepseek-v4-flash
-> 只依据证据回答并给出页码引用
```
选择 PostgreSQL + pgvector 的原因是当前预计 1 万至 30 万切片,单机 PostgreSQL 已足够,并能把业务状态、元数据、向量和后台任务放在一个事务边界中。这样核心长期运行组件只有 Web、API、Worker、数据库一条 `docker compose up -d --build` 即可启动。达到百万级切片或高吞吐边界后再评估 Qdrant而不是在第一版提前承担双写一致性和第二套备份系统。
选择 PostgreSQL + pgvector 的原因是当前预计 1 万至 30 万切片,单机 PostgreSQL 已足够,并能把业务状态、元数据、向量和后台任务放在一个事务边界中。为把数据库权限与云凭证/公网出口分开,运行拓扑额外保留一个很小的 `model-gateway` 安全边界;它复用后端镜像和领域端口,不把业务拆成多套数据库或分布式事务。达到百万级切片或高吞吐边界后再评估 Qdrant而不是在第一版提前承担双写一致性和第二套备份系统。
## 2. 项目目标和边界
@@ -136,13 +140,14 @@ flowchart LR
G -->|"internal data"| A["FastAPI API"]
A --> D[("PostgreSQL + pgvector")]
A --> F[("文件卷 / OSS 适配器")]
A --> E["text-embedding-v4"]
A --> R["qwen3-rerank"]
A --> L["deepseek-v4-flash"]
A -->|"Bearer + X-RAG-Caller: api"| M["内部 model-gateway"]
M --> E["text-embedding-v4"]
M --> R["qwen3-rerank"]
M --> L["deepseek-v4-flash"]
J["DB 持久化任务"] --> K["Python Worker"]
K --> D
K --> F
K --> E
K -->|"Bearer + X-RAG-Caller: worker"| M
A --> J
```
@@ -154,13 +159,14 @@ flowchart LR
| `gateway` | 固定 API 上游、请求大小/头边界、脱敏错误和流式转发 | 数据库凭证、模型凭证、业务权限判断 |
| `api` | 认证、知识库、检索、问答、评测 API | 长时间 OCR/批量入库 |
| `worker` | 解析、OCR、分块、向量化、索引、评测批任务 | 对外开放端口 |
| `model-gateway` | 唯一读取百炼 Key、统一模型协议/限流/脱敏错误;校验 API/Worker 内部身份 | 数据库、上传卷、外部端口、业务授权 |
| `db` | 元数据、向量、任务、会话、评测事实来源 | 原始大文件长期存储 |
| `storage` | 开发期 Docker volume生产可切换 OSS | 访问控制的最终判定 |
| 百炼适配器 | 统一超时、重试、计量、错误映射 | 记录密钥或正文日志 |
| 百炼适配器 | 仅在 `model-gateway` 进程内统一超时、重试、计量、错误映射 | 记录密钥或正文日志 |
### 4.3 为什么不先拆微服务
解析、检索、生成和评测共享同一领域模型、配置和数据库。拆分微服务会提前引入服务发现、分布式追踪、数据一致性和接口版本问题,却没有对应吞吐收益。第一版使用一个 Python 包,由 `api``worker` 两个进程加载;当某个模块具有独立扩容证据时再拆分
解析、检索、生成和评测共享同一领域模型、配置和数据库。拆分业务微服务会提前引入服务发现、分布式追踪、数据一致性和接口版本问题,却没有对应吞吐收益。第一版使用一个 Python 包,由 `api``worker` `model-gateway` 三种进程加载;其中 `model-gateway` 不是独立业务服务,而是最小权限的凭证/出口隔离边界。详细决策见 [ADR-0005](adr/0005-isolate-model-egress.md)
## 5. 技术选型
@@ -209,7 +215,7 @@ ADR 见 [ADR-0001](adr/0001-use-pgvector.md)。
### 6.1 端点必须分开
业务空间专属域名的 Key、地域和 Base URL 必须匹配;官方推荐生产使用专属域名,见[Base URL 总览](https://help.aliyun.com/zh/model-studio/base-url)。配置只保存以下形态
业务空间专属域名的 Key、地域和 Base URL 必须匹配;官方推荐生产使用专属域名,见[Base URL 总览](https://help.aliyun.com/zh/model-studio/base-url)。这些配置和 Key 只注入 `model-gateway`
```dotenv
BAILIAN_OPENAI_BASE_URL=https://<workspace-id>.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
@@ -227,6 +233,17 @@ DASHSCOPE_API_KEY_FILE=/run/secrets/bailian_api_key
`/apps/anthropic` 是 Anthropic 协议入口,本项目不使用。尤其不能把 `/reranks` 拼到 `/compatible-mode/v1` 后面;官方的 Rerank 路径是独立的 `/compatible-api/v1/reranks`
业务进程不接受任意模型名、供应商 URL 或向量维度,而只调用固定内部接口:
```text
POST /internal/v1/embeddings
POST /internal/v1/rerank
POST /internal/v1/chat/completions
POST /internal/v1/chat/stream
```
调用必须同时携带 `Authorization: Bearer <internal-token>``X-RAG-Caller: api|worker`。服务端以常量时间比较 token 并要求 token 身份与 caller 一致;`api` 只允许查询向量、重排和聊天,文档向量化只允许 `worker` 身份。两个内部 token 相互独立、只通过 Docker Secret 注入。API、seed 和 smoke 工具均经内部客户端访问,既不挂载百炼 Key也不加入公网 `egress` 网络。
### 6.2 Embedding 契约
基线请求:
@@ -365,6 +382,10 @@ UPLOADED
| `evaluation_sets/cases` | 版本化问题、证据和标签 |
| `evaluation_runs/results` | 参数快照、指标、延迟、成本和错误 |
| `audit_logs` | 管理操作和安全审计 |
| `model_profiles` | 模型种类、模型名、API 模式、1024 维约束、端点身份哈希和无凭证配置快照;受控 cache epoch 必须进入不可变 profile hash |
| `embedding_cache` | 以 `profile_hash + embedding_text_sha256` 唯一缓存 1024 维向量和脱敏调用元数据 |
| `chunk_embedding_assignments` | chunk/profile 到缓存项的状态化绑定,保证 READY 只指向同文本同 profile 向量 |
| `model_invocations` | 只记录 trace、caller、模型、用量、耗时、request ID 和脱敏错误,不保存请求/响应正文 |
### 8.2 核心向量表(示意)
@@ -441,6 +462,8 @@ COMMIT;
- 删除先清空 `active_version_id` 并令投影不可检索,再异步物理删除文件和向量;
- 任何回答保留当时的 chunk ID、模型和 Prompt 版本,保证复现。
Alembic `0002_model_profiles` 已实现上述 profile、缓存、assignment 和调用审计表,为 `knowledge_bases` 增加激活 Embedding profile并为 `chunks` 增加稳定唯一的 `citation_id`。迁移只会为可明确识别的单一 synthetic fake profile 做安全回填,绝不从模型别名或 URL 猜测真实供应商身份;同一知识库存在多个候选 profile 时保持未激活,等待显式治理。已在独立全新 volume 验证 `空库 -> 0001 -> 0002 -> 0001 -> 0002`,并在已有 20 条合成向量的数据卷完成相同升降级;重复 seed 后 profile/active KB 为 1/1cache/READY assignment/citation 均为 20且 citation 无重复。
## 9. 在线 RAG 流程
### 9.1 主流程
@@ -527,6 +550,8 @@ Rerank 只能重新排序已召回候选,无法找回初召回遗漏的证据
## 10. 后端 API 设计
FastAPI 入口通过 `create_app()` 应用工厂构建导入阶段不打开数据库、不读取百炼凭证。HTTP 请求由统一中间件生成或透传安全的 trace ID业务错误使用稳定的 Problem JSON 响应并回传 trace未知异常不得泄漏连接串、Secret 路径或供应商正文。`/health/live` 只证明进程存活,`/health/ready` 只验证本地数据库依赖;百炼能力状态由独立模型探测展示,避免供应商短时故障触发 API 重启风暴。
统一前缀 `/api/v1`
```text
@@ -703,6 +728,10 @@ pgvector 官方给出的单精度 `vector` 存储约为 `4 * dimensions + 8` 字
详细指标与阶段完成定义分别见 [01-data-and-evaluation.md](01-data-and-evaluation.md) 和 [03-implementation-plan.md](03-implementation-plan.md)。
### 15.1 2026-07-13 实现边界
当前已实现安全运行骨架、内部 `model-gateway` 及 token 身份、模型内部客户端、应用工厂/Problem/trace 契约和 `0002_model_profiles` 迁移代码;`provider-smoke`、真实 `seed-demo` 和 API 进程不再直接持有百炼 Key。当前工作空间对 `text-embedding-v4``qwen3-rerank``deepseek-v4-flash` 的实际请求仍返回 401因此三能力真实可用性尚未验收。数字文档上传/解析/审核、正式检索 API、grounded chat、Worker 租约恢复、评测运行器和完整产品 UI 仍是后续实现项;不得把本节理解为“整个项目完成”。
## 16. 参考资料
1. [阿里云百炼 Base URL 总览](https://help.aliyun.com/zh/model-studio/base-url)

View File

@@ -443,7 +443,7 @@ embedding_cache_key = SHA256(
chunk_embedding_assignment = UNIQUE(chunk_id, embedding_profile_hash)
```
相同文本可以安全复用向量缓存,但每个 chunk 都必须建立独立 assignment不能用缓存键替代任务/行唯一键。模型别名无法解析到稳定 revision 时,每次冻结实验显式设置 `cache_epoch`避免别名漂移后复用旧向量。激活前再次核对当前 `embedding_text_sha256`、完整 profile 与 assignment任何一项变化都生成新索引版本。
相同文本可以安全复用向量缓存,但每个 chunk 都必须建立独立 assignment不能用缓存键替代任务/行唯一键。模型别名无法解析到稳定 revision 时,每次冻结实验显式设置 `cache_epoch`并把它纳入不可变的 `embedding_profile_hash`epoch 变化必须创建新 profile禁止原地修改 profile 后继续复用旧缓存。激活前再次核对当前 `embedding_text_sha256`、完整 profile 与 assignment任何一项变化都生成新索引版本。
`endpoint_identity_hash` 由规范化后的工作空间/部署端点身份计算,仅保存哈希而不在实验导出中暴露真实工作空间。这样同地域、同模型别名但不同 MaaS 工作空间的向量不会错误共用缓存。

View File

@@ -3,7 +3,7 @@
| 项目 | 设计值 |
|---|---|
| 部署方式 | Docker Compose 单机 |
| 长期服务 | `web``api``worker``db` |
| 长期服务 | 当前 `web``gateway``api``model-gateway``db`;后续加入 `worker` |
| 一次性服务 | `migrate` |
| 对外端口 | 仅 Nginx/Web |
| 密钥 | Docker Secret / Secret Manager |
@@ -17,7 +17,7 @@
docker compose up -d --build
```
这条命令完成数据库健康等待、迁移、Web/API/Worker 启动。它不等于“所有数据和模型凭证自动生成”首次部署仍必须完成密钥轮换、Secret 创建和三模型能力探测。
这条命令当前完成数据库健康等待、一次性迁移、`model-gateway`、API、入口 Gateway 和 Web 启动Worker 落地后也遵循同一依赖链。它不等于“所有数据和模型凭证自动生成”首次部署仍必须完成密钥轮换、Secret 创建和三模型能力探测。
架构定位是适合毕设、演示和单机内部使用的生产式单节点,不宣称跨主机高可用。互联网生产环境应替换托管数据库、对象存储和集中密钥管理。
@@ -29,8 +29,9 @@ docker compose up -d --build
|---|---|---|---|
| `db` | 固定版本 pgvector 镜像 | 元数据、向量、任务、会话、评测 | 仅内部网络 |
| `migrate` | 与后端同镜像 | 运行 Alembic一次成功退出 | 无 |
| `api` | `backend/Dockerfile` | FastAPI、检索、问答、管理 API | 仅 internal data 网络 |
| `api` | `backend/Dockerfile` | FastAPI、检索、问答、管理 API | 仅 internal data/model 网络 |
| `gateway` | 与后端同镜像 | 固定上游、请求边界、脱敏错误和流式转发 | 仅 internal ingress/data 网络 |
| `model-gateway` | 与后端同镜像 | 唯一读取百炼 Key代理 Embedding/Rerank/Chat | 仅 internal model + egress 网络,无宿主机端口 |
| `worker` | 同一后端镜像 | 解析、向量化、评测后台任务 | 无 |
| `web` | `frontend/Dockerfile` | React 静态资源、Nginx 同源入口和 SSE 代理 | `127.0.0.1:8000`;生产部署 HTTPS |
| `ocr-worker` | 可选 profile | PaddleOCR 重型任务 | 无 |
@@ -72,6 +73,11 @@ x-rag-config: &rag-config
MODEL_MAX_RETRIES: "${MODEL_MAX_RETRIES:-3}"
MODEL_MAX_CONCURRENCY: "${MODEL_MAX_CONCURRENCY:-4}"
x-model-client-config: &model-client-config
MODEL_GATEWAY_BASE_URL: http://model-gateway:8000
MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_api_token
MODEL_GATEWAY_CALLER: api
services:
db:
image: pgvector/pgvector:0.8.2-pg17
@@ -115,15 +121,35 @@ services:
networks: [data]
restart: "no"
model-gateway:
build: ./backend
command: ["uvicorn", "app.model_gateway:app", "--host", "0.0.0.0", "--port", "8000"]
environment:
<<: *rag-config
MODEL_GATEWAY_ALLOWED_TOKEN_FILES: >-
api=/run/secrets/model_gateway_api_token,worker=/run/secrets/model_gateway_worker_token
secrets:
- bailian_api_key
- model_gateway_api_token
- model_gateway_worker_token
networks: [model, egress]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2)"]
restart: unless-stopped
api:
build: ./backend
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
depends_on:
migrate:
condition: service_completed_successfully
environment: *runtime-config
model-gateway:
condition: service_healthy
environment:
<<: [*runtime-config, *model-client-config]
secrets:
- postgres_app_password
- model_gateway_api_token
volumes:
- uploads:/data/uploads
healthcheck:
@@ -132,7 +158,7 @@ services:
timeout: 3s
retries: 12
start_period: 10s
networks: [data]
networks: [data, model]
restart: unless-stopped
gateway:
@@ -151,24 +177,29 @@ services:
migrate:
condition: service_completed_successfully
environment:
<<: [*runtime-config, *rag-config]
<<: [*runtime-config, *model-client-config]
MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_worker_token
MODEL_GATEWAY_CALLER: worker
WORKER_CAPABILITIES: ${WORKER_CAPABILITIES:-document_parse,embedding,rerank,evaluation}
secrets:
- postgres_app_password
- bailian_api_key
- model_gateway_worker_token
volumes:
- uploads:/data/uploads
networks: [data, egress]
networks: [data, model]
restart: unless-stopped
provider-smoke:
build: ./backend
command: ["python", "-m", "app.tools.provider_smoke"]
profiles: ["tools"]
environment: *rag-config
depends_on:
model-gateway:
condition: service_healthy
environment: *model-client-config
secrets:
- bailian_api_key
networks: [egress]
- model_gateway_api_token
networks: [model]
restart: "no"
seed-demo:
@@ -179,11 +210,13 @@ services:
migrate:
condition: service_completed_successfully
environment:
<<: [*runtime-config, *rag-config]
<<: [*runtime-config, *model-client-config]
MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_worker_token
MODEL_GATEWAY_CALLER: worker
secrets:
- postgres_app_password
- bailian_api_key
networks: [data, egress]
- model_gateway_worker_token
networks: [data, model]
restart: "no"
web:
@@ -209,6 +242,9 @@ networks:
data:
driver: bridge
internal: true
model:
driver: bridge
internal: true
egress:
driver: bridge
@@ -219,13 +255,17 @@ secrets:
file: ./secrets/postgres_migrator_password
postgres_app_password:
file: ./secrets/postgres_app_password
model_gateway_api_token:
file: ./secrets/model_gateway_api_token
model_gateway_worker_token:
file: ./secrets/model_gateway_worker_token
bailian_api_key:
file: ./secrets/bailian_api_key
```
最终实现固定镜像 digest,示意版本只说明可行结构`migrate` 只有一个实例执行API 和 Worker 不在启动时并发自动迁移。首次初始化脚本只在 `db` 容器内使用 bootstrap 凭据创建 `vector` 扩展、无超级用户权限的 migrator/app 登录角色和专属 schemamigrator 拥有 schema/DDLapp 仅获运行期所需的 DML/sequence 权限及默认权限API/Worker 永远不挂载 bootstrap 或 migrator Secret。最后一个初始化脚本只有在全部 SQL 事务和授权成功后,才在 `PGDATA` 内用同文件系统 rename 原子写入 `.rag-bootstrap-complete`healthcheck 同时检查 `pg_isready` 与该哨兵,防止临时初始化 server 提前放行迁移。备份再使用独立只读角色。Compose 的 `.env` 仅做变量插值,不会自动注入容器,因此可配置的非敏感项和 `*_FILE` 路径通过 YAML anchor 在 `environment` 中完整声明。后端从各自的 `POSTGRES_PASSWORD_FILE` 读取密码后在内存中组装 DSN,不把明文密码放进 `.env``DATABASE_URL`
实际实现固定数据库镜像 digest。`migrate` 只有一个实例执行;成功应用到 head 后退出码为 0随后在 `docker compose ps -a` 显示 `Exited (0)` 是一次性任务的正常终态,不是服务暂停。API 和 Worker 不在启动时并发自动迁移。首次初始化脚本只在 `db` 容器内使用 bootstrap 凭据创建 `vector` 扩展、无超级用户权限的 migrator/app 登录角色和专属 schemamigrator 拥有 schema/DDLapp 仅获运行期所需的 DML/sequence 权限及默认权限API/Worker 永远不挂载 bootstrap 或 migrator Secret。最后一个初始化脚本只有在全部 SQL 事务和授权成功后,才在 `PGDATA` 内用同文件系统 rename 原子写入 `.rag-bootstrap-complete`healthcheck 同时检查 `pg_isready` 与该哨兵,防止临时初始化 server 提前放行迁移。Compose 的 `.env` 仅做变量插值,不会自动注入容器;后端从 `*_FILE` 读取 Secret,不把明文密码放进 `.env``DATABASE_URL`
网络分为层:`web` 位于普通 `edge` 和 internal `ingress``gateway` 位于 `ingress + data`API、迁移、Worker 和数据库位于 internal `data`;只有明确需要云调用的一次性工具或未来 Worker 才能加入命名的 `egress`。因此浏览器入口不能横向连接数据库,数据库感知 API 也没有公网默认出口。只有无 Secret Web 容器发布回环端口gateway 固定上游并继续执行请求边界和脱敏错误契约。详细理由和被否决方案见 [ADR-0004](adr/0004-secretless-web-ingress.md)。必须注意:普通 `edge` bridge 为 Web 提供宿主机端口发布时,也给 Web 留有技术上的默认公网出口;当前接受这一点的前提是 Web 无 Secret、无数据网络、无挂载且代理上游固定。若部署基线要求入口容器也完全禁网必须在宿主机防火墙/出口代理层阻断,不能把“未加入名为 egress 的网络”误写成“没有外网出口”。普通 bridge 本身也不构成域名白名单,正式联网 Worker 仍需主机防火墙或出口代理只放行百炼和对象存储域名。
网络分为层:`web` 位于普通 `edge` 和 internal `ingress`入口 `gateway` 位于 `ingress + data`API 位于 internal `data + model`;数据库/迁移位于 internal `data`;只有 `model-gateway` 位于 internal `model + egress``provider-smoke` 只持 API 内部 token真实 `seed-demo` 只持数据库 Secret Worker 内部 token二者都不挂载百炼 Key、不加入 `egress`。未来 Worker 沿用相同边界。`model-gateway` 不连接数据库、不挂载上传卷、不发布端口。入口隔离见 [ADR-0004](adr/0004-secretless-web-ingress.md),模型出口隔离见 [ADR-0005](adr/0005-isolate-model-egress.md)。必须注意:普通 `edge` bridge 为 Web 提供宿主机端口发布时,也给 Web 留有技术上的默认公网出口;生产仍需主机防火墙出口代理只允许 `model-gateway` 到百炼域名。
### 2.3 后台任务为何不用 Redis
@@ -289,7 +329,9 @@ def read_secret(file_env: str, value_env: str) -> str:
return os.environ[value_env]
```
生产只允许文件/Secret Manager本地值环境变量仅作为临时兼容。应用启动后不打印 Settings 的 Secret 字段,异常对象不得包含请求头。
生产只允许文件/Secret Manager本地值环境变量仅作为临时兼容。`bailian_api_key` 只挂载到 `model-gateway`。API 与 Worker 分别挂载 `model_gateway_api_token``model_gateway_worker_token`;调用同时使用 Bearer token 和 `X-RAG-Caller`,服务端以常量时间比较并拒绝 token/身份不匹配。应用启动后不打印 Settings 的 Secret 字段,异常对象不得包含请求头。
Secret 或模型端点配置在运行中发生变化时,`model-gateway` 会原子清空旧 provider 与内部 token并进入 `restart_required`;旧、新 token 都不能在旧进程继续调用。Docker 的 `restart: unless-stopped` 不会仅因 unhealthy 自动重启,因此轮换操作必须协调执行 `docker compose restart model-gateway api`Worker 落地后同时重启 Worker待 readiness 恢复后再运行 provider smoke。禁止只替换文件而让旧进程长期携带旧凭据。
### 3.3 Git 防泄漏
@@ -326,6 +368,10 @@ EMBEDDING_MODEL=text-embedding-v4
EMBEDDING_DIMENSION=1024
RERANK_MODEL=qwen3-rerank
LLM_MODEL=deepseek-v4-flash
MODEL_GATEWAY_BASE_URL=http://model-gateway:8000
MODEL_GATEWAY_TOKEN_FILE=/run/secrets/model_gateway_api_token
MODEL_GATEWAY_CALLER=api
```
启动时 fail fast
@@ -336,6 +382,8 @@ LLM_MODEL=deepseek-v4-flash
- Embedding 维度为 schema 支持的 1024
- 生产没有默认密码和通配 CORS
- Secret 文件存在、非空且权限合理;
- 内部模型地址必须精确为 `http://model-gateway:8000`,不能通过配置改成公网或用户控制的 URL
- API/Worker 内部 token 必须不同caller 必须与 token 身份一致;
- `APP_ENV=production` 时关闭开发文档或增加管理员保护。
### 4.2 地域与计费方案
@@ -373,13 +421,15 @@ LLM_MODEL=deepseek-v4-flash
`GET {OPENAI_BASE}/models` 可作为辅助清单,但不能证明 Rerank 路径和三个模型权限都正常,最小实际调用才是最终验证。
截至 2026-07-13最近一次真实探测已到达百炼端点但 Embedding、Rerank、Chat 三项均返回 401。改由内部 `model-gateway` 路由后必须重新运行同一探测;已有 401 证明不了模型授权可用。必须核对 Key 所属工作空间、北京地域、端点和计费/模型权限后重新验收。401 不进入自动重试风暴,也不能把 Stage 1 标记完成。
## 6. 网络与接口安全
### 6.1 网络边界
- 只发布 Nginx 端口数据库、API、Worker 不直接暴露公网;
- Nginx 到 API 使用内部网络;
- `data` 网络启用 Docker internal 隔离Web 不加入API/Worker 通过独立 `egress` 网络访问云服务;
- `data``model` 网络启用 Docker internal 隔离Web 不加入API/Worker 只经 `model-gateway` 访问云服务,自身不加入 `egress`
- 生产入口启用 TLS、HSTS 和安全响应头;
- 出站只允许百炼/OSS 等必要域名;
- 数据库账号按迁移、应用、备份职责分权;
@@ -442,11 +492,12 @@ LLM_MODEL=deepseek-v4-flash
### 8.1 健康端点
- `/health/live`:进程事件循环可用,不检查外部模型;
- `/health/ready`数据库、迁移版本存储路径和配置可用
- API `/health/ready`当前验证数据库可用;后续再纳入迁移版本存储路径对账
- `model-gateway /health/ready`:只验证本地 Key/token/端点配置可加载且未漂移,不产生计费云调用;
- `/admin/providers/health`:按需调用三个模型,结果短期缓存;
- `/admin/index/health``documents.active_version_id`、searchable 切片投影、非空向量、文本哈希和 embedding profile 一致性。
百炼短时波动不应让编排器不断重启健康 API 容器。
百炼短时波动不应让编排器不断重启健康 API 容器。真实三能力是否可用只能由显式 provider smoke 证明,不能从 `model-gateway` readiness 推断。
### 8.2 日志脱敏

View File

@@ -2,12 +2,12 @@
| 项目 | 当前值 |
|---|---|
| 基线日期 | 2026-07-12 |
| 基线日期 | 2026-07-13 |
| 当前阶段 | Stage 1模型与数据库 PoC`IN_PROGRESS` |
| 已完成阶段 | Stage 0仓库、安全和设计基线 |
| 整体完成度 | 约 12%,合理区间 10%15% |
| 设计完成度 | 设计基线已完成;实现中发现新约束时继续通过 ADR 和文档修订维护 |
| 业务代码完成度 | 约 18%Stage 1 的 Docker API、离线检索、数据库 PoC以及 React 离线演示、Nginx 单入口和四网络隔离前置均已验收;真实百炼 smoke 仍待轮换后的新 Key |
| 业务代码完成度 | 不以新增文件提前抬高里程碑;已增加独立 `model-gateway`、内部 token 身份、应用工厂/Problem/trace 和 `0002` profile/cache 迁移,完整产品主链路仍未完成 |
| 当前预计剩余工期 | 约 913 周,含 300 题正式标注、盲测、论文和答辩缓冲 |
| 进度权威来源 | 本文的阶段状态、验收证据和已推送提交 |
@@ -50,7 +50,7 @@
| Stage 9 答辩、发布与归档 | 3% | `TODO` | 0% |
| **合计** | **100%** | — | **12%** |
Stage 1 已完成可离线验收的数据库、适配器契约、synthetic seed、只读 API 和 Web 演示子闭环,安全离线子阶段内部约完成 95%;但真实三模型 smoke 仍依赖轮换后的新 Key,因此 Stage 1 不提前计入整体里程碑。Stage 2 中不依赖云密钥的 React、Nginx、网络隔离和质量门禁前置已经完成,但 Worker 租约、完整产品工作流等必选项尚未完成Stage 2 仍为 `TODO`。本文复选框表示“已通过验收”,不表示“文件是否已经出现”。当前可诚实表述为:**设计基线完成,整体里程碑约 12%业务代码约 18%Stage 1 安全离线链路可运行,真实百炼验证尚未完成。**
Stage 1 已完成可离线验收的数据库、适配器契约、synthetic seed、只读 API 和 Web 演示子闭环,并已把百炼 Key/公网出口收敛到独立 `model-gateway`;但真实三模型请求当前均返回 401,因此 Stage 1 不提前计入整体里程碑。Stage 2 中应用工厂、Problem/trace、React、Nginx、网络隔离和质量门禁已作为前置落地,`0002_model_profiles` 迁移代码也已建立Worker 租约、上传入库、正式检索/聊天和评测等必选项尚未完成Stage 2 仍为 `TODO`。本文复选框表示“已通过对应层级的验收”,不表示“文件是否已经出现”。当前可诚实表述为:**设计基线完成,整体里程碑约 12%,安全离线链路可运行,真实百炼验证和产品主链路尚未完成。**
## 2. 阶段依赖与关键路径
@@ -99,8 +99,8 @@ Stage 0 DONE
- `git status --short --branch` 显示 `main...origin/main`Stage 0 基线提交远端可见。
- `make verify-design` 可检查 Secret、设计文档、相对链接和 diff。
- [00-overall-design.md](00-overall-design.md) 明确状态为“设计基线,待实现验证”。
- 当前不存在可运行应用,因此 Stage 0 的完成只代表设计和工程防线完成
- [00-overall-design.md](00-overall-design.md) 现已同步“设计基线;安全运行骨架部分实现,产品主链路未完成”。
- Stage 0 完成时尚无可运行应用;后续运行骨架不改变“Stage 0 只代表设计和工程防线”的历史验收口径
### 已完成提交/推送节点
@@ -133,11 +133,13 @@ Stage 0 DONE
- [x] 创建 `backend/pyproject.toml` 和依赖锁文件,不引入未说明依赖。
- [x] 创建最小 `backend/Dockerfile`,使用非 root 用户和固定基础镜像 digest。
- [x] 创建最小 `compose.yaml`,包含 `db + migrate + provider-smoke + seed-demo/offline`
- [x] 创建 Compose,包含 `db + migrate + model-gateway + api + gateway + web + provider-smoke + seed-demo/offline`Worker 仍待实现
- [x] 启动最小 FastAPI `api + gateway`,内部 API 无 egressgateway 无 Secret 并仅发布回环端口;提供真实数据库 readiness、Swagger 和只读 synthetic demo 检索。
- [x] 创建 PostgreSQL bootstrap、migrator 和 app 分权角色初始化脚本;备份只读角色在备份功能落地时单独创建。
- [x] 创建 Alembic 基线迁移,启用 pgvector 并建立 1024 维向量表/HNSW 基线。
- [x] 确保运行期服务不挂载 bootstrap 或 migrator Secret。
- [x] 将百炼 Key 和公网出口隔离到独立 `model-gateway`API/seed/smoke 只持对应内部 token不直持 Key。
- [x]`Authorization: Bearer` + `X-RAG-Caller: api|worker` 建立内部身份,常量时间比较并限制 API 身份不得执行文档向量化。
#### 4.3 三模型适配器
@@ -157,6 +159,7 @@ Stage 0 DONE
- [x] 建立并执行迁移空 volume、角色权限和 Docker 重启持久化测试。
- [x] 明确文档族 split、盲测保管人和“盲测用于调参即退役”规则。
- [x] 扩展 `make verify`,纳入 Stage 1 后端格式、类型和测试门禁。
- [x] 实现并实跑 `0002_model_profiles`profile、embedding cache、chunk assignment、model invocation 与稳定 `citation_id`;空卷和已有数据均完成 upgrade/downgrade/upgrade。
### 验收证据
@@ -165,12 +168,13 @@ Stage 0 DONE
- [x] `docker compose run --rm seed-demo-offline` 完成 20 条虚构数据写入、检索和重排;真实模式待新 Key。
- [x] `api + gateway` 镜像构建并达到 healthylive/ready/meta/demo status/demo search 均通过真实 HTTP 验收。
- [x] 第二次运行 seed 后数据库计数与第一次相同,均为 chunks/vectors/searchable = 20/20/20。
- [x] 65 项后端测试证明模型契约、健康检查、固定上游 gateway、SSE 代理分块与早断连释放契约、离线 demo、数据治理和失败路径正确。
- [x] 已验证测试证明模型契约、健康检查、固定上游 gateway、SSE 代理分块与早断连释放契约、离线 demo、数据治理和失败路径正确;本轮新增 model-gateway/0002 契约须随阶段提交重新运行全量门禁
- [x] `make verify`、Secret 扫描、固定镜像构建和 `git diff --check` 通过。
### 2026-07-12 已验证运行证据
- 全新 volume 上 `db` 达到 healthy`migrate` 以非超级用户退出码 0版本为 `0001_initial_schema`
- `0002_model_profiles` 已在独立全新 volume 完成 `空库 -> 0001 -> 0002 -> 0001 -> 0002`,并在已有 20 条合成数据的 volume 完成 downgrade/upgrade重复 seed 后 profile/active KB=1/1、cache/READY assignment/citation=20/20/20citation 无重复。
- `vector` 扩展与 HNSW 基线存在app/migrator 均非超级用户app 无 `rag` schema DDL 权限但具有所需 DML。
- 两次离线 seed 均输出 20/20/209 个可回答虚构问题 Hit@3 = 9/9。
- FastAPI 容器使用 app 最小权限角色,根文件系统只读且无 egress无 Secret gateway 仅提供回环入口Swagger 可见demo search 返回合成片段与不透明 citation ID。
@@ -179,9 +183,9 @@ Stage 0 DONE
- app 尝试修改已 `CLOUD_APPROVED``cloud_text` 被审批不可变触发器拒绝;尝试建表被权限拒绝。
- React + TypeScript strict 离线演示已实现工作台与系统状态页14 项前端测试、ESLint、类型检查和生产构建通过。
- Nginx 成为唯一回环入口Compose 仅发布 `127.0.0.1:8000``web -> gateway -> api` 同源链路、Swagger、OpenAPI、健康检查和 synthetic search 均通过真实 HTTP 验收。
- `edge / ingress / data / egress` 网络边界已落地;Web 不可解析数据库API 与 gateway 无公网 TCP 出口Web 与 gateway 均不持有 Secret
- `edge / ingress / data / model / egress` 网络边界已落地;API、seed、smoke 不挂载百炼 Key只有不连数据库/上传卷的 `model-gateway` 同时连接 internal model 与 egress
- gateway 与 Nginx 已关闭代理缓冲,自动测试证明两个 SSE 事件保持为独立下游分块,且响应头阶段断连也会释放上游流;真实聊天生成与取消传播仍属于 Stage 5尚未实现。
- 唯一未验收项是真实 `text-embedding-v4` / `qwen3-rerank` / `deepseek-v4-flash` smoke 与真实模式 seed
- 当前真实 `text-embedding-v4` / `qwen3-rerank` / `deepseek-v4-flash` 请求均到达供应商但返回 401三能力与真实模式 seed 未验收。上传、正式检索、grounded chat、Worker 和评测仍属于后续阶段,不能称为“唯一剩余项”
### 提交/推送节点
@@ -206,16 +210,16 @@ Stage 0 DONE
- **状态:** `TODO`
- **预计工期:** 34 个工作日
- **依赖:** Stage 1 `DONE`
- **已完成前置:** 不依赖云密钥的 React 离线演示、OpenAPI 类型、Nginx 单入口、四网络隔离和当前质量门禁已验收;这不代表 Stage 2 已启动或完成。
- **已完成前置:** 不依赖云密钥的 React 离线演示、OpenAPI 类型、Nginx 单入口和质量门禁已验收;模型隔离网络加入后当前为五层网络。这不代表 Stage 2 已启动或完成。
### 具体实现任务
- [ ] 建立 FastAPI 应用工厂、配置加载、稳定错误码和 OpenAPI
- [x] 建立 FastAPI 应用工厂、配置加载、稳定 Problem JSON、trace middleware 和 OpenAPI导入阶段不连库、不读模型凭证
- [ ]`domain -> ports -> services -> adapters/persistence` 落实后端依赖边界。
- [ ] 建立 SQLAlchemy 仓储、Alembic 迁移运行器和事务边界
- [ ] 建立完整 SQLAlchemy 仓储和业务事务边界Alembic 已推进到 `0002_model_profiles`,且空卷/已有数据升降级已验证
- [ ] 建立 PostgreSQL `background_jobs`、租约、lease token、fencing 和重试状态机。
- [ ] 建立 Worker maintenance loop、advisory-lock reaper 和强杀恢复测试。
- [ ] 建立 `/health/live``/health/ready` 和管理员模型能力探测端点
- [ ] `/health/live`数据库 `/health/ready` 及 model-gateway 本地 readiness 已有;管理员模型能力探测 API 仍待实现
- [x] 建立 React + TypeScript strict、路由、TanStack Query 和 OpenAPI 客户端生成。
- [x] 建立 Nginx仅发布 Web 端口;当前 API、DB 只在内部网络Worker 落地后必须沿用同一边界。
- [ ] 建立完整 Compose`web/api/worker/db/migrate`OCR profile 默认关闭。
@@ -229,11 +233,11 @@ Stage 0 DONE
- [x] 当前 `docker compose ps` 只暴露 Nginx/Web 回环端口;加入 Worker 后须重新验收。
- [ ] Docker 重启后数据库数据保留。
- [ ] 强杀 Worker 后验证可重领、耗尽进入 `FAILED`、旧 token 不能提交。
- [x] 当前后端 65 项、前端 14 项测试及两端 lint、类型和构建门禁通过;Stage 2 新功能仍需增量验证
- [x] 已提交基线的两端 lint、类型、测试和构建门禁通过;本轮新增 model-gateway、应用工厂和 `0002` 仍须在提交前以 `make verify` 重新确认
### 提交/推送节点
- [x] `S2-PREP`React 离线演示、Nginx 单入口、四网络隔离、SSE 代理安全修复与质量门禁已随 `c3bad0f` 验证并推送;该前置提交不代表 Stage 2 已启动或完成。
- [x] `S2-PREP`React 离线演示、Nginx 单入口、当时的四网络隔离、SSE 代理安全修复与质量门禁已随 `c3bad0f` 验证并推送;后续 `model` 网络把当前拓扑扩为五层,该前置提交不代表 Stage 2 已启动或完成。
- [ ] `S2-A`FastAPI、持久化、健康端点和错误契约。
- [ ] `S2-B`任务租约、reaper、fencing 和恢复测试。
- [ ] `S2-C`React、Nginx、完整 Compose、CI 和文档;其中 React/Nginx/CI 前置已完成,缺 Worker 与完整 Compose 验收,节点不得提前关闭。
@@ -561,12 +565,12 @@ Stage 0 DONE
## 15. 下一步执行顺序
当前主线仍是关闭 Stage 1 的真实供应商门禁;已完成的 React/Nginx 工作仅作为 Stage 2 安全前置保留。建议按以下顺序继续:
当前主线仍是关闭 Stage 1 的真实供应商门禁,同时继续不依赖云授权的安全骨架。建议按以下顺序继续:
1. 在百炼控制台撤销聊天中暴露的旧 Key创建新 Key并仅通过未提交的 Docker Secret 注入
2. 分别运行 `text-embedding-v4``qwen3-rerank``deepseek-v4-flash` 受控 live smoke记录脱敏 provider profile、能力指纹、维度、流式协议和错误映射
3. 用现有 20 条 synthetic 数据运行真实 Embedding/Rerank/Chat 链路和幂等 seed真实地质资料仍不得出域。
4. 关闭 S1-B 并把 Stage 1 标记为 `DONE` 后,再实现 Stage 2 的 Worker 租约、fencing、reaper、恢复测试和完整 Compose。
1. 在百炼控制台确认旧 Key 已撤销,并核对当前本地 Secret 对应的工作空间、北京地域、端点、计费方案和模型权限Key 只保存在未提交 Secret
2. 通过独立 `model-gateway` 重跑三能力 smoke当前三项 401 未消除前保持 S1-B 未完成
3. 三能力成功后,用现有 20 条 synthetic 数据运行真实 Embedding/Rerank/Chat 链路和幂等 seed真实地质资料仍不得出域。
4. 继续实现 Stage 2 的 Worker 租约、fencing、reaper、恢复测试和完整 Compose,不让外部 401 阻塞可离线验证的工程工作
5. Stage 3 实现数字 PDF/DOCX/TXT/Markdown 的隔离解析、页码/章节恢复、审批状态机、切分、向量化写库和可追溯版本激活。
6. Stage 4 完成真实向量召回、重排与验证集评测Stage 5 再实现 grounded chat SSE、取消传播、引用解析和聊天 UI不把当前代理分块测试误称为聊天功能完成。

View File

@@ -14,7 +14,7 @@ make backend-sync
bash scripts/init-local-secrets.sh
```
脚本生成三组彼此不同的随机 PostgreSQL 密码,文件权限为 `0600`。不带 `--with-bailian` 时不会创建模型 Key
脚本生成三组彼此不同的 PostgreSQL 密码,以及互不相同的 `model_gateway_api_token``model_gateway_worker_token`文件权限为 `0600`。不带 `--with-bailian` 时不会创建或修改百炼 Key若本地已存在 Key脚本会明确提示保留原文件
## 2. 离线数据库与向量 PoC
@@ -46,7 +46,7 @@ Seed 的实际流程是:
### 2.1 查看后端运行效果
内部 API 只连接 `internal` data 网络,只挂载 app 数据库 Secret不连接网,也不挂载百炼 Key。无 Secret、无数据库凭证的 gateway 连接 internal ingress/dataReact/Nginx Web 连接 edge/ingress并且是唯一发布到本机回环地址 `127.0.0.1:8000` 的容器。三个运行容器均使用非 root 用户、只读根文件系统并移除 Linux capabilities。
内部 API 只连接 internal `data + model`挂载 app 数据库 Secret 和 API 内部 token,不连接网,也不挂载百炼 Key。独立 `model-gateway` 只连接 internal `model + egress`,是唯一挂载百炼 Key 的服务;它不连接数据库、不挂载上传卷、不发布端口。真实 `provider-smoke``seed-demo` 也只通过内部 token 调用该服务,不直持 Key。无数据库/模型 Secret 的入口 gateway 连接 internal ingress/dataReact/Nginx Web 连接 edge/ingress并且是唯一发布到本机回环地址 `127.0.0.1:8000` 的容器。
```bash
curl http://127.0.0.1:8000/health/live
@@ -82,7 +82,7 @@ bash scripts/init-local-secrets.sh --with-bailian
docker compose --profile tools run --rm provider-smoke
```
`provider-smoke` 依次验证:
`provider-smoke` 以 API 内部身份调用 `model-gateway`,再由后者依次验证:
- `text-embedding-v4` 返回 1 个 1024 维有限非零向量;
- `qwen3-rerank` 返回可映射到本地候选的下标和分数;
@@ -97,7 +97,9 @@ docker compose --profile tools run --rm seed-demo
docker compose --profile tools run --rm seed-demo
```
真实模式仍只允许本仓库的虚构样例;任何真实地质报告必须等 Stage 3 的许可、涉密和 outbound manifest 审核链完成。
真实模式使用 Worker 内部身份,仍只允许本仓库的虚构样例;任何真实地质报告必须等 Stage 3 的许可、涉密和 outbound manifest 审核链完成。
截至 2026-07-13已通过独立 `model-gateway` 重跑三项真实调用:三项都到达供应商,但均返回安全脱敏的 `authentication` 类别(内部非流式调用由 Gateway 以 502 封装Chat SSE 以终态错误事件返回;根因仍是供应商鉴权失败)。此时应核对 Key 所属工作空间、北京地域、专属端点、计费方案和模型权限,不要反复自动重试。在三项成功前不得声明百炼接入验收通过。
## 4. 质量门禁与排障
@@ -113,6 +115,7 @@ docker compose logs --no-log-prefix migrate
|---|---|
| `invalid_local_configuration` | 检查是否仍是占位 URL、两个 URL 是否同一北京工作空间、新 Key 文件是否存在 |
| migrate 等不到 DB | 检查三个数据库 Secret 是否存在且互不相同;查看 bootstrap 日志 |
| `migrate` 显示 `Exited (0)` | 正常:它是一次性 Alembic 服务,成功升级到 head 后就应退出;只有非 0 或反复重启才是故障 |
| 401/403 | 不重试风暴;检查新 Key、工作空间和模型授权 |
| 429/5xx/timeout | 适配器执行有界指数退避;持续失败时保留脱敏 request ID 后停止 |
| seed 计数不是 20 | 不进入下一阶段检查迁移、manifest 约束和事务日志 |
@@ -127,4 +130,4 @@ docker compose down
## 5. 当前完成边界
可在没有百炼 Key 时验收:配置/Secret 安全、MockTransport 契约、离线向量/重排、Compose 渲染、空卷迁移、20 条写入和幂等。只有轮换后的新 Key 完成三模型真实 smoke 与真实 seed 后Stage 1 才能从 `IN_PROGRESS` 改为 `DONE`
可在没有可用百炼权限时验收:配置/Secret 安全、内部 token 身份、MockTransport 契约、离线向量/重排、Compose 渲染、迁移、20 条写入和幂等。当前代码还包含 FastAPI 应用工厂、稳定 Problem/trace 契约,以及 `0002_model_profiles` 的 profile/cache/assignment/invocation/citation 迁移这些不代表上传、正式检索、grounded chat、Worker 和评测已完成。只有有效 Key 完成三模型真实 smoke 与真实 seed 后Stage 1 才能从 `IN_PROGRESS` 改为 `DONE`

View File

@@ -4,10 +4,12 @@
- Stage 0“仓库、安全和设计基线”已完成并推送。
- Stage 1“模型与数据库 PoC”正在进行。
- 当前整体完成度约 12%,合理区间为 10%15%。
- 0003 的设计基线已完成;业务代码完成度约 18%Stage 1 安全离线子阶段约 95%
- 后端离线链路、20 条 synthetic 数据、React 离线演示、Nginx 单入口和四网络隔离均已验收;真实百炼三模型 smoke 仍待旧 Key 撤销并轮换
- Stage 2 仍为 `TODO`当前只完成了不依赖云密钥的前置Worker 租约、完整 Compose 和产品工作流尚未完成
- 当前整体里程碑仍约 12%,合理区间为 10%15%;新增代码不在阶段验收前提前计入完成度
- 0003 的设计基线已完成;后端离线链路、20 条 synthetic 数据、React 离线演示和 Nginx 单入口已验收
- 五层网络与独立 `model-gateway` 已实现:只有该服务持百炼 Key/egressAPI、seed、smoke 使用区分 API/Worker 身份的内部 token
- FastAPI 应用工厂、稳定 Problem/trace 契约,以及 `0002_model_profiles` 的 profile/cache/assignment/invocation/citation 迁移代码已落地
- 真实 `text-embedding-v4``qwen3-rerank``deepseek-v4-flash` 当前仍全部返回 401真实百炼能力尚未验收。
- Stage 2 仍为 `TODO`Worker 租约/fencing/reaper、上传入库、正式检索、grounded chat、评测和完整产品 UI 尚未完成。
- 后续状态、依赖、验收证据、工期和提交节点以 [项目全生命周期 TODO](04-project-todo.md) 为准。
## 文档列表

View File

@@ -0,0 +1,68 @@
# ADR-0005使用独立 Model Gateway 隔离百炼出口
- **状态:** accepted
- **日期:** 2026-07-13
## 背景
在线 RAG 必须同步完成 Query Embedding、Rerank 和 Chat SSE但 ADR-0004 又要求数据库感知 API 不持有百炼 Key、没有公网默认出口。让 API 或 Worker 直接调用百炼,会把业务数据、数据库凭据、模型凭据和公网出口集中到同一信任边界;只保留一次性 smoke/seed 工具则无法支持在线问答。
## 决策
新增与 API 共用 backend 镜像的长期 `model-gateway` 服务:
```text
browser
-> web (edge + ingress)
-> gateway (ingress + data)
-> api (data + model)
-> PostgreSQL/pgvector (data)
-> model-gateway (model + egress)
-> Alibaba Cloud Model Studio
worker (data + model)
-> PostgreSQL/pgvector
-> model-gateway
```
- 只有 `model-gateway` 挂载 `bailian_api_key` 并连接 `egress`;它不连接 `data`、不挂数据库 Secret、上传卷或宿主端口。
- API 与 Worker 不挂百炼 Key、不连接 `egress`,只通过 internal `model` 网络访问固定的 `http://model-gateway:8000`
- API 与 Worker 使用不同的内部令牌。调用必须同时携带 `Authorization: Bearer ...``X-RAG-Caller`Gateway 用常量时间比较把令牌映射到调用者,不能只信任 header 或 Docker 来源地址。
- API 允许 Query Embedding、Rerank、Chat 和受控 provider healthWorker 额外允许 Document Embedding。任意越权调用失败关闭。
- Model Gateway 只接受固定 profile、固定模型和固定端点的 vendor-neutral DTO不接受客户端提供 URL、模型名、维度或任意转发目标。
- Embedding、Rerank 和 Chat 继续复用现有 provider ports 与百炼 adapter不引入 SDK、Redis、消息队列、服务网格或第二套实现。
- 日志只允许 trace、调用者、操作、profile、输入数量、usage、耗时、request ID 和脱敏错误码;禁止记录 Key、Authorization、URL、query、messages、候选正文或响应正文。
- 容器 readiness 只验证本地配置、Secret 和客户端初始化,不产生模型费用;真实探测使用受控内部端点和显式命令。
## 内部接口
- `POST /internal/v1/embeddings`:接收 `texts``input_type=query|document`Query 恰好一条Document 为 110 条;返回 1024 维向量、resolved model、usage、request ID 和耗时。
- `POST /internal/v1/rerank`:接收 query、候选正文和 Top N返回原始下标、score、受校验正文与 provider 元数据。
- `POST /internal/v1/chat/completions`:非流式内部调用,用于需要先完成引用校验的场景。
- `POST /internal/v1/chat/stream`vendor-neutral SSE流开始后的 provider 错误转换为终态 `error` 事件,取消必须关闭上游流。
- `GET /health/live``GET /health/ready`:不调用百炼。
公共 API 不把 Model Gateway 的内部 401/403 原样返回浏览器;这类错误统一映射为可观测的 503 配置故障。429、timeout、5xx 和非法响应分别映射为稳定、脱敏的错误码。首个输出之后不得自动重试 Chat避免重复文本。
## 被否决方案
1. **API 直接挂 Key 与 egress** 数据库感知进程同时拥有数据、模型凭据和公网出口。
2. **Worker 直接调用百炼:** 会形成两个供应商调用实现和两个 Key 边界。
3. **Model Gateway 连接数据库验证审批:** 重新形成“数据 + Key + egress”的高风险组合。
4. **只依赖 Docker DNS 或来源 IP** 网络成员不是应用层身份,不能替代内部令牌。
5. **引入通用 API Gateway、Redis、Celery 或服务网格:** 当前单机毕设规模没有足够收益,并增加部署与恢复面。
## 后续约束
- `provider-smoke` 与真实 seed 必须使用 Model Gateway 客户端,不允许恢复直连百炼旁路。
- 普通在线查询只能使用知识库已激活的 embedding profileFake 与 Bailian profile 永不混查。
- Docker bridge 不是域名白名单。生产环境仍需主机防火墙或出口代理只放行获批的百炼域名。
- 改变模型调用者、内部鉴权、网络成员、Key 边界或 SSE 协议时,必须更新本 ADR 并重跑容器网络、Secret、取消和错误脱敏验收。
## 验证要求
- Compose 合同证明百炼 Key 与 `egress` 只存在于 `model-gateway`
- Model Gateway 无数据库 Secret、`data` 网络、上传卷和宿主端口API/Worker 无百炼 Key和 `egress`
- 缺失/错误令牌、调用者冒充、scope 越权、URL/模型/维度注入均有失败测试。
- Embedding、Rerank、Chat 的成功、401、429、timeout、5xx、非法响应、SSE 取消均由 hermetic contract test 覆盖。
- `make verify`、镜像 Secret 扫描、Docker 健康检查、网络隔离与真实三模型 smoke 全部通过后,才能宣称在线百炼模型边界可用。

View File

@@ -8,3 +8,4 @@ ADR 用于记录会长期影响系统的技术决策。状态使用 `proposed`
- [0002-separate-bailian-protocols.md](0002-separate-bailian-protocols.md):分离百炼 Chat/Embedding 与 Rerank 协议适配器。
- [0003-text-first-scope.md](0003-text-first-scope.md):第一版采用文本优先边界,不宣称地质图空间理解。
- [0004-secretless-web-ingress.md](0004-secretless-web-ingress.md):用无 Secret 的 Nginx Web 与固定上游 gateway 隔离浏览器、API 和数据库网络。
- [0005-isolate-model-egress.md](0005-isolate-model-egress.md):用独立 Model Gateway 隔离百炼 Key、模型出口与数据库感知服务。