系统架构设计.md 29 KB

系统架构设计

本文引用的文件

  • ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java
  • ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java
  • ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java
  • ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java
  • ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteRepository.java
  • ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java
  • ai-gateway/src/main/java/com/zsjz/gateway/route/NodeSelector.java
  • ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java
  • ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java
  • ai-gateway/src/main/java/com/zsjz/gateway/registry/NodeHealthRegistry.java
  • ai-gateway/src/main/java/com/zsjz/gateway/config/GatewayProperties.java
  • sql/gateway.sql
  • ai-electron/electron/preload/bridge.ts
  • ai-electron/electron/main.ts
  • ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java
  • ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java
  • ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java
  • ai-server/src/main/java/com/zsjz/ai/module/agent/rag/RagSchemaService.java

目录

  1. 引言
  2. 项目结构
  3. 核心组件
  4. 架构总览
  5. 详细组件分析
  6. 依赖关系分析
  7. 性能与可扩展性
  8. 故障排查指南
  9. 结论
  10. 附录:部署拓扑

引言

本架构文档描述清鉴 AI 调查工具的分层模型:Electron 客户端 → Preload Bridge → Spring Cloud Gateway(ai-gateway)→ ai-server 业务模块 → PostgreSQL/pgvector/Redis/文件系统。
该系统的核心约束是“粘性路由”:用户会话必须固定到同一 ai-server 节点,因为案件数据源、本地工作区、RAG 索引和 AgentScope 中间状态都位于节点本地。网关在登录后把用户绑定到某个后端节点,并在后续请求中强制复用;若绑定节点不可用,网关返回 503,而不是自动切换。

项目结构

仓库采用多模块 Maven 工程,根 pom.xml 声明两个子模块:

graph TB
    Root["根工程 zsjz-ai<br/>Maven POM"] --> Server["ai-server<br/>Spring Boot 业务服务"]
    Root --> Gateway["ai-gateway<br/>Spring Cloud Gateway"]
    Server --> PG["PostgreSQL / pgvector"]
    Server --> Redis["Redis"]
    Server --> FS["本地文件系统"]
    Gateway --> Server

图表来源

  • pom.xml:1-23

章节来源

  • pom.xml:1-23

核心组件

  • Electron 客户端:桌面应用外壳,通过 preload 桥接暴露 IPC 能力,渲染进程使用受限上下文。
  • Preload Bridge:隔离主进程 API,仅导出受控的 electron.ipcRenderer,避免渲染进程直接访问 Electron 内部对象。
  • Spring Cloud Gateway:统一入口,承担跨域、管理端白名单、服务发现、节点健康检查、用户粘性路由、SSE 流式转发等职责。
  • ai-server:按业务模块划分(Agent、DM、Call、Track、Trans、Govern 等),承载案件数据源、AgentScope RAG、Python/SQL 分析、附件处理、SSE 研判输出。
  • 持久层:PostgreSQL/pgvector 存储业务数据与向量检索结果,Redis 作为网关 Token 缓存和用户路由热缓存,文件系统承载附件、工作区、DuckDB 等本地数据。

章节来源

  • ai-electron/electron/preload/bridge.ts:1-10
  • ai-electron/electron/main.ts:1-19
  • ai-gateway/pom.xml:37-37

架构总览

从端到端视角,一次带登录和分析的请求流程如下:

sequenceDiagram
    participant Client as "Electron 客户端"
    participant Preload as "Preload Bridge"
    participant Gateway as "ai-gateway"
    participant Service as "ai-server 节点"
    participant DB as "PostgreSQL/pgvector"
    participant Cache as "Redis"
    participant File as "本地文件系统"

    Client->>Preload: 发起登录或业务请求
    Preload-->>Client: 经 ipcRenderer 转发请求
    Client->>Gateway: POST /js/a/auth/login 或业务接口
    Gateway->>Gateway: LoginRequestFilter 解析 username
    Gateway->>Gateway: StickyRouteFilter 选择并固定节点
    alt 首次登录
        Gateway->>Service: 转发登录请求
        Service->>DB: 校验账号
        Service->>Cache: sa-token 写入
        Service-->>Gateway: Result{token, userId, username}
        Gateway->>Gateway: LoginResponseFilter 写库绑定 + 缓存 token
    else 已登录
        Gateway->>Cache: 查询 x-token → userId
        Gateway->>Gateway: 查找用户绑定的 nodeId
        Gateway->>Service: 转发业务请求
        Service->>DB: 读取案件数据源
        Service->>File: 读取工作区/附件
        Service-->>Gateway: SSE 或 JSON 响应
    end
    Gateway-->>Client: 透传响应

