LangChain4j 源码透析:核心抽象与设计模式

2 阅读22分钟

概述

系列定位:本文是“AI 应用核心框架与协议”系列的第 2 篇。在上一篇《LangChain 生态全景:Python 与 Java 双版本的架构对比》中,我们建立了全局认知,尤其厘清了 Java 阵营 LangChain4j 的设计哲学——以强类型、Builder 模式和编译期安全换取生产级稳定性。本文将进一步打开这个“黑盒”,直抵源码内核,揭开 AiServices.builder().chatModel(model).tools(tools).chatMemory(memory).build() 背后运转的整套机器。掌握这些内核设计,不仅是读懂框架,更是你后续进行 Spring Boot 深度集成、MCP 协议扩展,乃至自行开发企业级 AI 中间件的认知地基。

总结性引言:无论是 Spring IoC 的 BeanFactory,还是 MyBatis 的 Mapper 代理,它们都有一个共同点:通过精妙的设计模式将极端复杂的流程封装成简洁的 API。LangChain4j 正是如此。当你调用 assistant.chat(userId, message) 时,你或许会想:这几行代码究竟创建了什么“怪物”?今天,我们就来亲手解剖它。你会发现,AiServices 并非黑魔法,而是一个教科书式的 JDK 动态代理 应用:它拦截你的 chat 方法,自动从 ChatMemory 中拉取历史消息,调用 ChatLanguageModel 获取 LLM 回复,一旦 LLM 返回 ToolCall,它便在一个 while 循环里持续执行工具、追加结果、再次推理,直到大模型给出最终答案。同时,它也完美体现了“对扩展开放,对修改关闭”的原则——通过 SPI 机制,任意模型厂商(哪怕是企业内部闭源的)都能无缝接入,无需修改框架一行代码。本文将从接口设计、代理机制、设计模式三个层面,带你彻底吃透 LangChain4j 的架构内核。

核心要点

  • 核心接口体系ChatLanguageModelStreamingChatLanguageModel 等定义了框架的“世界语”,以依赖倒置实现模型层的无限扩展。
  • 动态代理魔法AiServices 通过 JDK Proxy 自动实现了 SystemMessage 组装、Memory 管理、ReAct 循环驱动的全套 Agent 逻辑。
  • 三大设计模式:Builder 模式解决复杂对象创建;责任链模式实现消息处理流程的灵活编排;SPI 机制实现厂商的无缝切换。
  • Tool 与 Memory 内幕@Tool 注解如何被解析成 JSON Schema,命令模式如何反射调用你的 Java 方法,以及多租户会话隔离如何实现。

文章组织架构图

flowchart TD
    A[1. 核心接口与层级设计] --> B[2. AiServices 动态代理拆解]
    B --> C[3. 设计模式与 SPI 扩展]
    C --> D[4. Tools 与 Memory 实现]
    D --> E[5. 与前后系列衔接]
    E --> F[6. 面试高频专题]

1. 核心接口与层级设计:定义 AI 应用的“世界语”

LangChain4j 与所有优秀的框架一样,始于一套精心设计的接口体系。这些接口是框架的“词汇表”,定义了“能做什么”的契约,而上层编排逻辑完全面向接口编程,从而与具体实现解耦。我们先从一张类图俯瞰整体关系。

classDiagram
    class ChatLanguageModel {
        <<interface>>
        +Response~AiMessage~ generate(List~ChatMessage~ messages)
    }
    class StreamingChatLanguageModel {
        <<interface>>
        +void generate(List~ChatMessage~ messages, StreamingResponseHandler handler)
    }
    class EmbeddingModel {
        <<interface>>
        +Response~Embedding~ embed(String text)
        +Response~List~Embedding~~ embedAll(List~String~ texts)
    }
    class ImageModel {
        <<interface>>
        +Response~Image~ generate(String prompt)
    }
    class ModerationModel {
        <<interface>>
        +Response~Moderation~ moderate(String text)
    }
    class OpenAiChatModel {
        +Response~AiMessage~ generate(List~ChatMessage~ messages)
    }
    class AzureOpenAiChatModel {
        +Response~AiMessage~ generate(List~ChatMessage~ messages)
    }
    class OllamaChatModel {
        +Response~AiMessage~ generate(List~ChatMessage~ messages)
    }
    class OpenAiStreamingChatModel {
        +void generate(List~ChatMessage~ messages, StreamingResponseHandler handler)
    }
    ChatLanguageModel <|-- OpenAiChatModel
    ChatLanguageModel <|-- AzureOpenAiChatModel
    ChatLanguageModel <|-- OllamaChatModel
    StreamingChatLanguageModel <|-- OpenAiStreamingChatModel
    EmbeddingModel <|-- AllMiniLmL6V2EmbeddingModel

