LangChain4j 工厂模式

从 new 对象到 AI 代理:LangChain4j 工厂模式的演进与实践

写在前面

在平时的开发中,你是否写过这样的代码?

java 复制代码
ChatModel model = new OpenAiChatModel("api-key", 30000);
ImageCollectionPlanService service = new ImageCollectionPlanServiceImpl(model);

看起来没什么问题,但当项目逐渐变大,你就会发现:

  • 同样的创建逻辑散落在各处
  • 切换 AI 供应商时需要修改大量代码
  • 配置参数无法集中管理
  • 单元测试变得困难

本文将通过一个真实的代码案例------基于 LangChain4j 的图片收集规划服务,带你深入理解工厂模式在 Spring 生态下的演进历程,以及它是如何优雅解决上述问题的。

一、需求背景

我们有一个业务场景:用户输入一段提示词(Prompt),系统需要根据提示词分析出需要收集哪些类型的图片,以及收集的参数(数量、风格、分辨率等)。

架构上选择了 LangChain4j 框架,通过 OpenAI 的大模型来理解用户意图并生成结构化的 ImageCollectionPlan 对象。

核心接口定义如下:

java 复制代码
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;

public interface ImageCollectionPlanService {
    @SystemMessage(fromResource = "prompt/image-collection-plan-system-prompt.txt")
    ImageCollectionPlan planImageCollection(@UserMessage String userPrompt);
}

关键点:

  • @SystemMessage:从资源文件加载系统级提示词,定义 AI 的角色和任务
  • @UserMessage:标记用户输入的动态内容
  • 返回值 ImageCollectionPlan:AI 返回的结构化规划结果(LangChain4j 会自动将 AI 的 JSON 输出映射为 Java 对象)

但问题来了:我们没有写这个接口的实现类,那么谁来提供真正的实现呢?

二、第一层:简单工厂模式(静态工厂)

最直观的思路:写一个工厂类,集中封装创建逻辑。

java 复制代码
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;

public class AiServiceFactory {
    public static ImageCollectionPlanService create() {
        ChatModel model = OpenAiChatModel.builder()
                .apiKey("your-api-key")
                .timeout(Duration.ofSeconds(30))
                .build();
        return AiServices.builder(ImageCollectionPlanService.class)
                .chatModel(model)
                .build();
    }
}

优点

  • 创建逻辑集中管理,业务代码只需调用 AiServiceFactory.create()
  • 切换 AI 模型只需修改工厂内部

缺点

  • 静态方法无法享受 Spring 的依赖注入(@Value 读取配置、@PostConstruct 初始化等)
  • 无法与 Spring 生命周期整合
  • 难以进行单元测试(静态方法难以 Mock)

三、第二层:Spring 工厂 Bean(@Configuration + @Bean)

为了解决上述问题,Spring 提供了 @Configuration + @Bean 模式,让工厂方法成为 Spring 容器的一部分。

java 复制代码
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

@Configuration
public class ImageCollectionPlanServiceFactory {

    @Resource(name = "openAiChatModel")
    private ChatModel chatModel;

    @Bean
    public ImageCollectionPlanService createImageCollectionPlanService() {
        return AiServices.builder(ImageCollectionPlanService.class)
                .chatModel(chatModel)
                .build();
    }
}

这背后发生了什么?

  1. 依赖注入ChatModel(配置了 API Key、超时等参数)由 Spring 自动注入。这个 Bean 可能在其他配置类中定义,例如:

    java 复制代码
    @Bean(name = "openAiChatModel")
    public ChatModel openAiChatModel() {
        return OpenAiChatModel.builder()
                .apiKey(apiKey)
                .timeout(Duration.ofSeconds(30))
                .build();
    }
  2. Bean 注册 :Spring 容器启动时,执行 @Bean 方法,将返回的对象以 ImageCollectionPlanService 类型注册到容器中

  3. 完全受管:该 Bean 享受 Spring 的全部生命周期管理

此时,业务代码已经不需要关心对象是如何创建的,只需要通过依赖注入获取即可:

java 复制代码
@Service
public class ImageService {
    @Autowired
    private ImageCollectionPlanService planService;
    
    public void doSomething() {
        ImageCollectionPlan plan = planService.planImageCollection("用户提示词");
    }
}

