Spring 的 Bean 管理 + 多个 ChatClient 实例如何区分

假设你在 AiChatConfig 里面创建:

@Bean
public ChatClient tutorChatClient(...) {
    ...
}

@Bean
public ChatClient writingChatClient(...) {
    ...
}

@Bean
public ChatClient readingChatClient(...) {
    ...
}

那么 Spring 容器里就有 3 个 ChatClient Bean

这时候如果你业务里直接:

private final ChatClient chatClient;

Spring 就不知道该注入哪一个。

所以核心就是:

给不同的 ChatClient 一个明确的 Bean 名称,然后在业务层使用 @Qualifier 指定。


一、最直接的方式:@Qualifier

例如你的 AiChatConfig

@Configuration
public class AiChatConfig {

    @Bean
    public ChatClient tutorChatClient(
            ChatClient.Builder builder,
            ChatMemory chatMemory
    ) {

        return builder
                .defaultSystem("""
                        你是一名专业的雅思AI教师。
                        负责帮助学生制定学习方案、分析成绩和回答学习问题。
                        """)
                .defaultAdvisors(
                        MessageChatMemoryAdvisor
                                .builder(chatMemory)
                                .build()
                )
                .build();
    }

    @Bean
    public ChatClient writingChatClient(
            ChatClient.Builder builder,
            ChatMemory chatMemory
    ) {

        return builder
                .defaultSystem("""
                        你是一名专业的雅思写作老师。
                        负责分析学生作文并给出详细修改建议。
                        """)
                .defaultAdvisors(
                        MessageChatMemoryAdvisor
                                .builder(chatMemory)
                                .build()
                )
                .build();
    }

    @Bean
    public ChatClient readingChatClient(
            ChatClient.Builder builder,
            ChatMemory chatMemory
    ) {

        return builder
                .defaultSystem("""
                        你是一名专业的雅思阅读老师。
                        负责分析阅读文章、题目和错题。
                        """)
                .defaultAdvisors(
                        MessageChatMemoryAdvisor
                                .builder(chatMemory)
                                .build()
                )
                .build();
    }
}

现在 Spring 容器里:

tutorChatClient
writingChatClient
readingChatClient

三个 Bean。


二、业务层指定使用哪个

比如:

@Service
public class WritingAiService {

    private final ChatClient writingChatClient;

    public WritingAiService(
            @Qualifier("writingChatClient")
            ChatClient writingChatClient
    ) {
        this.writingChatClient = writingChatClient;
    }

    public String analyze(String essay) {

        return writingChatClient
                .prompt()
                .user(essay)
                .call()
                .content();
    }
}

这里:

@Qualifier("writingChatClient")

就是告诉 Spring:

我要的不是随便一个 ChatClient,我要名字叫 writingChatClient 的那个。


三、Tutor 也是一样

@Service
public class TutorAiService {

    private final ChatClient tutorChatClient;

    public TutorAiService(
            @Qualifier("tutorChatClient")
            ChatClient tutorChatClient
    ) {
        this.tutorChatClient = tutorChatClient;
    }

    public String chat(String message) {

        return tutorChatClient
                .prompt()
                .user(message)
                .call()
                .content();
    }
}

所以最终关系就是:

AiChatConfig
│
├── tutorChatClient
│       ↓
│   TutorAiService
│
├── writingChatClient
│       ↓
│   WritingAiService
│
└── readingChatClient
        ↓
    ReadingAiService

四、如果使用 Lombok 要注意

如果你写:

@RequiredArgsConstructor
@Service
public class WritingAiService {

    @Qualifier("writingChatClient")
    private final ChatClient chatClient;
}

一般也可以通过构造器注入场景使用限定符,但为了让代码非常明确,我更推荐你在多 Bean 场景下直接写构造器:

public WritingAiService(
        @Qualifier("writingChatClient")
        ChatClient writingChatClient
) {
    this.writingChatClient = writingChatClient;
}

尤其是你现在正在学习 Spring AI,这样最容易理解 Spring 到底做了什么。


五、其实还可以使用 @Primary

假设:

@Bean
public ChatClient tutorChatClient(...) {}

@Bean
public ChatClient writingChatClient(...) {}

@Bean
public ChatClient readingChatClient(...) {}

你可以指定:

@Primary
@Bean
public ChatClient tutorChatClient(...) {
    ...
}

那么:

private final ChatClient chatClient;

默认就会选择:

tutorChatClient

但是:

@Qualifier("writingChatClient")

依然可以强制选择:

writingChatClient

六、不过我不太推荐大量使用 @Primary

因为你的 AI 场景越来越多之后:

Tutor
Writing
Reading
Speaking
Planning
Knowledge
Exam

你会发现:

@Primary

并不能解决核心问题。

真正应该表达的是:

这个 Service 到底使用哪个 AI。

所以:

@Qualifier

更清晰。


七、但是这里还有一个更重要的问题

你刚才问的是:

如果都创建在 AiChatConfig 里,那我在具体业务创建实例的时候怎么正确创建对应的实例?

这里其实有两种完全不同的架构。


方案一:多个 ChatClient Bean

就是我们刚才说的:

AiChatConfig
│
├── tutorChatClient
├── writingChatClient
├── readingChatClient
└── planningChatClient

业务:

TutorAiService
    ↓
@Qualifier("tutorChatClient")

WritingAiService
    ↓
@Qualifier("writingChatClient")

这个方案非常简单。


八、方案二:只创建一个 ChatClient

其实我更建议你不要为了不同业务场景,就一定创建多个 ChatClient

因为:

ChatClient

本身并不是:

“一个 AI 专家实例”。

它更像:

对底层 ChatModel 的调用入口 + 一些默认配置。

真正变化的可能只是:

System Prompt
Advisor
Tool
Response Format

所以你完全可以:

@Bean
public ChatClient chatClient(
        ChatClient.Builder builder
) {
    return builder.build();
}

然后:

写作

chatClient
    .prompt()
    .system("你是一名雅思写作老师")
    .user(essay)
    .call();

阅读

chatClient
    .prompt()
    .system("你是一名雅思阅读老师")
    .user(question)
    .call();

学习规划

chatClient
    .prompt()
    .system("你是一名雅思学习规划老师")
    .tools(studyPlanTools)
    .user(message)
    .call();

这时候:

一个 ChatClient
        │
        ├── 写作场景
        ├── 阅读场景
        ├── 学习规划
        └── AI Tutor

九、那么什么时候应该创建多个 ChatClient?

这个问题非常重要。

我给你一个简单判断。

如果只是 System Prompt 不一样

没必要创建多个 ChatClient。

直接:

.prompt()
.system(...)

如果只是 Tool 不一样

通常也可以:

.tools(...)

如果只是 Advisor 不一样

也可以:

.advisors(...)

如果默认能力完全不同

这时候可以创建多个 ChatClient。

例如:

TutorChatClient
    Memory
    RAG
    ScoreTools
    StudyPlanTools

WritingChatClient
    Memory
    WritingRAG
    WritingCorrectionTools

这种情况下创建两个 ChatClient 就比较合理。


十、所以我比较推荐你的 IELTS 项目这样设计

不要:

ChatClient1
ChatClient2
ChatClient3
ChatClient4
ChatClient5
ChatClient6

而是:

                         ChatModel
                            │
                            ↓
                       ChatClient
                            │
        ┌───────────────────┼───────────────────┐
        ↓                   ↓                   ↓
      Tutor               Writing             Reading
        │                   │                   │
   Memory + RAG        Memory + RAG        Memory + RAG
   ScoreTools          WritingTools        ReadingTools

绝大多数场景:

chatClient
    .prompt()
    .system(...)
    .advisors(...)
    .tools(...)
    .call();

就够了。


十一、如果你真的有多个 ChatClient,我建议使用“配置类 + Qualifier”

例如:

@Configuration
public class AiChatConfig {

    @Bean("tutorChatClient")
    public ChatClient tutorChatClient(
            ChatClient.Builder builder,
            ChatMemory chatMemory
    ) {
        return builder
                .defaultSystem("你是一名雅思AI导师")
                .defaultAdvisors(
                        MessageChatMemoryAdvisor
                                .builder(chatMemory)
                                .build()
                )
                .build();
    }

    @Bean("writingChatClient")
    public ChatClient writingChatClient(
            ChatClient.Builder builder,
            ChatMemory chatMemory
    ) {
        return builder
                .defaultSystem("你是一名雅思写作老师")
                .defaultAdvisors(
                        MessageChatMemoryAdvisor
                                .builder(chatMemory)
                                .build()
                )
                .build();
    }
}

然后:

@Service
public class WritingAiService {

    private final ChatClient chatClient;

    public WritingAiService(
            @Qualifier("writingChatClient")
            ChatClient chatClient
    ) {
        this.chatClient = chatClient;
    }
}

十二、如果以后有十几个 ChatClient 呢?