图表说明

  • 设计意图:将所有与 LLM 交互的通道抽象为稳定接口,使上层 Chains、Agents、RAG 流水线完全与具体模型提供商解耦。无论底层是 OpenAI、Azure、Ollama 还是私有化部署,对于调用者而言都只是一个 ChatLanguageModel
  • 核心参与者ChatLanguageModel 代表同步聊天模型,StreamingChatLanguageModel 代表流式模型,EmbeddingModel 处理文本向量化,ImageModelModerationModel 分别覆盖多模态和内容安全。每个接口都有对应的 OpenAi*Azure* 等实现。
  • 协作流程:上层组件(如 Agent)依赖注入 ChatLanguageModel 接口,在运行时由 SPI 或 DI 容器提供具体实例。调用 generate(messages) 将触发实现内部的 HTTP 请求、协议转换和响应解析。
  • 源码映射:核心接口位于 dev.langchain4j.model.chatdev.langchain4j.model.embedding 等包下,而 OpenAiChatModel 位于 dev.langchain4j.model.openai 包。

1.1 ChatLanguageModel:LLM 调用的统一契约

ChatLanguageModel 是整个框架中使用频率最高的接口,只定义了一个核心方法:

public interface ChatLanguageModel {
    Response<AiMessage> generate(List<ChatMessage> messages);
}

这个极简的签名背后蕴含着深思熟虑的设计:

  • 输入是 List<ChatMessage>:统一了系统消息、用户消息、助手回复和工具执行结果等多种消息类型的集合,完全屏蔽了不同模型 API 的差异。OpenAI 需要 messages 数组,而 Llama.cpp 可能接受不同的格式,但在这里都收敛为相同的 List
  • 输出是 Response<AiMessage>Response 是一个泛型封装,除了承载最终消息文本外,还携带 FinishReasonTokenUsage 等元信息。这比直接返回 String 更稳健,因为生产环境需要统计 Token 用量、判断停止原因。
  • 唯一方法 generate:同步阻塞调用。框架同时提供 StreamingChatLanguageModel 处理流式场景,两者职责清晰,避免了臃肿的“上帝接口”。

OpenAiChatModel 为例,其内部实现大致简化为:

public Response<AiMessage> generate(List<ChatMessage> messages) {
    // 1. 将 List<ChatMessage> 转换为 OpenAI API 所需的 messages JSON 数组
    List<Map<String, Object>> openAiMessages = messages.stream()
        .map(this::toOpenAiMessage)
        .collect(Collectors.toList());
    
    // 2. 构建请求体,包含 model、temperature 等参数
    Map<String, Object> requestBody = new HashMap<>();
    requestBody.put("model", modelName);
    requestBody.put("messages", openAiMessages);
    // ... 其他配置
    
    // 3. 发送 HTTP 请求到 https://api.openai.com/v1/chat/completions
    String responseJson = httpClient.post("/v1/chat/completions", requestBody);
    
    // 4. 解析响应,特别处理 tool_calls 字段
    AiMessage aiMessage = parseResponse(responseJson);
    TokenUsage usage = parseUsage(responseJson);
    return Response.from(aiMessage, usage, finishReason);
}

这里的关键在于 协议转换ChatMessage 的子类 UserMessageSystemMessageAiMessageToolExecutionResultMessage 分别被映射为 OpenAI 格式的 role: "user", role: "system", role: "assistant", role: "tool"。当 LLM 返回 tool_calls 时,OpenAiChatModel 会将其填充到 AiMessagetoolCalls 字段中,从而驱动后续的 Agent 循环——这正是下一节 AiServices 的核心逻辑。

1.2 StreamingChatLanguageModel:流式响应的回调设计

对于需要逐 Token 推送的交互式场景,框架提供了 StreamingChatLanguageModel

public interface StreamingChatLanguageModel {
    void generate(List<ChatMessage> messages, StreamingResponseHandler handler);
}

它不再返回 Response,而是通过传入的 StreamingResponseHandler 回调与调用者交互。这个处理器包含四个核心方法:

public interface StreamingResponseHandler {
    void onNext(String token);                                 // 每个新Token到达
    void onComplete(Response<AiMessage> response);             // 正常结束
    void onError(Throwable error);                             // 异常结束
    default void onToolCall(ToolCall toolCall) {}              // 工具调用发起
}

与 SSE(Server-Sent Events)协议的对应关系十分清晰:

  • data: {"choices":[{"delta":{"content":"Hello"}}]} → 触发 onNext("Hello")
  • data: {"choices":[{"delta":{"tool_calls":[{"function":{"name":"get_weather"}}]}}]} → 触发 onToolCall(...)
  • data: [DONE] → 触发 onComplete(response)
  • 网络中断 → 触发 onError(throwable)

