概述
系列定位:本文是“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 的架构内核。
核心要点:
- 核心接口体系:
ChatLanguageModel、StreamingChatLanguageModel等定义了框架的“世界语”,以依赖倒置实现模型层的无限扩展。 - 动态代理魔法:
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处理文本向量化,ImageModel与ModerationModel分别覆盖多模态和内容安全。每个接口都有对应的OpenAi*、Azure*等实现。 - 协作流程:上层组件(如 Agent)依赖注入
ChatLanguageModel接口,在运行时由 SPI 或 DI 容器提供具体实例。调用generate(messages)将触发实现内部的 HTTP 请求、协议转换和响应解析。 - 源码映射:核心接口位于
dev.langchain4j.model.chat、dev.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是一个泛型封装,除了承载最终消息文本外,还携带FinishReason、TokenUsage等元信息。这比直接返回 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 的子类 UserMessage、SystemMessage、AiMessage、ToolExecutionResultMessage 分别被映射为 OpenAI 格式的 role: "user", role: "system", role: "assistant", role: "tool"。当 LLM 返回 tool_calls 时,OpenAiChatModel 会将其填充到 AiMessage 的 toolCalls 字段中,从而驱动后续的 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);
}
其实现类(如 OpenAiEmbeddingModel、AllMiniLmL6V2EmbeddingModel)分别对接到 OpenAI Embeddings API 或本地 ONNX 模型。上层 RAG 组件只需调用 embed(userQuery) 得到向量,剩下的相似度检索完全不需要关心向量从何而来。
ImageModel 与 ModerationModel 同样定义了各自领域的契约,它们的实现同样可以通过 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 循环源码级分析
InvocationHandler 的 invoke 方法是整个代理逻辑的入口。当调用 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 给出纯文本回答或达到最大次数。
- 源码映射:循环位于
ServiceInvocationHandler的invoke方法内部,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 向调用者暴露 onNext、onComplete 等操作。其原理类似,但实现复杂度更高,因为工具调用可能在流式传输中途发生,需要特殊的协调机制,此处不再赘述。
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),存储chatModel、tools、memory等。.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/接口全限定名文件、各模型模块的实现类。 - 加载流程:框架在初始化
AiServices或ModelFactory时调用ServiceLoader.load(接口.class)获取所有实现,然后根据用户配置的模型名称(如“openai”)匹配对应的实现。 - 源码映射:在 LangChain4j 中,有一个
ServiceHelper工具类封装了ServiceLoader,并处理类加载、异常降级等。
自定义模型接入示例:假设你需要接入公司内部的“MyCorp LLM”,只需:
- 实现
ChatLanguageModel接口。 - 在
META-INF/services/dev.langchain4j.model.chat.ChatLanguageModel中添加一行:com.mycorp.ai.MyCorpChatModel。 - 打包成 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 实现。InvocationHandler 的 invoke 方法拦截 chat 等方法,自动解析 @MemoryId、@UserMessage 等参数,从 ChatMemory 获取历史,组装消息列表,调用 ChatLanguageModel,若返回 ToolCall 则进入 while 循环执行工具并再次调用模型,直至得到最终回答,最后更新记忆并返回。这使得开发者只需定义接口,无需关心内部流程。
Q2:LangChain4j 是如何实现厂商无关的?SPI 机制起了什么作用?
参考答案:框架定义 ChatLanguageModel 等接口,具体实现(如 OpenAiChatModel)由独立模块提供。每个模块通过 META-INF/services/ 文件声明实现类。框架使用 ServiceLoader 在运行时发现并加载这些实现。这样,切换厂商只需更换依赖和配置文件,核心代码零改动。
Q3:如果让你在 LangChain4j 中添加一个全新的模型支持,你会怎么做?
参考答案:实现 ChatLanguageModel 或 StreamingChatLanguageModel 接口,编写 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 的?
参考答案:列表可包含 SystemMessage、UserMessage、AiMessage、ToolExecutionResultMessage。在 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 工厂 | AiServices | Builder + 代理模式 | 创建 @AiService 接口的动态代理 |
| 代理逻辑 | ServiceInvocationHandler | 命令模式 | 拦截方法调用,驱动 ReAct 循环 |
| 记忆管理 | ChatMemory / ChatMemoryStore | 策略 + 模板方法 | 会话状态存储与多租户隔离 |
| 工具抽象 | @Tool / ToolSpecification | 命令模式 | 将 Java 方法转换为 LLM 可调用工具 |
| SPI 扩展 | ServiceLoader / META-INF/services | 服务提供者模式 | 模型厂商的即插即用 |
延伸阅读
- LangChain4j 官方 GitHub 仓库 - 阅读最新源码
- Java Dynamic Proxy API 官方文档 - 理解动态代理底层
- Java SPI 机制介绍 - 掌握服务发现标准
- 本系列下一篇文章:《LangChain4j Spring Boot 深度集成:自动配置、条件装配与生产级特性》- 将揭秘 Spring Starter 如何将本文的所有机制转变为即插即用的企业级能力。