Files
RAG/docs/02-deployment-and-security.md
YoVinchen 75592af33a
Some checks failed
verify / verify (push) Has been cancelled
Isolate cloud model access before enabling product RAG workflows
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
2026-07-13 04:09:06 +08:00

24 KiB
Raw Blame History

Docker 部署、配置与安全设计

项目 设计值
部署方式 Docker Compose 单机
长期服务 当前 webgatewayapimodel-gatewaydb;后续加入 worker
一次性服务 migrate
对外端口 仅 Nginx/Web
密钥 Docker Secret / Secret Manager
数据库 PostgreSQL 17 + pgvector 0.8.x实现时固定 digest

1. 部署目标

第一版的“最简单启动”定义为:

docker compose up -d --build

这条命令当前完成数据库健康等待、一次性迁移、model-gateway、API、入口 Gateway 和 Web 启动Worker 落地后也遵循同一依赖链。它不等于“所有数据和模型凭证自动生成”首次部署仍必须完成密钥轮换、Secret 创建和三模型能力探测。

架构定位是适合毕设、演示和单机内部使用的生产式单节点,不宣称跨主机高可用。互联网生产环境应替换托管数据库、对象存储和集中密钥管理。

2. Compose 服务设计

2.1 服务列表

服务 镜像/构建 职责 网络暴露
db 固定版本 pgvector 镜像 元数据、向量、任务、会话、评测 仅内部网络
migrate 与后端同镜像 运行 Alembic一次成功退出
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 重型任务
prometheus/grafana 可选 profile 本地观测 默认不对公网

2.2 Compose 骨架(设计示意)

name: geological-rag

x-runtime-config: &runtime-config
  APP_ENV: ${APP_ENV:-production}
  APP_NAME: ${APP_NAME:-geological-rag}
  POSTGRES_HOST: db
  POSTGRES_PORT: "5432"
  POSTGRES_DB: ${POSTGRES_DB:-geological_rag}
  POSTGRES_USER: ${POSTGRES_APP_USER:-geological_rag_app}
  POSTGRES_PASSWORD_FILE: /run/secrets/postgres_app_password
  UPLOAD_ROOT: ${UPLOAD_ROOT:-/data/uploads}
  MAX_UPLOAD_MB: "${MAX_UPLOAD_MB:-100}"