OpenAiStreamingChatModel 中,这些回调由底层的 SSE 事件循环驱动:

// 简化的流式处理逻辑
sseEventSource.onEvent(event -> {
    if (event.data.contains("delta")) {
        String token = extractDeltaContent(event.data);
        handler.onNext(token);
    } else if (event.data.contains("tool_calls")) {
        ToolCall toolCall = parseToolCall(event.data);
        handler.onToolCall(toolCall);
    } else if ("[DONE]".equals(event.data)) {
        handler.onComplete(buildResponse());
    }
});

这种设计将推送模型完美映射到了反应式编程中的观察者模式,使得开发者可以在 onNext 中实时更新 UI,而在 onToolCall 中获知代理即将执行工具。

1.3 EmbeddingModel 及其他多模态接口

EmbeddingModel 是 RAG(检索增强生成)的基石,其接口也遵循同样的“统一契约”原则:

public interface EmbeddingModel {
    Response<Embedding> embed(String text);
    Response<List<Embedding>> embedAll(List<String> texts);
}

其实现类(如 OpenAiEmbeddingModelAllMiniLmL6V2EmbeddingModel)分别对接到 OpenAI Embeddings API 或本地 ONNX 模型。上层 RAG 组件只需调用 embed(userQuery) 得到向量,剩下的相似度检索完全不需要关心向量从何而来。

ImageModelModerationModel 同样定义了各自领域的契约,它们的实现同样可以通过 SPI 自由替换。至此,框架通过六大核心接口(Chat、StreamingChat、Embedding、Image、Moderation 外加 LanguageModel)构成了完整的 AI 能力抽象层,奠定了“依赖倒置”的坚实基础。

2. AiServices 动态代理:黑盒的核心

如果说核心接口是框架的“骨骼”,那么 AiServices 就是它的“大脑”。AiServices 这个名字起得极为贴切:它并不是一个服务,而是一个能够将你的 @AiService 注解接口动态生成实现的服务工厂。其本质是 JDK 动态代理的教科书级应用,而代理内部的 InvocationHandler 则封装了一个完整的 Agent 循环。接下来我们通过时序图和源码逐步拆解。

2.1 从 AiServices.builder().build() 到代理对象的诞生

在 Spring Boot 环境下,AiServicesAutoConfiguration 自动扫描所有 @AiService 注解的接口,并为每个接口调用 AiServices.builder() 创建代理 Bean。但为了理解纯粹的原理,我们以手动创建为例:

Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(chatModel)
    .tools(new Calculator())
    .chatMemory(chatMemory)
    .build();

上述代码的执行流程可以用时序图清晰表达:

sequenceDiagram
    participant Caller as 调用者
    participant Builder as AiServices.Builder
    participant Factory as Proxy工厂
    participant Handler as InvocationHandler
    participant Memory as ChatMemory
    participant Model as ChatLanguageModel
    participant Tools as ToolExecutor

    Caller->>Builder: AiServices.builder(Assistant.class)
    Builder-->>Caller: Builder实例
    Caller->>Builder: .chatLanguageModel(model)
    Caller->>Builder: .tools(calculator)
    Caller->>Builder: .chatMemory(chatMemory)
    Caller->>Builder: .build()
    Builder->>Factory: Proxy.newProxyInstance(ClassLoader, interfaces, handler)
    Factory-->>Builder: 代理对象 (Assistant)
    Builder-->>Caller: assistant代理
    
    Note over Caller, Tools: --- 后续调用assistant.chat()时 ---
    Caller->>Handler: assistant.chat(userId, message)
    Handler->>Memory: 通过memoryId获取历史消息
    Memory-->>Handler: List<ChatMessage> (历史)
    Handler->>Handler: 组装SystemMessage+History+UserMessage
    Handler->>Model: chatModel.generate(messages)
    Model-->>Handler: Response<AiMessage> (可能包含ToolCall)
    alt 包含ToolCall
        loop ReAct循环 (maxIterations次)
            Handler->>Tools: 根据ToolCall执行工具方法
            Tools-->>Handler: 工具执行结果
            Handler->>Handler: 将结果包装成ToolExecutionResultMessage
            Handler->>Model: 再次generate(追加工具结果)
            Model-->>Handler: 新的Response (可能仍有ToolCall或最终回答)
        end
    end
    Handler->>Memory: 更新记忆(添加本轮UserMessage和AiMessage)
    Handler-->>Caller: 最终AiMessage文本

