构建企业 AI 中台——能力抽象、服务编排与多租户治理一、背景与动机当企业内的 AI 应用从一个试点扩展到多个业务线时重复建设的问题就会暴露——每个业务线独立接入模型、独立管理 Prompt、独立搭建 RAG 流程导致能力碎片化、成本失控、治理缺失。这正是AI 中台诞生的背景。AI 中台的核心定位是将分散的 AI 能力抽象为可复用的服务通过统一的服务编排降低业务接入门槛通过多租户治理实现成本可控和安全合规。它不是一个新的技术框架而是对已有能力Spring AI、向量数据库、模型服务的组织方式——从每个业务自建转变为平台统一提供业务按需调用。但 AI 中台的建设容易陷入过度抽象的陷阱——过早地将所有能力收归平台导致业务线的灵活性丧失。本文将给出一个务实的 AI 中台架构设计明确哪些能力应该中台化、哪些应该留在业务线并提供完整的 Java 实现代码。二、核心原理与技术细节AI 中台的三层架构graph TB subgraph 业务接入层 B1[智能客服br/业务线A] B2[文档审核br/业务线B] B3[数据分析br/业务线C] end subgraph 服务编排层 S1[对话服务br/ChatService] S2[RAG检索服务br/KnowledgeService] S3[Agent编排服务br/AgentService] S4[模型管理服务br/ModelService] end subgraph 能力抽象层 C1[模型接入适配br/OpenAI/Ollama/智谱] C2[Prompt模板库br/版本化管理] C3[向量存储引擎br/多租户命名空间] C4[Function注册中心br/权限白名单] end B1 -- S1 B1 -- S2 B2 -- S2 B2 -- S3 B3 -- S1 B3 -- S3 S1 -- C1 S1 -- C2 S2 -- C3 S2 -- C2 S3 -- C4 S3 -- C1 S4 -- C1 style 业务接入层 fill:#ffebee style 服务编排层 fill:#fff3e0 style 能力抽象层 fill:#e8f5e9中台化 vs 业务线自治的边界划分能力维度中台化平台统一业务线自治各自负责划分依据模型接入统一的多模型路由 降级业务线选择具体模型降级策略需要全局协调Prompt模板公共模板库 版本管理业务线自定义业务Prompt公共模板可复用业务Prompt不可向量存储统一的向量引擎 多租户隔离业务线的知识库内容存储引擎复用知识内容自治Function Calling注册中心 权限白名单业务线注册自己的Function权限需要统一管理安全防护全链路Advisor 脱敏业务线的业务级安全规则基础安全中台化业务规则自治可观测性统一指标 分布式追踪业务线的业务指标技术指标统一业务指标自治成本管理全局Token预算 租户配额业务线的使用策略成本必须全局管控多租户治理的核心设计多租户是 AI 中台最关键的治理维度。不同业务线租户之间的数据、配额、模型权限必须严格隔离graph TB subgraph 租户隔离维度 D1[数据隔离br/向量存储命名空间br/Prompt模板租户分区] D2[配额隔离br/Token消耗配额br/模型调用次数限制] D3[权限隔离br/模型访问权限br/Function调用白名单] D4[成本隔离br/租户独立计费br/成本归因到业务线] end D1 -- IMPL[实现方式br/Redis配额 Nacos命名空间br/ Advisor拦截 审计日志] D2 -- IMPL D3 -- IMPL D4 -- IMPL style 租户隔离维度 fill:#ffebee style IMPL fill:#4caf50三、实践案例与代码实现租户上下文管理/** * 租户上下文管理器——AI中台多租户的基础组件 * 从请求中提取租户标识贯穿整个调用链路 * 与灰度发布的GrayTagContextHolder设计思路一致 */ Component Slf4j public class TenantContextHolder { private static final ThreadLocalTenantContext CONTEXT new ThreadLocal(); /** * 设置当前租户上下文——由入口Interceptor调用 */ public static void setContext(TenantContext context) { CONTEXT.set(context); } /** * 获取当前租户上下文——由中台服务调用 */ public static TenantContext getContext() { TenantContext context CONTEXT.get(); if (context null) { throw new IllegalStateException(租户上下文未设置请检查TenantEntryInterceptor); } return context; } /** * 清除上下文——请求结束时必须调用防止ThreadLocal泄漏 */ public static void clear() { CONTEXT.remove(); } } /** * 租户上下文数据——包含租户的所有治理信息 */ public class TenantContext { private String tenantId; // 租户标识如 business-line-a private String tenantName; // 租户名称 private QuotaConfig quotaConfig; // 配额配置 private SetString allowedModels; // 允许访问的模型列表 private SetString allowedFunctions; // 允许调用的Function列表 private String vectorNamespace; // 向量存储命名空间 // getters and setters... } /** * 租户入口拦截器——从请求中提取租户标识并设置上下文 * 通常通过API Key或认证Token中的租户信息来识别 */ Component Slf4j public class TenantEntryInterceptor implements HandlerInterceptor { private final TenantConfigRepository tenantConfigRepository; public TenantEntryInterceptor(TenantConfigRepository tenantConfigRepository) { this.tenantConfigRepository tenantConfigRepository; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 从请求Header中提取租户标识 String tenantId request.getHeader(X-Tenant-Id); if (tenantId null || tenantId.isBlank()) { response.setStatus(401); log.warn(请求缺少租户标识); return false; } // 加载租户配置 TenantConfig config tenantConfigRepository.findById(tenantId) .orElseThrow(() - new IllegalArgumentException(未知租户: tenantId)); // 构建租户上下文 TenantContext context new TenantContext(); context.setTenantId(tenantId); context.setTenantName(config.getTenantName()); context.setQuotaConfig(config.getQuotaConfig()); context.setAllowedModels(config.getAllowedModels()); context.setAllowedFunctions(config.getAllowedFunctions()); context.setVectorNamespace(config.getVectorNamespace()); TenantContextHolder.setContext(context); log.debug(租户上下文已设置: tenantId{}, tenantId); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { TenantContextHolder.clear(); } }租户配额管理/** * 租户配额管理服务——Token消耗与调用次数的限流控制 * 核心逻辑每次模型调用前检查配额余量超限则拒绝或降级 */ Service Slf4j public class TenantQuotaService { private final StringRedisTemplate redisTemplate; private final MeterRegistry meterRegistry; // 配额Key前缀 private static final String TOKEN_QUOTA_KEY ai:quota:token:; private static final String CALL_QUOTA_KEY ai:quota:call:; public TenantQuotaService(StringRedisTemplate redisTemplate, MeterRegistry meterRegistry) { this.redisTemplate redisTemplate; this.meterRegistry meterRegistry; } /** * 检查Token配额——调用前校验超限则拒绝 * param tenantId 租户ID * param estimatedTokens 预估本次调用消耗的Token数 * return true配额充足可继续, false配额不足需降级 */ public boolean checkTokenQuota(String tenantId, int estimatedTokens) { String key TOKEN_QUOTA_KEY tenantId : getCurrentMonth(); String remainingStr redisTemplate.opsForValue().get(key); if (remainingStr null) { // 月初首次访问初始化配额 long monthlyQuota getMonthlyQuota(tenantId); redisTemplate.opsForValue().set(key, String.valueOf(monthlyQuota), Duration.ofDays(35)); // 配额Key有效期35天覆盖整月缓冲 remainingStr String.valueOf(monthlyQuota); } long remaining Long.parseLong(remainingStr); if (remaining estimatedTokens) { log.warn(租户Token配额不足: tenantId{}, remaining{}, estimated{}, tenantId, remaining, estimatedTokens); meterRegistry.counter(ai.quota.token.rejected, tenant, tenantId).increment(); return false; } return true; } /** * 记录Token消耗——调用完成后扣减配额 * param tenantId 租户ID * param actualTokens 实际消耗的Token数 */ public void recordTokenConsumption(String tenantId, int actualTokens) { String key TOKEN_QUOTA_KEY tenantId : getCurrentMonth(); redisTemplate.opsForValue().decrement(key, actualTokens); meterRegistry.counter(ai.quota.token.consumed, tenant, tenantId) .increment(actualTokens); } /** * 获取租户月度Token配额——从配置中心或数据库加载 */ private long getMonthlyQuota(String tenantId) { // 实际实现中从TenantConfig加载 return 1_000_000L; // 默认100万Token/月 } private String getCurrentMonth() { return LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy-MM)); } }AI 中台服务编排层/** * AI中台对话服务——统一的模型调用入口 * 职责租户校验 配额检查 模型路由 安全过滤 审计记录 * 业务线只需调用这一个接口无需关心底层细节 */ Service Slf4j public class AiPlatformChatService { private final ChatClient chatClient; private final TenantQuotaService quotaService; private final PromptTemplateManager promptManager; private final AiCallAuditLogger auditLogger; private final PromptInjectionDefenseAdvisor injectionDefense; public AiPlatformChatService(ChatClient chatClient, TenantQuotaService quotaService, PromptTemplateManager promptManager, AiCallAuditLogger auditLogger, PromptInjectionDefenseAdvisor injectionDefense) { this.chatClient chatClient; this.quotaService quotaService; this.promptManager promptManager; this.auditLogger auditLogger; this.injectionDefense injectionDefense; } /** * 统一对话接口——业务线通过此接口接入AI能力 * param scenario 业务场景标识如 customer-service, doc-review * param userMessage 用户输入 * param options 可选参数模型偏好、温度等 * return AI响应内容 */ public ChatResult chat(String scenario, String userMessage, ChatOptions options) { TenantContext tenant TenantContextHolder.getContext(); long startTime System.currentTimeMillis(); // 1. 租户权限校验——检查租户是否有权使用该场景 if (!tenant.getAllowedModels().contains(options.getModelId())) { log.warn(租户无权使用模型: tenant{}, model{}, tenant.getTenantId(), options.getModelId()); return ChatResult.ofError(当前租户无权使用该模型请联系管理员); } // 2. Prompt注入检测——输入层安全防护 if (injectionDefense.isPotentialInjection(userMessage)) { log.warn(检测到Prompt注入租户: {}, tenant.getTenantId()); return ChatResult.ofError(输入内容不符合安全规范请调整后重新提问); } // 3. 配额检查——Token预算控制 int estimatedTokens estimateTokens(userMessage); if (!quotaService.checkTokenQuota(tenant.getTenantId(), estimatedTokens)) { // 配额不足时降级——使用预设FAQ或小模型 log.info(Token配额不足降级到FAQ模式: tenant{}, tenant.getTenantId()); return ChatResult.ofFallback(智能客服配额已用尽请联系人工客服或下月再试); } // 4. Prompt模板加载——从模板库获取场景对应的系统指令 PromptTemplate template promptManager.loadTemplate(scenario, tenant.getTenantId()); String systemPrompt template.render(Map.of( tenant_name, tenant.getTenantName(), scenario, scenario)); // 5. 模型调用 try { String response chatClient.prompt() .system(systemPrompt) .user(userMessage) .options(options.toSpringAiOptions()) .call() .content(); // 6. 记录Token消耗和审计日志 long duration System.currentTimeMillis() - startTime; quotaService.recordTokenConsumption(tenant.getTenantId(), estimatedTokens); auditLogger.recordCall( tenant.getTenantId(), system, options.getModelId(), truncate(userMessage, 200), truncate(response, 200), new TokenUsage(estimatedTokens, estimateTokens(response)), duration); return ChatResult.ofSuccess(response); } catch (Exception e) { log.error(模型调用失败: tenant{}, scenario{}, error{}, tenant.getTenantId(), scenario, e.getMessage()); // 调用失败不扣减配额允许重试 return ChatResult.ofError(AI服务暂时不可用请稍后再试); } } /** * Token估算——基于字符数粗略估算中文约1.5字符/Token */ private int estimateTokens(String text) { if (text null) return 0; // 简化估算英文约4字符/Token中文约1.5字符/Token // 取中间值2.5字符/Token作为通用估算 return (int) (text.length() / 2.5) 100; // 加100作为系统Prompt的缓冲 } private String truncate(String content, int maxLen) { if (content null) return [NULL]; return content.length() maxLen ? content : content.substring(0, maxLen) [TRUNCATED]; } } /** * 对话结果封装——统一中台的返回格式 */ public class ChatResult { private String content; private ResultType type; // SUCCESS, FALLBACK, ERROR private String message; public static ChatResult ofSuccess(String content) { ChatResult result new ChatResult(); result.setContent(content); result.setType(ResultType.SUCCESS); return result; } public static ChatResult ofFallback(String message) { ChatResult result new ChatResult(); result.setType(ResultType.FALLBACK); result.setMessage(message); return result; } public static ChatResult ofError(String message) { ChatResult result new ChatResult(); result.setType(ResultType.ERROR); result.setMessage(message); return result; } public enum ResultType { SUCCESS, FALLBACK, ERROR } // getters... }Prompt 模板的多租户管理/** * Prompt模板管理器——支持公共模板和租户自定义模板 * 加载优先级租户自定义 公共模板 默认模板 * 这是中台平台提供基础业务线可定制原则的体现 */ Component Slf4j public class PromptTemplateManager { private final ResourceLoader resourceLoader; private final TenantCustomPromptRepository customPromptRepository; // 模板加载优先级路径 // 1. 租户自定义: prompts/{scenario}/{tenantId}/latest.st // 2. 公共模板: prompts/{scenario}/public/latest.st // 3. 默认模板: prompts/{scenario}/default.st public PromptTemplateManager(ResourceLoader resourceLoader, TenantCustomPromptRepository customPromptRepository) { this.resourceLoader resourceLoader; this.customPromptRepository customPromptRepository; } /** * 加载Prompt模板——按优先级依次查找 * param scenario 业务场景 * param tenantId 租户ID * return PromptTemplate实例 */ public PromptTemplate loadTemplate(String scenario, String tenantId) { // 优先级1租户自定义模板数据库存储支持动态修改 OptionalString customTemplate customPromptRepository .findLatestContent(scenario, tenantId); if (customTemplate.isPresent()) { log.debug(使用租户自定义模板: scenario{}, tenant{}, scenario, tenantId); return new PromptTemplate(customTemplate.get()); } // 优先级2公共模板文件存储版本化管理 String publicPath classpath:prompts/ scenario /public/latest.st; try { Resource resource resourceLoader.getResource(publicPath); if (resource.exists()) { String templateText StreamUtils.copyToString( resource.getInputStream(), StandardCharsets.UTF_8); log.debug(使用公共模板: scenario{}, scenario); return new PromptTemplate(templateText); } } catch (IOException e) { log.warn(公共模板加载失败: {}, e.getMessage()); } // 优先级3默认模板兜底 String defaultPath classpath:prompts/ scenario /default.st; try { Resource resource resourceLoader.getResource(defaultPath); String templateText StreamUtils.copyToString( resource.getInputStream(), StandardCharsets.UTF_8); log.debug(使用默认模板: scenario{}, scenario); return new PromptTemplate(templateText); } catch (IOException e) { throw new RuntimeException(默认模板加载失败: scenario, e); } } }四、常见问题与避坑指南问题一过度中台化——所有能力收归平台过早地将所有 AI 能力收归中台会导致业务线丧失灵活性。每个业务线的 AI 需求不同——客服需要对话能力审核需要分类能力分析需要推理能力。建议中台只抽象共性能力模型接入、安全防护、配额管理业务线保留个性能力业务Prompt、知识库内容、业务Function的自治权。问题二多租户配额的精度问题Token 配额的扣减基于估算而非实际消耗可能导致配额计算不准确。建议调用完成后从模型响应中获取实际 Token 数大多数模型 API 会返回usage.prompt_tokens和usage.completion_tokens用实际值修正配额扣减。问题三向量存储的多租户隔离不同租户的知识库必须严格隔离——租户A不应检索到租户B的文档。建议使用向量数据库的命名空间功能如 Milvus 的 Collection、Redis 的 Key前缀、Pinecone 的 Namespace按租户ID分区存储和检索。问题四中台服务的可用性依赖所有业务线依赖同一个中台服务中台故障会导致所有AI功能瘫痪。建议中台服务必须具备高可用设计——多实例部署、数据库主从、Redis集群。关键业务线应有独立的降级方案如本地缓存的热点FAQ不完全依赖中台。问题五Prompt 模板的变更管理公共Prompt模板的修改会影响所有租户可能导致业务行为突变。建议公共模板变更需经过灰度验证——先在测试租户上验证效果再逐步推广。租户自定义模板不受公共模板变更影响这是租户自治原则的保障。五、总结与展望AI 中台的建设不是一步到位的项目而是渐进演进的体系graph LR S1[阶段1br/能力整合br/统一模型接入安全防护] -- S2[阶段2br/服务编排br/对话/RAG/Agent标准化] S2 -- S3[阶段3br/多租户治理br/配额权限成本隔离] S3 -- S4[阶段4br/生态开放br/业务线自助接入模板市场] style S1 fill:#c8e6c9 style S2 fill:#a5d6a7 style S3 fill:#66bb6a style S4 fill:#4caf50核心要点AI 中台的价值是减少重复建设而非剥夺业务灵活性——共性能力中台化个性能力业务线自治。三层架构是务实的设计——能力抽象层基础设施、服务编排层标准服务、业务接入层各业务线。多租户治理是中台最关键的设计维度——数据隔离、配额控制、权限管理、成本归因四维缺一不可。Prompt模板的三级优先级设计租户自定义 公共模板 默认模板体现了平台提供基础业务线可定制的原则。配额管理必须从估算进化到精确——调用前估算校验调用后实际扣减形成闭环。AI 中台是架构层面的决策而非技术层面的创新。它的成功取决于是否真正降低了业务接入门槛、是否实现了成本可控、是否保障了安全合规。这三个目标的达成才是中台建设的真正价值。