x-rag-config: &rag-config
  DASHSCOPE_API_KEY_FILE: /run/secrets/bailian_api_key
  BAILIAN_OPENAI_BASE_URL: "${BAILIAN_OPENAI_BASE_URL:?set BAILIAN_OPENAI_BASE_URL}"
  BAILIAN_NATIVE_BASE_URL: "${BAILIAN_NATIVE_BASE_URL:?set BAILIAN_NATIVE_BASE_URL}"
  BAILIAN_RERANK_BASE_URL: "${BAILIAN_RERANK_BASE_URL:?set BAILIAN_RERANK_BASE_URL}"
  EMBEDDING_MODEL: ${EMBEDDING_MODEL:-text-embedding-v4}
  EMBEDDING_DIMENSION: "${EMBEDDING_DIMENSION:-1024}"
  RERANK_MODEL: ${RERANK_MODEL:-qwen3-rerank}
  LLM_MODEL: ${LLM_MODEL:-deepseek-v4-flash}
  CHUNK_TARGET_TOKENS: "${CHUNK_TARGET_TOKENS:-512}"
  CHUNK_MAX_TOKENS: "${CHUNK_MAX_TOKENS:-800}"
  CHUNK_OVERLAP_TOKENS: "${CHUNK_OVERLAP_TOKENS:-64}"
  VECTOR_TOP_K: "${VECTOR_TOP_K:-50}"
  RERANK_TOP_N: "${RERANK_TOP_N:-10}"
  CONTEXT_TOP_N: "${CONTEXT_TOP_N:-8}"
  MAX_CONTEXT_TOKENS: "${MAX_CONTEXT_TOKENS:-10000}"
  MODEL_TIMEOUT_SECONDS: "${MODEL_TIMEOUT_SECONDS:-90}"
  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
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-geological_rag}
      POSTGRES_USER: ${POSTGRES_BOOTSTRAP_USER:-postgres}
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_bootstrap_password
      POSTGRES_MIGRATOR_USER: ${POSTGRES_MIGRATOR_USER:-geological_rag_migrator}
      POSTGRES_MIGRATOR_PASSWORD_FILE: /run/secrets/postgres_migrator_password
      POSTGRES_APP_USER: ${POSTGRES_APP_USER:-geological_rag_app}
      POSTGRES_APP_PASSWORD_FILE: /run/secrets/postgres_app_password
    secrets:
      - postgres_bootstrap_password
      - postgres_migrator_password
      - postgres_app_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./ops/postgres/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB && test -f $$PGDATA/.rag-bootstrap-complete"]
      interval: 5s
      timeout: 3s
      retries: 20
    networks: [data]
    restart: unless-stopped

  migrate:
    build: ./backend
    command: ["alembic", "upgrade", "head"]
    depends_on:
      db:
        condition: service_healthy
    environment:
      POSTGRES_HOST: db
      POSTGRES_PORT: "5432"
      POSTGRES_DB: ${POSTGRES_DB:-geological_rag}
      POSTGRES_USER: ${POSTGRES_MIGRATOR_USER:-geological_rag_migrator}
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_migrator_password
    secrets:
      - postgres_migrator_password
    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
      model-gateway:
        condition: service_healthy
    environment:
      <<: [*runtime-config, *model-client-config]
    secrets:
      - postgres_app_password
      - model_gateway_api_token
    volumes:
      - uploads:/data/uploads
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2)"]
      interval: 10s
      timeout: 3s
      retries: 12
      start_period: 10s
    networks: [data, model]
    restart: unless-stopped

  gateway:
    build: ./backend
    command: ["uvicorn", "app.gateway:app", "--host", "0.0.0.0", "--port", "8000"]
    depends_on:
      api:
        condition: service_healthy
    networks: [ingress, data]
    restart: unless-stopped

  worker:
    build: ./backend
    command: ["python", "-m", "app.workers.main"]
    depends_on:
      migrate:
        condition: service_completed_successfully
    environment:
      <<: [*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
      - model_gateway_worker_token
    volumes:
      - uploads:/data/uploads
    networks: [data, model]
    restart: unless-stopped

  provider-smoke:
    build: ./backend
    command: ["python", "-m", "app.tools.provider_smoke"]
    profiles: ["tools"]
    depends_on:
      model-gateway:
        condition: service_healthy
    environment: *model-client-config
    secrets:
      - model_gateway_api_token
    networks: [model]
    restart: "no"

  seed-demo:
    build: ./backend
    command: ["python", "-m", "app.tools.seed_demo"]
    profiles: ["tools"]
    depends_on:
      migrate:
        condition: service_completed_successfully
    environment:
      <<: [*runtime-config, *model-client-config]
      MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_worker_token
      MODEL_GATEWAY_CALLER: worker
    secrets:
      - postgres_app_password
      - model_gateway_worker_token
    networks: [data, model]
    restart: "no"

  web:
    build: ./frontend
    depends_on:
      gateway:
        condition: service_healthy
    ports:
      - "127.0.0.1:8000:8080"
    networks: [edge, ingress]
    restart: unless-stopped

volumes:
  postgres_data:
  uploads:

networks:
  edge:
    driver: bridge
  ingress:
    driver: bridge
    internal: true
  data:
    driver: bridge
    internal: true
  model:
    driver: bridge
    internal: true
  egress:
    driver: bridge

secrets:
  postgres_bootstrap_password:
    file: ./secrets/postgres_bootstrap_password
  postgres_migrator_password:
    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 只有一个实例执行;成功应用到 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-completehealthcheck 同时检查 pg_isready 与该哨兵,防止临时初始化 server 提前放行迁移。Compose 的 .env 仅做变量插值,不会自动注入容器;后端从 *_FILE 读取 Secret不把明文密码放进 .envDATABASE_URL

网络分为五层:web 位于普通 edge 和 internal ingress;入口 gateway 位于 ingress + dataAPI 位于 internal data + model;数据库/迁移位于 internal data;只有 model-gateway 位于 internal model + egressprovider-smoke 只持 API 内部 token真实 seed-demo 只持数据库 Secret 和 Worker 内部 token二者都不挂载百炼 Key、不加入 egress。未来 Worker 沿用相同边界。model-gateway 不连接数据库、不挂载上传卷、不发布端口。入口隔离见 ADR-0004,模型出口隔离见 ADR-0005。必须注意:普通 edge bridge 为 Web 提供宿主机端口发布时,也给 Web 留有技术上的默认公网出口;生产仍需主机防火墙或出口代理只允许 model-gateway 到百炼域名。

2.3 后台任务为何不用 Redis

第一版用 PostgreSQL 任务表和租约。领取必须在一个事务中完成选择、fencing token 更新和返回,不能先 SELECT 再 UPDATE

WITH candidate AS (
  SELECT id
  FROM background_jobs
  WHERE attempt < max_attempts
    AND required_capability = ANY(:worker_capabilities)
    AND (
      (status = 'QUEUED' AND run_after <= now())
      OR (status = 'RUNNING' AND lease_until < now())
    )
  ORDER BY priority DESC, run_after, created_at
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
UPDATE background_jobs AS job
SET status = 'RUNNING',
    lease_owner = :worker_id,
    lease_token = gen_random_uuid(),
    lease_until = now() + :lease_interval,
    attempt = attempt + 1,
    updated_at = now()
FROM candidate
WHERE job.id = candidate.id
RETURNING job.*;

background_jobs 还包含 job_typerequired_capabilityresource_type/resource_id 和版本化 payload普通 Worker 只领取解析/向量/评测能力,ocr-worker 只领取 OCR 能力。心跳、阶段完成和最终提交都必须同时匹配 job_id + lease_owner + lease_token;旧 Worker 即使恢复也无法覆盖新租约结果。捕获到任务失败时必须在同一事务分支:若当前 attempt < max_attempts,转回 QUEUED 并按指数退避设置 run_after;否则直接转 FAILED,绝不能留下已耗尽且不可领取的 QUEUED 任务。

普通 Worker 内置每 30 秒运行一次的 maintenance loop并用固定 PostgreSQL advisory lock 保证同一时刻只有一个实例执行 reaper。reaper 只兜底 Worker 崩溃来不及回写的 RUNNING 任务:租约过期且尚有重试次数则原子转回 QUEUED,已耗尽则转 FAILED。领取、reaper 和最终提交都使用 fencing 条件;测试必须强杀执行中的 Worker分别验证“有剩余次数后可重领”“最后一次崩溃后进入 FAILED”“旧 lease token 不能提交”。为领取路径建立覆盖 required_capability/status/run_after/lease_until/priority/created_at 的索引,并监控过期租约与失败率。这样才可证明崩溃恢复,同时避免增加 Redis、Celery 和另一套持久化语义。只有当队列吞吐、调度或跨服务 fan-out 有实测瓶颈时再新增消息系统,并记录 ADR。

3. 凭证生命周期

3.1 立即轮换

任何已出现在对话中的旧 Key

  1. 在百炼北京地域控制台重置或删除;
  2. 审查旧 Key 的调用和账单记录;
  3. 新建最小权限 Key仅允许本项目所需模型和部署出口 IP
  4. 新 Key 写入本机 secrets/bailian_api_key,权限设为仅部署用户可读;
  5. 不使用命令行参数或会进入 shell history 的写法传递密钥;
  6. 运行三模型最小探测;
  7. 记录轮换时间和责任人,不记录 Key 值。

百炼文档说明北京业务空间 Key 可配置模型范围和 IP 白名单;生产应使用最小权限,见API Key 权限

3.2 应用读取

配置层支持 *_FILE

def read_secret(file_env: str, value_env: str) -> str:
    secret_file = os.getenv(file_env)
    if secret_file:
        return Path(secret_file).read_text(encoding="utf-8").strip()
    return os.environ[value_env]

生产只允许文件/Secret Manager本地值环境变量仅作为临时兼容。bailian_api_key 只挂载到 model-gateway。API 与 Worker 分别挂载 model_gateway_api_tokenmodel_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 apiWorker 落地后同时重启 Worker待 readiness 恢复后再运行 provider smoke。禁止只替换文件而让旧进程长期携带旧凭据。

3.3 Git 防泄漏

仓库已有三层控制:

  • .gitignore 忽略 .envsecrets/、私钥、原始/私有数据和备份;
  • .githooks/pre-commit 只扫描 staged 文件,发现长密钥格式即阻止提交;
  • .gitea/workflows/secret-scan.yml 在 push/PR 后复检。

首次克隆执行:

make setup-hooks
make verify

扫描器只输出可疑文件名不回显密钥。它是防线而非保证Code Review 仍需检查配置、测试 fixture、截图、Notebook 输出和文档。

如果密钥进入 Git 历史,先轮换,再评估历史清理;顺序不能反过来。

4. 配置契约

4.1 非敏感配置

.env.example 只保留占位符:

BAILIAN_OPENAI_BASE_URL=https://<workspace-id>.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
BAILIAN_NATIVE_BASE_URL=https://<workspace-id>.cn-beijing.maas.aliyuncs.com/api/v1
BAILIAN_RERANK_BASE_URL=https://<workspace-id>.cn-beijing.maas.aliyuncs.com/compatible-api/v1
DASHSCOPE_API_KEY_FILE=/run/secrets/bailian_api_key

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

  • URL 使用 HTTPS
  • 三个 URL 主机相同且地域为北京;
  • Rerank 路径不是 compatible-mode/v1
  • Embedding 维度为 schema 支持的 1024
  • 生产没有默认密码和通配 CORS
  • Secret 文件存在、非空且权限合理;
  • 内部模型地址必须精确为 http://model-gateway:8000,不能通过配置改成公网或用户控制的 URL
  • API/Worker 内部 token 必须不同caller 必须与 token 身份一致;
  • APP_ENV=production 时关闭开发文档或增加管理员保护。

4.2 地域与计费方案

业务空间专属域名只能用所属工作空间的 Key北京、新加坡等地域 Key 不通用Token Plan/Coding Plan 的 Key 和 Base URL 也不能用于后端服务。错误组合通常表现为 401。官方说明见Base URL 总览

5. 首次部署与模型 Preflight

5.1 部署前

1. 轮换 Key
2. 建立 secrets/ 文件(不提交)
3. 配置真实工作空间 URL不提交
4. make setup-hooks
5. make verify
6. docker compose config确认无密钥被渲染到日志
7. docker compose up -d --build
8. 检查 /health/live 和 /health/ready
9. 执行 provider-smoke
10. 导入无敏感演示数据

不要在支持工单、截图或 CI 日志中粘贴 docker compose config 的完整敏感输出。

5.2 三模型最小探测

管理员命令 provider-smoke 的预期行为:

  1. Embedding一个短文本断言 1024 维;
  2. Rerank一个 query、两个 documents、top_n=1,断言下标和分数;
  3. Chat要求返回极短固定文本关闭思考并限制输出
  4. 输出只含能力名、状态、模型、耗时、request ID 和脱敏错误码;
  5. 不输出请求头、Key、完整 Base URL 查询参数和文档正文。

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 使用内部网络;
  • datamodel 网络启用 Docker internal 隔离Web 不加入API/Worker 只经 model-gateway 访问云服务,自身不加入 egress
  • 生产入口启用 TLS、HSTS 和安全响应头;
  • 出站只允许百炼/OSS 等必要域名;
  • 数据库账号按迁移、应用、备份职责分权;
  • 容器使用非 root 用户、只读根文件系统(需要写入的 volume 单独挂载);
  • Docker socket 不挂入应用容器。

6.2 认证和授权

  • 管理员、资料维护者、普通用户、研究者 RBAC
  • Refresh Token 使用 HttpOnly、Secure、SameSite Cookie
  • 每次检索 SQL 都包含授权知识库/文档过滤;
  • 权限过滤必须在向量候选生成时生效;
  • 文档下载、页图和原文 API 再做对象级权限校验;
  • 可选 PostgreSQL Row-Level Security 作为纵深防御,不能代替应用层测试。

6.3 上传安全

  • 扩展名和真实 MIME 双重验证;
  • 流式上传,不把大文件整体放入内存;
  • 限制文件大小、页数、压缩比和处理时间;
  • 服务端 UUID 文件名,禁止用户路径;
  • 拒绝宏、可执行文件和未知类型;
  • 一期不允许用户输入任意 URL 抓文档,避免 SSRF
  • 可选 ClamAV/云安全扫描;
  • 加密 PDF 进入人工处理,不让 Worker 暴力破解。

7. RAG 特有安全

7.1 Prompt Injection

检索到的文档始终是不可信数据:

  • 系统 Prompt 明确忽略文档内命令;
  • 上下文使用结构化 <source> 包裹并编号;
  • 模型无直接数据库、文件或网络工具权限;
  • 不根据文档内容修改系统配置或权限;
  • 回答引用编号由后端校验;
  • 将注入样例纳入安全回归集。

7.2 数据出域

向量化、重排和生成都会把相应文本发送到百炼。因此:

  • cloud_processing_allowed=false 的文档不可进入云模型链;
  • 权限和保密字段在排队前检查Worker 再次检查;
  • 日志与遥测不附带文档正文;
  • 对个人信息和精确敏感坐标执行脱敏或拒绝;
  • 若未来需要处理受限资料,必须采用审批后的本地模型部署并新增 ADR不能悄悄切换。

7.3 输出安全

  • Markdown 清理 HTML 和危险 URL
  • 对资源量、储量、投资、施工类问题显示专业免责声明;
  • 生成内容不自动写回知识库;
  • 用户反馈与更正进入人工审核,不直接成为新事实;
  • 不让模型生成不存在的下载链接和文件路径。

8. 健康检查与可观测性

8.1 健康端点

  • /health/live:进程事件循环可用,不检查外部模型;
  • API /health/ready:当前验证数据库可用;后续再纳入迁移版本和存储路径对账;
  • model-gateway /health/ready:只验证本地 Key/token/端点配置可加载且未漂移,不产生计费云调用;
  • /admin/providers/health:按需调用三个模型,结果短期缓存;
  • /admin/index/healthdocuments.active_version_id、searchable 切片投影、非空向量、文本哈希和 embedding profile 一致性。

百炼短时波动不应让编排器不断重启健康 API 容器。真实三能力是否可用只能由显式 provider smoke 证明,不能从 model-gateway readiness 推断。

8.2 日志脱敏

结构化日志保留 trace_id、阶段、状态、模型、Token、耗时、request ID。下列字段永久屏蔽

authorization
api_key
cookie
password
secret
raw_document
embedding vector values

Provider 异常可能把请求信息嵌入消息,记录前使用 allowlist 选择安全字段,而不是对整段异常做不可靠字符串替换。

9. 备份、恢复和删除

9.1 备份

  • PostgreSQL定期 pg_dump;生产可使用 WAL/PITR
  • 原始文件:开发 volume 备份,生产 OSS 版本控制;
  • 评测产物:配置、指标和脱敏预测与 Git commit 关联;
  • Secret 不进入普通数据备份,使用密钥管理系统单独恢复流程;
  • 每次正式演示前生成已验证的恢复点。

9.2 恢复演练

至少完成一次:

  1. 在空白 Compose project 中启动数据库;
  2. 恢复数据库和允许恢复的文件;
  3. 校验 schema revision
  4. 比较文档数、切片数、向量维度、哈希抽样;
  5. 运行 20 道固定冒烟问题;
  6. 保存恢复耗时和缺口。

只有生成备份文件而未恢复不算验收完成。

9.3 删除

用户删除文档时:

  1. 事务内清空 documents.active_version_id 并令对应切片 searchable=false,阻断检索;
  2. 创建物理删除任务;
  3. 清理切片、文件和衍生缓存;
  4. 保留最小审计记录和不可逆哈希,不保留原文;
  5. 对备份中的保留周期和销毁策略做文档说明。

10. 上云演进

单机组件 生产替代 触发条件
Docker volume 阿里云 OSS 多主机、备份和生命周期需求
单机 PostgreSQL RDS PostgreSQL + pgvector 高可用、PITR、容量需求
DB 任务队列 Redis/Celery 或云消息服务 实测队列吞吐/调度瓶颈
pgvector Qdrant/专用向量服务 百万级、高 QPS、复杂过滤瓶颈
本地日志 集中日志/指标 多实例和审计需求

任何演进都需要迁移计划、回滚方案、数据对账和 ADR不以“更云原生”为唯一理由。