时序图说明

  • 设计意图:将 AiServices.builder() 创建代理与后续方法调用的所有复杂逻辑完整可视化,揭示“一行代码调用”背后的全貌。
  • 核心参与者Builder 负责收集配置;Proxy 工厂创建动态代理;InvocationHandler 拦截方法调用;ChatMemory 管理历史;ChatLanguageModel 与 LLM 交互;ToolExecutor 执行工具。
  • 协作流程:Builder 在 build() 时将所有配置注入到 InvocationHandler 中,然后由 Proxy 返回一个实现了 Assistant 接口的代理。当调用 chat 方法时,InvocationHandler 接管控制权,依次执行记忆加载、消息组装、模型调用、可能的 ReAct 循环,最后更新记忆并返回结果。
  • 源码映射AiServices.Builder 是静态内部类,build() 方法位于 dev.langchain4j.service.AiServices 中,InvocationHandler 实现在同包的 ServiceInvocationHandler 或类似类中。

2.2 InvocationHandler 内部的魔法:ReAct 循环源码级分析

InvocationHandlerinvoke 方法是整个代理逻辑的入口。当调用 assistant.chat(userId, userMessage) 时,JVM 自动路由到此方法。以下是其核心逻辑的简化版(基于 LangChain4j 1.0.0-alpha1 源码风格):

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 如果是Object方法(如toString),直接执行
    if (method.getDeclaringClass() == Object.class) {
        return method.invoke(this, args);
    }
    
    // 1. 解析方法参数,提取 @MemoryId 和 @UserMessage 等
    Object memoryId = null;
    String userMessage = null;
    Parameter[] parameters = method.getParameters();
    for (int i = 0; i < parameters.length; i++) {
        if (parameters[i].isAnnotationPresent(MemoryId.class)) {
            memoryId = args[i];
        }
        if (parameters[i].isAnnotationPresent(UserMessage.class)) {
            userMessage = (String) args[i];
        }
        // 也可处理 @V 等其他注解
    }
    
    // 2. 从 ChatMemory 中获取历史消息
    List<ChatMessage> messages = chatMemory.messages(memoryId);
    
    // 3. 组装最终消息列表:SystemMessage + History + UserMessage
    List<ChatMessage> conversation = new ArrayList<>();
    conversation.add(systemMessage);   // 从 AiServices 配置中注入的 SystemMessage
    conversation.addAll(messages);     // 历史对话
    conversation.add(new UserMessage(userMessage));
    
    // 4. 调用 LLM,进入可能的 ReAct 循环
    Response<AiMessage> response = chatModel.generate(conversation);
    int iteration = 0;
    while (response.content().hasToolCalls() && iteration < maxIterations) {
        // 4.1 执行所有工具调用
        for (ToolCall toolCall : response.content().toolCalls()) {
            ToolExecutionRequest execution = toolExecutor.execute(toolCall, memoryId);
            // 将工具执行结果封装为 ToolExecutionResultMessage
            conversation.add(new ToolExecutionResultMessage(toolCall.id(), toolCall.name(), execution.result()));
        }
        // 4.2 再次调用 LLM
        response = chatModel.generate(conversation);
        iteration++;
    }
    if (iteration >= maxIterations && response.content().hasToolCalls()) {
        throw new RuntimeException("ReAct loop exceeded max iterations");
    }
    
    // 5. 将本轮对话写入记忆
    chatMemory.add(memoryId, new UserMessage(userMessage));
    chatMemory.add(memoryId, response.content());
    
    // 6. 返回最终 AiMessage 文本(或完整对象,根据方法返回类型)
    return response.content().text();
}

上述代码清晰地展示了 ReAct(Reasoning + Acting)循环的自动化驱动机制,其决策流程可提炼为如下流程图:

flowchart TD
    classDef nodeStyle fill:#f1f5f9,stroke:#334155,stroke-width:1.5px,color:#1e293b
    classDef decisionStyle fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e

    A["接收方法调用"] --> B["解析@MemoryId/@UserMessage"]
    B --> C["从ChatMemory获取历史"]
    C --> D["组装完整消息列表"]
    D --> E["调用 chatModel.generate"]
    E --> F{"AiMessage中是否包含ToolCall?"}
    F -- "是" --> G["执行所有ToolCall"]
    G --> H["将工具结果追加到消息列表"]
    H --> I{"迭代次数 < maxIterations?"}
    I -- "是" --> E
    I -- "否" --> J["抛出异常或返回部分结果"]
    F -- "否" --> K["更新ChatMemory"]
    K --> L["返回最终结果"]

    class A,B,C,D,E,G,H,J,K,L nodeStyle
    class F,I decisionStyle

ReAct 流程图说明

  • 设计意图:将 Agent 的推理-行动循环彻底自动化,开发者无需手动编写任何循环或条件判断。
  • 核心组件chatModel(推理器)、ToolCall(行动触发)、ToolExecutor(执行器)、maxIterations(安全阀)。
  • 执行流程:每次 LLM 返回后检查是否包含工具调用,若是则执行工具并将结果反馈给 LLM,持续迭代直到 LLM 给出纯文本回答或达到最大次数。
  • 源码映射:循环位于 ServiceInvocationHandlerinvoke 方法内部,maxIterations 可通过 @AiService(maxIterations = 10) 配置。