四、第三层:LangChain4j 的动态代理工厂(AiServices 的内部魔法)

但这里有个更深的玄机:我们并没有写 ImageCollectionPlanServiceImpl 这个类,那 @Bean 方法返回的到底是什么?

AiServices.builder(...).build() 是 LangChain4j 框架提供的核心工厂 ,它的本质是一个动态代理工厂

  1. 读取接口元数据 :扫描 ImageCollectionPlanService 接口上的 @SystemMessage@UserMessage 等注解
  2. 生成代理类:利用 JDK 动态代理,在内存中动态生成一个实现类的字节码
  3. 绑定执行逻辑 :代理对象的每个方法调用,都会被拦截并转换为调用 ChatModel 发送请求的逻辑
  4. 自动序列化 :AI 返回的 JSON/文本结果自动映射为 ImageCollectionPlan 对象(LangChain4j 内置了 Jackson/Gson 序列化器)

这体现了工厂模式的终极形态 :工厂不仅能创建"对象",还能创建"对象的实现逻辑"。这正是 Spring Cloud Feign、MyBatis Mapper 等框架的核心思想------你只写接口,框架替你生成能干活的对象

LangChain4j 官方文档中将此称为 "声明式 AI 服务"(Declarative AI Services),是其最核心、最优雅的特性之一。

五、第四层:静态工具获取 Bean(SpringContextUtil)

在某些特殊场景(如静态工具类、非 Spring 管理的组件)中,我们无法使用 @Autowired。这时需要一个优雅的解决方案:

java 复制代码
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    public static <T> T getBean(String name, Class<T> clazz) {
        return applicationContext.getBean(name, clazz);
    }
}

工作原理

  1. 实现 ApplicationContextAware 接口,Spring 容器启动时会自动注入 ApplicationContext
  2. 将容器引用保存在静态变量中
  3. 提供静态 getBean 方法,按类型从容器中获取 Bean

使用方式

java 复制代码
ImageCollectionPlanService planService = 
    SpringContextUtil.getBean(ImageCollectionPlanService.class);
ImageCollectionPlan plan = planService.planImageCollection(originalPrompt);
log.info("获取到图片收集计划,开始并发执行");
// ... 后续并发处理

注意 :虽然这种方式很方便,但它会增强代码与 Spring 的耦合。建议仅在无法使用依赖注入的场景(如静态方法、遗留代码适配)中使用。

六、完整调用链路图解

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    项目启动阶段(Spring 容器初始化)              │
├─────────────────────────────────────────────────────────────────┤
│  1. Spring 扫描到 @Configuration 工厂类                        │
│  2. 注入 openAiChatModel(配置了 API Key、超时等)             │
│  3. 执行 @Bean 方法,调用 AiServices.builder()                 │
│  4. LangChain4j 动态读取接口注解,生成 JDK 代理对象             │
│  5. 代理对象以 ImageCollectionPlanService 类型注册到 Spring 容器│
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                    业务调用阶段                                  │
├─────────────────────────────────────────────────────────────────┤
│  6. SpringContextUtil.getBean(ImageCollectionPlanService.class)│
│  7. Spring 容器返回 LangChain4j 生成的动态代理对象              │
│  8. 调用 planImageCollection(userPrompt)                       │
│  9. 代理对象拦截方法,拼接 SystemPrompt(从文件加载)+ UserPrompt│
│ 10. 代理对象调用 ChatModel.sendRequest() 发送请求到 OpenAI     │
│ 11. 接收到 AI 返回的 JSON,LangChain4j 自动反序列化为 Plan 对象 │
│ 12. 返回规划结果,业务逻辑继续执行                              │
└─────────────────────────────────────────────────────────────────┘

七、这种设计的核心价值

设计要素 解决的问题
@Configuration + @Bean 将复杂对象的创建从业务代码剥离,交由 Spring 容器管理
依赖注入 配置参数(API Key、超时等)集中管理,环境切换方便
AiServices 动态代理(LangChain4j) 无需编写实现类,大幅减少样板代码,只需声明接口和注解
ApplicationContextAware 在非 Spring 管理环境中也能优雅地获取 Bean

八、与其他框架的类比

理解了这套模式,你会发现很多主流框架都在使用同样的思想------"接口声明 + 动态代理生成实现"

