企业私有 AI 算力服务器架构设计:异构四节点 + QUIC 微服务
一、为什么需要这台一体机
在企业级私有化部署场景中,AI 应用普遍面临三个痛点:
- 数据安全:业务数据出不了内网,公有云 API 不可接受;
- 算力利用率:整租 GPU 服务器跑一个对话应用,显存和算力大量闲置;
- 离线环境:工厂、涉密单位等场景没有稳定外网,依赖拉取困难。
我们的答案是造一台企业私有 AI 算力服务器 :一个机箱、四个节点、七个微服务,对外只暴露一个 API 网关,对内跑通"对话 → RAG → Agent → 工具调用"全链路。算力核心是一块 NVIDIA RTX 5090(32GB GDDR7) ,跑在 x86-64 平台上;而网关、存储、消息这三个 I/O 密集节点,则采用 aarch64(ARM64) 平台,构成一套异构混合部署架构。
本文不谈硬件选型的来龙去脉,重点聊四个架构决策:异构节点怎么分、协议栈怎么分层、单卡 32GB 显存怎么调度、Agent 工具调用怎么做才稳。
软件采用微服务架构,每个服务负责特定功能模块,通过 API 通信。当前方案将多个算力微服务部署在单台服务器上;后续视资源情况,可拆分部署到多台算力服务器上,提升算力管理与并发能力。本方案作为初始版本,会随实际开发情况和需求变化持续调整,硬件配置(GPU 型号、数量、内存)也可能随之升级。
术语约定
| 术语 | 说明 |
|---|---|
| aarch64 | ARM64 架构(如鲲鹏、飞腾等国产化服务器平台),用于网关/存储/消息节点 |
| x64 | 服务器级 x86-64 CPU 平台(如 Intel Xeon / AMD EPYC),仅用于算力节点 |
| RTX 5090 | NVIDIA Blackwell 架构旗舰 GPU,32GB GDDR7 显存,FP4 算力约 3352 TOPS |
| vLLM | 高吞吐 LLM 推理框架,PagedAttention KV Cache 管理 |
| M1/M2/M3 | 推理调度三种模式(见第四节) |
| Two-Stage | Agent 二次推理范式(先工具决策,再最终回答) |
| SSE / MQTT / QUIC | 流式推送 / 轻量级消息协议 / 基于 UDP 的现代传输协议 |
| libhv | 高性能 C++ HTTP/QUIC 网络框架 |
二、总体架构:一套机箱,四种角色
2.1 设计原则
| 原则 | 说明 |
|---|---|
| 软硬一体 | 四类物理节点集成于单一机箱,形成完整 AI 一体机 |
| 异构分工 | I/O 密集节点用 aarch64(低功耗、低成本、国产化友好),算力节点独占 x64 + CUDA 生态 |
| 协议分层 | 对外 HTTP(APISIX),内部 QUIC 统一传输(流式 RPC + JSON-RPC),异步走 MQTT,存储走 gRPC |
| 轻量优先 | MQTT Broker 选用 NanoMQ,内存占用低至 200KB |
| 前端现代化 | Vue 3 + Composition API + TypeScript |
| 合规底线 | GPL/AGPL 组件进程/网络隔离 |
| 数据驱动决策 | 所有性能目标(TPS、并发、带宽)必须基于实测冻结 |
| Two-Stage 范式 | Agent 工具调用采用二次推理,废弃流式解析器 |
| 模型适配硬件 | 模型选型以 RTX 5090 32GB 显存为约束 |
2.2 四节点物理部署
#mermaid-svg-lxzPJrkpYYaIR2zE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lxzPJrkpYYaIR2zE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lxzPJrkpYYaIR2zE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lxzPJrkpYYaIR2zE .error-icon{fill:#552222;}#mermaid-svg-lxzPJrkpYYaIR2zE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lxzPJrkpYYaIR2zE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lxzPJrkpYYaIR2zE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lxzPJrkpYYaIR2zE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lxzPJrkpYYaIR2zE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lxzPJrkpYYaIR2zE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lxzPJrkpYYaIR2zE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lxzPJrkpYYaIR2zE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lxzPJrkpYYaIR2zE .marker.cross{stroke:#333333;}#mermaid-svg-lxzPJrkpYYaIR2zE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lxzPJrkpYYaIR2zE p{margin:0;}#mermaid-svg-lxzPJrkpYYaIR2zE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lxzPJrkpYYaIR2zE .cluster-label text{fill:#333;}#mermaid-svg-lxzPJrkpYYaIR2zE .cluster-label span{color:#333;}#mermaid-svg-lxzPJrkpYYaIR2zE .cluster-label span p{background-color:transparent;}#mermaid-svg-lxzPJrkpYYaIR2zE .label text,#mermaid-svg-lxzPJrkpYYaIR2zE span{fill:#333;color:#333;}#mermaid-svg-lxzPJrkpYYaIR2zE .node rect,#mermaid-svg-lxzPJrkpYYaIR2zE .node circle,#mermaid-svg-lxzPJrkpYYaIR2zE .node ellipse,#mermaid-svg-lxzPJrkpYYaIR2zE .node polygon,#mermaid-svg-lxzPJrkpYYaIR2zE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lxzPJrkpYYaIR2zE .rough-node .label text,#mermaid-svg-lxzPJrkpYYaIR2zE .node .label text,#mermaid-svg-lxzPJrkpYYaIR2zE .image-shape .label,#mermaid-svg-lxzPJrkpYYaIR2zE .icon-shape .label{text-anchor:middle;}#mermaid-svg-lxzPJrkpYYaIR2zE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lxzPJrkpYYaIR2zE .rough-node .label,#mermaid-svg-lxzPJrkpYYaIR2zE .node .label,#mermaid-svg-lxzPJrkpYYaIR2zE .image-shape .label,#mermaid-svg-lxzPJrkpYYaIR2zE .icon-shape .label{text-align:center;}#mermaid-svg-lxzPJrkpYYaIR2zE .node.clickable{cursor:pointer;}#mermaid-svg-lxzPJrkpYYaIR2zE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lxzPJrkpYYaIR2zE .arrowheadPath{fill:#333333;}#mermaid-svg-lxzPJrkpYYaIR2zE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lxzPJrkpYYaIR2zE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lxzPJrkpYYaIR2zE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lxzPJrkpYYaIR2zE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lxzPJrkpYYaIR2zE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lxzPJrkpYYaIR2zE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lxzPJrkpYYaIR2zE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lxzPJrkpYYaIR2zE .cluster text{fill:#333;}#mermaid-svg-lxzPJrkpYYaIR2zE .cluster span{color:#333;}#mermaid-svg-lxzPJrkpYYaIR2zE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lxzPJrkpYYaIR2zE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lxzPJrkpYYaIR2zE rect.text{fill:none;stroke-width:0;}#mermaid-svg-lxzPJrkpYYaIR2zE .icon-shape,#mermaid-svg-lxzPJrkpYYaIR2zE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lxzPJrkpYYaIR2zE .icon-shape p,#mermaid-svg-lxzPJrkpYYaIR2zE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lxzPJrkpYYaIR2zE .icon-shape .label rect,#mermaid-svg-lxzPJrkpYYaIR2zE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lxzPJrkpYYaIR2zE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lxzPJrkpYYaIR2zE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lxzPJrkpYYaIR2zE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🌐 外部
📦 企业私有AI算力服务器 --- 完整机箱
📨 节点4: 消息机 (aarch64)
⚡ 节点3: AI算力机 (x64)
💾 节点2: 存储机 (aarch64)
🖥️ 节点1: 网关机 (aarch64)
HTTP
HTTP
HTTP
HTTP
HTTP
HTTP
配置读写
QUIC RPC
QUIC RPC
QUIC RPC
QUIC JSON-RPC
MQTT
MQTT
MQTT
MQTT
MQTT
gRPC
文件读取
文件读取
Apache APISIX
(路由·认证·限流·RBAC)
etcd
(配置存储)
Vue 3 静态资源
Milvus
(向量数据库)
MinIO / 本地FS
(文档存储)
PostgreSQL
(元数据·审计·日志)
服务2: 会话服务
服务3: 推理调度⭐
(RTX 5090 32GB GDDR7)
服务4: RAG服务
服务5-A: Agent Gateway
(Two-Stage推理)
服务5-B: Tool Registry
服务6: 硬件监控
(含Watchdog)
服务7: 模型管理
NanoMQ
(MQTT Broker)
Web UI / SDK / 管理台
2.3 为什么是"三轻一重"的异构结构
| 节点 | 角色 | 核心组件 | 硬件配置 |
|---|---|---|---|
| 节点1:网关机 | API 网关 + 前端交付 | APISIX + etcd + Vue3 静态资源 | 8核 aarch64 + 16GB + 256GB NVMe |
| 节点2:存储机 | 数据持久化 | Milvus + PostgreSQL + MinIO + Loki | 16核 aarch64 + 64GB + 4TB NVMe |
| 节点3:AI算力机 | 核心推理 | 7个微服务 + RTX 5090(32GB) | 32核 x64 + 128GB DDR5 + RTX 5090 32GB GDDR7(575W TGP) + 2TB NVMe |
| 节点4:消息机 | 异步消息中枢 | NanoMQ | 4核 aarch64 + 8GB + 64GB |
这个划分背后的逻辑很直接:
- 网关、存储、消息都是 I/O 密集型负载。APISIX 转发、etcd 读写、Milvus 向量检索、MQTT 收发,瓶颈在磁盘和网络而非 CPU 主频,aarch64 平台完全够用,且功耗低、成本低,对国产化/信创需求友好。这些组件(APISIX、etcd、Milvus、PostgreSQL、MinIO、NanoMQ)在 ARM64 上均有成熟的容器镜像与长期支持。
- 算力节点必须是 x86。CUDA / TensorRT / vLLM 生态目前只落在 x86_64 平台,GPU 推理栈没有替代方案,这一步不能省。
- 功耗预算向 GPU 倾斜。RTX 5090 单卡 TGP 575W,整机峰值功耗超 1000W。三个 aarch64 节点的整机功耗可以压得很低,等于把电源和散热预算尽量留给 GPU,机箱内的电源、风道设计也因此更从容。
于是整台机器形成"三轻一重":三个 aarch64 轻节点处理外围 I/O,一个 x64 重节点独占 GPU 做推理。节点之间通过机箱内部网络互通,部署上采用容器化(Docker Compose 编排),保证四个节点的软件交付一致。
三、微服务划分:七个服务,一条链路
服务编号有个跳号说明:服务1 是基础设施集合(APISIX + etcd + NanoMQ + Milvus + PG + MinIO),随节点部署,不单独开发;业务微服务从服务2开始。服务5 拆分为 5-A(Agent Gateway)和 5-B(Tool Registry)两个独立部署单元。
| 服务 | 端口 | 主协议 | 内部通信 | 核心职责 |
|---|---|---|---|---|
| 服务2:会话 | 8081 | HTTP/SSE | QUIC RPC(→服务3) | 多轮对话、上下文生命周期、流式响应 |
| 服务3:推理调度⭐ | 8082(UDP/QUIC) | QUIC RPC | 算力节点内部核心 | GPU 资源调度、M1/M2/M3 模式、PagedAttention、故障降级 |
| 服务4:RAG | 8083 | HTTP | QUIC RPC(→服务3)+ gRPC(→Milvus) | 文档解析、切片向量化、多路召回、引用溯源 |
| 服务5-A:Agent Gateway | 8084 | HTTP/WS | QUIC RPC(→服务3)+ QUIC JSON-RPC(→5-B) | Assistants API 兼容、MCP 协议、Two-Stage 推理编排 |
| 服务5-B:Tool Registry | ---(复用8082) | QUIC JSON-RPC | 被服务5-A调用 | 工具注册、调用路由、插件沙箱 |
| 服务6:硬件监控 | 8085 | HTTP | MQTT | 温度/功耗采集、PID 风扇调速、多级告警、Watchdog |
| 服务7:模型管理 | 8086 | HTTP | MQTT | 模型版本管理、加载/卸载、格式转换 |
业务微服务统一使用 C++17 + libhv(Apache-2.0)实现,理由有三:与 QUIC 框架同源、减少第三方依赖;单二进制部署轻量;推理链路对时延敏感,C++ 兜得住底。服务4、服务7 另带 Python 辅助子进程处理文档解析与模型转换。
3.1 服务2:会话与对话服务
定位:对话链路的"门面"------所有外部对话请求从这里进来,但这里没有一行代码碰 GPU。
端口:8081(HTTP/SSE)。
核心职责:
- 多轮对话上下文生命周期管理(会话创建、上下文窗口裁剪、超时回收);
- 对话历史持久化;
- SSE 流式响应------token 从服务3 到达后原样推给客户端,不做任何加工;
- 作为 QUIC 客户端调用推理调度服务;
- 订阅硬件告警,降级发生时同步会话状态。
对外接口(OpenAI 兼容是刻意为之------现有 SDK、Dify、LangChain 等工具零改造即可接入):
| 端点 | 方法 | 说明 |
|---|---|---|
/api/v1/chat/completions |
POST | OpenAI 兼容对话(SSE 流式) |
/api/v1/session/create |
POST | 创建会话 |
/api/v1/session/{id}/history |
GET | 获取历史 |
/api/v1/session/{id}/delete |
DELETE | 删除会话 |
对内通信:QUIC RPC → 服务3。会话服务对推理的全部诉求收敛为一条流式方法:
protobuf
// 序列化 Protobuf,传输层 QUIC
service Inference {
rpc Infer(InferRequest) returns (stream InferResponse);
}
此外通过 MQTT 订阅硬件告警主题(alerts/hardware/*,示例省略业务命名空间前缀):GPU 过温、模式降级发生时,会话层能感知并调整后续请求行为,而不是等用户看到超时才后知后觉。
数据存储:
| 数据 | 位置 | 说明 |
|---|---|---|
| 会话元数据 | PostgreSQL(节点2) | session_id、user_id、created_at |
| 消息历史 | PostgreSQL(节点2) | role、content、timestamp |
| KV Cache | 服务3 显存 | 推理上下文缓存,不落盘 |
架构要点 :服务2 只做"对话的壳",服务3 独占推理。切分的价值在于推理服务可以独立重启、独立降级、独立扩容------将来要把推理拆到多台算力服务器,只需把服务3 的 QUIC 端点换成多实例负载均衡,服务2 及上游一概不改。
3.2 服务3:推理调度服务(核心护城河)
定位:全机唯一拥有大模型推理调度权的服务,是所有推理请求的必经之路,也是整套架构中技术密度最高的一层。
端口:8082(UDP/QUIC)。
核心职责 :① RTX 5090 GPU 资源调度;② M1/M2/M3 三模式管理与在线切换;③ 模型加载/卸载;④ PagedAttention KV Cache 管理;⑤ 故障隔离与自动降级。推理引擎不重复造轮子,集成 vLLM / TensorRT-LLM(PagedAttention + Continuous Batching),自研部分聚焦在调度、模式管理与降级控制。
QUIC RPC 服务定义------四组接口本身就是一份架构说明书:
protobuf
service InferenceScheduler {
// 核心推理接口(流式)------ 1条QUIC stream = 1次推理
rpc Infer(InferRequest) returns (stream InferResponse);
// 模型管理
rpc LoadModel(LoadRequest) returns (LoadResponse);
rpc UnloadModel(UnloadRequest) returns (UnloadResponse);
rpc ListModels(Empty) returns (ModelList);
// 模式管理
rpc SwitchMode(SwitchModeRequest) returns (SwitchModeResponse);
rpc GetMode(Empty) returns (ModeResponse);
// 状态查询
rpc GetStatus(Empty) returns (StatusResponse);
rpc GetGPUStatus(GPUStatusRequest) returns (GPUStatusResponse);
rpc GetBandwidth(Empty) returns (BandwidthResponse);
}
Infer:每次推理占一条独立 QUIC stream,天然并发、互不阻塞;SwitchMode:把"重排显存"变成一个 RPC 调用,运维接口和自动降级共用同一条路径;GetGPUStatus/GetBandwidth:把硬件探测能力暴露成 RPC,启动自检和监控面板复用同一套实现。
MQTT 订阅(服务6 告警与服务7 命令的接入点):
| 主题 | QoS | 动作 |
|---|---|---|
alerts/hardware/critical |
1 | 执行降载,暂停新模型加载 |
commands/scheduler/shutdown |
1 | 安全卸载所有模型,释放显存 |
commands/scheduler/degrade |
1 | M1→M2 切换(卸大模型、载小模型) |
数据存储:模型权重存放在节点2 模型存储区,加载时经 NVMe + PCIe 5.0 流式进 GPU;KV Cache 驻留 GDDR7、纯显存不持久化;GPU 状态(显存占用、温度、功耗、SM 利用率)保留在本服务内存中。
M1/M2/M3 三模式定义与 KV Cache 的深入展开见第四节(决策二、决策三)。
3.3 服务4:知识工程服务(RAG)
定位:企业知识的"摄取 + 检索"双引擎------前半段把文档变成向量,后半段把问题变成答案并附上出处。
端口:8083(HTTP)。
核心职责:① 多格式文档解析;② 语义切片与向量化;③ 向量数据库管理;④ 多路召回 + 重排序;⑤ 引用溯源。最后一点在企业场景常常比检索精度更致命:答案里的每个结论都要能指回原文档的具体位置,否则知识库就会沦为"不可信的黑盒"。
对外接口:
| 端点 | 方法 | 说明 |
|---|---|---|
/api/v1/knowledge/upload |
POST | 上传文档 |
/api/v1/knowledge/search |
POST | 语义检索 |
/api/v1/knowledge/chat |
POST | RAG 问答 |
/api/v1/knowledge/collections |
GET | 列出知识库 |
处理流水线与合规隔离:
#mermaid-svg-xwSTXbcbmtBj6PCZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xwSTXbcbmtBj6PCZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xwSTXbcbmtBj6PCZ .error-icon{fill:#552222;}#mermaid-svg-xwSTXbcbmtBj6PCZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xwSTXbcbmtBj6PCZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xwSTXbcbmtBj6PCZ .marker.cross{stroke:#333333;}#mermaid-svg-xwSTXbcbmtBj6PCZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xwSTXbcbmtBj6PCZ p{margin:0;}#mermaid-svg-xwSTXbcbmtBj6PCZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xwSTXbcbmtBj6PCZ .cluster-label text{fill:#333;}#mermaid-svg-xwSTXbcbmtBj6PCZ .cluster-label span{color:#333;}#mermaid-svg-xwSTXbcbmtBj6PCZ .cluster-label span p{background-color:transparent;}#mermaid-svg-xwSTXbcbmtBj6PCZ .label text,#mermaid-svg-xwSTXbcbmtBj6PCZ span{fill:#333;color:#333;}#mermaid-svg-xwSTXbcbmtBj6PCZ .node rect,#mermaid-svg-xwSTXbcbmtBj6PCZ .node circle,#mermaid-svg-xwSTXbcbmtBj6PCZ .node ellipse,#mermaid-svg-xwSTXbcbmtBj6PCZ .node polygon,#mermaid-svg-xwSTXbcbmtBj6PCZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xwSTXbcbmtBj6PCZ .rough-node .label text,#mermaid-svg-xwSTXbcbmtBj6PCZ .node .label text,#mermaid-svg-xwSTXbcbmtBj6PCZ .image-shape .label,#mermaid-svg-xwSTXbcbmtBj6PCZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-xwSTXbcbmtBj6PCZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xwSTXbcbmtBj6PCZ .rough-node .label,#mermaid-svg-xwSTXbcbmtBj6PCZ .node .label,#mermaid-svg-xwSTXbcbmtBj6PCZ .image-shape .label,#mermaid-svg-xwSTXbcbmtBj6PCZ .icon-shape .label{text-align:center;}#mermaid-svg-xwSTXbcbmtBj6PCZ .node.clickable{cursor:pointer;}#mermaid-svg-xwSTXbcbmtBj6PCZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xwSTXbcbmtBj6PCZ .arrowheadPath{fill:#333333;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xwSTXbcbmtBj6PCZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xwSTXbcbmtBj6PCZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xwSTXbcbmtBj6PCZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xwSTXbcbmtBj6PCZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xwSTXbcbmtBj6PCZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xwSTXbcbmtBj6PCZ .cluster text{fill:#333;}#mermaid-svg-xwSTXbcbmtBj6PCZ .cluster span{color:#333;}#mermaid-svg-xwSTXbcbmtBj6PCZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xwSTXbcbmtBj6PCZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xwSTXbcbmtBj6PCZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-xwSTXbcbmtBj6PCZ .icon-shape,#mermaid-svg-xwSTXbcbmtBj6PCZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xwSTXbcbmtBj6PCZ .icon-shape p,#mermaid-svg-xwSTXbcbmtBj6PCZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xwSTXbcbmtBj6PCZ .icon-shape .label rect,#mermaid-svg-xwSTXbcbmtBj6PCZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xwSTXbcbmtBj6PCZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xwSTXbcbmtBj6PCZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xwSTXbcbmtBj6PCZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ⚠️ 进程隔离
popen()
上传文档
📄 文档解析
(LibreOffice子进程)
✂️ 语义切片
🧠 向量化
(RTX 5090 CUDA + bge-m3)
💾 存储
(Milvus + MinIO)
LibreOffice
(GPLv3)
流水线中最危险的一环是格式转换------LibreOffice 是 GPLv3 组件,且有解析恶意文档的历史前科。架构上把它关进"进程隔离的笼子":以独立子进程调用、不链接进主程序,崩溃和漏洞都被限制在子进程边界内,主服务与许可合规两不误。
另有一个容易被忽略的细节:向量化(bge-m3 embedding)与大模型推理共享同一块 RTX 5090。embedding 是毫秒级短任务,与长推理流天然错峰,这也是 RAG 服务与推理调度同节点部署的原因之一。
数据存储:
| 数据 | 位置 | 说明 |
|---|---|---|
| 向量数据 | Milvus(节点2,gRPC) | 向量 + 元数据 |
| 原始文档 | MinIO(节点2) | PDF / Word 等 |
| 文档元数据 | PostgreSQL(节点2) | doc_id、filename、chunk_count |
3.4 服务5-A:Agent Gateway
定位:Agent 能力的协议适配层------把 OpenAI Assistants API 和 MCP 协议翻译成内部调用,自己不执行任何工具。
端口:8084(HTTP + WebSocket)。
核心职责:① OpenAI Assistants API 兼容;② MCP 协议支持;③ Two-Stage 推理编排(工具决策 → 执行 → 最终回答);④ 工具调用编排。Two-Stage 范式的完整推演与效果对比见第四节(决策四),这里只强调一点:5-A 是"编排者",每次工具调用都经过 QUIC JSON-RPC 下发给 5-B 执行,自己保持无状态、可随时重启。
对外接口:
| 端点 | 方法 | 说明 |
|---|---|---|
/api/v1/assistants |
POST/GET | 智能体管理 |
/api/v1/threads |
POST | 创建线程 |
/api/v1/threads/{id}/runs |
POST | 执行(SSE 流式) |
/mcp |
WebSocket | MCP 协议端点 |
MCP 交付边界(按"够用就好"裁剪):
| 功能 | 交付状态 | 说明 |
|---|---|---|
initialize 握手 |
✅ 交付 | 协议版本 + 服务能力 |
tools/list |
✅ 交付 | 列出可用工具 |
tools/call |
✅ 交付 | 执行工具调用 |
| Streamable HTTP | ⚠️ 简化版 | 支持 SSE 流式返回 |
| 完整 MCP 规范 | ❌ 后续版本 | 采样、资源订阅等复杂场景后续迭代 |
数据存储:Assistant 配置、Thread 消息、Run 状态均落 PostgreSQL(节点2),5-A 自身无本地状态。
3.5 服务5-B:Tool Registry
定位:工具执行的"总线"------所有工具在这里注册、被路由、被执行,第三方插件与 Agent 主链路之间的防火墙。
端口:无独立对外端口,监听在 QUIC 8082 上,与推理调度按 stream ID 区分,零额外网络开销。
核心职责:① 工具元数据注册;② 工具调用路由;③ 内置工具执行;④ 自定义插件沙箱。
QUIC JSON-RPC 2.0 方法(JSON-RPC 消息体承载于 QUIC 流之上,每条工具调用一条独立 stream,天然并发):
| 方法 | 参数 | 返回 |
|---|---|---|
tools.register |
{name, description, schema} |
{tool_id} |
tools.unregister |
{tool_id} |
{success} |
tools.list |
{} |
{tools: [...]} |
tools.call |
{tool_name, args} |
{result} |
内置工具:
| 工具 | 实现 | 说明 |
|---|---|---|
web_search |
HTTP 调用 SearXNG | 联网搜索,外联流量与内网服务边界隔离 |
calculator |
自研表达式解析器 | 纯本地计算 |
get_time |
系统调用 | 最简单的可验证工具 |
架构要点:协议适配(5-A)与工具执行(5-B)拆成两个部署单元,是 Agent 链路最重要的一次"泄压"------第三方插件崩溃、超时甚至被恶意构造的输入打挂,都只发生在 5-B 进程内,杀掉重启即可,Agent 入口始终在线。
3.6 服务6:硬件监控服务
定位:整机的"体温计 + 心跳器",软件世界与硬件世界之间的翻译官。
端口:8085(HTTP)。
核心职责:① 温度/功耗采集;② PID 风扇调速;③ 多级告警;④ 内核 Watchdog 喂狗。
它有两个架构角色值得单独强调:
- 事件翻译器 :把 GPU 温度、功耗等硬件信号翻译成 MQTT 告警(
alerts/hardware/*)广播出去,服务3、服务2 各自订阅后按需降级。硬件状态走总线广播,而不是每个服务自己去读传感器------一套采集,多方消费; - 最后防线:Watchdog 每 10 秒喂一次、30 秒超时硬重启。进程死锁、内核卡死这些软件兜不住的故障,物理兜底。
多级温度告警的完整联动策略(70℃ 提风扇 → 88℃ 切模式 → 95℃ 关机)见第五节 5.3。
3.7 服务7:模型管理服务
定位:模型资产的"仓库管理员"------管版本、管上下架、管格式转换。
端口:8086(HTTP)。
核心职责:① 模型版本管理;② 模型加载/卸载(实际调用 vLLM / TensorRT-LLM);③ 模型格式转换(HuggingFace → TensorRT Engine)。
模型基线(单卡 32GB GDDR7 约束下的"菜单",每一行都是一次显存预算审查的结论):
| 模型 | 量化方式 | 显存占用 | 状态 | 吞吐量级 | 说明 |
|---|---|---|---|---|---|
| Qwen3-32B | Q4_K_M | ~20 GB | ✅ 默认主推(M1) | 十级 TPS | 高质量问答、推理 |
| Qwen3-14B | Q4_K_M | ~9 GB | ✅ M2 主推 | 数十级 TPS | 多实例并发场景 |
| Qwen3-14B | FP8 (Blackwell) | ~14 GB | ✅ 推荐 | 数十~百级 TPS | 利用 Blackwell FP8 |
| Qwen3-7B | FP16 | ~15 GB | ✅ 可选 | 百级 TPS | 低延迟场景 |
| Llama-3.3-70B | Q2_K | ~25 GB | ⚠️ 实验性 | 个位~十级 TPS | 极限量化,质量损失 |
| DeepSeek-V3-0324 | FP8 蒸馏 | ~30 GB | ⚠️ 紧凑 | 个位数 TPS | 满显存运行,KV Cache 极少 |
| Qwen3-72B | Q4_K_M | ~40 GB | ❌ 不支持 | --- | 超过 32GB 显存上限 |
推荐组合:M1 模式用 Qwen3-32B-Q4_K_M;M2/M3 模式用 Qwen3-14B-Q4_K_M ×2 或 FP8 ×2(表中吞吐仅标注量级,具体基线数值随实测冻结、不对外公开)。这张"菜单"不是下载下来试试看拼凑的,而是模型适配硬件原则的落地------加载前逐个过显存预检(第四节决策三),不达标直接拒绝。
四、核心架构决策
决策一:三层协议栈------对外 HTTP,对内 QUIC,异步 MQTT
| 层级 | 协议 | 用途 | 承载路径 |
|---|---|---|---|
| 对外 | HTTP/HTTPS(APISIX) | 外部 API + 前端资源 | 节点1 ↔ 节点3 |
| 对外 | SSE / WebSocket | 流式推理响应 / MCP 协议 | 节点3 → 外部 |
| 对内(同步) | QUIC RPC | 推理调度(流式,Protobuf) | 节点3 内部 |
| 对内(同步) | QUIC JSON-RPC | 工具调用(JSON-RPC 2.0) | 节点3 内部 |
| 对内(异步) | MQTT | 告警、事件、命令 | 节点3 ↔ 节点4 |
| 存储 | gRPC | Milvus 向量检索 | 节点3 → 节点2 |
对内为什么不用 gRPC 而选 QUIC?对比一下:
| 维度 | gRPC | QUIC RPC |
|---|---|---|
| 传输层 | TCP(HTTP/2) | UDP(QUIC) |
| 多路复用 | 连接内流多路复用 | 原生 QUIC stream |
| 队头阻塞 | TCP 级队头阻塞 | 无队头阻塞 |
| 握手开销 | TCP 三次握手 + TLS | 0-RTT 支持 |
| 流控 | HTTP/2 窗口 | QUIC 自适应流控 |
| 依赖 | grpc-core 全家桶 | 基于 libhv 自研薄封装 |
推理场景是典型的"多条并发流式推理共享一条链路":一次对话一条 token 流,TCP 队头阻塞会让一条慢流拖累同连接上的所有推理;QUIC 的 stream 之间互不影响,0-RTT 还能让服务重启后快速重连。同时推理 RPC 与工具调用 JSON-RPC 共享同一个 QUIC 传输层,网络栈只有一种------所有内部同步通信收敛到 UDP 8082 一个端口,靠 stream ID 多路复用。
┌─────────────────────────────────────────────────────────┐
│ 节点3:AI算力机 (x64) │
│ │
│ ┌──────┐ QUIC RPC ┌──────────┐ QUIC JSON-RPC │
│ │ 服务2 │ ═════════════►│ │◄═════════════════│
│ │ │ (Protobuf) │ 服务3 │ (JSON-RPC) │
│ └──────┘ │ 推理调度 │ │
│ ┌──────┐ │ │ │
│ │ 服务4 │ ═════════════►│ │ │
│ └──────┘ └──────────┘ │
│ ┌──────┐ QUIC JSON-RPC ┌──────────┐ │
│ │ 5-A │ ═══════════════►│ 服务5-B │ │
│ │ │ (JSON-RPC) │ Tool Reg │ │
│ └──────┘ └──────────┘ │
│ │
│ ▲ 所有内部 QUIC 连接共享一个 UDP 端口,靠 stream 多路复用 │
└─────────────────────────────────────────────────────────┘
MQTT 则负责所有"不需要立刻返回结果"的事:硬件告警、模式切换命令、安全停机。主题设计约定了 QoS 1 与明确的命名空间(如 alerts/hardware/critical、commands/scheduler/degrade,示例省略业务命名空间前缀),同步链路与异步事件互不干扰。
决策二:单卡 32GB 的三种调度模式(M1/M2/M3)
RTX 5090 单卡 32GB 显存是所有推理架构决策的物理约束。我们没有做虚的弹性调度,而是定义了三种互斥、可在线切换的静态模式:
| 模式 | 显存分配 | 适用场景 | 启用条件 |
|---|---|---|---|
| M1(大模型单实例) | 1× Qwen3-32B-Q4_K_M(约 20GB)+ KV Cache 12GB | 高质量问答、复杂推理 | 默认启用 |
| M2(多实例并发) | 2× Qwen3-14B-Q4_K_M(各约 9GB)+ KV Cache 各 7GB | 客服/多用户工作台 | 高并发场景 |
| M3(内外分区) | 1× 14B 内网 + 1× 14B 外网,显存按比例切分 | 内外隔离 | 合规场景 |
M1 与 M2 互斥------显存就那么多,同一时刻只能选"一个大模型"或"两个小模型"。切换通过服务3 的 SwitchMode RPC 在线完成:卸载当前模型 → 释放 KV Cache → 加载新模型。
更重要的是模式选择由实测数据驱动,而不是拍脑袋。服务3 启动时先探测 GPU 能力:
cpp
// 服务3启动时执行 GPU 显存带宽 + 算力检测
class GPUBenchmark {
public:
struct GPUCapability {
size_t total_vram_mb; // 总显存(MB)
size_t usable_vram_mb; // 可用显存(扣除 CUDA context 预留)
float vram_bandwidth_gbps; // GDDR7 实测显存带宽(GB/s)
float tokens_per_second; // 14B-Q4_K_M 实测 TPS(基线内部冻结)
bool fp8_supported; // Blackwell FP8 支持
bool fp4_supported; // Blackwell FP4 支持
};
GPUCapability probe() {
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, 0);
GPUCapability cap;
cap.total_vram_mb = prop.totalGlobalMem / (1024 * 1024);
cap.usable_vram_mb = cap.total_vram_mb - 512; // 预留 CUDA runtime
cap.vram_bandwidth_gbps = runBandwidthTest();
cap.tokens_per_second = runTPSBenchmark("qwen3-14b-q4_k_m");
cap.fp8_supported = (prop.major >= 10); // Blackwell SM 10.x
cap.fp4_supported = (prop.major >= 10);
return cap;
}
ModeRecommendation recommendMode(const GPUCapability& cap) {
// tps_baseline_:启动时实测冻结的基线值(数值不对外)
if (cap.tokens_per_second >= tps_baseline_) {
return {.recommended = "M1(默认)/M2/M3", .note = "性能达标,按业务选择"};
}
return {.recommended = "M2/M3", .note = "TPS 不足,建议多实例并发"};
}
};
冻结的基准指标作为架构的"物理常数":14B、32B 各有一条实测 TPS 基线,显存带宽以 GDDR7 理论峰值为锚。具体基线数值在项目内部冻结存档,本文只保留量级------14B 量化版吞吐在数十 TPS 量级,32B 在十 TPS 量级,实测带宽贴近理论峰值。所有上层承诺都建立在这组实测数字之上。
决策三:PagedAttention 管 KV Cache------把显存当内存管理
推理服务最大的隐形成本不是模型权重,而是 KV Cache。我们采用 vLLM 的 PagedAttention 方案:把 KV Cache 切成固定大小 page(每页 16 token),按需分配,配合前缀复用(相同 system prompt 共享 KV)和 LRU 淘汰:
cpp
class PagedKVCacheManager {
public:
CacheHandle allocate(const string& session_id, int initial_pages);
void appendTokens(CacheHandle handle, int num_new_tokens); // 自动扩展 page
CacheHandle lookupPrefix(const string& prompt_prefix); // 前缀复用
void evictPages(size_t target_free_mb); // LRU 淘汰
void releaseForModeSwitch(); // M1→M2 切换时回收
private:
unordered_map<PageID, PageMeta> page_table_; // 物理页 → 引用计数
unordered_map<string, vector<PageID>> session_pages_; // 会话 → 逻辑页表
size_t total_vram_used_;
size_t vram_capacity_; // RTX 5090: 32768 MB
int page_size_; // 每 page 16 tokens
};
以 M1 模式为例算一笔显存账:权重约 20GB,KV Cache 可用约 12GB;按 32B 模型的单 token KV 开销折算,可支撑数千 token 上下文与个位数并发会话------前缀复用后实际更高。每一次模型加载都要过这道校验,含 15% 余量,不达标直接拒绝:
cpp
bool validateModelLoad(const string& model_id, Mode target_mode) {
ModelInfo info = getModelInfo(model_id);
size_t total_vram_mb = getGPUVramBytes() / (1024 * 1024); // 32768
size_t usable_mb = total_vram_mb - 512 /*runtime*/ - 4096 /*KV最小值*/;
size_t per_instance_mb = (target_mode == M2) ? usable_mb / 2 : usable_mb;
if (info.model_size_mb > per_instance_mb * 0.85f) {
return false; // 显存超限,拒绝加载
}
if (info.requires_fp8 && !getGPUCapability().fp8_supported) {
return false; // FP8 模型必须 Blackwell
}
return true;
}
决策四:Agent 用 Two-Stage 推理,不用流式解析
让模型在流式输出中"边说边决定"是否调用工具,看起来优雅,实际误触发率高到无法接受。架构上我们彻底放弃这条路,改为 Two-Stage 二次推理:
- Stage 1(工具决策) :强制 System Prompt 只输出 JSON,temperature = 0.0 保证确定性,严格解析出
{tool, args};解析失败一律视为"不调用工具",安全兜底。 - Stage 2(最终回答):需要工具则先执行工具,再把结果拼进 prompt 做一次流式推理;不需要则直接流式回答。
代价是首 token 时延增加一次完整的推理往返,换来的是误触发率下降一个数量级、达到可量产水平,且代码从复杂状态机简化为两段顺序逻辑。对于企业内部工具调用场景,稳定性的优先级高于数百毫秒的时延。
MCP 协议同样按"够用就好"的原则裁剪:交付 initialize / tools/list / tools/call 三个核心方法与简化版 Streamable HTTP,采样、资源订阅等复杂能力留待后续迭代。
五、横切关注点:安全与可观测
5.1 认证:一处鉴权,内部免验
所有外部请求经节点1 APISIX 网关统一鉴权,采用 JWT + API-Key 双轨:
| 角色 | 认证方式 | 权限范围 |
|---|---|---|
| admin | JWT | 模型管理、监控、用户管理 |
| user | JWT | 对话、知识库检索 |
| external | API-Key(限频) | 仅外部问答接口 |
内部微服务间调用(QUIC RPC、QUIC JSON-RPC、MQTT)依赖 QUIC 连接级身份标识 + 节点内网络隔离,不重复鉴权------鉴权逻辑只存在于边界上,内部链路保持简单。
5.2 可观测:结构化日志一路到底
所有微服务统一输出 JSON 结构化日志,采集链路为:
微服务 → stdout JSON → Promtail(节点3 sidecar)→ Loki(节点2)→ Grafana 查询
GPU 侧由服务6 采集温度、功耗、显存占用,经 MQTT 上报,与日志体系在 Grafana 汇合。
5.3 硬件看门狗:软件故障的最后防线
RTX 5090 是单点,硬件监控服务(服务6)因此承担三件事:
- 多级温度告警联动:70~80℃ 提风扇转速 → 80~88℃ 降载限并发 → 88~95℃ 触发 M1→M2 切换(卸载 32B 大模型加载 14B)→ >95℃ 安全关机。温控动作直接映射到第四节的三模式架构,硬件保护与软件调度是同一套语言。
- 内核 Watchdog 喂狗:每 10 秒喂一次,超时 30 秒硬重启,进程级死锁也有兜底。
- 功耗监测:整机峰值 >1000W,实时监测 GPU 功耗曲线,配合 ≥1500W 金牌电源与浪涌保护。
六、风险与架构应对
| 风险 | 等级 | 架构级应对 |
|---|---|---|
| RTX 5090 供货短缺 | 🔴 | 启动前锁定采购;备选 RTX 5080 16GB 方案 |
| 32B 模型 TPS 不达标 | 🟠 | 启用 Blackwell FP8 量化;M1 默认降级为 14B-FP8 |
| 算力单卡 SPOF | 🟠 | Watchdog + 故障自动降级(M1→M2)+ 电源冗余 |
| 散热不足导致 GPU 降频 | 🟠 | 独立 GPU 风道 + 温度联动模式切换 |
| libhv QUIC 成熟度 | 🟡 | 部署前验证 hquic 模块稳定性,保留 TCP 回退路径 |
| MCP 规范复杂度 | 🟡 | 简化交付,核心方法优先 |
可以看到,多数风险的应对手段不是"加人加班",而是架构里预埋好的降级路径------模式切换、量化降级、传输层回退,都是第一节设计原则的具体兑现。
七、结语
回顾整套架构,最值得复用的三条经验:
- 异构分工:不是所有节点都需要最强 CPU。I/O 密集的网关/存储/消息交给 aarch64,把功耗、成本、散热预算全部让给算力节点的 GPU;
- 协议收敛:对外一种入口(HTTP),对内一种同步传输(QUIC)、一种异步通道(MQTT)、一种存储协议(gRPC)。协议种类少,故障排查和性能调优才有抓手;
- 约束即架构:单卡 32GB 显存不是缺陷,而是把 M1/M2/M3 三模式、PagedAttention、显存预检、温度联动降级串成一条线的那根轴。性能目标全部实测冻结,架构决策全部可回溯。
后续随业务演进,这套架构还有两个明确的演进方向:推理服务横向拆分到多台算力服务器(服务3 的 QUIC 端点天然支持多实例),以及 MCP/工具生态的逐步完善。架构的价值不在于一次设计到位,而在于每个扩展点都在该在的位置。