正是这个“黑盒”循环,让开发者可以写出极简的接口定义:

@AiService
public interface Assistant {
    String chat(@MemoryId int userId, @UserMessage String message);
}

仅仅两行代码,背后却隐藏着记忆管理、多轮对话、工具调用和防无限循环的多层保护。这便是框架设计智慧的集中体现。

2.3 流式代理的变体

对于流式场景,AiServices 同样支持返回类型为 TokenStream 的方法。代理内部会检测方法返回类型,如果是 TokenStream,则不再进入同步的 ReAct 循环,而是切换到流式处理管道:调用 streamingChatModel.generate(messages, handler),并通过 TokenStream 向调用者暴露 onNextonComplete 等操作。其原理类似,但实现复杂度更高,因为工具调用可能在流式传输中途发生,需要特殊的协调机制,此处不再赘述。

3. 贯穿始终的三大设计模式与 SPI 扩展

LangChain4j 的优雅并不仅仅体现在接口和代理上,其内部组件之间的协作更是充满了经典设计模式的影子。我们从中提炼出三个最核心的模式,它们共同构成了框架的灵活性与扩展性。

3.1 Builder 模式:让复杂对象的创建回归直觉

AiServices.builder() 是 Builder 模式的典范。如果使用传统的构造函数或 setter 方法,一个拥有众多可选配置(模型、工具、记忆、检索器、系统消息、最大迭代次数……)的 AiServices 对象将难以构建。Builder 模式通过流式接口解决了这一难题:

flowchart TD
    classDef startStyle fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
    classDef stepStyle fill:#f1f5f9,stroke:#334155,stroke-width:1.5px,color:#1e293b
    classDef endStyle fill:#d1fae5,stroke:#10b981,stroke-width:1.5px,color:#064e3b

    Start(["AiServices.builder(接口.class)"]) --> A[".chatLanguageModel(model)"]
    A --> B[".tools(tool1, tool2)"]
    B --> C[".chatMemory(chatMemory)"]
    C --> D[".retrievers(retriever)"]
    D --> E[".systemMessage('你是专业助手')"]
    E --> F[".maxIterations(10)"]
    F --> End([".build() 完成校验与组装"])

    class Start startStyle
    class A,B,C,D,E,F stepStyle
    class End endStyle

Builder 模式示意图说明

  • 设计意图:将对象构造过程与表示分离,允许用户以声明式、可读的方式逐步提供配置。
  • 核心角色AiServices.Builder 是具体建造者,内部维护一个配置对象(或许叫 AiServiceContext),存储 chatModeltoolsmemory 等。.build() 时进行必要校验(如 chatModel 非空)并生成代理。
  • 组装流程:每一步调用都返回 Builder 自身,形成连贯链。最终 build() 触发代理创建,将所有配置植入 InvocationHandler
  • 源码映射AiServices.Builder 是静态内部类,其 build() 方法实现约 30 行,主要进行 ServiceInvocationHandler 的构造和 Proxy.newProxyInstance 调用。

3.2 责任链模式:消息的流水线处理器

在 LangChain4j 的 RAG 或 Agent 流程中,用户消息并非直接抛给 LLM,而是要经过一系列预处理。这正是一条责任链:

[用户消息] → ChatMemory(附加上下文) → Retriever(注入外部知识) → ChatLanguageModel(推理)

每个组件都是一个处理器。ChatMemory 负责将历史消息追加到当前会话;Retriever(若配置)则从向量数据库或搜索引擎中检索相关文档,并将结果拼接到上下文;最终 ChatModel 进行推理。更妙的是,这些处理器完全是可插拔的——你可以随时替换 Memory 的实现(内存 → Redis → 数据库),也可以添加自定义的 ChatMemory 装饰器实现“总结记忆”等高级策略,而不影响下游的 ChatModel。这正是责任链模式“解耦发送者和接收者”的体现。

3.3 SPI 扩展机制:厂商无关的即插即用

让框架真正成为“生态”而非“孤岛”的,是 Java 原生 SPI(Service Provider Interface) 机制。LangChain4j 核心模块只定义接口,不包含任何具体模型实现。当你在 pom.xml 中引入 langchain4j-openai 时,该模块的 META-INF/services/dev.langchain4j.model.chat.ChatLanguageModel 文件中写入了:

dev.langchain4j.model.openai.OpenAiChatModel

框架在运行时通过 ServiceLoader 加载该实现类。这一过程的流程如下:

