深入浅出LangChain4J

深入浅出LangChain4J

1.走进LangChain

1.1.什么是LangChain4J?

The goal of LangChain4J is to simplify integrating LLMs into Java applications.

​ -- 《LangChain4J Doc》

LangChain4J就像Java的Spring框架一样,为LLM的接入提供了一套标准的接入能力。LangChain4J是LangChain在Java生态下的具体实现,用于构建基于LLM的应用和Agent系统。

LangChain4J为不同的LLM提供了一个统一的API,屏蔽了底层API供应商的实现和调用差异,开发者可以使用LangChain无缝切换不同的LLM。

2.Architecture of LangChain4J

LangChain4J的架构可以分为两层,分别为高层和底层。

高层AI服务屏蔽了底层实现的细节,开发者可以实现一些业务服务的API和LLM进行交互。

低层具有更高的自由度,你可以任意定义组件的具体实现,控制组件的组合方式等等。

下面,就LangChain4J的核心组件,一一详细介绍。

2.1.Chat and Language Model

一般地,LLMs的API可以分为两类,

  • LanguageModel:输入输出都为String类型,现在使用的越来越少了
  • ChatModel:应用广泛的一类API,接收多个ChatMessage作为输入,输出一个AiMessage,除了文本类型之外,还支持图片、音频、视频等其他模式。

ChatModel是LangChain4J的低层API,提供强大的灵活性和可扩展性。

根据消息来源进行分类,LangChain4J支持以下五种消息类型:

消息类型 描述 主要方法
UserMessage 用户输入的消息数据 content(), name(), attributes()
AiMessage AI根据输入数据生成的消息 text(), thinking(), toolExceptionRequests(), attributes()
ToolExecutionResultMessage 工具执行结果消息
SystemMessage 系统发送的消息,可以由开发者自定义
CustomMessage 自定义消息

2.2.Chat Memory

我们知道,大模型是无状态的,模型本身并不会记录对话上下文。因此,如果你想要在对话中使用上下文的内容,就必须主动维护管理ChatMessage。但是手动维护比较麻烦,而LangChain4J提供了ChatMemory用来维护管理chatMessage.

2.2.1.History VS Memory

历史记录和记忆听起来似乎是同一种东西,但是对于LLM Agent来说,history和memory具有两种截然不同的语义:

  • history保留了用户和AI的所有会话记录,用户可以在界面中看到
  • memory记录的是专门呈现给大模型使用的信息,使其表现得好像是"记住了"对话的内容,实际底层实现是截然不同的算法。

LangChain4J目前只提供Memroy ,而不提供History

下面介绍关于Memory的几点高级特性。

2.2.2.Eviction Policy 淘汰策略

为什么要使用淘汰策略?

  • 存储空间受限:Memory组件可以理解为缓存,当存放的内容超过容量大小的时候,需要使用一定的策略淘汰一些记忆;
  • Token成本昂贵:更多的上下文记忆就代表使用更多的Token,而token的增加会增加每次调用LLM的成本;
  • 控制延迟:发送的LLM的token越多,处理他们所需要的时长越长,则用户等待输出的时间越长

LangChain4J目前支持两种开箱即用的记忆淘汰策略:

  1. 基于消息滑动窗口:使用MessageWindowChatMemory作为滑动窗口,仅保留最近的N条信息,淘汰窗口之外的旧消息;
  2. 基于token的滑动窗口:TokenWindowChatMemory只保留最近的N个token。但是消息是不可再分的,因此如果消息不合适的话,整条消息会直接被丢弃。
2.2.3.Persistence 持久化

默认情况下Memory是存放在内存中的,如果需要保留消息,可以自定义实现MessageStore,将chatMessage存储在数据库中。

java 复制代码
class PersistentChatMemroyStore implements ChatMemoryStore {
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {

    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {

    }

    @Override
    public void deleteMessages(Object memoryId) {
      // TODO: Implement deleting all messages in the persistent store by memory ID.
    }
}
2.2.4.SystemMessage

SystemMessage是一种特殊的消息,具有如下的特性:

  • 一旦添加,就会一直被保留,不会被淘汰策略淘汰
  • 一次只能持有一个SystemMessage
  • SystemMessage具有唯一性,重复的SystemMessage会被忽略,如果不一致,则会覆盖掉之前的SystemMessage
2.2.5.Tool 消息配对

AIMessage 中的 ToolExecutionRequestToolExecutionResponse 总是成对出现的。如果 ToolExecutionRequest 被淘汰了,则其对应的 ToolExecutionResponse 消息也会被自动淘汰。

2.3.Tools(Function Calling)

工具(Tools)允许LLM在必要的时候调用开发者定义的一个或者多个可用工具,工具是一个泛称,可以表示任何东西,例如网页搜索、外部API、或者执行特定的代码等等。

实际上LLM并不会去主动调用工具,而是通过开发人员在业务层接收LLM的回复,根据LLM的意愿进行工具的调用并将执行结果反馈给LLM。

一个良好的工具应当具有以下几个属性:

  1. 清晰明确的工具名称
  2. 工具的具体描述,以及应该何时使用工具;
  3. 每个工具的具体参数;

如果这个工具对于人来说一眼就能理解应该如何使用,那么这个工具对于LLM也是易于使用的。

Tools的定义和使用可以参考文档:LangChain4J Documents

2.4.Agent

3.实战

3.1.ChatModel

3.1.1.基本聊天
java 复制代码
public static void main(String[] args) {
    // 1.创建 chatModel
    OpenAiChatModel chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .baseUrl(System.getenv("OPENAI_BASE_URL"))
            .modelName(System.getenv("OPENAI_MODEL"))
            .build();

    // 2.使用 chat 方法发起对话
    ChatResponse response = chatModel.chat(ChatRequest.builder()
            .messages(UserMessage.from("Hello, could you introduce yourself ?"))
            .build());


    // 3.控制台打印
    System.out.println(response.aiMessage().text());
}
3.1.2.多轮对话

多轮对话和基本对话的实现方式相同,只不过构建对话请求的时候,需要将前文传入到messages中,LLM会参考message得出合理的响应。

java 复制代码
private static void chatWithMultipleMessages() {
    // 1.构建 User 和AI 的多轮对话
    UserMessage firstUserMessage = UserMessage.from("Hello, My name is shepi, could you introduce yourself ?");
    AiMessage firstAiMessage = CHAT_MODEL.chat(
            ChatRequest.builder()
                    .messages(firstUserMessage)
                    .build())
            .aiMessage();
    UserMessage secondUserMessage = UserMessage.from("Please tell me my name");
    AiMessage secondAiMessage = CHAT_MODEL.chat(
            ChatRequest.builder()
                    .messages(firstUserMessage, firstAiMessage, secondUserMessage)
                    .build()).aiMessage();

    // 2.控制台打印
    System.out.println(secondAiMessage.text());
}

3.2.ChatMemory

ChatMemory可以帮助我们维护上下文,LangChain4J提供了两种开箱即用的窗口聊天记忆

  • MessageWindowChatMemory
  • TokenWindowChatMemory

下面是一个基于消息数量的窗口对话记忆:

java 复制代码
public static void main(String[] args) {

    // 1.新建 messageWindow,滑动窗口最大长度设置为 3
    MessageWindowChatMemory chatWindow = MessageWindowChatMemory.builder()
            .maxMessages(4)
            .build();
    // tryChatWithCommonMessages(chatWindow);
    // 测试特殊的系统消息 systemMessage
    tryChatWithSystemMessage(chatWindow);
}

/**
 * 系统消息永远不会被驱逐
 * @param chatWindow
 */
private static void tryChatWithSystemMessage(MessageWindowChatMemory chatWindow) {
    // 创建系统消息
    SystemMessage systemMessage = SystemMessage.from("The user talking with you is shepi");
    chatWindow.add(systemMessage);
    System.out.println(systemMessage.text());
    for (int i = 0; i < 4; i++) {
        UserMessage userMessage = null;
        if (i == 3) {
            userMessage = UserMessage.from("Please tell me my name");
        } else {
            userMessage = UserMessage.from("let's talk about something interesting");
        }
        System.out.println(userMessage.contents());
        chatWindow.add(userMessage);
        // 使用 chatMemory 对话
        AiMessage aiMessage = CHAT_MODEL.chat(ChatRequest.builder().messages(chatWindow.messages()).build()).aiMessage();
        System.out.println(aiMessage.text());
        chatWindow.add(aiMessage);
    }
}

private static void tryChatWithCommonMessages(MessageWindowChatMemory chatWindow) {
    // 2.模拟进行多轮对话
    // 第一轮给出自己的名字,第四轮询问AI我的名字,其他两轮随便聊点什么
    for (int i = 0; i < 4; i++) {
        UserMessage userMessage = null;
        if (i == 0) {
            userMessage = UserMessage.from("Hello, My name is shepi, could you introduce yourself ?");
        } else if (i == 3) {
            userMessage = UserMessage.from("Please tell me my name");
        } else {
            userMessage = UserMessage.from("let's talk about something interesting");
        }
        System.out.println(userMessage.contents());
        chatWindow.add(userMessage);
        // 使用 chatMemory 对话
        AiMessage aiMessage = CHAT_MODEL.chat(ChatRequest.builder().messages(chatWindow.messages()).build()).aiMessage();
        System.out.println(aiMessage.text());
        chatWindow.add(aiMessage);
    }
}

3.3.Tools

LangChain中定义Tool非常简单,只需要在工具接口方法上使用注解 @Tool("Tool description")即可,下面是一个简答的例子。

java 复制代码
public class WeatherService {
    @Tool("获取指定城市的天气情况")
    public String getWeather(String city) {
        // 模拟天气数据
        return MessageFormat.
                format("Today's weather in {0} is sunny with a temperature of 22 degrees.", city);
    }
}

3.4.AiService (Agent 封装)

AiService 是什么?

AiService 是 LangChain4J 提供的高层 API,用于快速构建 Agent 应用。它屏蔽了工具调用的底层细节(如检测 LLM 返回的函数调用请求、执行工具、将结果反馈给 LLM),让开发者可以专注于业务逻辑。

AiService 与 Agent 的关系:

  • AiService 是 LangChain4J 对 Agent 概念的具体实现
  • 它自动完成了 Agent Loop 中的:接收输入 → 判断是否需要 Tool → 调用 Tool → 观察结果 → 生成响应
  • 开发者只需定义接口和 Tool,AiService 会自动完成工具调用的编排

如果想要使用上面创建的 Tool,我们需要在定义 AiService 时,传入 Tool 对象。

java 复制代码
private static final OpenAiChatModel CHAT_MODEL = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .baseUrl(System.getenv("OPENAI_BASE_URL"))
            .modelName(System.getenv("OPENAI_MODEL"))
            .build();

public static void main(String[] args) {

    // 根据CHAT_MODEL创建一个 chatService
    ChatAssistant chatService = AiServices.builder(ChatAssistant.class)
            .chatLanguageModel(CHAT_MODEL)
            .tools(new WeatherService())
            .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
            .build();

    // 问题
    String question = "今天北京什么天气?";
    String answer = chatService.chat(question);
    System.out.println(answer);
}

4.流式响应 (Streaming)

LangChain4J 支持流式响应,即 LLM 在生成内容时实时返回每个 token,而不是等待完整响应后再返回。

使用场景

  • 需要实时显示生成过程的场景(如 ChatGPT 的打字机效果)
  • 生成较长内容时提升用户体验
  • 需要提前终止不满意的输出

实现方式

使用 StreamingAiServices 代替 AiServices

java 复制代码
interface StreamingChatAssistant {
    void chat(String message, StreamingResponseHandler handler);
}

StreamingChatAssistant agent = StreamingAiServices.builder(StreamingChatAssistant.class)
        .chatLanguageModel(model)
        .build();

agent.chat("介绍一下 LangChain4J", new StreamingResponseHandler() {
    @Override
    public void onToken(String token) {
        // 每收到一个 token 就调用
        System.out.print(token);
    }

    @Override
    public void onComplete(String fullResponse) {
        System.out.println("\n完成!");
    }

    @Override
    public void onError(Throwable error) {
        System.err.println("错误: " + error.getMessage());
    }
});

流式 vs 非流式

特性 非流式 (AiServices) 流式 (StreamingAiServices)
响应方式 等待完整响应后返回 实时返回每个 token
用户体验 有等待感 实时反馈,体验更好
实现复杂度 简单 稍复杂(需要处理回调)
适用场景 短文本、后台任务 长文本、交互式对话

完整示例代码见:StreamingAgent.java


相关推荐
侠客行03175 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
、BeYourself6 小时前
LangChain4j 流式响应
langchain
、BeYourself7 小时前
LangChain4j之Chat and Language
langchain
老毛肚7 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎7 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Yvonne爱编码7 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚7 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂7 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
fuquxiaoguang8 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析