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

@@ -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 日志脱敏