flowchart TD
    classDef nodeStyle fill:#f1f5f9,stroke:#334155,stroke-width:1.5px,color:#1e293b
    classDef decisionStyle fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e

    A["启动时: ServiceLoader.load(ChatLanguageModel.class)"] --> B{"遍历 META-INF/services 下的配置"}
    B -->|"发现 langchain4j-openai"| C["加载 OpenAiChatModel"]
    B -->|"发现 langchain4j-ollama"| D["加载 OllamaChatModel"]
    B -->|"无实现"| E["回退到默认或报错"]
    C --> F["加入候选模型列表"]
    D --> F
    F --> G["应用程序通过条件选择或配置文件指定具体模型"]

    class A,C,D,E,F,G nodeStyle
    class B decisionStyle

SPI 流程图说明

  • 设计意图:实现“对扩展开放,对修改封闭”。新增一种模型提供商只需编写实现类并添加 SPI 配置文件,完全无需触碰核心框架代码。
  • 核心参与者ServiceLoader(JDK 标准 SPI 框架)、META-INF/services/接口全限定名 文件、各模型模块的实现类。
  • 加载流程:框架在初始化 AiServicesModelFactory 时调用 ServiceLoader.load(接口.class) 获取所有实现,然后根据用户配置的模型名称(如“openai”)匹配对应的实现。
  • 源码映射:在 LangChain4j 中,有一个 ServiceHelper 工具类封装了 ServiceLoader,并处理类加载、异常降级等。

自定义模型接入示例:假设你需要接入公司内部的“MyCorp LLM”,只需:

  1. 实现 ChatLanguageModel 接口。
  2. META-INF/services/dev.langchain4j.model.chat.ChatLanguageModel 中添加一行:com.mycorp.ai.MyCorpChatModel
  3. 打包成 JAR 放入 classpath。框架即可自动识别,通过 AiServices.builder().chatLanguageModel(new MyCorpChatModel()) 直接使用。

这种低侵入性的扩展方式,让 LangChain4j 成为真正的“AI 应用开发粘合剂”。

4. 工具与记忆的实现原理

4.1 @Tool 注解与命令模式:从 Java 方法到 OpenAI Function Calling

LangChain4j 通过 @Tool 注解将普通的 Java 方法转换为 LLM 可理解的工具。背后的核心机制是命令模式的应用:

  • 命令的定义@Tool 标注的方法,其 name@P 参数描述构成了命令的元数据。
  • 命令的调用者:LLM 通过返回 ToolCall 发起工具执行请求。
  • 命令的接收者与执行者:框架扫描所有 @Tool 方法,生成 ToolSpecification(包含名称、描述、参数 JSON Schema),并注册到 ToolProvider。当 ToolCall 到来时,ToolProvider 根据名称查找对应的 ToolSpecification 和方法引用,由 ToolExecutor 反射调用。

转换过程:对于方法:

@Tool("计算两个数的和")
public double add(@P("第一个数") double a, @P("第二个数") double b) {
    return a + b;
}

框架会生成如下 JSON Schema(与 OpenAI Function Calling 格式兼容):

{
    "type": "function",
    "function": {
        "name": "add",
        "description": "计算两个数的和",
        "parameters": {
            "type": "object",
            "properties": {
                "a": {"type": "number", "description": "第一个数"},
                "b": {"type": "number", "description": "第二个数"}
            },
            "required": ["a", "b"]
        }
    }
}

当 LLM 决定调用该工具时,返回的 ToolCall 中携带 {"name": "add", "arguments": {"a": 3, "b": 5}}ToolExecutor 通过反射执行 add(3, 5),返回 8.0,再封装成 ToolExecutionResultMessage 喂回给 LLM。这种 “查找-执行”分离 的设计正是命令模式的精髓,使得工具管理、调用链追踪、安全控制都变得异常清晰。

4.2 ChatMemory 的多租户隔离与持久化

ChatMemory 接口看似简单,却内含精妙的多租户设计:

public interface ChatMemory {
    List<ChatMessage> messages(Object memoryId);
    void add(Object memoryId, ChatMessage message);
}

memoryId 通常是用户 ID、会话 ID 等业务标识。框架默认实现为 MessageWindowChatMemory(保留最近 N 轮对话)或 TokenWindowChatMemory(限制总 Token 数)。每一条消息都与 memoryId 绑定,天然实现了不同用户/会话之间的严格隔离,避免了在微服务多租户场景下的数据泄露。

ChatMemoryStore 则打开了持久化的大门:

public interface ChatMemoryStore {
    List<ChatMessage> getMessages(Object memoryId);
    void updateMessages(Object memoryId, List<ChatMessage> messages);
}

通过自定义实现,可以将对话历史存储到任意位置。以下是一个存储到 Redis 的简化示例:

public class RedisChatMemoryStore implements ChatMemoryStore {
    private final RedisTemplate<String, List<ChatMessage>> redis;
    
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        String key = "chat:memory:" + memoryId;
        List<ChatMessage> messages = redis.opsForValue().get(key);
        return messages != null ? messages : Collections.emptyList();
    }
    
    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        String key = "chat:memory:" + memoryId;
        redis.opsForValue().set(key, messages, Duration.ofDays(7));
    }
}

然后将其注入 ChatMemory

ChatMemory chatMemory = MessageWindowChatMemory.builder()
    .maxMessages(20)
    .chatMemoryStore(new RedisChatMemoryStore())
    .build();

此时,用户对话历史将在 Redis 中持久化,服务重启不丢失,且支持水平扩展——这正是企业级 AI 应用所必需的。

5. 与前后系列的衔接:构建完整知识体系

本文深入 LangChain4j 源码内核,剖析了其核心接口、动态代理和设计模式,这既是对本系列第 1 篇《LangChain 生态全景》中“Java 强类型与编译期安全”论点的技术实证,也是为后续篇章铺设的基石。

  • 衔接前文:第 1 篇从宏观对比了 Python 与 Java 的设计哲学差异,本文则展示了 AiServices 如何通过 Builder 模式和 JDK Proxy 将“安全与灵活”完美融合——Assistant 接口的声明式定义保证编译期类型安全,而代理内部的动态组装又赋予了运行时的极致灵活,Python 的简易与 Java 的严谨在此处殊途同归。
  • 引出后文(第 3 篇 Spring Boot 自动配置):本文中 AiServices 的 JDK 代理创建过程是纯手动演示。然而在 Spring Boot 环境下,AiServicesAutoConfiguration 会自动扫描 @AiService 注解,为你完成 Bean 注册和依赖注入,并借助条件装配适配不同模型。第 3 篇将详细拆解这一自动化过程,以及 GraalVM Native Image 的兼容性处理。
  • 关联系列一 Agent 四要素:Agent 的 LLM、Memory、Tools、Planning 四要素,在本文中体现为 ChatLanguageModel(LLM)、ChatMemory(Memory)、@Tool(Tools)、以及 InvocationHandler 中的 ReAct 循环(Planning)。掌握这些内核,再去阅读系列一中的架构蓝图,理解将更加透彻。

6. 面试高频专题

以下精心设计 12 道面试题,覆盖 LangChain4j 核心机制,助你系统化巩固知识。

Q1:LangChain4j 中的 AiServices 是如何使用 JDK 动态代理实现自动 Agent 逻辑的?
考察点:动态代理、框架设计。
参考答案要点AiServices.builder().build() 内部调用 Proxy.newProxyInstance,传入接口 Class、InvocationHandler 实现。InvocationHandlerinvoke 方法拦截 chat 等方法,自动解析 @MemoryId@UserMessage 等参数,从 ChatMemory 获取历史,组装消息列表,调用 ChatLanguageModel,若返回 ToolCall 则进入 while 循环执行工具并再次调用模型,直至得到最终回答,最后更新记忆并返回。这使得开发者只需定义接口,无需关心内部流程。

Q2:LangChain4j 是如何实现厂商无关的?SPI 机制起了什么作用?
参考答案:框架定义 ChatLanguageModel 等接口,具体实现(如 OpenAiChatModel)由独立模块提供。每个模块通过 META-INF/services/ 文件声明实现类。框架使用 ServiceLoader 在运行时发现并加载这些实现。这样,切换厂商只需更换依赖和配置文件,核心代码零改动。

Q3:如果让你在 LangChain4j 中添加一个全新的模型支持,你会怎么做?
参考答案:实现 ChatLanguageModelStreamingChatLanguageModel 接口,编写 HTTP 客户端或本地调用逻辑。在 META-INF/services/ 下创建以接口全限定名命名的文件,写入自己的实现类全限定名。如果需要自动配置,可额外提供 Spring Boot Starter,利用条件装配启用。

Q4:StreamingResponseHandler 的各个回调分别在什么时候触发?
参考答案onNext(String token) 在每收到一个新 Token 时触发;onComplete(Response<AiMessage>) 在流正常结束时触发,可获取完整 Response(含 Token 统计);onError(Throwable) 在网络异常或服务端错误时触发;onToolCall(ToolCall) 在 LLM 流式返回工具调用时触发(部分模型支持流式工具调用)。

Q5:解释 LangChain4j 中 Builder 模式的具体应用,为什么不用工厂模式?
参考答案AiServices 配置项多且可选,若使用工厂模式则需多参数构造或繁琐的 setter,容易出错。Builder 模式通过流式 API 让用户逐步设置,内部维护不可变配置,最后 build() 时统一校验,解决了复杂对象创建的 clarity 和健壮性问题。