框架 接口定义方式 动态代理工厂
LangChain4j @SystemMessage + @UserMessage AiServices.builder()
Spring Cloud Feign @FeignClient + 接口 Feign.builder().target()
MyBatis Mapper 接口 + XML/注解 SQL MapperProxyFactory
Spring Data JPA Repository 接口 RepositoryFactorySupport
Retrofit @GET@POST 注解接口 Retrofit.create()

它们的共同模式是:你只关心接口的"契约"定义(参数、返回值、路径/提示词),框架在运行时负责生成执行这些契约的代理实现

九、LangChain4j 的独特优势

相比直接在业务代码中调用 ChatModel,LangChain4j 的 AiServices 提供了:

  1. 自动提示词拼接 :无需手动 SystemMessage + UserMessage 组装
  2. 自动结果映射 :AI 返回的 JSON 直接转为 ImageCollectionPlan,省去 ObjectMapper 解析代码
  3. 流式响应支持 :接口方法可返回 StreamingResponseHandlerFlux<String>
  4. 工具调用(Function Calling) :通过 @Tool 注解让 AI 自动调用 Java 方法
  5. 记忆(Memory) :通过 chatMemory() 注入,轻松实现多轮对话

这些特性使得 AiServices 不仅仅是一个工厂,更是一个完整的 "AI 服务声明式开发框架"

十、注意事项与最佳实践

  1. 优先使用依赖注入SpringContextUtil 是备用方案,常规场景应使用 @Autowired 或构造器注入

  2. 多实现类的处理 :如果同一个接口有多个 Bean,使用 @Primary@Qualifier 指定

  3. 工厂方法的隔离性:工厂类应只负责"创建",不应包含业务逻辑

  4. 配置外部化ChatModel 的配置(API Key、超时、模型名称等)应放在 application.yml 中,而非硬编码。例如:

    yaml 复制代码
    langchain4j:
      open-ai:
        api-key: ${OPENAI_API_KEY}
        timeout: 30s
  5. 异常处理 :可在 AiServices 构建时通过 .onFailure() 添加全局重试或降级策略

十一、总结

从最初的 new 对象,到静态工厂,再到 Spring 管理的工厂 Bean,最后到 LangChain4j 的动态代理工厂------我们走过的每一步,都是对"对象创建"这一问题的持续优化。

工厂模式的核心价值并不在于"创建对象"这个动作本身,而在于:

  • 封装变化:将易变的创建逻辑(模型地址、API Key、超时)与稳定的业务逻辑隔离
  • 解耦依赖 :业务代码只依赖 ImageCollectionPlanService 接口,不依赖具体实现
  • 提升可测试性 :方便 Mock 和替换(测试时可注入 InMemoryChatModel
  • 拥抱框架:与 Spring 的 IOC 容器深度整合,享受生命周期管理

而 LangChain4j 的 AiServices 更进一步,将工厂模式与 "声明式编程" 理念结合------你只需通过注解声明"要做什么"(调用哪个提示词、传入什么参数),框架自动完成"怎么做"(拼接 Prompt、调用模型、解析结果)。

当你下次在 Spring 中写 @Configuration + @Bean 结合 AiServices 时,希望你能意识到:这不只是一段集成代码,而是工厂模式 + 动态代理 + 声明式编程在 AI 原生应用开发中的最佳实践。


扩展阅读


相关推荐
小小龙学IT1 小时前
Day 28 项目调试与优化 —— 给聊天室做一次“全面体检“
c语言·开发语言
小小小米粒1 小时前
idea常用搭配
java
吃饱了得干活1 小时前
Java Map 核心原理:从数据结构到 put/get 执行,一篇彻底讲透
java·后端
晚安code1 小时前
Java并发集合详解:从HashMap到ConcurrentHashMap
java
hy.z_7771 小时前
【C++】9. Vector
开发语言·c++
不才不才不不才1 小时前
Spring 源码系列(17): HandlerMapping 与 HandlerAdapter 两大体系
java·后端·spring
lisin-lee-cooper1 小时前
JVM知识体系
java·jvm
Tim_102 小时前
【C++】023、移动语义&深拷贝
开发语言·c++·算法
ctlover2 小时前
Python模块与包
开发语言·python