图表来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java:1-120
  • ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180
  • ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java:1-176
  • ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java:1-100

详细组件分析

粘性路由设计目标

粘性路由的目标不是负载均衡,而是“会话一致性”。每个 ai-server 节点持有:

  • 案件的动态数据源注册;
  • 本地工作区、附件、DuckDB 等文件系统状态;
  • AgentScope RAG 的嵌入索引和会话上下文;
  • 由 StpUtil 管理的 session/token 信息(可通过共享 Redis 跨实例恢复,但本地工作区不能)。

因此,一旦用户绑定到某个节点,后续请求必须继续命中该节点。如果节点被停用或健康检查失败,网关拒绝自动切换,而是返回 503,提示管理员修复节点或重新绑定。

章节来源

  • sql/gateway.sql:1-107

StickyRouteFilter:在 RouteToRequestFilter 之后重写目标 URL

StickyRouteFilter 是粘性路由的核心全局过滤器,执行顺序为 10001,排在 Spring Cloud Gateway 内置的 RouteToRequestUrlFilter(顺序 10000)之后。它的工作是:

  1. 根据当前路由元数据确定目标服务;
  2. 区分登录请求与普通请求;
  3. 登录请求优先按 username 查绑定,未绑定时轮询候选节点但不落库;
  4. 普通请求按 x-token 解析 userId,再查绑定;未绑定时尝试绑定到服务的默认节点;
  5. 对匿名请求走健康节点轮询;
  6. 校验选定节点的启用状态与健康状态;
  7. 设置 GATEWAY_REQUEST_URL_ATTR,将目标 URL 重写为“节点 base_url + 原始 rawPath + rawQuery”;
  8. 将 X-Gateway-Node 写入响应头,便于客户端验证会话是否始终落在同一节点。

    flowchart TD
    Start(["进入 StickyRouteFilter"]) --> CheckRoute["检查是否匹配 ai-server 路由"]
    CheckRoute --> IsLogin{"是否登录请求?"}
    IsLogin -->|是| DecideLogin["按 username 查绑定<br/>未绑定则轮询候选节点"]
    IsLogin -->|否| DecideNormal["按 x-token 解析 userId<br/>查绑定或绑定默认节点"]
    DecideLogin --> SelectNode["确定 nodeId"]
    DecideNormal --> SelectNode
    SelectNode --> ValidateNode{"节点启用且健康?"}
    ValidateNode -->|否| Return503["抛出 503 服务不可用"]
    ValidateNode -->|是| RewriteURL["重写 GATEWAY_REQUEST_URL_ATTR<br/>base_url + rawPath + rawQuery"]
    RewriteURL --> NextChain["交给 NettyRoutingFilter 流式转发"]
    

图表来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180

章节来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180

LoginRequestFilter:登录前解析 username

LoginRequestFilter 只在配置的完整登录路径上生效,负责:

  • 限制最大请求体大小(64KB);
  • 解析登录体中的 username;
  • 标记 LOGIN_REQUEST 属性;
  • 强制把 Accept-Encoding 改为 identity,避免后端 gzip 压缩导致响应无法被 LoginResponseFilter 正确解析;
  • 重建可重复读取的请求体,确保原业务内容不被修改。

如果请求体为空、超限、非 JSON 或缺少 username,过滤器不会阻断请求,而是按匿名轮询处理,由后端自行校验登录逻辑。

章节来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java:1-120

LoginResponseFilter:登录成功后写库绑定

LoginResponseFilter 的执行顺序为 NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1,即比真正写出响应的过滤器更早一层。它在响应写出时:

  1. 确认当前请求属于某个服务的登录路径;
  2. 装饰响应,收集最多 256KB 的响应体;
  3. 判断响应是否为成功登录结构(code=200,data 包含 userId、username);
  4. 调用 UserRouteService.bindIfAbsent 把用户绑定到 StickyRouteFilter 选定的节点;
  5. 调用 TokenResolver.cacheToken 把 gateway:token:{serviceId}:{token} 写入 Redis,TTL 24 小时;
  6. 无论绑定成功与否,都不改变原始登录响应。

绑定操作是 fire-and-forget:即使客户端断开连接或数据库写入失败,也不会中断本次登录响应。

