把大模型当作一个普通的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依赖
java
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都封装在这里,下游完全不感知模型变更。
java
/**
* 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());
});
}
}
这段代码的几个关键设计:
X-Biz-Source头部:强制业务方标识来源,用量统计、计费、限流的依据。
AiResponse统一包装:所有AI接口必须返回这个结构,前端对接成本最低。
- 业务校验与AI调用分离:业务校验失败绝不打模型,节省Token。
- 兜底降级:AI挂掉不能影响业务------这是微服务的基本素养。
3.3 模型路由层:按场景选择模型与降级
业务能力层下面,就是模型路由层。这里是AI微服务最体现工程能力的地方。
java
/**
* 模型路由:根据场景动态选择模型
* 核心思想:把"调哪个模型"的决策从业务代码里剥离出来
*/
@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「真正当微服务」的关键一步------没有计费的微服务,老板没法给你批预算。
java
/**
* 用量记录器
* 高频调用场景,必须异步,不能阻塞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%的问题:
java
/**
* 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返回的全部日志就齐了:
java
{
"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后端+AI系列第48篇,转载请注明出处。