Q6:LangChain4j 中的 ReAct 循环是如何实现的?如何防止无限循环?
参考答案:在 InvocationHandler 中,调用 LLM 后检查 AiMessage.hasToolCalls(),若为 true 则执行工具并将结果追加到消息列表,然后再次调用 LLM,形成循环。框架通过 maxIterations 参数(默认值可配)限制最大循环次数,超过即抛异常或返回部分结果。同时,工具执行失败或超时也会被捕获处理,避免死循环。

Q7:ChatMemory 如何实现多租户隔离?它的持久化机制是怎样的?
参考答案ChatMemory 接口方法均以 memoryId 为维度操作消息列表。memoryId 通常为用户或会话 ID,不同 ID 的消息互不干扰,天然隔离。持久化通过 ChatMemoryStore 接口实现,用户可以自定义存储到 Redis、数据库等,通过 MessageWindowChatMemory.builder().chatMemoryStore(...) 注入。

Q8:简述 @Tool 注解转换为 OpenAI Function Calling JSON Schema 的过程。
参考答案:框架扫描 @Tool 方法,提取方法名、@Tool 的 value 作为描述,通过 @P 注解获取参数名和描述,借助 Java 反射获取参数类型,生成对应的 JSON Schema(type、properties、required 等)。该 Schema 在每次请求 LLM 时与消息一同发送,让 LLM 知道可用的工具。

Q9:LangChain4j 中 ChatLanguageModel.generate(List<ChatMessage>) 方法的输入消息列表都包含哪些类型?它们是如何被转换到特定 API 的?
参考答案:列表可包含 SystemMessageUserMessageAiMessageToolExecutionResultMessage。在 OpenAiChatModel 内部,通过 message 类型映射为 role:system、user、assistant、tool。ToolExecutionResultMessage 还携带 tool_call_id 和名称,确保多轮工具调用的上下文正确。

Q10:为什么 LangChain4j 要分别为同步和流式定义不同的接口,而不是用一个接口加选项?
参考答案:这是接口隔离原则的体现。同步和流式的交互模型截然不同:一个返回结果,一个通过回调推送。若合并在一个接口中,将迫使所有实现都带上 StreamingResponseHandler 参数或返回类型的歧义,增加实现者的心智负担。分开后,使用者可根据场景显式选择,接口职责单一,便于扩展和测试。

Q11:如何在不修改框架源码的情况下,为 ChatMemory 增加“自动总结历史”的功能?
参考答案:利用装饰器模式或实现 ChatMemory 接口包装原有实现。在新的 SummarizingChatMemory 中,当历史消息超过阈值时,调用 LLM 对早期消息进行摘要,替换掉部分旧消息。只需将该装饰器注入给 AiServices 即可,无需改动框架代码。这体现了框架的开放封闭原则。

Q12:(系统设计题)请参考 LangChain4j 的设计模式,设计一个能够灵活切换不同 LLM 厂商和记忆存储的 AI 应用架构,并说明如何保证高可用与可观测性。
考察点:架构设计、SPI、责任链、设计模式综合应用。
参考答案要点

  • 定义 LLMProvider 接口(类比 ChatLanguageModel),并通过 SPI 实现多个厂商。使用配置中心动态指定当前厂商。
  • 记忆存储使用 ChatMemory + ChatMemoryStore 接口,实现 Redis、MySQL 等多种存储,通过责任链模式给 Memory 添加日志、加密等装饰器。
  • 高可用:对 LLM 调用引入重试、熔断(如 Resilience4j);模型服务采用多实例负载均衡;记忆存储使用 Redis Cluster。
  • 可观测性:在 InvocationHandler 代理层或 AOP 切入,记录每次 LLM 调用的耗时、Token 消耗、工具调用链和异常;集成 Micrometer 输出指标,便于 Prometheus 监控。架构整体遵循“面向接口编程”和“Builder”模式,使各组件灵活替换。

附录:核心知识点速查表

核心概念关键接口/类设计模式职责
LLM 调用ChatLanguageModel策略模式统一同步 LLM 调用契约
流式调用StreamingChatLanguageModel观察者模式基于回调的流式响应
向量化EmbeddingModel适配器模式文本转向量,适配不同嵌入服务
Agent 工厂AiServicesBuilder + 代理模式创建 @AiService 接口的动态代理
代理逻辑ServiceInvocationHandler命令模式拦截方法调用,驱动 ReAct 循环
记忆管理ChatMemory / ChatMemoryStore策略 + 模板方法会话状态存储与多租户隔离
工具抽象@Tool / ToolSpecification命令模式将 Java 方法转换为 LLM 可调用工具
SPI 扩展ServiceLoader / META-INF/services服务提供者模式模型厂商的即插即用

延伸阅读