sequenceDiagram
    participant GW as "ai-gateway"
    participant LRF as "LoginResponseFilter"
    participant URS as "UserRouteService"
    participant TR as "TokenResolver"
    participant PG as "PostgreSQL"
    participant RD as "Redis"

    GW->>LRF: 拦截登录响应
    LRF->>LRF: 收集响应体(≤256KB)
    LRF->>LRF: 解析 Result{code,data.token,data.userId,data.username}
    alt 登录成功
        LRF->>URS: bindIfAbsent(serviceId, userId, username, chosenNodeId)
        URS->>PG: ON CONFLICT DO NOTHING 插入
        URS-->>LRF: 生效的 nodeId
        LRF->>TR: cacheToken(serviceId, token, userId)
        TR->>RD: 写入 gateway:token:...
    else 登录失败或结构不匹配
        LRF->>LRF: 跳过绑定
    end
    LRF-->>GW: 原样写出响应

图表来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java:1-176
  • ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java:1-117
  • ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java:1-100
  • sql/gateway.sql:1-107

章节来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java:1-176
  • ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java:1-117

TokenResolver:x-token → userId 两级解析

TokenResolver 提供三类能力:

  • resolveUserId:先查 Redis 缓存键 gateway:token:{serviceId}:{token},未命中时调用 StpUtil 从 sa-token 共享 Redis 解析,再回填缓存;
  • cacheToken:登录成功后主动写入缓存;
  • resolveUserIdDirect:管理端低频认证直接查 sa-token。

解析失败返回空 Mono,由 StickyRouteFilter 降级为匿名请求,让后端返回 401 引导重新登录。

章节来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java:1-100

UserRouteService 与 UserRouteRepository:用户路由缓存与持久化

UserRouteService 是 Redis 两级缓存入口,键包括:

  • gateway:route:user:{serviceId}:{userId};
  • gateway:route:name:{serviceId}:{lower(username)}。

读失败降级直查数据库;写失败只记日志,下次读回源。UserRouteRepository 使用 JdbcTemplate 访问 PostgreSQL,并发安全由唯一索引裁决:

  • (service_id, user_id);
  • (service_id, lower(username))。

bindIfAbsent 使用 ON CONFLICT DO NOTHING,并发下由数据库决定赢家,随后再次读取生效节点。

章节来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java:1-117
  • ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteRepository.java:1-227
  • sql/gateway.sql:60-107

NodeSelector:匿名与新用户轮询

NodeSelector 只对无绑定请求生效。它会过滤出启用且健康的节点,单节点直接返回,多节点按服务维度原子计数器轮询。注意:已绑定用户的节点即使 DOWN 也不会被换掉。

章节来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/NodeSelector.java:1-45

服务注册与健康检查

GatewayRegistry 维护服务与节点的内存快照,事实来源是 PostgreSQL。启动时用 YAML 播种,运行中每 10 秒对账,变化时发布 RefreshRoutesEvent 刷新路由缓存。

HealthChecker 定时探测每个启用的节点健康路径,连续失败达到阈值标记 DOWN,一次成功恢复 UP。NodeHealthRegistry 是每网关实例的内存状态,UNKNOWN 乐观视为可用,避免刚启动时全部 503。

章节来源

  • ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java:1-116
  • ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java:1-62
  • ai-gateway/src/main/java/com/zsjz/gateway/registry/NodeHealthRegistry.java:1-68
  • ai-gateway/src/main/java/com/zsjz/gateway/config/GatewayProperties.java:1-81

客户端与跨进程安全隔离

Electron 主进程通过 main.ts 注册生命周期与 preload,preload 通过 contextBridge.exposeInMainWorld('electron', ...) 仅暴露 ipcRenderer。渲染进程不能直接访问 Electron 原生 API,从而降低 XSS 或恶意前端代码对宿主系统的风险。

graph LR
    Renderer["渲染进程<br/>Vue 前端"] --> Bridge["contextBridge<br/>bridge.ts"]
    Bridge --> MainProcess["主进程<br/>main.ts + ee-core"]
    MainProcess --> Network["网络请求<br/>指向 ai-gateway"]

图表来源

  • ai-electron/electron/preload/bridge.ts:1-10
  • ai-electron/electron/main.ts:1-19

章节来源

  • ai-electron/electron/preload/bridge.ts:1-10
  • ai-electron/electron/main.ts:1-19

SSE 心跳保活

Spring Cloud Gateway 基于 WebFlux/Netty,支持流式转发,天然兼容 SSE、multipart 和 Range 下载。配置中明确不设全局响应超时,以容纳长时间运行的 SSE 研判连接。粘性路由保证同一次会话落到同一 ai-server,避免 SSE 中途切到另一个没有对应 AgentScope 状态的节点。

