ChatModel、ChatClient和自建AI Service怎么选?Spring AI三层调用架构指南

文章摘要

Spring AI同时提供ChatModel和ChatClient,真实企业项目通常还会再封装一层AI Service。三者并不是重复接口:ChatModel负责底层模型抽象,ChatClient负责Prompt、Advisor和响应转换,AI Service负责业务语义、权限、路由、成本与错误治理。本文通过架构分层、代码示例和典型场景,说明不同层级应该承担什么职责,以及如何避免Controller直接耦合模型SDK。

一、先看三者各自负责什么

推荐分层:

Controller
→ 业务AI Service
→ ChatClient
→ ChatModel
→ 模型Provider

ChatModel

定位:底层模型调用抽象。

负责:

  • 接收Prompt;
  • 调用具体模型;
  • 返回ChatResponse;
  • 暴露模型能力;
  • 处理模型级选项。

ChatClient

定位:面向应用开发的流式API。

负责:

  • System和User消息;
  • Prompt模板;
  • Advisor;
  • Memory;
  • RAG;
  • Tool Calling;
  • 结构化输出;
  • 同步和流式调用。

AI Service

定位:面向业务的稳定接口。

负责:

  • 业务用例;
  • 模型路由;
  • 用户权限;
  • 配额;
  • Prompt版本;
  • 错误码;
  • 审计;
  • 降级;
  • 领域对象。

二、什么时候直接使用ChatModel

ChatModel适合:

  • 框架基础设施开发;
  • 自己构造完整Prompt;
  • 需要访问完整ChatResponse;
  • 做模型适配器;
  • 实现自定义ChatClient;
  • 需要控制底层选项;
  • 编写框架级测试。

示例:

@Service
public class RawModelService {

    private final ChatModel chatModel;

    public RawModelService(ChatModel chatModel) {
        this.chatModel = chatModel;
    }

    public ChatResponse call(String message) {
        Prompt prompt = new Prompt(
                new UserMessage(message)
        );

        return chatModel.call(prompt);
    }
}

问题是业务代码需要自己处理System消息、模板变量、Advisor、内容提取、结构化输出和RAG增强。

因此,普通Controller通常不应直接依赖ChatModel。

三、ChatClient适合应用层编排

@Service
public class SimpleChatService {

    private final ChatClient chatClient;

    public SimpleChatService(
            ChatClient.Builder builder
    ) {
        this.chatClient = builder
                .defaultSystem(
                        "你是企业AI助手。"
                )
                .build();
    }

    public String chat(String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

ChatClient带来的价值:

更少样板代码
+Prompt模板
+Advisor链
+响应转换
+同步/流式统一入口

它类似于Spring中的RestClient或JdbcClient,是对底层能力的应用级封装。

四、为什么还要自建AI Service

很多项目在Controller中直接写ChatClient调用,Demo没有问题,企业项目很快会出现:

  • 每个Controller重复System Prompt;
  • 业务层写死模型名称;
  • 无法统一限流;
  • 无法统计用户成本;
  • 异常直接向外暴露;
  • Prompt升级要改多个类;
  • RAG权限散落;
  • 同一个业务被多个入口调用时逻辑不一致。

推荐业务接口:

public interface CustomerServiceAiService {

    CustomerReply answer(
            CustomerQuestion question,
            AiRequestContext context
    );
}

领域请求:

public record CustomerQuestion(
        String question,
        String conversationId
) {
}

上下文:

public record AiRequestContext(
        String userId,
        String tenantId,
        String requestId,
        String channel
) {
}

返回对象:

public record CustomerReply(
        String answer,
        List sources,
        String model,
        long durationMs
) {
}

业务代码不需要知道ChatClient细节。

五、推荐的统一调用层

public interface EnterpriseAiClient {

     T call(
            AiTask task,
            Class responseType
    );

    Flux stream(AiTask task);
}

任务对象:

public record AiTask(
        String taskType,
        String promptVersion,
        Map variables,
        AiRequestContext context,
        ModelTier modelTier
) {
}

模型等级:

public enum ModelTier {
    FAST,
    BALANCED,
    POWERFUL
}

实现层通过不同ChatClient完成模型路由,而不是让Controller判断具体模型名称。

六、三层职责如何划分

能力 ChatModel ChatClient AI Service
模型调用 间接 不直接
Prompt对象 使用业务模板
System/User消息 手动 方便 按业务定义
Advisor 选择和传参
Memory 手动 Advisor 业务会话规则
RAG 手动 Advisor 权限与知识库选择
结构化输出 手动 entity 领域对象
模型路由 手动 多客户端 业务策略
用户配额 可扩展 负责
错误码 模型异常 调用异常 业务错误
审计 底层信息 Trace 业务审计

七、多模型项目应该在哪一层路由

不推荐Controller判断:

if (request.vip()) {
    usePowerfulModel();
}

应该放在业务AI Service:

public ModelTier route(
        String taskType,
        UserPlan plan,
        int complexity
) {
    if (complexity >= 8) {
        return ModelTier.POWERFUL;
    }

    if (plan == UserPlan.FREE) {
        return ModelTier.FAST;
    }

    return ModelTier.BALANCED;
}

路由考虑:

  • 任务复杂度;
  • 用户等级;
  • 数据敏感性;
  • 延迟要求;
  • 成本预算;
  • 模型可用性;
  • 失败次数。

八、Prompt应该在哪一层管理

ChatModel层

不应该感知业务Prompt。

ChatClient层

定义通用默认System和Advisor。

AI Service层

选择:

业务场景
Prompt ID
Prompt版本
变量
输出类型

Prompt不应散落在Controller和Service的字符串中。

九、异常应该如何转换

底层异常可能包括:

  • 401;
  • 429;
  • 5xx;
  • 超时;
  • 连接中断;
  • 输出解析失败;
  • 内容安全拦截。

AI Service应转换成业务错误:

public enum AiErrorCode {
    MODEL_UNAVAILABLE,
    RATE_LIMITED,
    INVALID_OUTPUT,
    CONTENT_BLOCKED,
    REQUEST_TIMEOUT,
    QUOTA_EXCEEDED
}

Controller只处理稳定错误码,不暴露SDK异常。

十、什么情况下不需要AI Service

小型验证项目可以直接使用ChatClient:

  • 单一模型;
  • 单一接口;
  • 无权限;
  • 无成本统计;
  • 不需要长期维护;
  • 代码只用于演示。

只要出现以下任意两项,就建议封装:

多个业务场景
多个模型
多租户
配额
Prompt版本
RAG权限
工具调用
成本统计
审计

十一、测试策略

ChatModel测试

验证Provider配置、模型连接、基础响应和Metadata。

ChatClient测试

验证Prompt模板、Advisor、结构化输出和流式响应。

AI Service测试

验证业务路由、权限、配额、降级、错误转换、Prompt版本和领域对象。

AI Service单元测试可以Mock ChatClient适配层,不需要每次调用真实模型。

十二、最终建议

框架和基础设施层

使用ChatModel

普通AI应用调用

使用ChatClient

企业业务系统

ChatClient之上再封装AI Service

三者不是互相替代,而是分层协作。

总结

一个可维护的Spring AI项目应该形成:

业务接口稳定
→ AI Service表达业务语义
→ ChatClient负责调用编排
→ ChatModel负责模型抽象

不要让Controller、模型SDK、Prompt和业务规则直接绑在一起。

延伸阅读

如果你正在关注企业级 AI 应用、Agent、RAG、MCP 与大模型工程化落地,欢迎访问 智元界

https://www.zyentor.com/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。