三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Day49-AI微服务化-将大模型能力封装为标准微服务

Day49-AI微服务化-将大模型能力封装为标准微服务

把大模型当作一个普通的HTTP微服务来对待,是这一代Java工程师必须建立的工程思维。

今天这篇,我会用我们项目组的真实改造过程为例,从架构分层、代码实现到模型路由,一步步把AI能力封装成「别的业务系统愿意调用」的微服务。


一、为什么大模型必须微服务化?

很多人第一次接触AI能力,第一反应就是:「业务代码里直接调一下ChatClient.call()不就完了?」

行,但只在你这个项目就一个AI调用点的阶段。一旦出现下面任意一种情况,你就该把它服务化了:

触发场景

直接调用的痛点

多个业务团队都要用AI

每个团队都要接一遍API、申请一遍Key、重写一遍Prompt

公司用了多家模型

不同团队锁死在某个供应商,议价权丢失

要做AI用量计费

用量分散在各业务系统表里,月底对账对到哭

出了幻觉/Prompt注入

没有统一的输入净化层,没有审计日志

老板要看AI大屏

没有统一的调用指标采集点

核心原则:AI能力就像数据库一样,是企业级基础设施——你不会让每个业务系统直接连生产DB,为什么要让它们直接调大模型?


二、AI微服务的四层架构

把大模型封装成标准微服务,我们采用四层架构。我把各层的职责划清楚:

关键原则:业务团队只看到第②层,他们不需要知道底下是GPT-4还是Qwen。这就是服务化的意义——对调用方隐藏复杂性


三、代码实现:搭建一个AI能力微服务

下面我用一个真实的「文档问答」能力为例,把这套架构跑通。所有代码基于 Spring Boot 3.3 + Spring AI 1.0 + JDK 17。

3.1 Maven依赖

xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-zhipuai</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> </dependencies>

3.2 业务能力服务:对外的稳定接口

这是给业务团队调的入口。我把所有业务字段、错误码、SLA都封装在这里,下游完全不感知模型变更。