这时候我反而不建议继续:

AiChatConfig.java

全部堆在一起。

可以拆成:

ai
├── config
│   ├── AiModelConfig.java
│   ├── TutorChatConfig.java
│   ├── WritingChatConfig.java
│   ├── ReadingChatConfig.java
│   └── PlanningChatConfig.java

例如:

@Configuration
public class WritingChatConfig {

    @Bean("writingChatClient")
    public ChatClient writingChatClient(
            ChatClient.Builder builder,
            ChatMemory chatMemory
    ) {

        return builder
                .defaultSystem("""
                    你是一名雅思写作专家。
                    """)
                .defaultAdvisors(
                        MessageChatMemoryAdvisor
                                .builder(chatMemory)
                                .build()
                )
                .build();
    }
}

这样比一个几百行的:

AiChatConfig.java

更容易维护。


十三、再进一步:如果你想让业务完全不知道 Bean 名称

可以再封装一层:

AiChatClientFactory

例如:

@Component
public class AiChatClientFactory {

    private final ChatClient tutorChatClient;
    private final ChatClient writingChatClient;

    public AiChatClientFactory(
            @Qualifier("tutorChatClient")
            ChatClient tutorChatClient,

            @Qualifier("writingChatClient")
            ChatClient writingChatClient
    ) {
        this.tutorChatClient = tutorChatClient;
        this.writingChatClient = writingChatClient;
    }

    public ChatClient tutor() {
        return tutorChatClient;
    }

    public ChatClient writing() {
        return writingChatClient;
    }
}

业务:

@Service
public class WritingAiService {

    private final AiChatClientFactory factory;

    public WritingAiService(AiChatClientFactory factory) {
        this.factory = factory;
    }

    public String analyze(String essay) {

        return factory
                .writing()
                .prompt()
                .user(essay)
                .call()
                .content();
    }
}

十四、但是这里我给你一个更重要的建议

不要为了“看起来高级”而创建大量 ChatClient。

你现在学习 Spring AI,最好把这几个层次分清楚:

ChatModel
   ↓
ChatClient
   ↓
Prompt
   ↓
Advisor
   ↓
Tool

其中:

ChatModel

负责:

真正调用大模型。


ChatClient

负责:

提供一个方便的 AI 调用入口,并承载默认配置。


Prompt

负责:

这一次到底告诉 AI 什么。


Advisor

负责:

在调用过程中自动增加 Memory、RAG 等能力。


Tool

负责:

允许 AI 调用你的业务系统。

所以很多时候:

一个 ChatClient
+
不同 Prompt
+
不同 Advisor
+
不同 Tool

已经能够覆盖大量业务。


十五、你可以按照这个标准决定

情况 推荐
System Prompt 不同 一个 ChatClient
用户消息不同 一个 ChatClient
Tool 不同 一个 ChatClient + .tools()
RAG 不同 一个 ChatClient + .advisors()
Memory 不同 可以使用不同 Advisor/Memory
模型不同 多个 ChatClient / Model 配置
默认 Tool 完全不同 可以多个 ChatClient
默认 Advisor 完全不同 可以多个 ChatClient
模型参数完全不同 可以多个 ChatClient

最后给你一个最实用的架构

如果是你现在的 IELTS 项目,我建议:

                    ChatModel
                       │
                       ↓
                  ChatClient
                       │
        ┌──────────────┼──────────────┐
        ↓              ↓              ↓
       Prompt        Advisor         Tool
        │              │              │
        │          ┌───┴───┐       ┌──┴────┐
        │          ↓       ↓       ↓       ↓
        │        Memory   RAG    Score   StudyPlan
        │
   ┌────┼────────────┐
   ↓    ↓            ↓
写作   阅读         学习规划

也就是说,先不要把“不同业务 = 不同 ChatClient”画等号。

真正合理的是:

一个 ChatClient
        +
场景化 Prompt
        +
场景化 Advisor
        +
场景化 Tools

只有当某个场景的默认能力、模型、参数、工具体系都和其他场景明显不同时,再创建:

@Bean("writingChatClient")
@Bean("tutorChatClient")

然后通过:

@Qualifier("writingChatClient")

注入。

这也是为什么你前面看到我说的:

chatClient
    .prompt()
    .system(...)
    .advisors(...)
    .tools(...)
    .call();

其实并不是“每个业务都创建一个 ChatClient”,而是一个 ChatClient 可以通过 Prompt / Advisor / Tool 在调用时组装成不同的 AI 能力。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