建议运维侧在网关上游再叠加反向代理心跳策略(如 Nginx proxy_read_timeout、云负载均衡空闲超时),但不应把业务长连接误判为空闲而切断。

章节来源

  • ai-gateway/pom.xml:37-37
  • ai-gateway/README.md:108-108

动态数据源切换

ai-server 内按案件维度管理动态数据源。CaseDataSourceRegistry 负责注册和查询数据源,CaseRoutingDataSource 作为动态路由数据源,把线程或会话上下文的 caseId 映射到具体数据库连接。这样不同案件可以访问不同的 PostgreSQL 库表,同时共享 ai-server 自身的平台库。

classDiagram
    class CaseDataSourceRegistry {
        +注册数据源()
        +获取数据源()
        +清理数据源()
    }
    class CaseRoutingDataSource {
        +determineCurrentLookupKey()
    }
    class DataSourceConfig {
        +初始化平台数据源()
    }
    CaseRoutingDataSource --> CaseDataSourceRegistry : "查询当前案件数据源"
    DataSourceConfig --> CaseDataSourceRegistry : "注册平台数据源"

图表来源

  • ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java
  • ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java
  • ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java

章节来源

  • ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java
  • ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java
  • ai-server/src/main/java/com/zsjz/ai/common/config/DataSourceConfig.java

AgentScope RAG 调用

ai-server 的 agent 模块包含 RAG 相关类,例如 RagSchemaService、EmbeddingProvider、WorkspaceSchemaCatalog、AttachmentContentRouter 等,负责 schema 管理、向量嵌入、工作区目录扫描、附件内容路由等。Agent 聊天实现 AgentChatServiceImpl 承载会话、意图、工具调用和结果聚合。

这些能力依赖 PostgreSQL/pgvector 做向量检索,依赖本地文件系统存取工作区、技能和附件,因此必须保持用户会话固定在同一个 ai-server 节点。

章节来源

  • ai-server/src/main/java/com/zsjz/ai/module/agent/rag/RagSchemaService.java
  • ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java

依赖关系分析

graph TB
    Client["Electron 客户端"] --> Preload["Preload Bridge"]
    Preload --> Gateway["ai-gateway"]
    subgraph GatewayLayer
        LRF["LoginRequestFilter"]
        SRF["StickyRouteFilter"]
        LResF["LoginResponseFilter"]
        TR["TokenResolver"]
        URS["UserRouteService"]
        UR["UserRouteRepository"]
        NS["NodeSelector"]
        GR["GatewayRegistry"]
        HC["HealthChecker"]
        HReg["NodeHealthRegistry"]
    end
    subgraph BackendLayer
        AS["ai-server"]
        AChat["AgentChatServiceImpl"]
        RAG["RagSchemaService"]
        CDR["CaseRoutingDataSource"]
        CDS["CaseDataSourceRegistry"]
    end
    subgraph Storage
        PG["PostgreSQL/pgvector"]
        RD["Redis"]
        FS["本地文件系统"]
    end

    Client --> Gateway
    LRF --> SRF
    SRF --> TR
    SRF --> URS
    SRF --> NS
    LResF --> URS
    LResF --> TR
    URS --> UR
    TR --> RD
    UR --> PG
    GR --> PG
    HC --> RD
    HC --> HReg
    AS --> PG
    AS --> RD
    AS --> FS
    AChat --> RAG
    AChat --> CDR
    CDR --> CDS

图表来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/LoginRequestFilter.java:1-120
  • ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180
  • ai-gateway/src/main/java/com/zsjz/gateway/route/LoginResponseFilter.java:1-176
  • ai-gateway/src/main/java/com/zsjz/gateway/route/TokenResolver.java:1-100
  • ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteService.java:1-117
  • ai-gateway/src/main/java/com/zsjz/gateway/route/UserRouteRepository.java:1-227
  • ai-gateway/src/main/java/com/zsjz/gateway/route/NodeSelector.java:1-45
  • ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java:1-116
  • ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java:1-62
  • ai-gateway/src/main/java/com/zsjz/gateway/registry/NodeHealthRegistry.java:1-68
  • ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java
  • ai-server/src/main/java/com/zsjz/ai/module/agent/rag/RagSchemaService.java
  • ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java
  • ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java

性能与可扩展性