/** * AI能力微服务的对外Controller * 业务团队只依赖这个接口,与模型解耦 */ @RestController @RequestMapping("/api/v1/ai") @RequiredArgsConstructor public class AiCapabilityController { private final DocumentQaService documentQaService; private final UsageRecorder usageRecorder; /** * 文档问答接口 * 业务系统只需要传:文档ID + 问题 + 用户ID */ @PostMapping("/doc-qa") public ResponseEntity<AiResponse<DocAnswer>> docQa(@Valid @RequestBody DocQaRequest request, @RequestHeader("X-Biz-Source") String bizSource) { long start = System.currentTimeMillis(); try { // 1. 参数基础校验(鉴权在外层网关,这里只做业务校验) Document doc = documentService.getById(request.getDocId()); if (doc == null) { return AiResponse.fail("DOC_NOT_FOUND", "文档不存在"); } // 2. 调用业务能力(内部实现对调用方完全透明) DocAnswer answer = documentQaService.answer( request.getDocId(), request.getQuestion(), request.getUserId() ); // 3. 记录用量(这里是最关键的环节,后面看) long cost = System.currentTimeMillis() - start; usageRecorder.record(bizSource, "doc-qa", answer.getPromptTokens(), answer.getCompletionTokens(), cost); return AiResponse.ok(answer); } catch (BusinessException e) { log.warn("业务异常 biz={} code={}", bizSource, e.getCode()); return AiResponse.fail(e.getCode(), e.getMessage()); } catch (Exception e) { log.error("AI调用异常", e); // 兜底降级:返回预先准备好的标准答案或转人工 return AiResponse.fail("AI_UNAVAILABLE", "AI服务暂时不可用,请稍后重试"); } } /** * 流式问答(SSE)—— 长对话场景必备 */ @GetMapping(value = "/doc-qa/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> streamDocQa(@RequestParam String docId, @RequestParam String question, @RequestParam String userId) { return documentQaService.streamAnswer(docId, question, userId) .map(chunk -> ServerSentEvent.builder(chunk).build()) // 异常也要返回给前端,不能让连接挂死 .onErrorResume(e -> { log.error("SSE流式异常", e); return Flux.just(ServerSentEvent.builder("[ERROR]AI服务异常").build()); }); } }

这段代码的几个关键设计

  1. X-Biz-Source头部:强制业务方标识来源,用量统计、计费、限流的依据。
  1. AiResponse统一包装:所有AI接口必须返回这个结构,前端对接成本最低。
  1. 业务校验与AI调用分离:业务校验失败绝不打模型,节省Token。
  1. 兜底降级:AI挂掉不能影响业务——这是微服务的基本素养。

3.3 模型路由层:按场景选择模型与降级

业务能力层下面,就是模型路由层。这里是AI微服务最体现工程能力的地方。

/** * 模型路由:根据场景动态选择模型 * 核心思想:把"调哪个模型"的决策从业务代码里剥离出来 */ @Service @Slf4j public class ModelRouter { private final ChatModel openAiModel; // GPT-4o private final ChatModel qwenModel; // 通义千问 private final ChatModel deepseekModel; // DeepSeek private final TokenBucketLimiter limiter; // 限流器 /** * 路由策略封装 */ public ChatModel route(RouteContext ctx) { // 1. 限流检查(最重要的护栏) if (!limiter.tryAcquire(ctx.getBizSource())) { throw new BusinessException("RATE_LIMIT", "调用频率超限"); } // 2. 按业务来源路由 return switch (ctx.getBizSource()) { case "customer-service" -> { // 客服场景:中文友好、成本敏感 → 通义 log.debug("路由到通义千问"); yield qwenModel; } case "code-assistant" -> { // 代码场景:英文能力强、JSON格式稳 → DeepSeek log.debug("路由到DeepSeek"); yield deepseekModel; } case "image-desc" -> { // 多模态 → GPT-4o yield openAiModel; } default -> { // 兜底用主力模型 yield qwenModel; } }; } } /** * 路由上下文:把决策依据集中管理 */ @Data @Builder public class RouteContext { private String bizSource; // 业务来源 private String userId; // 用户ID private int priority; // 优先级 0-9,9最高 private boolean isVip; // 是否VIP private int estimatedTokens; // 预估Token }

为什么要做路由?

  • 成本优化:简单问题用便宜模型,复杂问题用强模型;
  • 风险分散:一家模型API挂掉,自动切到备胎;
  • 差异化SLA:VIP用户走专用通道。

3.4 用量统计:大模型版「拦路虎」

这是把AI「真正当微服务」的关键一步——没有计费的微服务,老板没法给你批预算。

/** * 用量记录器 * 高频调用场景,必须异步,不能阻塞AI调用主链路 */ @Component @Slf4j public class UsageRecorder { private final UsageMapper usageMapper; private final ThreadPoolExecutor asyncPool; /** * 记录一次AI调用 * 用线程池异步落库,不阻塞调用链路 */ public void record(String bizSource, String capability, int promptTokens, int completionTokens, long costMs) { asyncPool.execute(() -> { try { UsageRecord record = UsageRecord.builder() .bizSource(bizSource) .capability(capability) .promptTokens(promptTokens) .completionTokens(completionTokens) .totalTokens(promptTokens + completionTokens) .costMs(costMs) .estimatedCost(calcCost(bizSource, promptTokens, completionTokens)) .timestamp(LocalDateTime.now()) .build(); usageMapper.insert(record); } catch (Exception e) { // 用量记录失败不能影响主流程,记录到日志兜底 log.error("用量记录失败 biz={} capability={}", bizSource, capability, e); } }); } /** * 成本计算 * 真实生产场景这里应该读配置中心,模型价格随时会变 */ private BigDecimal calcCost(String bizSource, int pt, int ct) { // 单位:元/千Token BigDecimal ptRate = switch (bizSource) { case "customer-service" -> new BigDecimal("0.0008"); // 通义 case "code-assistant" -> new BigDecimal("0.001"); // DeepSeek default -> new BigDecimal("0.005"); // GPT-4o }; return ptRate.multiply(new BigDecimal(pt + ct)) .divide(new BigDecimal(1000), 6, RoundingMode.HALF_UP); } }

到这里,一个完整的AI微服务就出来了。再用 Spring Cloud Gateway 给它加个网关限流,一个企业级的AI能力中台就成形了。


四、AI调用链追踪:把日志串起来

微服务架构下,AI调用会跨越多个内部服务。没有TraceId,出问题排查能让你薅秃头发。

下面是个轻量级的TraceId方案,不依赖SkyWalking这种重武器,但能解决80%的问题:

/** * AI调用上下文,贯穿整个调用链 * 用ThreadLocal传,跨服务通过Header传递 */ public class AiCallContext { private static final ThreadLocal<CallContext> CONTEXT = new ThreadLocal<>(); public static void set(String traceId, String userId, String bizSource) { CONTEXT.set(new CallContext(traceId, userId, bizSource, System.currentTimeMillis())); } public static CallContext get() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } @Data @AllArgsConstructor public static class CallContext { private String traceId; private String userId; private String bizSource; private long startTime; } } /** * 在请求入口设置TraceId */ @Component public class AiCallContextFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) { String traceId = req.getHeader("X-Trace-Id"); if (StringUtils.isEmpty(traceId)) { traceId = UUID.randomUUID().toString().replace("-", ""); } AiCallContext.set(traceId, req.getHeader("X-User-Id"), req.getHeader("X-Biz-Source")); resp.setHeader("X-Trace-Id", traceId); try { chain.doFilter(req, resp); } finally { AiCallContext.clear(); // 必须清理,否则线程复用会污染 } } }

日志里带上traceId,出问题时grep一下,一个调用从进入到AI返回的全部日志就齐了:

{ "traceId": "a3f8c91b9e2d4f5c", "bizSource": "customer-service", "capability": "doc-qa", "promptTokens": 425, "completionTokens": 188, "costMs": 1245, "level": "INFO", "msg": "AI调用完成" }

五、生产环境的几个坑

最后说几个我们踩过的坑,给大家提个醒:

1. 输入净化是AI微服务的标配别以为你接的是「自己公司的模型」就不做Prompt注入防御。我见过业务方把用户原始输入直接拼到Prompt里,被攻击后模型输出违规内容被监管处罚的案例。所有用户输入必须过一道InputSanitizer,至少做长度限制 + 敏感词过滤 + 角色锁定。

2. 超时一定要短,熔断一定要狠大模型调用动辄5~30秒,你如果不在网关层设个2秒超时把请求cut掉,整个调用链都会被拖死。我们生产上AI网关默认超时1.5秒,熔断阈值是20%错误率就开跳。

3. Token限流比QPS限流更有意义普通微服务按QPS限流,AI微服务要按Token限流。因为同样是「1次调用」,问"你好"和问"翻译一篇5000字文档"消耗的资源差100倍。我们用 Redis 滑动窗口 + Token 计数器,1分钟窗口最多消耗100万Token,超了就降级到便宜模型或直接拒绝。

4. 用量计费一定要做实时看板别等月底拉Excel对账。我们搞了个Grafana看板,按业务方/能力/时间段三维展示用量、成功率、平均Token、花费。老板每周看一次,预算批得飞快。


写到这里,你会发现,把大模型当一个普通微服务对待,本质就是抽象和分层

这两件事,是软件工程40年不变的底层能力。不会因为你把数据库换成了大模型,就变得更高深了。它仍然是:把变化的部分隔离出来,把不变的部分稳定暴露出去。

下篇预告:第50篇《分布式一致性:Raft协议动画式讲解》—— 微服务之后必须啃的分布式一致性硬骨头,从Leader选举到日志复制一次讲透。


系列文章回顾

  • 90篇JAVA高级工程师深度进阶文章,从实践案例、项目代码出发,用事实说话

版权声明:本文为「老梁」原创出品,90天Java后端+AI系列第48篇,转载请注明出处。

← 返回列表