一、问题拆解:存量系统 AI 升级的三个现实约束
企业培训平台的存量系统通常有这些特征:技术栈老旧(可能是十年前的单体架构)、核心业务稳定运行(不敢动)、数据资产庞大(迁移成本高)。AI 升级要在这三个约束下完成。
约束一:不能替换系统。 存量系统承载了课程、学员、考试、证书等核心业务数据,替换意味着巨大的迁移成本和业务中断风险。CIO 的决策永远是"优先在现有系统上加能力"。
约束二:不能大改存量代码。 存量系统的代码可能是外包团队写的、核心开发早已离职、文档严重缺失。在这种代码库上做大改动,改坏一个地方就是事故。AI 接入必须做到"低侵入"——最好只加一个依赖、配一段配置。
约束三:部署模式多元。 AI 能力的落地方式取决于客户的环境:合规要求高的客户要求私有化部署(模型跑在客户内网);有自有云账号的客户倾向于对接云服务商模型服务(token 费用走客户自己的云账单);采用 SaaS 模式的客户则由平台统一提供能力。网关方案必须同时适配这三种模式。
这三个约束指向同一个技术方案:把 AI 能力从业务系统里抽出来,做成独立的服务,通过统一的 API 网关暴露给存量系统调用。存量系统不需要知道 AI 能力背后的实现细节(调用了哪个大模型、用了哪家 ASR、算力跑在哪),只需要按标准 API 规范发起请求。
二、整体架构:AI 能力网关 + 三层适配
整体方案的核心是"一网关、两中心、三层适配":
┌─────────────────────────────────────────────────────────────┐
│ 存量培训系统(旧系统) │
│ 课程管理 学员管理 考试系统 学习平台 …… │
└──────────────────────────┬──────────────────────────────────┘
│ 标准 REST API / 异步任务
┌──────────────────────────▼──────────────────────────────────┐
│ 统一 AI 能力网关 (AI Gateway) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ 认证鉴权 │ │ 路由分发 │ │ 用量日志 │ │ 限流熔断 │ │
│ │ (API Key) │ │ (能力路由) │ │ (全量审计) │ │ (兜底) │ │
│ └────────────┘ └────────────┘ └────────────┘ └──────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ AI 能力适配层 (Adapter Layer) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────────┐ │ │
│ │ │ ASR适配 │ │ TTS适配 │ │ LLM适配 │ │ 数字人适配 │ │ │
│ │ │ 器 │ │ 器 │ │ 器 │ │ 器 │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └─────┬──────┘ │ │
│ └───────┼───────────┼───────────┼────────────┼─────────┘ │
└──────────┼───────────┼───────────┼────────────┼────────────┘
│ │ │ │
┌─────▼───┐ ┌────▼───┐ ┌───▼────┐ ┌────▼─────┐
│ 云ASR │ │ 云TTS │ │ 云LLM │ │ 私有化 │
│ 服务商 │ │ 服务商 │ │ 服务商 │ │ 推理节点 │
└─────────┘ └─────────┘ └─────────┘ └──────────┘
一网关:统一 AI 能力网关,是所有 AI 调用的唯一入口。存量系统只需要对接这一个地址,认证、路由、用量日志、限流全部在网关完成。
两中心:能力注册中心(管理有哪些 AI 能力、每个能力的路由和供应商配置)和用量日志中心(记录每一笔调用的完整信息,作为审计和成本核算的依据)。
三层适配:适配层屏蔽供应商差异。ASR 可能来自供应商 A、TTS 可能来自供应商 B、LLM 可能跑在私有化环境——存量系统不需要知道,网关按能力路由到正确的供应商。
关键设计:网关层不做"计费",只做"记账"
这是本方案与 SaaS 模式计量计费体系的关键区别。在存量系统接入场景中,AI 能力的计费主体由部署模式决定,网关不介入计价决策:
| 部署模式 | 能力来源 | 计费主体 | 网关职责 |
|---|---|---|---|
| 客户自有云 | 云服务商模型服务(阿里云百炼 / 华为云 ModelArts / 腾讯云混元等) | 客户自己的云账号(token 按云服务商价格计费) | 只路由 + 记用量日志,不介入计费 |
| 私有化部署 | 客户内网的模型推理节点 | 软件授权费(一次性/年费),不按量计费 | 只路由 + 记用量日志,供客户本地成本分析 |
| 平台 SaaS | 平台统一提供 AI 能力 | 平台计费体系(如企学豆按量计费) | 路由 + 用量日志(作为计费系统的事件源) |
网关层统一做"用量日志"(每次调用记录:谁调用了什么能力、消耗了多少时长/token/次数),但"单价多少、向谁收钱"由上层计费系统决定。这样网关的逻辑在三种模式下完全一致,不需要为每种部署模式定制。
三、网关核心:统一 API 规范与认证鉴权
3.1 统一 API 规范
存量系统接入 AI 能力,统一通过 /ai/v1/{capability} 路径调用。规范的核心是"一个能力一个接口、请求响应格式统一"。
POST /ai/v1/asr/transcribe 语音识别(录音文件)
POST /ai/v1/tts/synthesize 语音合成
POST /ai/v1/llm/chat 大模型对话
POST /ai/v1/llm/extract 知识点萃取
POST /ai/v1/llm/qa AI 问答
POST /ai/v1/question/generate AI 出题
POST /ai/v1/grading/objective 客观题批阅
POST /ai/v1/grading/subjective 主观题批阅
POST /ai/v1/avatar/generate 数字人课件生成
POST /ai/v1/translate/document 文档翻译
POST /ai/v1/subtitle/generate 字幕生成
统一请求头:
X-Api-Key: <存量系统分配的 API Key>
X-Tenant-Id: <租户 ID>
X-Request-Id: <请求追踪 ID,幂等用>
Content-Type: application/json
统一响应结构:
{
"code": 0,
"message": "success",
"requestId": "req_8f3a2b9c",
"data": {
... }
}
错误码规范:
| code | 含义 | 处理建议 |
|---|---|---|
| 0 | 成功 | - |
| 40001 | 认证失败(API Key 无效) | 检查 API Key 配置 |
| 40002 | 权限不足(能力未开通) | 联系平台开通能力 |
| 40003 | 参数错误 | 检查请求参数 |
| 42901 | 触发限流 | 降低调用频率 |
| 50001 | AI 服务内部错误 | 重试或联系平台 |
| 50002 | 供应商超时 | 重试或降级 |
3.2 网关认证与路由
网关基于 Spring Cloud Gateway 实现,核心是认证过滤器 + 路由断言。新增一个存量系统接入时,只需在配置中心注册 API Key 和开通的能力列表。
/**
* AI 能力网关入口
* 基于 Spring Cloud Gateway
*/
@Configuration
public class AiGatewayConfig {
/**
* 认证过滤器:校验 API Key 和租户权限
*/
@Bean
public GlobalFilter apiKeyAuthFilter() {
return (exchange, chain) -> {
// 1. 提取 API Key
String apiKey = exchange.getRequest()
.getHeaders().getFirst("X-Api-Key");
if (apiKey == null || apiKey.isEmpty()) {
return unauthorized(exchange, "缺少 X-Api-Key 请求头");
}
// 2. 校验 API Key 并获取租户信息
TenantInfo tenant = tenantService.getByApiKey(apiKey);
if (tenant == null) {
return unauthorized(exchange, "API Key 无效");
}
// 3. 校验该租户是否开通了此能力
String capability = extractCapability(exchange.getRequest().getPath().value());
if (!tenant.hasCapability(capability)) {
return forbidden(exchange, "未开通能力: " + capability);
}
// 4. 把租户信息放入请求上下文(后续路由和用量日志使用)
exchange.getAttributes().put("tenant", tenant);
return chain.filter(exchange);
};
}
/**
* 能力路由:把请求转发到对应的 AI 能力服务
* 路由规则在配置中心维护,支持动态调整供应商
*/
@Bean
public RouteLocator aiRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 语音识别能力 → ASR 服务
.route("asr-route", r -> r
.path("/ai/v1/asr/**")
.uri("lb://ai-asr-service")) // 服务发现,按能力路由
// 大模型能力 → LLM 服务
.route("llm-route", r -> r
.path("/ai/v1/llm/**")
.uri("lb://ai-llm-service"))
// 文档翻译 → 翻译服务
.route("translate-route", r -> r
.path("/ai/v1/translate/**")
.uri("lb://ai-translate-service"))
// 其他能力 → 默认服务
.route("default-route", r -> r
.path("/ai/v1/**")
.uri("lb://ai-generic-service"))
.build();
}
}
3.3 存量系统的接入:三行代码
存量系统的接入成本被压到极低——只需要一个 HTTP 客户端封装。以 Java 存量系统为例:
/**
* 存量系统侧的 AI 网关客户端
*
* 接入成本:引入一个依赖 + 配置 3 个参数
* 存量系统不需要引入任何 AI SDK,只需要 HTTP 调用
*/
@Component
public class AiGatewayClient {
@Value("${ai.gateway.url:https://ai-gateway.example.com}")
private String gatewayUrl;
@Value("${ai.gateway.apiKey}")
private String apiKey;
@Value("${ai.gateway.tenantId}")
private String tenantId;
private RestTemplate restTemplate = new RestTemplate();
/**
* 通用调用方法:所有 AI 能力都走这里
*/
public <T> T call(String capability, Object requestBody, Class<T> responseType) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-Api-Key", apiKey);
headers.set("X-Tenant-Id", tenantId);
headers.set("X-Request-Id", UUID.randomUUID().toString());
HttpEntity<String> entity = new HttpEntity<>(
JSON.toJSONString(requestBody), headers
);
String url = gatewayUrl + "/ai/v1/" + capability;
ResponseEntity<AiResponse> response = restTemplate.postForEntity(
url, entity, AiResponse.class
);
if (response.getBody().getCode() != 0) {
throw new AiGatewayException(
response.getBody().getCode(),
response.getBody().getMessage()
);
}
return JSON.parseObject(
JSON.toJSONString(response.getBody().getData()), responseType
);
}
/**
* 业务示例:存量考试系统调用 AI 出题
*/
public List<Question> aiGenerateQuestions(String courseId, String courseText, int count) {
GenerateQuestionRequest req = new GenerateQuestionRequest();
req.setCourseId(courseId);
req.setCourseText(courseText);
req.setQuestionCount(count);
req.setQuestionTypes(Arrays.asList("single", "multiple", "judge"));
return call("question/generate", req, QuestionListResponse.class).getQuestions();
}
}
存量系统接入的完整步骤只有三步:引入网关客户端依赖 → 配置 gateway.url / apiKey / tenantId → 业务代码中调用 aiGatewayClient.call(...)。不需要改数据库、不需要迁移数据、不需要改造已有接口。
四、用量日志:三种部署模式通用的"记账"层
网关不做计费,但必须做"记账"——每一笔 AI 调用的用量记录是后续所有成本核算的基础。这个设计对三种部署模式都成立。
4.1 用量日志过滤器
/**
* 用量日志过滤器:记录每一笔 AI 调用的完整信息
*
* 记录的字段:
* - 谁调的(租户、调用方)
* - 调了什么(能力、供应商)
* - 用了多少(次数、时长、token 数)
* - 结果如何(成功/失败、耗时)
*
* 这些原始记录不参与计费决策,只作为事实数据沉淀,
* 供上层按需加工(成本核算、审计、优化)。
*/
@Component
public class UsageLogFilter implements GlobalFilter {
@Autowired
private UsageLogRepository logRepository;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long startTime = System.currentTimeMillis();
TenantInfo tenant = exchange.getAttribute("tenant");
String path = exchange.getRequest().getPath().value();
String capability = extractCapability(path);
String requestId = exchange.getRequest().getHeaders().getFirst("X-Request-Id");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 从响应中提取用量信息(不同能力提取方式不同)
UsageRecord usage = extractUsage(exchange, capability);
// 组装完整的用量记录
UsageLog log = new UsageLog();
log.setRequestId(requestId);
log.setTenantId(tenant.getTenantId());
log.setCapability(capability);
log.setVendor(resolveVendor(path)); // 实际路由到的供应商
log.setAmount(usage.getAmount()); // 用量(次/分钟/token)
log.setUnit(usage.getUnit());
log.setDurationMs(System.currentTimeMillis() - startTime);
log.setSuccess(exchange.getResponse().getStatusCode().is2xxSuccessful());
log.setRequestTime(new Date(startTime));
// 异步落库(不阻塞业务响应)
logRepository.saveAsync(log);
}));
}
/**
* 从请求/响应中提取用量
* 不同能力的提取逻辑不同:
* - 按次:amount = 1
* - 按时长:从响应中的时长字段提取
* - 按 token:从 LLM 响应的 usage 字段提取
*/
private UsageRecord extractUsage(ServerWebExchange exchange, String capability) {
switch (capability) {
case "llm/chat":
case "llm/extract":
case "llm/qa":
// 从响应体解析 token 用量
return extractTokenUsage(exchange); // 如 prompt_tokens + completion_tokens
case "asr/transcribe":
case "subtitle/generate":
// 从请求体提取音频时长
return extractDurationUsage(exchange);
default:
// 按次计量
return new UsageRecord(BigDecimal.ONE, "count");
}
}
}
为什么用量日志不能替代计费? 用量日志是"事实记录",计费是"商业决策"。同一个用量,在 SaaS 模式下乘以平台豆值单价就是客户账单;在客户自有云模式下,客户直接在云控制台看自己的 token 账单,网关日志只用来做内部成本分析;在私有化模式下,客户按授权付费,日志只用来评估资源使用率。网关层不掺和"单价",让用量日志保持纯粹的客观性,这是它能在三种模式下通用的原因。
4.2 用量日志的存储与查询
/**
* 用量日志存储
*
* 存储选型:
* - 热数据(近 7 天):Elasticsearch,支持多维聚合查询
* - 冷数据(7 天前):归档到对象存储,按天分桶
*/
@Service
public class UsageLogService {
@Autowired
private ElasticsearchTemplate esTemplate;
/**
* 按租户 × 能力聚合用量(近 7 天)
* 三种模式通用的查询:客户/内部看"每个能力用了多少"
*/
public List<UsageSummary> aggregateByCapability(Long tenantId, String from, String to) {
// ES 聚合查询:按 capability 分组,求和 amount
return esTemplate.aggregate(
"""
{
"size": 0,
"query": {
"bool": {
"filter": [
{
"term": {
"tenantId": %d } },
{
"range": {
"requestTime": {
"gte": "%s", "lte": "%s" } } }
]
}
},
"aggs": {
"by_capability": {
"terms": {
"field": "capability.keyword" },
"aggs": {
"total_amount": {
"sum": {
"field": "amount" } }
}
}
}
}
""".formatted(tenantId, from, to)
);
}
/**
* 按供应商聚合用量
* 客户自有云模式下,这个数据可以直接和云账单对账
*/
public List<UsageSummary> aggregateByVendor(Long tenantId, String from, String to) {
// 类似聚合,按 vendor 分组
}
}
五、能力适配层:异构供应商的统一封装
适配层是网关背后的"翻译官"。同一个能力背后可能对接多家供应商——云服务商(阿里云百炼、华为云 ModelArts、腾讯云混元)、私有化推理节点、开源模型——适配层把差异全部隔离在内部,网关和存量系统看到的是统一的接口。
5.1 适配器接口设计
以"语音识别(ASR)"为例——供应商 A 按音频时长计费、支持 URL 上传;供应商 B 按文件大小计费、需要 Base64 上传。适配层把它们统一成同一个接口:
/**
* ASR 能力适配器接口
*
* 设计原则:适配器内部处理供应商差异,对外暴露统一语义
*/
public interface AsrAdapter {
/**
* 提交录音文件识别任务
*
* @param audioUrl 音频文件的公网/私有 URL
* @param options 识别选项(语言、是否需要说话人分离等)
* @return 任务 ID(异步任务)
*/
String submitTranscribe(String audioUrl, AsrOptions options);
/**
* 查询任务结果
*/
AsrResult queryResult(String taskId);
/**
* 供应商名称(用于路由和日志)
*/
String vendorName();
/**
* 供应商成本模型描述(内部使用,用于成本分析)
* 例如:按时长计费 / 按 token 计费 / 本地算力
*/
CostModel costModel();
}
/**
* 适配器统一响应
* 无论供应商返回什么格式,都转换为这个统一结构
*/
@Data
public class AsrResult {
private String status; // SUCCESS / FAILED / PROCESSING
private List<Segment> segments; // 分段识别结果
private String errorMessage;
@Data
public static class Segment {
private String text;
private Double startTime; // 秒
private Double endTime; // 秒
private String speakerId; // 说话人 ID(开启分离时)
}
}
/**
* 供应商成本模型
* 区分三种计费来源,供内部成本分析使用
*/
@Data
public class CostModel {
/** 计费模式:per_use / per_duration / per_token / local_compute */
private String billingMode;
/** 计费来源:cloud_vendor(客户云账号) / local(私有化) / platform(SaaS 平台) */
private String billingSource;
/** 说明(如"客户云账号按 token 计费") */
private String description;
}
5.2 云服务商适配实现(客户自有云模式)
客户自有云模式下,AI 能力直接对接客户云账号下的模型服务。适配器使用客户提供的云凭证调用,token 费用直接记在客户云账单上:
/**
* 阿里云百炼适配器(客户自有云模式)
*
* 关键点:
* - 使用客户自己的 DashScope API Key(从租户配置读取)
* - token 消耗直接计入客户云账号账单
* - 适配器只负责调用和格式转换,不做任何计费
*/
@Component
@ConditionalOnProperty(name = "ai.deploy.mode", havingValue = "customer-cloud")
public class AliyunBailianAdapter implements LlmAdapter {
@Autowired
private TenantConfigService configService;
@Override
public LlmResult chat(LlmRequest request) {
// 1. 从租户配置读取客户自己的 DashScope API Key
String apiKey = configService.getTenantSecret(
request.getTenantId(), "aliyun.dashscope.api_key"
);
// 2. 构造 OpenAI 兼容请求(百炼支持)
OpenAiClient client = OpenAiClient.builder()
.apiKey(apiKey)
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.build();
ChatCompletion completion = client.chat().completions().create(
ChatCompletionCreateParams.builder()
.model(request.getModel() != null ? request.getModel() : "qwen-plus")
.addUserMessage(request.getPrompt())
.build()
);
// 3. 转换为统一格式(token 用量从响应中提取,记入用量日志)
LlmResult result = new LlmResult();
result.setContent(completion.choices().get(0).message().content());
result.setPromptTokens(completion.usage().promptTokens());
result.setCompletionTokens(completion.usage().completionTokens());
return result;
}
@Override
public String vendorName() {
return "aliyun-bailian";
}
@Override
public CostModel costModel() {
// 客户云账号按 token 计费
return new CostModel("per_token", "cloud_vendor", "客户云账号账单按 token 计费");
}
}
5.3 私有化适配器实现(私有化部署模式)
私有化模式下,模型跑在客户内网的推理节点上(如 vLLM 部署的开源模型)。适配器指向本地推理服务:
/**
* 私有化推理节点适配器(客户内网部署)
*
* 关键点:
* - 指向客户内网部署的 vLLM / Triton 等推理服务
* - 无按量计费,成本已包含在软件授权费中
* - 仍记录 token 用量(供客户评估资源使用率和容量规划)
*/
@Component
@ConditionalOnProperty(name = "ai.deploy.mode", havingValue = "private")
public class LocalVllmAdapter implements LlmAdapter {
@Value("${ai.model.local.vllm-url:http://127.0.0.1:8000/v1}")
private String vllmUrl;
@Override
public LlmResult chat(LlmRequest request) {
// 本地 vLLM 提供 OpenAI 兼容接口
OpenAiClient client = OpenAiClient.builder()
.apiKey("local-dummy-key") // 本地推理无需鉴权
.baseUrl(vllmUrl)
.build();
ChatCompletion completion = client.chat().completions().create(
ChatCompletionCreateParams.builder()
.model(request.getModel() != null ? request.getModel() : "qwen2.5-14b")
.addUserMessage(request.getPrompt())
.build()
);
// 转换统一格式(token 用量记入日志,供容量分析)
LlmResult result = new LlmResult();
result.setContent(completion.choices().get(0).message().content());
result.setPromptTokens(completion.usage().promptTokens());
result.setCompletionTokens(completion.usage().completionTokens());
return result;
}
@Override
public String vendorName() {
return "local-vllm";
}
@Override
public CostModel costModel() {
// 本地算力,无按量计费
return new CostModel("local_compute", "local", "本地算力,包含在授权费中");
}
}
5.4 供应商路由:按部署模式选择
适配层通过"供应商路由策略"决定每次调用走哪家。路由规则的核心维度是部署模式:
/**
* 供应商路由服务
*
* 路由策略(按优先级):
* 1. 部署模式:客户自有云 → 云服务商适配器;私有化 → 本地节点;SaaS → 平台服务
* 2. 租户指定:部分客户合同约定使用指定供应商
* 3. 故障切换:主供应商失败后自动切换到备用
*/
@Service
public class VendorRouter {
@Autowired
private ApplicationContext context;
/**
* 获取当前部署模式下应该使用的 LLM 适配器
*/
public LlmAdapter routeLlm(TenantInfo tenant) {
// 1. 按部署模式筛选
String deployMode = tenant.getDeployMode(); // saas / customer-cloud / private
Map<String, LlmAdapter> allAdapters = context.getBeansOfType(LlmAdapter.class);
// 2. 租户指定优先
String preferred = tenant.getPreferredVendor("llm");
if (preferred != null && allAdapters.containsKey(preferred)) {
return allAdapters.get(preferred);
}
// 3. 按部署模式 + 健康状态选择
return allAdapters.values().stream()
.filter(a -> matchesDeployMode(a, deployMode))
.filter(a -> !circuitBreaker.isOpen(a.vendorName()))
.findFirst()
.orElseThrow(() -> new NoAvailableVendorException("llm"));
}
private boolean matchesDeployMode(LlmAdapter adapter, String deployMode) {
CostModel costModel = adapter.costModel();
switch (deployMode) {
case "private":
return "local".equals(costModel.getBillingSource());
case "customer-cloud":
return "cloud_vendor".equals(costModel.getBillingSource());
default: // saas
return "platform".equals(costModel.getBillingSource());
}
}
}
六、接入模式:同步调用与异步任务
不同的 AI 能力耗时差异巨大。AI 问答(LLM)几秒内返回,可以同步调用;AI 字幕(ASR + 翻译)可能几分钟,必须异步任务。网关需要同时支持两种模式。
6.1 同步调用(低延迟能力)
存量系统 ──POST /ai/v1/llm/qa──▶ 网关 ──▶ LLM 适配器 ──▶ 大模型
存量系统 ◀──────── 2-5s 返回 ──── 网关 ◀── 适配器响应
同步调用适合:AI 问答、客观题批阅、知识点萃取(单段)。
/**
* 同步调用示例:AI 问答
*/
public String aiQa(Long tenantId, String question) {
QaRequest req = new QaRequest();
req.setQuestion(question);
req.setKnowledgeBaseId("kb_product_manual");
return gatewayClient.call("llm/qa", req, QaResponse.class).getAnswer();
}
6.2 异步任务(高耗时能力)
对于耗时超过 30 秒的能力,网关提供标准的"提交任务 + 查询状态 + 结果回调"三接口模式:
存量系统 ──POST /ai/v1/asr/transcribe──▶ 网关 ──提交任务──▶ 适配器
存量系统 ◀──── {taskId: "task_123"} ───── 网关
存量系统 ──GET /ai/v1/asr/tasks/task_123──▶ 网关
存量系统 ◀──── {status: "PROCESSING"} ──── 网关
... 轮询或等回调 ...
存量系统 ◀──── {status: "SUCCESS", data: {...}} ──── 网关
/**
* 异步任务管理(网关侧)
*/
@RestController
@RequestMapping("/ai/v1")
public class AsyncTaskController {
@Autowired
private AsyncTaskService taskService;
/**
* 提交异步任务
*/
@PostMapping("/{capability}/tasks")
public Result<SubmitResponse> submitTask(
@PathVariable String capability,
@RequestBody SubmitRequest request,
ServerWebExchange exchange) {
TenantInfo tenant = (TenantInfo) exchange.getAttributes().get("tenant");
// 创建任务记录
AsyncTask task = taskService.createTask(
tenant.getTenantId(), capability, request
);
// 异步执行(投递到任务队列)
taskService.executeAsync(task.getTaskId());
return Result.ok(new SubmitResponse(task.getTaskId()));
}
/**
* 查询任务状态
*/
@GetMapping("/{capability}/tasks/{taskId}")
public Result<AsyncTaskStatus> queryTask(
@PathVariable String taskId) {
return Result.ok(taskService.getStatus(taskId));
}
/**
* 结果回调(可选,替代轮询)
* 存量系统注册回调地址,任务完成时网关主动通知
*/
@PostMapping("/{capability}/tasks/{taskId}/callback")
public Result<Void> registerCallback(
@PathVariable String taskId,
@RequestBody CallbackConfig config) {
taskService.registerCallback(taskId, config.getCallbackUrl());
return Result.ok();
}
}
七、私有化部署架构:数据不出域的合规方案
金融、国企客户的合规要求是"数据不出域"——员工的培训数据、考试数据不能离开企业网络。私有化模式下,网关和模型推理全部部署在客户内网:
┌────────────────────────────────────────────────┐
│ 企业内网(客户私有环境) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 存量系统 │ │ AI 能力 │ │ 私有化模型 │ │
│ │ │ │ 网关 │ │ 推理服务 │ │
│ │ (不动) │──▶│ (私有化部署)│──▶│ (vLLM等) │ │
│ └──────────┘ └──────────┘ └────────────┘ │
│ │ │
│ ┌────▼────┐ ┌────────────┐ │
│ │ 用量日志 │ │ ASR/TTS │ │
│ │ (本地存储) │ │ 私有化节点 │ │
│ └─────────┘ └────────────┘ │
└────────────────────────────────────────────────┘
│(仅同步用量汇总/审计摘要,不传业务数据)
┌───────▼────────────────────────────────────────┐
│ 平台管理面(可选,用于运维支持) │
│ 能力配置下发 · 模型更新包 · 用量汇总上报 │
└────────────────────────────────────────────────┘
私有化部署的关键设计:
能力配置可下发。 网关的能力路由规则、供应商配置支持从平台管理面下发,客户侧部署的网关定期同步配置。新增一个能力时,客户不需要重新部署网关。
模型更新离线化。 大模型、ASR、TTS 模型的更新以"更新包"形式提供,客户下载后在私有环境离线安装,模型推理全程不离开企业网络。
用量日志本地存储。 私有化模式下用量日志只存在客户本地(用于客户自己的容量规划和成本分析),只向平台上报"用量汇总摘要"(按功能聚合的统计值),不上报具体业务内容。
# 私有化部署的网关配置示例
ai:
gateway:
mode: private # 私有化模式
sync-config-interval: 3600 # 每小时从平台管理面同步配置
model:
llm: local-vllm # 本地大模型推理服务
asr: local-asr-node # 本地 ASR 节点
tts: local-tts-node # 本地 TTS 节点
usage-log:
storage: local # 用量日志本地存储
report-summary: "0 0 2 * * ?" # 每天上报聚合摘要
compliance:
data-residency: on-prem # 数据驻留本地
audit-log: enabled # 全量审计日志
八、工程细节:限流、熔断与降级
8.1 按能力限流
存量系统接入后,如果调用方代码有 bug(比如死循环调用 AI 问答),可能把 AI 服务打爆。网关按"租户 × 能力"维度限流:
/**
* 限流过滤器:按租户 × 能力维度限流
* 实现:Redis 滑动窗口计数
*/
@Component
public class RateLimitFilter implements GlobalFilter {
@Autowired
private StringRedisTemplate redisTemplate;
/** Lua 脚本:滑动窗口限流 */
private static final String RATE_LIMIT_LUA =
"local key = KEYS[1] " +
"local window = tonumber(ARGV[1]) " + // 窗口大小(秒)
"local limit = tonumber(ARGV[2]) " + // 窗口内最大次数
"local now = redis.call('TIME')[1] " +
"redis.call('ZREMRANGEBYSCORE', key, 0, now - window) " +
"local count = redis.call('ZCARD', key) " +
"if count < limit then " +
" redis.call('ZADD', key, now, now .. '_' .. ARGV[3]) " +
" redis.call('EXPIRE', key, window) " +
" return 1 " +
"end " +
"return 0 ";
public boolean tryAcquire(TenantInfo tenant, String capability) {
String key = "ai:ratelimit:" + tenant.getTenantId() + ":" + capability;
String reqId = UUID.randomUUID().toString().substring(0, 8);
// 默认:每分钟 60 次(可在配置中心调整)
Long result = redisTemplate.execute(
new DefaultRedisScript<>(RATE_LIMIT_LUA, Long.class),
List.of(key),
"60", "60", reqId
);
return result != null && result == 1L;
}
}
8.2 供应商故障熔断
某个供应商(如客户云账号下的模型服务)故障时,不能把存量系统的调用全部拖死。熔断机制配合"供应商路由"实现自动切换:
/**
* 供应商熔断器
*
* 策略(滑动窗口):
* - 10 秒内错误率超过 50%,熔断 30 秒
* - 熔断期间自动切换到备用供应商
* - 30 秒后进入半开状态,放少量请求探测
*/
@Component
public class CircuitBreaker {
private final Map<String, CircuitState> states = new ConcurrentHashMap<>();
public void recordFailure(String vendorName) {
CircuitState state = states.computeIfAbsent(vendorName, k -> new CircuitState());
state.recordFailure();
}
public boolean isOpen(String vendorName) {
CircuitState state = states.get(vendorName);
return state != null && state.isOpen();
}
@Data
public static class CircuitState {
private final Deque<Long> failureTimes = new ArrayDeque<>();
private long openedAt = 0;
public void recordFailure() {
long now = System.currentTimeMillis();
// 清理 10 秒前的记录
while (!failureTimes.isEmpty() && now - failureTimes.peekFirst() > 10000) {
failureTimes.pollFirst();
}
failureTimes.addLast(now);
// 10 秒内错误超过 5 次 → 熔断 30 秒
if (failureTimes.size() >= 5) {
openedAt = now;
log.warn("Circuit opened for vendor, failures: {}", failureTimes.size());
}
}
public boolean isOpen() {
if (openedAt == 0) return false;
// 熔断 30 秒后自动半开
if (System.currentTimeMillis() - openedAt > 30000) {
openedAt = 0;
failureTimes.clear();
return false;
}
return true;
}
}
}
8.3 存量系统的降级策略
存量系统接入 AI 后,必须考虑"AI 不可用时业务怎么办"。降级策略在存量系统侧配置:
/**
* AI 能力降级策略(存量系统侧)
*
* 原则:AI 是增强能力,不是核心依赖。
* AI 故障时,业务应该回到"人工模式",而不是让整个业务挂掉。
*/
@Component
public class AiFallbackManager {
/**
* 统一降级处理
*
* @param capability AI 能力标识
* @param fallback 人工/传统模式的处理逻辑
*/
public <T> T withFallback(String capability, Supplier<T> aiCall,
Supplier<T> fallback) {
try {
return aiCall.get();
} catch (AiGatewayException e) {
log.warn("AI capability {} failed (code={}), falling back",
capability, e.getCode());
// 记录降级事件(供运营统计 AI 可用率)
metricCollector.recordFallback(capability, e.getCode());
// 执行降级逻辑
return fallback.get();
}
}
/**
* 示例:AI 出题失败 → 人工出题
*/
public List<Question> generateQuestionsWithFallback(String courseText) {
return withFallback(
"question/generate",
() -> aiGatewayClient.call("question/generate", ...),
() -> manualQuestionService.generateByTemplate(courseText) // 传统模板出题
);
}
/**
* 示例:AI 批阅失败 → 人工批阅
*/
public GradingResult gradeWithFallback(String answerText, String standardAnswer) {
return withFallback(
"grading/subjective",
() -> aiGatewayClient.call("grading/subjective", ...),
() -> {
// 传统模式:标记为待人工批阅,进入人工队列
manualGradingQueue.enqueue(answerText, standardAnswer);
return GradingResult.pendingManual();
}
);
}
}
九、效果总结
这套接入方案在多个存量培训系统升级项目中落地,关键数据:
| 指标 | 效果 |
|---|---|
| 存量系统接入成本 | 引入 1 个 HTTP 客户端依赖 + 配置 3 个参数 |
| 接入周期 | 单个存量系统从评估到联调上线约 2-3 天 |
| 存量代码改动量 | 0 行(业务数据、表结构、已有接口全部不动) |
| 部署模式适配 | SaaS / 客户自有云 / 私有化三模式切换只改配置 |
| AI 能力上线速度 | 新能力只需网关加一条路由 + 配一个适配器 |
| 供应商切换耗时 | 分钟级(改配置中心路由即可,业务无感) |
| 私有化合规 | 业务数据零出域,仅上报用量聚合摘要 |
| AI 可用率 | 网关层限流 + 熔断 + 降级,存量业务零中断 |
三个核心经验:
网关层不做计费,只做记账。 这是本方案与 SaaS 计量体系的关键区别。AI 能力的计费主体由部署模式决定——客户自有云走云服务商 token 账单、私有化走软件授权费、SaaS 才走平台计量。网关统一做"用量日志"(事实记录),计费决策留给上层,这样一套网关逻辑就能适配三种部署模式。
适配层决定可扩展性。 云服务商适配器用客户的云凭证调用(token 费用走客户账单),私有化适配器指向本地推理节点。切换部署模式或供应商只需要改配置,存量系统无感知。适配层的 CostModel 明确标注"计费来源",避免网关误判计费主体。
降级策略必须在接入前设计好。 AI 是增强能力,不是核心依赖。存量系统的业务连续性不能押在 AI 供应商的稳定性上,每项 AI 能力都要设计"AI 不可用时回到人工模式"的兜底路径。
如果你的培训系统也有 AI 升级需求但担心改动风险,这个方案的核心思路是:不动存量系统,把 AI 能力做成标准化的外部服务,通过网关接入。 业务系统还是那个业务系统,只是多了一个"AI 能力供应商"。