多 ai-server 节点扩展

  • 新增节点只需在 PostgreSQL 的 gateway_node 表中登记,或通过管理端更新后触发 RefreshRoutesEvent;
  • 新用户的登录会轮询健康节点,分散新用户负载;
  • 已登录用户仍粘在原有节点,避免共享状态不一致;
  • 健康检查周期和失败阈值可通过 GatewayProperties.HealthCheck 调整。

容错策略

  • 绑定节点 DOWN:StickyRouteFilter 直接抛 503,不自动切换;
  • 所有健康节点 DOWN:匿名请求和新用户分配也返回 503;
  • Token 过期或被顶下线:解析失败后按匿名处理,后端返回 401,客户端重登;
  • Redis 缓存故障:Token 解析和路由缓存均降级直查 sa-token 或数据库,不影响正确性;
  • 登录绑定失败:fire-and-forget,不阻塞登录响应,下次登录补绑。

SSE 与长连接

  • 网关不设置全局响应超时,允许 SSE 研判长时间保持;
  • 粘性路由保证同一次 SSE 会话不落盘不同节点;
  • 建议在网关上游负载均衡器中合理配置空闲超时,避免误杀长连接。

横切关注点总结

  • 跨进程安全隔离:Electron preload 仅暴露最小 API;
  • 身份认证:sa-token + Redis 共享缓存;
  • 粘性路由:PostgreSQL 用户路由表 + Redis 两级缓存;
  • 健康检查:WebClient 探活 + 内存状态机;
  • 流式转发:WebFlux/Netty 支持 SSE;
  • 动态数据源:按案件切换 PostgreSQL 连接;
  • RAG 调用:AgentScope 工作区、向量索引、附件内容路由。

[本节为通用性能讨论,不直接分析具体代码行]

故障排查指南

现象 可能原因 排查要点
登录后多次请求跳到不同 ai-server StickyRouteFilter 未命中绑定 检查 X-Gateway-Node 响应头是否一致;查看 gateway_user_route 是否有记录
绑定节点停止后业务报 503 粘性路由禁止自动切换 检查 HealthChecker 健康状态;恢复节点或手动重绑
登录成功但后续请求 401 Token 失效或被顶下线 检查 Redis gateway:token:*;确认 sa-token 是否有效
新用户一直 503 所有节点 DOWN 或未启用 检查 gateway_node.enabled、健康检查路径、网络连通性
SSE 中途断开 上游负载均衡或浏览器关闭 检查负载均衡空闲超时;确认浏览器标签页是否关闭
动态数据源找不到 案件未注册或 caseId 上下文丢失 检查 CaseDataSourceRegistry;确认 CaseRoutingDataSource 路由键
RAG 搜索不到结果 向量索引未构建或工作区不完整 检查 RagSchemaService、附件索引、本地文件系统

章节来源

  • ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java:1-180
  • ai-gateway/src/main/java/com/zsjz/gateway/registry/HealthChecker.java:1-62
  • sql/gateway.sql:1-107

结论

清鉴 AI 调查工具通过“Electron 客户端 + Preload 安全桥 + Spring Cloud Gateway + ai-server + PostgreSQL/pgvector/Redis/文件系统”的分层架构,把安全性、可观测性和稳定性放在前端与网关层,把业务复杂度和本地状态放在 ai-server 层。粘性路由是该系统的核心约束:它牺牲了传统负载均衡的自由度,换取了 AgentScope RAG、动态数据源和本地文件系统的一致性。扩展性通过多 ai-server 节点实现,容错通过“DOWN 返回 503”明确边界,避免静默数据错配。

[本节为总结性内容,不直接分析具体文件]

附录:部署拓扑

graph TB
    LB["外部入口<br/>Nginx / 云负载均衡"] --> GW1["ai-gateway 实例 1"]
    LB --> GW2["ai-gateway 实例 2"]
    GW1 --> AS1["ai-server 节点 1"]
    GW2 --> AS2["ai-server 节点 2"]
    GW1 --> PG["PostgreSQL/pgvector"]
    GW2 --> PG
    GW1 --> RD["Redis"]
    GW2 --> RD
    AS1 --> PG
    AS2 --> PG
    AS1 --> FS1["节点 1 本地文件系统"]
    AS2 --> FS2["节点 2 本地文件系统"]

关键约束:

  • 每个 ai-server 节点拥有独立本地文件系统;
  • PostgreSQL 与 Redis 是共享资源;
  • 多个 ai-gateway 实例共享同一 PostgreSQL 配置表和路由表;
  • 用户会话固定到 ai-server 节点,而非负载均衡随机分发。

[本节为概念性部署拓扑,不直接映射单一源码文件]