本文引用的文件
本指南面向生产环境,说明 ai-gateway 与 ai-server 的 Maven 构建、环境变量、启动流程、健康检查、日志位置、粘性路由策略和常见故障。系统由一个 Maven 父工程管理两个 Spring Boot 子模块:
| 组件 | 角色 | 默认端口 | 对外路径 |
|---|---|---|---|
ai-gateway |
按用户粘性转发请求到指定 ai-server 节点 |
8080 |
/js/a 前缀的服务配置在 YAML |
ai-server |
业务后端,承载平台、Agent、清洗、知识图谱、检索等模块 | 8980 |
/js/a |
网关不复制业务逻辑,只负责解析登录态、选择节点、重写目标地址并流式转发;鉴权、会话、案件数据源都保留在 ai-server。
graph TB
subgraph "Maven 父工程"
Parent["zsjz-ai<br/>packaging=pom"]
ServerMod["ai-server"]
GatewayMod["ai-gateway"]
end
subgraph "ai-server 业务进程"
App["App.java<br/>SpringBootApplication"]
Cors["CorsConfig.java<br/>跨域过滤器"]
HealthCtrl["SystemController.java<br/>/sys/health"]
HealthSvc["SystemHealthService.java<br/>PG/Redis/ES 探测"]
end
subgraph "ai-gateway 网关进程"
GwApp["GatewayApplication.java<br/>SpringBootApplication"]
LoginFilter["LoginRequestFilter.java<br/>登录预处理"]
StickyFilter["StickyRouteFilter.java<br/>粘性路由"]
HealthCheck["HealthChecker.java<br/>节点探活"]
end
DB["PostgreSQL"]
Redis["Redis"]
ES["Elasticsearch"]
Neo4j["Neo4j(可选)"]
Parent --> ServerMod
Parent --> GatewayMod
Client["浏览器 / Electron 前端"] --> GwApp
GwApp --> App
App --> DB
App --> Redis
App --> ES
App --> Neo4j
图示来源
章节来源
根 pom.xml 声明父为 Spring Boot 3.5.16,自身是 packaging=pom,并按以下顺序声明模块:
ai-serverai-gatewayMaven 聚合构建时按声明顺序处理模块,因此应先编译 ai-server,再编译 ai-gateway。若只构建某个模块,需确保父 POM 可从当前目录解析。
| 模块 | 构建插件 | 产物 | 启动方式 |
|---|---|---|---|
ai-server |
spring-boot-maven-plugin |
ai-server/target/ai-server-1.0.jar |
java -jar ai-server.jar |
ai-gateway |
spring-boot-maven-plugin |
ai-gateway/target/ai-gateway-1.0.jar |
java -jar ai-gateway.jar |
ai-gateway 还通过 maven-resources-plugin 在 generate-resources 阶段把仓库根的 sql/gateway.sql 复制到 target/classes/db/gateway.sql,供 spring.sql.init 使用。该 DDL 使用幂等建表语句,可重复执行。
mvn clean package -DskipTests
生产构建建议跳过测试,避免测试依赖本地开发数据库或外部中间件。若必须运行测试,应提前准备 PostgreSQL、Redis、Elasticsearch 等依赖。
章节来源
所有 ai-server 的配置项采用 ${ENV_NAME:默认值} 形式,未设置环境变量时回退到 YAML 默认值。统一变量定义位于 deploy/env.sh。
set -a; source deploy/env.sh; set +a
java -jar ai-server.jar
set -a:后续 export 形式的赋值自动导出为当前 shell 的环境变量。source deploy/env.sh:加载数据库、Redis、ES、DuckDB、RAG、向量模型等配置。set +a:关闭自动导出,避免污染后续命令。这种方式适合运维脚本、CI 流水线或临时调试。
systemd 单元中配置:
[Service]
EnvironmentFile=/opt/zsjz-ai/deploy/env.sh
ExecStart=java -jar /opt/zsjz-ai/ai-server.jar
systemd 会读取文件中 KEY="value" 形式的变量并注入进程环境。注意:
env.sh 注释明确标注其同时支持上述两种用法。Environment= 与 EnvironmentFile= 重复声明同一变量,否则可能覆盖预期值。SPRING_PROFILES_ACTIVE 由 Spring 原生机制控制,默认 dev;生产可取消注释该行启用正式 profile。deploy/env.sh 包含 PostgreSQL、Redis、Neo4j 的连接信息,其中数据库口令属于敏感凭据。部署时必须:
env.sh 放在受控目录,例如 /opt/zsjz-ai/deploy/env.sh。chmod 600 deploy/env.sh。章节来源
GatewayApplication 是网关唯一入口,注解包括:
@SpringBootApplication:启用自动装配与组件扫描。@EnableScheduling:开启定时任务。@ConfigurationPropertiesScan:扫描 gateway.* 配置属性。main 方法直接调用 SpringApplication.run(GatewayApplication.class, args)。
application.yaml,激活 dev profile,并拷贝 gateway.sql。classpath:db/gateway.sql 建表。x-token → loginId。HealthChecker 开始轮询节点健康状态。8080 端口。server.port=8080。connect-timeout=3000ms。DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE,防止后端跨域响应头重复。sa-token 配置(token 名称、超时、并发模式等)必须与 ai-server 一致,否则 token 键位无法解析。
sequenceDiagram
participant JVM as "JVM"
participant Boot as "SpringApplication"
participant SqlInit as "SQL 初始化"
participant Redis as "Reactive Redis"
participant SaToken as "sa-token 客户端"
participant Scheduler as "调度器"
participant Health as "HealthChecker"
JVM->>Boot: 启动 GatewayApplication
Boot->>SqlInit: 执行 gateway.sql
SqlInit-->>Boot: 建表完成
Boot->>Redis: 连接 Redis
Boot->>SaToken: 初始化 token 解析
Boot->>Scheduler: 启用 @EnableScheduling
Scheduler->>Health: initialDelay=5s 后首次探活
Health-->>JVM: 周期探测后端节点
图示来源
章节来源
App 是 ai-server 的启动类,注解包括:
@Slf4j@EnableScheduling:启用 SSE 心跳、数据源回收等定时任务。@SpringBootApplicationmain 方法先打印“开始启动服务...”,再调用 SpringApplication.run(App.class, args)。注释中的 init() 工作空间迁移逻辑当前被注释掉,不影响正常启动。
ai-server 不是按 Spring Module 拆分,而是按业务域组织在 com.zsjz.ai.module 下。启动时由 Spring 自动发现这些包下的控制器、服务、Mapper、配置等组件。主要业务域包括:
| 模块 | 职责 |
|---|---|
plat |
平台基础:用户、授权、系统信息、健康检查、文件下载 |
agent |
Agent 框架、LLM 调用、RAG、附件解析、Python 执行、洞察会话 |
aiclean |
AI 智能清洗、模板向量索引、模板匹配 |
knowledge |
知识库文档与向量索引 |
graph |
图谱相关能力 |
person |
人物关系与亲密度 |
call |
通话记录查询与分析 |
trans |
交易记录查询 |
track |
轨迹与同行分析 |
govern |
治理树、公共配置 |
search |
全文检索封装 |
dm |
数据管理、文件上传、AI 识别 |
otg |
时序数据 |
external |
外部数据接入 |
application.yaml,端口 8980,上下文路径 /js/a。SPRING_PROFILES_ACTIVE 加载 application-dev.yaml 或 application-pro.yaml。暴露 /js/a/** 业务接口。
flowchart TD
Start["启动 App.main()"] --> Log["打印启动日志"]
Log --> Boot["SpringApplication.run()"]
Boot --> Config["加载 application.yaml<br/>+ 环境变量覆盖"]
Config --> Cors["注册全局 CORS 过滤器"]
Config --> DataSource["初始化 PostgreSQL 主库"]
Config --> Redis["初始化 Redis"]
Config --> ES["初始化 Elasticsearch"]
Config --> DuckDB["初始化 DuckDB 预算与回收器"]
Config --> Modules["扫描 plat/agent/aiclean/knowledge/graph/person/call/trans/track/govern/search/dm/otg/external"]
Modules --> Scheduling["启用 @EnableScheduling"]
Scheduling --> Ready["服务就绪 8980/js/a"]
图示来源
章节来源
系统中存在两层健康检查:
| 层级 | 接口/实现 | 作用 | 是否检查下游 |
|---|---|---|---|
| 网关侧节点探活 | HealthChecker |
周期性 GET {base-url}{health-path} |
只检查 HTTP 可达性 |
| 业务侧健康接口 | SystemController.health() |
/js/a/sys/health |
只返回成功,不查 PG/Redis/ES |
| 业务侧服务健康详情 | SystemHealthService.check() |
/js/a/sys/serviceHealth |
并行探测 PG、Redis、ES |
HealthChecker 每 gateway.health-check.interval(默认 10s)遍历所有启用节点,调用其健康路径。连续失败达到 failure-threshold(默认 3)标记 DOWN,一次成功恢复 UP。探活只证明进程存活,不验证数据库或搜索引擎可用。
/sys/health 始终返回成功,适合 Kubernetes liveness/readiness 探针或负载均衡健康检查。/sys/serviceHealth 才真正探测 PostgreSQL、Redis、Elasticsearch,并使用虚拟线程并行执行,单项探测最多等待 3s。
sequenceDiagram
participant GW as "HealthChecker"
participant Node as "ai-server 节点"
participant Ctrl as "SystemController"
loop 每 10 秒
GW->>Node: GET {base-url}/js/a/sys/health
Node->>Ctrl: 映射 /sys/health
Ctrl-->>GW: 200 OK
end
图示来源
章节来源
StickyRouteFilter 是网关粘性路由的核心,order 为 10001,在 Spring Cloud Gateway 的 URL 计算过滤器之后执行。其职责如下:
| 场景 | 行为 |
|---|---|
| 登录请求且用户名已绑定 | 固定转发到绑定节点 |
| 登录请求且未绑定 | 从健康节点轮询选候选节点,但不写绑定关系 |
| 普通请求且 token 有效且有绑定 | 转发到绑定节点 |
| 普通请求且 token 有效但无绑定 | 尝试绑定到 default-node |
| token 无效、过期或被顶掉 | 按匿名请求轮询健康节点,后端返回 401 |
| 绑定节点停用或 DOWN | 返回 503,不自动切换 |
| 无 token 的匿名请求 | 轮询健康节点 |
LoginRequestFilter(order 9999)只对配置的登录路径生效:
64KB 的请求体。username。Accept-Encoding 改为 identity,避免后端 gzip 响应影响响应体解析。解析失败时不阻断请求,按匿名轮询处理。
flowchart TD
Req["网关收到请求"] --> CheckPath{"是否登录路径?"}
CheckPath -->|否| Normal["走普通粘性路由"]
CheckPath -->|是| ReadBody["缓存请求体 ≤64KB"]
ReadBody --> ParseUser{"能否解析 username?"}
ParseUser -->|能| MarkLogin["标记 LOGIN_REQUEST<br/>携带 username"]
ParseUser -->|否| Anonym["按匿名轮询"]
MarkLogin --> RewriteEncoding["Accept-Encoding = identity"]
RewriteEncoding --> Forward["转发给 ai-server"]
Anonym --> Forward
Normal --> Decide{"有 x-token?"}
Decide -->|有| Resolve["解析 loginId → userId → 绑定节点"]
Decide -->|无| SelectNode["轮询健康节点"]
Resolve --> Route["重写目标 URI 并转发"]
SelectNode --> Route
图示来源
网关默认关闭自身跨域,并在 default filter 中对响应头做去重。实际跨域由 ai-server 的 CorsConfig 处理:
Content-Disposition,以便跨域下载时取文件名。credentials: include 的 fetch 与 SSE。app.cors.allowed-origin-patterns 收敛到具体域名。章节来源
适合单机调试:
ai-server,端口 8980。application.yaml 的 gateway.services.nodes 中把 node-1 指向本机 http://localhost:8980。ai-gateway,端口 8080。http://host:8080/js/a。优点:链路完整,可验证网关粘性路由。缺点:没有多节点容灾。
生产常用形态:
ai-server 实例,每个实例配置不同的 node.id 和 base-url。ai-gateway 作为入口。当新用户登录时,网关从健康节点中选择候选;已绑定用户始终回到同一节点,保证案件本地文件和 DuckDB 快照一致性。
需要至少两台网关配合反向代理:
ai-gateway。gateway_service 表和 Redis。已绑定用户仍会被导向其原始节点,除非管理员解除绑定。
graph TB
LB["负载均衡器"]
GW1["ai-gateway #1"]
GW2["ai-gateway #2"]
SRV1["ai-server node-1"]
SRV2["ai-server node-2"]
PG["PostgreSQL"]
RDS["Redis"]
ES["Elasticsearch"]
LB --> GW1
LB --> GW2
GW1 --> SRV1
GW1 --> SRV2
GW2 --> SRV1
GW2 --> SRV2
GW1 --> PG
GW2 --> PG
GW1 --> RDS
GW2 --> RDS
SRV1 --> ES
SRV2 --> ES
图示来源
章节来源
默认值为 4。调度器承载 SSE 心跳、数据源回收、AI 清洗看门狗、附件孤儿清理等任务;任一批量任务变慢都可能拖住心跳,导致空闲 SSE 连接被上游代理断开。生产建议保持 4,仅在大量长耗时调度任务时才考虑扩大。
| 参数 | 默认值 | 生产建议 | 说明 |
|---|---|---|---|
SPRING_TASK_SCHEDULING_POOL_SIZE |
4 |
4 |
避免心跳被批量任务阻塞 |
默认最大内存大小为 10MB。大模型对话、图谱快照、JSON 响应较大时应适当提高。
| 参数 | 默认值 | 生产建议 | 说明 |
|---|---|---|---|
SPRING_HTTP_CODECS_MAX_IN_MEMORY_SIZE |
10MB |
50MB~200MB |
取决于图谱快照、Agent 响应大小 |
默认单文件 200MB,总请求 500MB。AI 清洗、升级包、附件解析可能触发大文件上传。
| 参数 | 默认值 | 生产建议 | 说明 |
|---|---|---|---|
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE |
200MB |
500MB~2GB |
视升级包和大附件需求 |
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE |
500MB |
1GB~4GB |
防止单请求堆溢出 |
DuckDB 使用 SET GLOBAL 配置,每个案件实例独立占用内存和线程。配置错误可能导致内存叠加膨胀。
| 参数 | 默认值 | 生产建议 | 说明 |
|---|---|---|---|
ZSJZ_DUCKDB_MAX_OPEN_CASES |
8 |
根据并发开案数调整 | 超出直接拒绝开案 |
ZSJZ_DUCKDB_MEMORY_RATIO |
0.6 |
0.3~0.6 |
所有案件合计物理内存比例 |
ZSJZ_DUCKDB_THREADS_PER_INSTANCE |
4 |
2~4 |
每个案件实例线程数 |
| 参数 | 默认值 | 生产建议 | 说明 |
|---|---|---|---|
ZSJZ_FILE_AI_MAX_CONCURRENT |
3 |
3~10 |
非表格文件 AI 识别并发 |
ZSJZ_FILE_AI_MAX_PENDING |
200 |
200~1000 |
排队上限,防止拖入大批文件撑爆队列 |
ZSJZ_EMBEDDING_BATCH_SIZE |
8 |
4~16 |
向量推理批大小 |
ZSJZ_EMBEDDING_INTRA_OP_THREADS |
0 |
0 或 CPU 核数的一半 |
0 表示交给 ONNX Runtime 默认策略 |
| 配置段 | 参数 | 默认值 | 生产建议 | 说明 |
|---|---|---|---|---|
| Agent RAG | ETL_RAG_TOP_K |
5 |
5~10 |
Agent 表结构语义检索召回条数 |
| AI 清洗向量 | ZSJZ_AI_CLEAN_VECTOR_TOP_K |
12 |
8~20 |
模板匹配候选补充条数 |
| 聊天附件 RAG | ZSJZ_CHAT_ATTACHMENT_TOP_K |
8 |
5~15 |
附件片段检索条数 |
top_k 越大,召回越全,但 LLM 输入 token 越多、延迟越高;太小则容易漏召同义模板或片段。
ZSJZ_EMBEDDING_EAGER_LOAD=false,让首次业务调用承担模型加载成本,避免启动时间过长。ZSJZ_EMBEDDING_MODEL_PATH,确保部署机上有 tokenizer.json 与 onnx/model.onnx。SPRING_ELASTICSEARCH_CONNECTION_TIMEOUT 和 SPRING_ELASTICSEARCH_SOCKET_TIMEOUT 应与网络延迟匹配。3s 适用于内网;跨机房或云专线可适当增大。章节来源
常见原因:
default-node。排查步骤:
ai-gateway 日志中的 StickyUnavailableException。gateway.services 中节点 enabled=true。gateway.health-check.interval 与 failure-threshold。curl http://{node-base-url}/js/a/sys/health。章节来源
现象:浏览器控制台报跨域错误,或对话出现 “Failed to fetch”。
排查步骤:
ai-server 的 app.cors.enabled=true。app.cors.allowed-origin-patterns 是否包含前端来源。credentials: include,且服务端允许凭据。file:// 加载前端时必须允许跨域。章节来源
现象:登录后刷新页面或切标签后需要重新登录。
排查步骤:
x-token 请求头。ai-gateway 与 ai-server 的 sa-token 配置是否完全一致,尤其是 token 名称、超时、Redis 地址。SystemHealthService 的 Redis 探测会返回 PING/PONG。ZSJZ_AUTH_IN_USE_WINDOW_MINUTES 控制“账号正在使用”窗口。章节来源
现象:登录接口异常、token 解析失败、会话无法维持。
排查步骤:
SPRING_DATA_REDIS_HOST、SPRING_DATA_REDIS_PORT。SystemHealthService 中 Redis 探测结果。章节来源
现象:搜索、RAG、向量索引相关接口超时或报错。
排查步骤:
SPRING_ELASTICSEARCH_URIS。SPRING_ELASTICSEARCH_CONNECTION_TIMEOUT 和 SPRING_ELASTICSEARCH_SOCKET_TIMEOUT。SystemHealthService 中 ES 探测返回的集群状态。章节来源
Neo4j 是可选依赖,仅图谱能力需要。若未启用图谱功能,连接失败不应阻断核心业务。
排查步骤:
NEO4J_URI、用户名、密码。章节来源
现象:上传大文件或升级包时报 multipart 超限。
排查步骤:
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE 和 SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE。client_max_body_size。STORAGE_LOCAL_ROOT_PATH 所在磁盘空间。STORAGE_LOCAL_BASE_URL 是否与 SERVER_PORT 和 context-path 一致。章节来源
生产部署应以 ai-gateway 作为用户入口,以 ai-server 作为业务节点。构建时先 ai-server 后 ai-gateway;启动前先通过 set -a; source deploy/env.sh; set +a 或 systemd EnvironmentFile 注入环境变量;严格保护含数据库口令的 deploy/env.sh。
网关的核心价值是粘性路由:登录后固定用户到同一节点,避免案件本地数据和 DuckDB 快照跨节点不一致。业务健康接口只证明进程存活,深层依赖探测通过 /js/a/sys/serviceHealth 获取。性能调优应优先关注调度池、HTTP 编解码、multipart 大小、DuckDB 内存比例、向量推理 batch size 和 RAG top_k,而不是盲目放大线程数。