LangChain4j 学习笔记

基本介绍

本章节主要介绍:

  1. LangChain4j 是什么
  2. 为什么需要 LangChain4j
  3. LangChain4j 的价值体现
  4. LangChain4j 的两层抽象
  5. LangChain4j 中模型的分类
  6. LangChain4j 的应用场景
  7. LangChain4j 和 Spring AI 的比较
  • LangChain4j 是什么

    LangChain4j 是一个专为 Java 和 Kotlin 开发者设计的开源框架,旨在简化将大型语言模型(LLM)集成到应用程序中的过程。你可以将它理解为 Python 生态中流行的 LangChain 框架在 JVM 平台上的“兄弟”,但它并非官方移植版本,而是为 Java 开发者量身打造,遵循 Java 的编程习惯和设计理念。LangChain4j 的核心目标是让 Java 开发者能够轻松构建由 AI 驱动的应用,例如聊天机器人、问答系统、智能代理(Agent)等

  • 为什么需要 LangChain4j

    当ChatGPT、QwenLM、DeepSeek等大语言模型(LLM)横空出世时,开发者们立刻意识到:LLM不是终点,而是构建智能应用的“大脑”。但要让这个“大脑”真正解决实际问题,还需要解决三个关键痛点:

    • 信息过时:LLM 的知识截止于训练数据的时间节点(如GPT-4的训练数据截止到2023年),无法回答诸如“2024年最新AI论文内容”或“今天纽约股市收盘价”这样的问题。

    • 无法动手:LLM 虽然能生成自然语言,但它不能执行外部操作,比如调用API、计算数值、查询数据库、发送邮件等。它就像一个只会思考的“脑壳”,没有“手脚”。

    • 记忆有限:LLM 的上下文窗口(例如GPT-4最多支持32,768个tokens)限制了它处理长文本的能力,难以记住对话历史或文档细节。

    因此,我们需要一个框架,将 LLM 的“大脑”与“感官(数据)”、“手脚(工具)”、“记忆(上下文)”连接起来,让它从“聊天机器人”升级为“能解决具体问题的助手”,于是 LangChain4j 便诞生了

  • LangChain4j 主要价值体现

    • 易学性:统一了个各个模型厂商的 API, LangChain4j 提供了一套统一的接口,让你可以轻松地在不同模型和存储之间切换,而无需重写大量代码。目前它已支持 15+ 个主流 LLM 提供商和 20+ 个向量存储。
    • 易用性: 它封装了构建 LLM 应用时常见的模式和组件,提供了一个“开箱即用”的工具箱。这包括了从底层的提示词模板、聊天记忆管理、函数调用,到高级的 RAG(检索增强生成)和 Agent 等功能。
    • 与 JVM 生态无缝集成: 作为纯 Java 框架,它可以与 Spring Boot、Hibernate 等成熟的 Java 生态组件无缝结合,非常适合用于构建企业级应用。
  • LangChain4j 的两层抽象

    • 低层次(Low-level): 在这个层面,你可以直接使用 ChatModelUserMessageEmbeddingStore 等基础组件。这提供了最大的自由度,让你可以完全控制应用的构建方式,但需要编写更多的“胶水代码”来组合这些组件。

    • 高层次(High-level): 这是 LangChain4j 最具特色的部分。通过像 AiService 这样的高级 API,你可以用声明式的方式与 LLM 交互。这种方式隐藏了底层的复杂性,让你能更专注于业务逻辑。

  • LangChain4j 种模型分类:在 LangChain 框架中,大语言模型主要根据交互方式输出形态被划分为三大核心类型

    模型类型 输入形式 输出形式 核心优势 典型应用场景
    聊天模型 (Chat Model) 消息列表 (System/Human/AI) AI 消息对象 (AIMessage) 理解上下文,支持多轮交互与角色设定 智能助手、客服机器人、Agent 工具调用
    文本生成模型 (LLM) 纯文本字符串 纯文本字符串 结构简单,适合单向生成任务 文章摘要、代码补全、文本润色
    文本嵌入模型 (Embeddings) 文本字符串/列表 向量数组 (List[float]) 将文本转化为可计算的语义向量 语义搜索、RAG 知识库检索、推荐系统
  • LangChain4j 的应用场景

    • 您希望实现一个自定义 AI 聊天机器人,可以访问您的数据并按照设定的方式运行
      • 客服机器人:
        • 礼貌回答客户问题
        • 处理订单变更或取消
      • 教学助手:
        • 授课不同学科内容
        • 解释难点
        • 评估用户的理解与知识水平
    • 您希望处理大量非结构化数据(文件、网页等),并提取结构化信息
      • 从客户评价和客服对话中提取洞察
      • 从竞争对手网站提取有价值信息
      • 从应聘者简历中提取关键信息
    • 您希望生成信息
      • 为客户生成个性化邮件
      • 为应用或网站生成内容,如博客文章、故事等
    • 您希望对信息进行转换
      • 总结
      • 校对与改写
      • 翻译
  • LangChain4j 和 Spring AI 的比较

    维度 LangChain4j Spring AI
    出身背景 社区驱动,功能迭代快 Spring 官方团队出品
    生态绑定 Spring生态的AI标准扩展 框架无关,可在任何Java项目中运行
    功能特性 功能更丰富,尤其在 RAG 和 Agent 方面 核心功能稳健,与 Spring 生态集成度更高
    编程风格 声明式接口 (@AiService) + 链式 API Client/Template 模式 + 配置驱动
    上手难度 低 (对Spring开发者而言) 中 (需理解其核心概念)
    核心优势 企业级集成、自动配置、可观测性 功能全面、模型/工具支持广泛、社区活跃
    适用场景 复杂的 AI 应用(多 Agent、复杂 RAG) 标准企业应用,快速集成基础对话能力

快速上手

现在让我们在纯 Java 环境下创建一个 LangChain4j 的 Demo,实现对大模型的调用

  • Step1:创建 Maven 工程

  • Step2:配置依赖

        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <langchain4j.version>1.3.0</langchain4j.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-open-ai</artifactId>
            </dependency>
        </dependencies>
    
    
        <!-- 使用 BOM (Bill of Materials) 统一管理 langchain4j 相关依赖的版本 -->
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>dev.langchain4j</groupId>
                    <artifactId>langchain4j-bom</artifactId>
                    <version>${langchain4j.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    

    注意:LangChain4j 要求 JDK 版本最低是 17

  • Step3:编写测试类

        public static void main(String[] args) {
            OpenAiChatModel model = OpenAiChatModel.builder()
                    .baseUrl("http://langchain4j.dev/demo/openai/v1")
                    .apiKey("demo")
                    .modelName("gpt-4o-mini")
                    .build();
            String answer = model.chat("介绍一下LangChain4j");
            System.out.println(answer);
        }
    

    image-20260326231040189

SpringBoot 整合 LangChain4j

上一章节我们在快速上手中,体验了在 Maven 工程中创建第一个 LangChain4j 程序,是一个很纯净的 Java 环境,但是在实际生产开发中,我们通常需要在 Spring 环境下来进行整合,这章节我们将快速学习 SpringBoot 整合 LangChain4j 创建我们第一个 SpringBoot 环境下的 LangChai4j 程序

  • Step1:创建Maven工程

  • Step2:引入依赖

        <!-- Spring Boot 父项目依赖,提供版本管理和默认配置 -->
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.3.8</version>
            <relativePath/>
        </parent>
            
        <dependencies>
            <!--SpringBoot 整合 langchain4j 依赖-->
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
            </dependency>
    
            <!--SpringBoot启动依赖,提供 SpringBoot 环境-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot</artifactId>
            </dependency>
    
            <!-- Spring Boot 测试依赖,提供单元测试和集成测试支持 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
    
        <dependencyManagement>
            <dependencies>
                <!-- langchain4j 版本控制依赖,管理 langchain4j 相关依赖的版本 -->
                <dependency>
                    <groupId>dev.langchain4j</groupId>
                    <artifactId>langchain4j-bom</artifactId>
                    <version>${langchain4j.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
  • Step3:编写配置文件

    server:
      port: 8080
    
    langchain4j:
      open-ai:
        chat-model:
          api-key: demo
          base-url: http://langchain4j.dev/demo/openai/v1
          model-name: gpt-4o-mini
          temperature: 0.7
    
  • Step4:编写测试类

        @Autowired
        private OpenAiChatModel openAiChatModel;
    
        @Test
        public void test(){
            String response = openAiChatModel.chat("Hello, how are you?");
            System.out.println(response);
        }
    

    image-20260328212946522

接口介绍

LangChain4j 中存在两类API,一类是低阶API,低阶 API 是 LangChain4j 的基石。它直接对应大模型的原生能力。如果你需要精确控制 temperaturetopP,或者需要手动拼接复杂的 Prompt 模板,低阶 API 是首选;一类是高阶API,高阶 API(通常称为 AI Services)是 LangChain4j 对 Java 开发者的“杀手锏”,它采用了类似 Spring Data JPA 或 Retrofit 的风格(定义接口,框架自动实现),将复杂的逻辑(如:记忆管理、工具调用、RAG 检索)封装在注解和配置中,让业务代码极其干净

特性 低阶 API 高阶 API (AI Services)
核心接口 ChatLanguageModel, EmbeddingModel AiServices, @SystemMessage, @Tool
控制粒度 极高 (手动管理每一个请求参数) 中等 (声明式配置,框架自动处理)
开发体验 类似写原生的 HTTP 请求,代码量较多 类似写 Java 接口 (Interface),代码简洁
状态管理 需手动维护 List<ChatMessage> 自动通过 @MemoryId 或配置管理
适用场景 需要精细控制 Prompt、参数、底层调试 快速构建业务应用、企业级开发、RAG

低阶接口介绍

本章节主要介绍 LangChain4j 中六大核心低阶接口,六大接口覆盖对话生成、向量表示、图像、安全、评分等关键能力,是构建 LLM 应用(尤其是 RAG、Agent)的基础组件

接口类型 核心任务 输入类型 输出类型 典型应用场景
ChatModel 多轮对话交互 消息列表 AI 消息对象 (AiMessage) 包含文本、思考过程或工具调用请求 智能客服、复杂助手、Agent 任务
LanguageModel 简单文本补全 纯文本字符串 纯文本字符串 (String) 已逐渐被淘汰,仅用于极简单的单向生成
EmbeddingModel 文本向量化 文本字符串 浮点数数组 (向量) 语义搜索、RAG 知识库检索
ImageModel 图像生成/处理 文本提示词 / 图像 图像 URL / Base64 营销素材生成、AI 绘画
ModerationModel 内容安全检测 文本 / 图像 违规标签 / 布尔值 敏感词过滤、合规审查
ScoringModel 质量/相关性评分 文本对 (查询 + 内容) 分数 (0.0-1.0) RAG 重排序 (Re-ranking)

接口具体演示,这里使用接口的具体实现类进行演示

public class QuickStartDemo {

    private static final String BASE_URL = "http://langchain4j.dev/demo/openai/v1";
    private static final String API_KEY = "demo";

    public static void main(String[] args) {
        // FIXME OpenAiLanguageModel 使用的是旧的 /completions API 端点,而演示环境 langchain4j.dev/demo/openai/v1 不支持这个端点会导致报错
        System.out.println("=== 测试 LanguageModel ===");
        testLanguageModel();

        System.out.println("\n=== 测试 ChatModel ===");
        testChatModel();

        System.out.println("\n=== 测试 EmbeddingModel ===");
        testEmbeddingModel();

        System.out.println("\n=== 测试 ImageModel ===");
        testImageModel();

        System.out.println("\n=== 测试 ModerationModel ===");
        testModerationModel();

        System.out.println("\n=== 测试 ScoringModel ===");
        testScoringModel();
    }

    /**
     * 测试 LanguageModel - 语言模型基础接口
     */
    public static void testLanguageModel() {
        // LanguageModel 是 LangChain4j 中语言模型的基础接口
        // 提供了 generate(String) 和 generate(List<T>) 方法用于文本生成
        LanguageModel model = OpenAiLanguageModel.builder()
                .baseUrl(BASE_URL)
                .apiKey(API_KEY)
                .modelName("gpt-4o-mini")
                .build();
        String answer = model.generate("用一句话概括人工智能").content();
        System.out.println("回答: " + answer);
    }

    /**
     * 测试 ChatModel - 基本对话
     */
    public static void testChatModel() {
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl(BASE_URL)
                .apiKey(API_KEY)
                .modelName("gpt-4o-mini")
                .build();
        String answer = model.chat("介绍一下LangChain4j");
        System.out.println("回答: " + answer);
    }

    /**
     * 测试 EmbeddingModel - 将文本转换为向量嵌入
     */
    public static void testEmbeddingModel() {
        // EmbeddingModel 可以将文本转换为 Embedding(向量表示)
        // 常用于语义搜索、文本相似度计算等场景
        EmbeddingModel model = OpenAiEmbeddingModel.builder()
                .baseUrl(BASE_URL)
                .apiKey(API_KEY)
                .modelName("text-embedding-ada-002")
                .build();

        // 将文本转换为向量
        String text = "LangChain4j 是一个用于 Java 的 LLM 应用开发框架";
        Response<Embedding> response = model.embed(text);
        Embedding embedding = response.content();

        System.out.println("文本: " + text);
        System.out.println("向量维度: " + embedding.dimension());
        System.out.println("向量前5个值: " + Arrays.toString(
                Arrays.copyOf(embedding.vector(), 5)));
    }

    /**
     * 测试 ImageModel - 生成和编辑图像
     */
    public static void testImageModel() {
        // ImageModel 可以根据文本描述生成图像,或对现有图像进行编辑
        // 注意:图像生成可能需要较长时间
        ImageModel model = OpenAiImageModel.builder()
                .baseUrl(BASE_URL)
                .apiKey(API_KEY)
                .modelName("dall-e-3")
                .build();

        String prompt = "一只可爱的橘猫在草地上玩耍,卡通风格";
        System.out.println("图像生成提示词: " + prompt);
        System.out.println("正在生成图像... (演示环境可能不支持实际生成)");

        try {
            Response<Image> response = model.generate(prompt);
            Image image = response.content();
            System.out.println("图像 URL: " + image.url());
        } catch (Exception e) {
            System.out.println("图像生成失败 (演示环境限制): " + e.getMessage());
        }
    }

    /**
     * 测试 ModerationModel - 内容审核
     */
    public static void testModerationModel() {
        // ModerationModel 可以检查文本是否包含有害内容
        // 常用于用户输入过滤、内容安全检测等场景
        ModerationModel model = OpenAiModerationModel.builder()
                .baseUrl(BASE_URL)
                .apiKey(API_KEY)
                .modelName("text-moderation-latest")
                .build();

        // 测试正常文本
        String safeText = "LangChain4j 是一个很好用的 Java AI 框架";
        Response<Moderation> safeResponse = model.moderate(safeText);
        System.out.println("文本: " + safeText);
        System.out.println("是否包含有害内容: " + safeResponse.content().flagged());

        // 测试可能有害的文本
        String riskyText = "我想学习如何做一些危险的事情";
        Response<Moderation> riskyResponse = model.moderate(riskyText);
        System.out.println("\n文本: " + riskyText);
        System.out.println("是否包含有害内容: " + riskyResponse.content().flagged());
    }

    /**
     * 测试 ScoringModel - 文本相关性打分
     */
    public static void testScoringModel() {
        // ScoringModel 可以针对查询对多段文本进行打分或排序
        // 常用于搜索结果排序、文档相关性评估等场景
        // 注意:OpenAI 官方没有直接的 ScoringModel,这里使用 EmbeddingModel 计算相似度作为示例
        EmbeddingModel model = OpenAiEmbeddingModel.builder()
                .baseUrl(BASE_URL)
                .apiKey(API_KEY)
                .modelName("text-embedding-ada-002")
                .build();

        String query = "Java 编程语言";
        List<String> documents = Arrays.asList(
                "Java 是一种面向对象的编程语言",
                "Python 在数据科学领域很流行",
                "Java 虚拟机是 Java 程序的运行环境",
                "JavaScript 主要用于网页开发"
        );

        System.out.println("查询: " + query);
        System.out.println("\n文档相关性评分 (使用余弦相似度):");

        // 获取查询的向量
        Response<Embedding> queryEmbedding = model.embed(query);
        float[] queryVector = queryEmbedding.content().vector();

        // 计算每个文档与查询的相似度
        for (String doc : documents) {
            Response<Embedding> docEmbedding = model.embed(doc);
            float[] docVector = docEmbedding.content().vector();
            double similarity = cosineSimilarity(queryVector, docVector);
            System.out.printf("  [%.4f] %s%n", similarity, doc);
        }
    }

    /**
     * 计算两个向量的余弦相似度
     */
    private static double cosineSimilarity(float[] vector1, float[] vector2) {
        double dotProduct = 0.0;
        double norm1 = 0.0;
        double norm2 = 0.0;
        for (int i = 0; i < vector1.length; i++) {
            dotProduct += vector1[i] * vector2[i];
            norm1 += Math.pow(vector1[i], 2);
            norm2 += Math.pow(vector2[i], 2);
        }
        return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
    }
}

高阶接口介绍

高阶 API(通常称为 AI Services)是 LangChain4j 对 Java 开发者的“杀手锏”。它采用了类似 Spring Data JPA 或 Retrofit 的风格:定义接口,框架自动实现。它将复杂的逻辑(如:记忆管理、工具调用、RAG 检索)封装在注解和配置中,让业务代码极其干净

注解 作用 示例
@SystemMessage 定义系统级指令(人设)。 @SystemMessage("你是数学专家")
@UserMessage 定义用户提示词模板,支持变量替换。 @UserMessage("翻译这句话: {{it}}")
@MemoryId 标识对话的会话 ID,用于区分不同用户的记忆。 String chat(@MemoryId String userId, ...)
  • Step1:创建接口

    @AiService
    public interface ChatService {
        /**
         * 聊天
         * @param message
         * @return
         */
        @SystemMessage("你是一个资深的Java大师")
        String chat(@UserMessage String message);
    }
    
  • Step2:调用接口

        @Autowired
        private ChatService chatService;
        /**
         * 测试高级API
         * @param userMessage
         * @return
         */
        @GetMapping(value = "/api/high")
        public String highLevelChat(@RequestParam String userMessage) {
            return chatService.chat(userMessage);
        }
    

除了上述 @AiService 方式以外,还可以直接通过 AiServices.create 来创建

        AiServices.create(ChatService.class, chatModel).chat(userMessage);

消息类型

LangChain4j 中总共有物种消息类型,分别是系统消息(SystemMessage)、用户消息(UserMessage)、助手消息(AiMessage)、工具消息(ToolExecutionResultMessage)、自定义消息(CustomMessage)

消息类型 对应角色 作用与场景
系统消息(SystemMessage) 系统 立规矩”。通常作为对话的第一条消息,用于设定 AI 的人设、行为准则或背景知识。例如:“你是一个专业的 Java 助手”。
用户消息(UserMessage) 用户 提问题”。代表真实用户(或调用者)的输入。它不仅包含文本,在支持多模态的模型中,还可以包含图片、音频等内容。
助手消息(AiMessage) 助手 给回答”。代表 AI 生成的回复。除了文本内容,它还可以包含“思考过程”或“工具调用请求”(比如 AI 说“我要调用天气接口”)。
工具消息(ToolExecutionResultMessage) 工具 返结果”。当 AI 请求调用工具后,外部系统执行完工具,需要将结果封装成这种消息返回给 AI,告诉它“接口调用成功了,结果是XXX”。
自定义消息(CustomMessage) 自定义 特殊用途”。一种灵活的消息类型,允许你定义任意角色(Role)。目前主要用于支持特定模型(如 Ollama)的特殊属性。

为了展示这四种核心消息类型在实际代码中是如何创建和流转的,这里模拟了一个“智能助手调用计算器”的完整闭环流程

    @Test
    public void testMessageType(){
        List<ChatMessage> messages = new ArrayList<>();

        // 1、创建系统消息:确定 AI 的角色和行为(立规矩)
        SystemMessage systemMessage = SystemMessage.from("你是一个数学助手。如果用户需要计算,请调用计算器工具。");
        messages.add(systemMessage);

        // 2、创建用户消息:模拟用户提出问题(提问题)
        UserMessage userMessage = UserMessage.from("请帮我计算 520 加上 1314 等于多少?");
        messages.add(userMessage);

        // 3、创建助手消息:AI 响应用户消息,并给出一个工具调用请求(给回答)
        // 注意:通常这一步是模型返回的,这里为了演示流程,我们模拟模型返回了一个“工具调用请求”
        // 假设 AI 决定调用名为 "calculator" 的工具,参数是 a=520, b=1314
        ToolExecutionRequest request = ToolExecutionRequest.builder()
                .name("calculator")
                .arguments("{\"a\": 520, \"b\": 1314}")
                .build();
        AiMessage aiMessage = AiMessage.from(request);
        messages.add(aiMessage);

        // 4、创建工具消息:模拟工具执行结果(给结果)
        // 这里模拟你的 Java 代码执行了加法运算,得到结果 1834
        String calculationResult = "1834";
        ToolExecutionResultMessage resultMessage = ToolExecutionResultMessage.from(request, calculationResult);
        messages.add(resultMessage);

        // 5、现在,我们将包含所有历史(系统指令、用户问题、AI意图、工具结果)的列表发给模型,让 AI 根据结果生成最终回答
        System.out.println(">>> 正在请求大模型生成最终回答...");
        String response = openAiChatModel.chat(messages).aiMessage().text();
        // 预期输出:520 加上 1314 等于 1834。
        System.out.println("AI 最终回答: " + response);
    }

image-20260328214718609

提示词工程

Prompt 介绍

  • Prompt 是什么?

    Prompt 翻译过来就是“提示”,它是你给 AI 的 “指令 + 上下文”,用来告诉 AI 要做什么、怎么做

  • Prompt 和 Message 有什么区别?

    Prompt 是一个更加广泛的概念,指的是你给模型的任何输入,目的是引导它产生你想要的输出。Message 是一个更加具体的概念,用户发送的消息,称为 userMessage,系统给出的消息,称之为 systemMessage,相当于给模型的输入信息进行了一个更加细化的分类。

    Prompt 包含了 Message,Prompt 好比一个剧本,Message 就是剧本中的一句台词。

    追问:为啥模型 API 即提供了 prompt 方法,有提供了 message 方法

    因为我们在构建大模型应用时,特别是多轮对话场景,我们需要构建 message 列表,然后放入 prompt 实现大模型上下文感知,而没次的单论对话就只需要放入对应的 message 中。你可以理解为 prompt 方法是用于给开发者构建模型上下文的,提高回复的流畅度和准确性,而 message 方法细化每轮对话,让每轮对话更加精确,两者相辅相成,目的都是为了让模型的回复更加精确

  • 一个完整的 Prompt 包含哪些内容?

    1. 角色设定:你是谁?例如:“你是一名资深 Java 后端工程师,帮我审查代码”
    2. 任务目标:要做什么事?例如:“帮我优化这段接口代码,提升性能。”
    3. 输入内容:给 AI 材料,例如:“代码如下:……”
    4. 输出要求:格式、风格、长度、语言,例如:“用中文解释,分点说明,不要废话,最后给出优化后代码。”
    5. 约束条件:限制、禁忌,例如:“不要改动业务逻辑,只优化查询效率。”
  • Prompt 的作用

    • 控制 AI 输出的内容
    • 控制输出格式
    • 提高准确性
    • 让 AI 模仿风格

提示词模板

  • 低阶API:k-v 键值对,key为{{it}}时,v 可以是任何参数,key为{{xxx}}it 时,此时 value 参数名必须和 xxx 保持一致

    // 用户输入
    String role = "Java";
    String question = "OOM该如何处理";
    // 定义模板
    PromptTemplate template = PromptTemplate.from("你是一个{{it}}助手,请回答{{qustion}}");
    Prompt prompt = template.apply(Map.of("it", role, "question", question))
    // 将模板转为消息
    UserMessage userMessage = prompt.toUserMessage();
    // 使用模板
    chatModel.chat(userMessage);
    
  • 高阶API@StructuredPrompt@V@UserMessage@SystemMessage

    // 1、定义提示词模板
    @StructuredPrompt("你是一个{{role}}助手,请回答{{qustion}})
    public interface MyPrompt {
        String generate(@V("role") String a, @V("qustion") String b);
    }
                      
    // 2、使用提示词模板
    String prompt = apiPrompt.generate("Java", "OOM该如何处理");
    

更详细完整的用法请参考:

输出方式

在 LangChain4j 中,输出方式主要可以从两个维度来划分:一是数据流的模式(流式 vs 非流式),二是返回数据的结构(非结构化文本 vs 结构化对象)

PS:通常这两个维度是混合的,排列组合共 4 种输出方式(比如:流式结构化、非流式结构化)

流式与非流式输出

LangChain4j 中按照“模型响应结果是否一次性输出”(决定了用户是“等待完整结果”还是“实时看到生成过程”),可以分为两种输出方式,一种是流式输出,一种是非流式输出

  • 流式输出(Stream):程序会阻塞等待,直到大模型生成完整的回复后才一次性返回
  • 非流式输出( Blocking):程序实时输出,模型每生成一个词,就会立即返回
特性 流式输出 非流式输出
别名 打字机效果、SSE 阻塞式调用、同步调用
返回类型 通常是 Flux<String> (Reactor) 或 StreamingResponseHandler 回调。 通常是 StringResponse<T>
用户体验 极佳。用户能立即看到内容逐字出现,感觉响应非常快,更有“对话感”。 较差。如果回答很长,用户会面对长时间的白屏或加载动画,不知道发生了什么。
适用场景 聊天机器人、智能助手、任何需要与人实时交互的场景 简单的后台任务、非交互式问答、需要一次性处理完整文本的分析任务
流式输出示例

在 LangChain4j 中存在两个流式输出的接口 StreamingChatModelStreamingLanguageModel,它们分别对应的是 ChatModelLanguageModel 两个接口,都是文字类回复类的接口,不提供图片流式响应接口,因为图片不可能以下生成一点点吧,没有意义。这里我也重点演示 StreamingChatModel,因为当前 LanguageModel 已经相对过时了,所以StreamingLanguageModel也相对过时

方式 核心类/接口 特点与适用场景
回调处理器 StreamingResponseHandler 最基础、最直接。通过实现 onNext(token)onComplete(response) 等回调方法来处理数据流。适合控制台打印或简单的自定义逻辑。
令牌流 TokenStream 更灵活、偏向拉取(Pull)模式。它实现了 Iterable 接口,你可以像遍历集合一样逐个获取 token。适合需要在后端对每个 token 进行复杂处理(如翻译、缓存、敏感词过滤)的场景。
响应式流 Flux<String> 响应式编程首选。与 Spring WebFlux 无缝集成,支持 mapfilter 等链式操作。这是实现前后端 SSE (Server-Sent Events) 实时推送的最佳实践,特别适合高并发、非阻塞的 Web 应用。

方式一StreamingChatResponseHandler

    /**
     * 测试 StreamingChatResponseHandler 流式输出
     */
    @Test
    public void testStreamOutput1() {
        streamingChatModel.chat("从一数到五,只需要输出数字,不需要输出其他任何东西", new StreamingChatResponseHandler() {
            /**
             * 当生成下一个部分思考/推理文本时调用
             * @param partialThinking
             */
            @Override
            public void onPartialThinking(PartialThinking partialThinking) {
                System.out.println("部分思考: " + partialThinking);
            }

            /**
             * 当生成下一个 部分工具调用 时调用
             * @param partialToolCall
             */
            @Override
            public void onPartialToolCall(PartialToolCall partialToolCall) {
                System.out.println("部分工具调用: " + partialToolCall);
            }

            /**
             * 当生成下一个部分文本响应时调用(根据 LLM 提供商的不同,部分响应可能包含一个或多个 token)
             * 如果输出的是中文会存在乱码,因为 LLM 响应的一个或多个 token 并不是一个完整的中文字符句子,存在 UTF-8 截断
             * @param s
             */
            @Override
            public void onPartialResponse(String s) {
                System.out.println("部分响应: " + s);
            }

            /**
             * 当 LLM 完成一个工具调用的流式传输时调用
             * @param completeToolCall
             */
            @Override
            public void onCompleteToolCall(CompleteToolCall completeToolCall) {
                System.out.println("完整工具调用: " + completeToolCall);
            }

            /**
             * 当 LLM 完成整个响应生成时调用
             * @param chatResponse
             */
            @Override
            public void onCompleteResponse(ChatResponse chatResponse) {
                System.out.println("完整响应: " + chatResponse);
            }

            /**
             * 当发生错误时调用
             * @param throwable
             */
            @Override
            public void onError(Throwable throwable) {
                System.out.println("发生错误: " + throwable);
            }
        });
    }

方式二TokenStream(已淘汰)

    /**
     * 测试 TokenStream 流式输出
     * 这种方式在我当前使用的 LangChain4j 1.3.0 版本已经不支持了,所以这里没法测,了解即可
     */
    @Test
    public void testStreamOutput2(){
        streamingChatModel.chat("从一数到五,只需要输出数字,不需要输出其他任何东西")
                .onNext(token -> {
                    System.out.println(token);
                })
                .onComplete(response -> System.out.println("\n--- 结束 ---"))
                .start();
    }

方式三Flux<String>(Web开发主推)

这里需要另外添加 Project Reactor 依赖,因为 Flux 来自于这个依赖,这里两种添加方式

方式一:保留 spring-boot-starter-web + 手动添加 Reactor(推荐)

        <!-- Spring Boot Web 依赖,提供 Web 开发支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Project Reactor 依赖(用于 Flux) -->
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
        </dependency>

方式二:改用 spring-boot-starter-webflux

        <!-- Spring Boot WebFlux 依赖,提供响应式Web开发支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

区别说明

  • spring-boot-starter-web:基于 Servlet 的传统 Web 应用,适合阻塞式 I/O
  • spring-boot-starter-webflux:基于 WebFlux 的响应式 Web 应用,适合非阻塞式 I/O,已包含 reactor-core

建议: 如果你的项目主要是传统的 REST API,只是偶尔用 Flux 做 SSE 流式输出,方式一更轻量。如果大量使用响应式编程,选方式二

    /**
     * 测试流式聊天
     *
     * @param userMessage
     * @return
     */
    @GetMapping(value = "/output/stream", produces = "text/event-stream")
    public Flux<String> streamChat(@RequestParam String userMessage) {
        return Flux.create(emitter -> {
            streamingChatModel.chat(userMessage, new StreamingChatResponseHandler() {
                /**
                 * 当生成下一个部分响应时调用(根据 LLM 提供商的不同,部分响应可能包含一个或多个 token)
                 * @param partialResponse
                 */
                @Override
                public void onPartialResponse(String partialResponse) {
                    // 将部分响应推送给前端
                    System.out.println(partialResponse);
                    emitter.next(partialResponse);
                }

                /**
                 * 当生成完整响应时调用
                 * @param chatResponse
                 */
                @Override
                public void onCompleteResponse(ChatResponse chatResponse) {
                    // 将完整响应推送给前端,完成响应流
                    emitter.complete();
                }

                /**
                 * 当生成响应时发生错误时调用
                 * @param throwable
                 */
                @Override
                public void onError(Throwable throwable) {
                    // 将错误信息推送给前端
                    emitter.error(throwable);
                }
            });
        });
    }

上述都是低阶 API 的流式输出演示,现在我们来演示高阶 API 的流式输出

  • Step1:需要额外引入一个依赖

            <!-- LangChain4j Reactor 支持,用于 Flux 流式输出 -->
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-reactor</artifactId>
            </dependency>
    
  • Step2:编写接口

    我们只需要申明返回值类型为 Flux<String> 即可,剩下的 LangChain4j 会自动帮我们完成

    @AiService
    public interface ChatService {
        /**
         * 流式聊天
         * @param message
         * @return
         */
        Flux<String> streamChat(String message);
    }
    
  • Step3:编写Controller

        /**
         * 测试流式聊天
         *
         * @param userMessage
         * @return
         */
        @GetMapping(value = "/output/stream", produces = "text/event-stream")
        public Flux<String> streamChat(@RequestParam String userMessage) {
            // 高阶API实现流式输出
            return chatService.streamChat(userMessage);
        }
    
非流式输出

低阶 API 和高阶 API 默认都是非流式输出,流式输出需要指定

    /**
     * 测试非流式输出
     */
    @Test
    public void testBlockingOutput(){
        String response = chatModel.chat("介绍一下Java");
        System.out.println(response);
    }

结构化与非结构化输出

LangChain4j 中按照“数据格式是否遵循模式规则”(决定了你拿到的是需要二次解析的“文本”,还是可以直接使用的“Java 对象”)可以分为两种输出方式,一种是结构化输出,一种是非结构化输出

  • 结构化输出:固定格式、有明确字段、可被程序直接解析的输出(一般给机器看)
  • 非结构化输出:无固定格式、无明确字段、无法直接被程序直接解析的输出(一般给人看)
维度 结构化输出 非结构化输出
解析方式 自动/强类型,框架(如 LangChain4j)自动将结果映射为 Java 对象(POJO)。你直接调用 person.getName() 手动/脆弱 ,你需要写正则表达式、String.split() 或 JSON 解析库。如果模型多说了几个字(如“好的,这是结果…”),解析就会报错。
模型内部机制 受控生成,通过 Function Calling(工具调用) 或 JSON Schema 强制模型只能输出符合特定结构的数据。 自由生成,模型根据概率预测下一个字,没有格式限制。
返回类型 对象/JSON,例如:Person(name="小明", age=25, job="程序员") 纯文本 (String) ,例如:“小明今年25岁,是个程序员。”
可靠性 高,如果模型输出不符合结构,框架通常会报错或自动重试,保证给你的数据是合规的。 低 ,易出现幻觉或格式错误(比如想让它输出 JSON,它却输出了 Markdown)。
适用场景 数据提取、分类标注、表单填充、数据库查询结果、报表导出 开放式生成、自由表达、解释说明、答疑、建议类输出
结构化输出

结构化输出通过约束模型的生成,使其返回预定义格式的数据(通常是 JSON,在 Java 中映射为 POJO 对象)。

  • 特点:严谨、类型安全、可被程序直接解析。

  • 实现原理:LangChain4j 底层利用模型的 Function Calling 能力或 JSON Mode,强制模型按照你定义的 Java 类(字段名、类型)来填充数据。

  • 使用场景

    • 信息抽取:从非结构化文本(如简历、合同、新闻)中提取关键字段(人名、日期、金额)。

    • API 对接/工具调用:模型需要返回参数来调用外部 API(如“查询天气”需要返回 {"city": "Beijing"})。

    • 分类任务:将用户输入分类为预定义的枚举值(如 POSITIVE, NEGATIVE)。

结构化输出总共有三种实现方式:

方式 实现原理 可靠性
基于 AI Service (推荐) 定义一个 Java 接口,方法返回你想要的 POJO 类型。LangChain4j 会自动处理底层的 Prompt 工程和结果转换。 高,开发体验最好
基于 JSON Schema 在请求中通过 response_format 参数明确指定 JSON Schema。这需要大模型本身支持该 API 参数(如 OpenAI, Gemini)。 最高,强制模型遵守格式
基于提示词 (Prompting) 在 System Prompt 中通过文字描述你希望的 JSON 格式。 较低,依赖模型的遵循指令能力

1)方式一:基于 AI Service(推荐)

这是 LangChain4j 最核心的优势。你不需要关心底层是用了 JSON Schema 还是 Prompt,你只需要定义 Java 接口

  • Step1:定义实体类

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class People {
        private String name;
        private int age;
        private String sex;
        private String job;
        private String address;
        private String phone;
        private String email;
        private String idCard;
        private String remark;
        private String company;
        private String department;
        private String position;
        private String education;
    }
    
  • Step2:定义接口

    @AiService
    public interface ChatService {
        /**
         * 获取人物信息
         * @return
         */
        People getPeopleInfo(String userName);
    }
    
  • Step3:编写Controller

        /**
         * 测试 AI Service 实现的结构化输出
         *
         * @param userName
         * @return
         */
        @GetMapping(value = "/output/struct")
        public String getPeople(String userName) {
            return chatService.getPeopleInfo(userName).toString();
        }
    

image-20260329203916514

2)方式二:基于 JSON Schema

这是最“硬核”的方式,直接利用大模型提供商(如 OpenAI, Gemini, Azure)提供的原生 API 参数 response_formattools

    /**
     * 测试通过 JSON Schema 实现结构化输出
     */
    @Test
    public void testStructuredOutputByJsonSchema() {
        // 定义技能对象的 JSON Schema
        JsonObjectSchema skillSchema = JsonObjectSchema.builder()
                .addStringProperty("name", "技能名称")
                .addStringProperty("level", "技能等级,如:初级、中级、高级")
                .build();

        // 定义数组 Schema
        JsonArraySchema skillsArraySchema = JsonArraySchema.builder()
                .description("技能列表")
                .items(skillSchema)
                .build();

        // 定义 JSON Schema,描述期望的输出结构
        JsonObjectSchema schema = JsonObjectSchema.builder()
                .addStringProperty("name", "人物姓名")
                .addIntegerProperty("age", "人物年龄")
                .addStringProperty("occupation", "人物职业")
                .addProperty("skills", skillsArraySchema)
                .required("name", "age", "occupation")
                .build();

        // 创建 JsonSchema 包装对象(jsonSchema 方法需要 JsonSchema 类型,而非 JsonObjectSchema)
        JsonSchema jsonSchema =
                dev.langchain4j.model.chat.request.json.JsonSchema.builder()
                        .name("PersonSchema")
                        .rootElement(schema)
                        .build();

        // 构建 ChatRequest,设置响应格式为 JSON,并指定 schema
        ChatRequest chatRequest = ChatRequest.builder()
                .messages(UserMessage.from("请介绍一位著名的计算机科学家"))
                .responseFormat(dev.langchain4j.model.chat.request.ResponseFormat.builder()
                        .type(ResponseFormatType.JSON)
                        .jsonSchema(jsonSchema)
                        .build())
                .build();

        // 发送请求并获取响应
        ChatResponse chatResponse = chatModel.chat(chatRequest);

        // 输出结构化结果
        System.out.println("结构化输出结果:");
        System.out.println(chatResponse.aiMessage().text());
    }

image-20260329205121523

3)方式三:基于提示词(不推荐)

这是最原始的方法,适用于不支持 JSON Schema 参数的老旧模型或开源小模型。

// 1. 构造包含格式指令的提示词
String systemPrompt = """
    你是一个信息提取助手。
    请从文本中提取信息,并**仅**返回合法的 JSON 格式。
    不要输出任何解释性文字。
    JSON 格式要求:{"name": "String", "age": "Integer"}
    """;

// 2. 构建消息
List<ChatMessage> messages = Arrays.asList(
    SystemMessage.from(systemPrompt),
    UserMessage.from("John is 42...")
);

// 3. 发送请求
String response = model.generate(messages);

// 4. 清洗数据 (通常需要手动去除 Markdown 标记)
String cleanJson = response.replace("```json", "").replace("```", "").trim();
// 然后手动解析
非结构化输出

这是大语言模型最原始、最自然的形态。模型根据提示词生成一段连续的文本(String)。

  • 特点:灵活、富有创造性,但格式不可控。
  • 处理难度:如果需要程序自动处理结果(如存入数据库),你需要编写复杂的正则表达式或 JSON 解析代码,且容易因为模型多输出一句“好的,这是结果:”而导致解析失败。
  • 使用场景
    • 智能客服/聊天机器人:直接与人对话,需要自然流畅的语言。
    • 文章/邮件写作:生成营销文案、翻译文本。
    • 头脑风暴:生成创意点子列表。

代码示例:

// 1. 定义 AI Service 接口
interface Assistant {
    String chat(String userMessage);
}

// 2. 构建服务
Assistant assistant = AiServices.create(Assistant.class, model);

// 3. 调用并获取纯文本
String response = assistant.chat("请给我讲一个关于猫的笑话");

// 输出示例:
// "好的,这是一个笑话:为什么猫坐在电脑上?因为它们想盯着鼠标!"
System.out.println(response);

模型记忆

在 LangChain4j 中,模型记忆(Memory)是构建多轮对话应用的核心。它让 AI 能够“记住”之前的交互,从而实现连贯、有上下文的对话体验,主要分为四个核心部分:核心概念、记忆组件、淘汰策略和持久化方案

在深入技术细节前,必须理解 LangChain4j 对这两个概念的关键区分:记忆 (Memory) vs 历史 (History)

  • 历史 (History):指用户与 AI 之间所有消息的完整、原始记录。它就像一份“对话录像”,用于 UI 展示、审计追溯或数据分析。LangChain4j 不直接提供历史的自动存储功能,需要开发者自行实现。
  • 记忆 (Memory):指经过处理后,提供给大模型(LLM)用于理解上下文的信息。它更像是一份“对话解说稿”,为了适应模型的上下文窗口限制和提升效率,可能会对原始历史进行筛选、总结或压缩。LangChain4j 的核心功能正是围绕“记忆”展开的。

简单来说,记忆是对历史的优化和利用

ChatMemory 介绍

  • ChatMemory 是什么

    ChatMemory 是 LangChain4j 中管理对话上下文的核心容器,是实现模型记忆的核心组件

  • ChatMemory的实现原理

    ChatMemory 内部维护了一个消息列表,并负责在每次对话时将这些消息注入到 LLM 的请求中,在多用户或复杂会话场景下,你需要使用 ChatMemoryProvider,它是一个函数式接口,根据传入的 memoryId(通常是用户ID或会话ID)来为每个会话提供独立的 ChatMemory 实例,从而实现会话隔

具体示例参考下方 Github 代码仓库地址

淘汰策略

淘汰策略是必要的,原因如下:

  • 为了适应 LLM 的上下文窗口。 LLM 一次可以处理的令牌(tokens)数量是有限制的。在某些时候,对话可能会超过这个限制。在这种情况下,需要淘汰一些消息。通常,最旧的消息会被淘汰,但如果需要,也可以实现更复杂的算法。
  • 为了控制成本。 每个令牌都有成本,这使得每次调用 LLM 的成本逐渐增加。淘汰不必要的消息可以降低成本。
  • 为了控制延迟。 发送给 LLM 的令牌越多,处理所需的时间就越长。

目前,LangChain4j 提供了两种开箱即用的实现:

策略类型 实现类 淘汰依据 适用场景
消息窗口 MessageWindowChatMemory 保留最近的 N 条消息 快速原型开发,对 Token 消耗不敏感。
Token 窗口 TokenWindowChatMemory 保留最近的 N 个 Token 生产环境推荐,能精确控制成本和上下文窗口,需配合 Tokenizer 使用。

由于系统消息和能工具消息的特殊性,淘汰策略需要特殊处理这两类消息

  • 对系统消息的特殊处理

    • 一旦添加,SystemMessage 始终被保留。

    • 每次只能保留一个 SystemMessage

    • 如果添加内容相同的新 SystemMessage,它将被忽略。

    • 如果添加内容不同新的 SystemMessage,它会替换旧的。


  • 对工具消息的特殊处理:如果包含 ToolExecutionRequestAiMessage 被淘汰,则其后续的孤立 ToolExecutionResultMessage(s) 也会自动被淘汰,以避免某些 LLM 提供商(如 OpenAI)禁止在请求中发送孤立 ToolExecutionResultMessage(s) 的问题

持久化方案

ChatMemory 默认是将消息保存到内存中的,使用一个map对象进行存储Map<Object, List<ChatMessage>>,具体实现类可以参考dev.langchain4j.store.memory.chat.InMemoryChatMemoryStore。在生产中是消息是一定不能放入内存中的,极易造成数据的丢失,所以我们需要将消息持久化,我们只需要编写一个类,然后实现 ChatMemoryStore 接口,即可实现消息的持久化保存,具体的持久化实现可以是 Redis

// 创建自定义的的消息持久化类
class PersistentChatMemoryStore implements ChatMemoryStore {

        @Override
        public List<ChatMessage> getMessages(Object memoryId) {
          // TODO: 实现通过 memory ID 从持久化存储中获取所有消息。
          // 可使用 ChatMessageDeserializer.messageFromJson(String) 
          // 和 ChatMessageDeserializer.messagesFromJson(String) 辅助方法从 JSON 反序列化。
        }

        @Override
        public void updateMessages(Object memoryId, List<ChatMessage> messages) {
            // TODO: 实现通过 memory ID 更新持久化存储中的所有消息。
            // 可使用 ChatMessageSerializer.messageToJson(ChatMessage) 
            // 和 ChatMessageSerializer.messagesToJson(List<ChatMessage>) 辅助方法将消息序列化为 JSON。
        }

        @Override
        public void deleteMessages(Object memoryId) {
          // TODO: 实现通过 memory ID 删除持久化存储中的所有消息。
        }
    }

// 将自定义的消息持久化类配置给模型使用
ChatMemory chatMemory = MessageWindowChatMemory.builder()
        .id("12345")
        .maxMessages(10)
        .chatMemoryStore(new PersistentChatMemoryStore())
        .build();

模型参数

模型的好坏不光取决去模型本身,还取决于模型参数的设置,所以如何配置一个合适的参数显得尤为重要,本章节主要介绍模型常见的参数,不同模型厂商的参数配置可能存在些许的不同。模型的参数按照功能主要可以分为三类:输出控制(决定说什么)、工程控制(决定怎么传)、格式控制(决定怎么说)

参数介绍

1)输出控制类参数:接决定了模型生成内容的风格和质量

参数名称 通俗解释
temperature 随机性。值越低越严谨(像做数学题),值越高越奔放(像写诗)。
maxTokens 篇幅限制。防止模型“滔滔不绝”导致费用失控,或限制输出太短。
topP 候选池大小。与 temperature 类似,但更平滑。通常建议二选一调节。
frequencyPenalty 防复读机。值越高,模型越不愿意重复使用已经出现过的词。
max-retries 最大重试次数,模型调用失败进行重试的最大次数
max-tokens 最大 token 数,模型调用最大耗费 token 数,超过限制拒绝响应
timeout 模型调用超时时间,超时中断请求

2)工程控制类参数:决定了你的 AI 应用是否健壮

  • timeout (超时时间):防止网络卡顿导致线程阻塞。一般设置为 30s - 60s。
  • maxRetries (重试次数):大模型 API 偶尔会抽风(5xx 错误),设置 3 次左右的重试可以大幅提升用户体验。
  • logRequests / logResponses:调试神器。开启后会在控制台打印发送给模型的原始数据和返回数据(注意脱敏)

3)格式控制类参数

  • responseFormat:强制模型输出符合特定规范的文本

    配置值 含义 适用场景
    "json_object" 强制 JSON 模式。模型必须输出合法的 JSON 字符串,否则报错。 数据提取、API 对接、配置生成。
    "json_schema" 严格 JSON 模式 (OpenAI 新特性)。不仅要求是 JSON,还要求符合你定义的 Schema (字段类型、必填项)。 极高可靠性的结构化数据输出。
    "text" 默认值。模型可以自由输出自然语言。 聊天、写作。

参数设置

SpringBoot环境下模型参数的配置主要有两种,一种是 Builder 模式,一种是配置文件模式

TIPS

  1. Temperature 和 TopP 的关系:通常建议只调节 temperature,将 topP 保持为 1.0。如果你同时调节这两个参数,模型的行为会变得难以预测。
  2. MaxTokens 不是越小越好:如果设置得太小(例如 100),模型可能会在句子说到一半时突然截断,导致输出不完整。
  3. 日志开关:在生产环境(Production)中,务必将 logRequestslogResponses 设置为 false,否则你的控制台会被大量的 Token 文本刷爆,且可能泄露用户隐私。

1)方式一:Builder模式配置参数

OpenAiChatModel.builder()
                // 基础连接信息
                .apiKey("your-api-key")
                .baseUrl("https://api.your-provider.com/v1") 
                .modelName("gpt-4o") // 或 qwen-max, ernie-bot 等
                
                // --- 核心输出参数 ---
                .temperature(0.7)      // 适度创意
                .maxTokens(2000)       // 允许较长回复
                .topP(1.0)             // 通常设为 1.0,让 temperature 主导
                .frequencyPenalty(0.0) // 不强制防重复,除非写长文
                
                // --- 工程化参数 ---
                .timeout(Duration.ofSeconds(60)) // 60秒超时
                .maxRetries(3)                   // 失败重试3次
                .logRequests(true)               // 开启请求日志(开发环境)
                .logResponses(true)              // 开启响应日志
                .build();

2)方式二:通过配置文件配置参数

langchain4j:
  open-ai: # 或者是 dashscope, qianfan 等
    chat-model:
      api-key: ${MY_API_KEY}
      model-name: gpt-4o
      # 输出控制
      temperature: 0.3          # 生产环境建议偏低,保证稳定性
      max-tokens: 1024
      top-p: 1.0
      frequency-penalty: 0.0
      # 工程控制
      timeout: 60s
      log-requests: false       # 生产环境建议关闭,防止日志泄露隐私
      log-responses: false

以下是提供的的关于模型参数常见的两个场景

场景一:严谨的客服/知识库问答

  • 目标:准确、不胡说、格式统一。
  • 配置策略:压低随机性,提高确定性。
OpenAiChatModel model = OpenAiChatModel.builder()
    .temperature(0.2)      // 低温度,几乎每次回答都一样
    .maxTokens(500)        // 限制长度,节省成本
    .frequencyPenalty(0.5) // 适度防重复
    .timeout(Duration.ofSeconds(10)) // 要求响应快
    .build();

场景二:创意写作 / 营销文案

  • 目标:有趣、多样化、用词丰富。
  • 配置策略:提高随机性,放宽限制。
OpenAiChatModel model = OpenAiChatModel.builder()
    .temperature(0.9)      // 高温度,激发创意
    .maxTokens(2000)       // 允许长文本
    .frequencyPenalty(0.2) // 允许适度重复(保持风格)
    .topP(0.95)            // 扩大选词范围
    .build();

模型监听器

在 LangChain4j 里,监听器(Listener / Handler) 就是一套回调接口,用来接收模型在运行过程中产生的各种事件:比如开始生成、输出一个 token、出错、完成、调用工具等。每个不同的模型都有对应的监听器接口,比如:ChatModelChatModelListenerAiServiceAiServiceStartedListenerAiServiceCompletedListener

监听器的工作方式

  1. 监听器是同步调用的,并且运行在同一个线程中
  2. 监听器被指定为 List<ChatModelListener>,会按照迭代顺序依次调用
  3. ChatModelListener.onRequest() 方法在调用 LLM 提供商 API 之前被调用
  4. ChatModelListener.onResponse() 方法在 LLM 提供商成功接收请求之后被调用
  5. 监听器回调方法一次调用只会被触发依次,即使出错重试调用模型也不会再次调用

监听器 Demo 示例

1)方式一:手动添加监听器

  • Step1:创建监听器。创建一个类,实现监听器接口,并且重写监听器方法

    public class MyChatModelListener implements ChatModelListener {
        /**
         * 请求前触发调用
         *
         * @param requestContext
         */
        @Override
        public void onRequest(ChatModelRequestContext requestContext) {
            Date requestId = new Date();
            requestContext.attributes().put("requestId", requestId);
            System.out.println("onRequest被调用了,当前请求为:" + requestId);
        }
    
        /**
         * 响应后触发调用
         *
         * @param responseContext
         */
        @Override
        public void onResponse(ChatModelResponseContext responseContext) {
            Object requestId = responseContext.attributes().get("requestId");
            System.out.println("onResponse被调用了,当前请求为:" + requestId);
        }
    
        /**
         * @param errorContext
         */
        @Override
        public void onError(ChatModelErrorContext errorContext) {
            Object requestId = errorContext.attributes().get("requestId");
            System.out.println("onError被调用了,当前请求为:" + requestId);
        }
    }
    
  • Step2:添加监听器

    ChatModel chatModel = OpenAiChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("")
            .listeners(List.of(new MyChatModelListener()))
            .build();
    

2)方式二:自动添加 Bean(推荐)

直接通过 @Bean 注解将监听器加入容器,Spring 会自动将监听器进行注册

@Configuration
public class MyChatModelListener {

    @Bean
    public ChatModelListener buildChatModelListener() {
        return new ChatModelListener() {
            /**
             * 请求前触发调用
             *
             * @param requestContext
             */
            @Override
            public void onRequest(ChatModelRequestContext requestContext) {
                Date requestId = new Date();
                requestContext.attributes().put("requestId", requestId);
                System.out.println("onRequest被调用了,当前请求为:" + requestId);
            }

            /**
             * 响应后触发调用
             *
             * @param responseContext
             */
            @Override
            public void onResponse(ChatModelResponseContext responseContext) {
                Object requestId = responseContext.attributes().get("requestId");
                System.out.println("onResponse被调用了,当前请求为:" + requestId);
            }

            /**
             * @param errorContext
             */
            @Override
            public void onError(ChatModelErrorContext errorContext) {
                Object requestId = errorContext.attributes().get("requestId");
                System.out.println("onError被调用了,当前请求为:" + requestId);
            }
        };
    }
}

工具函数

LLM 除了生成文本外,还可以触发操作,这个触发操作我们称之为工具(或函数调用),本篇文章将介绍 LangChain4j 框架是如何实现函数调用能力的。

  • 工具函数是什么

    在 LangChain4j 中,工具函数是大模型(LLM)用于访问和操作外部系统的能力载体,本质上就是给大模型装上 “手脚” 的业务代码,它是连接 LLM 与外部数据、接口、服务之间的桥梁。

    简单来说:工具函数 = 模型能看懂的说明书(元数据) + 实际执行的 Java 方法(业务逻辑)

  • 为什么需要工具函数

    大模型(LLM)本身只是一个“大脑”,擅长思考和生成文本,但它无法联网、无法计算复杂数学、也无法操作你的数据库。工具函数就是为了解决这个问题而存在的——它是一段具体的 Java 代码,模型在需要时可以“触发”它来执行实际操作。

    总结起来就是一下一点:

    1. 触发外部操作,LLM 只有大脑,没有手脚,需要工具函数来操作外部系统
    2. 解决大模型数据滞后性问题,可以将大模型链接到外部获取相关数据。比如搜索、数据库查询等
    3. 解决大模型无真逻辑问题,比如复杂的数据计算问题。

工具函数的定义

对于我们人类而言,想要使用一个工具,我们需要了解工具的用途、工具的使用方法,对于 LLM 也是一样的道理。对于 LLM 而言,一个合格的工具需要满足以下三要素:

  1. 工具名称
  2. 工具说明书:描述该工具的作用以及何时应使用该工具
  3. 工具参数:工具需要有哪些参数,以及每个参数的说明

在 LLM 中,工具函数有三种定义方式

1)方式一:手动构建ToolSpecification。必须搭配ToolExecutor使用,ToolSpecification 是工具,ToolExecutor 是工具执行器,封装了真正干活的逻辑

  • 特点:最底层、最灵活
  • 适用场景:适合动态生成工具、无注解场景
        // 1. 定义工具描述(告诉AI这个工具是干嘛的)
        ToolSpecification addToolSpec = ToolSpecification.builder()
                .name("add")
                .description("计算两个数字的和")
                .addParameter("a", type("integer"), "第一个数字")
                .addParameter("b", type("integer"), "第二个数字")
                .build();

        // 2. 定义执行器(真正干活的逻辑)
        ToolExecutor executor = (toolName, arguments) -> {
            if ("add".equals(toolName)) {
                int a = (Integer) arguments.get("a");
                int b = (Integer) arguments.get("b");
                return a + b;
            }
            return "未知工具";
        };

        // 3. AI 会根据 ToolSpecification 决定是否调用,由 executor 执行

2)方式二ToolSpecifications 工具扫描。必须配合 @Tools 注解标记工具类

  • 特点:能快速大量生成工具
  • 适用场景:适合批量注册多个工具
// 1、定义工具
@Tools // 标记这是一个工具类
public class MathTools {

    @Tool
    public int add(int a, int b) {
        return a + b;
    }

    @Tool
    public int multiply(int a, int b) {
        return a * b;
    }
}
// 2、
        // 从类扫描(推荐)
        List<ToolSpecification> specs = ToolSpecifications.toolSpecificationsFrom(MathTools.class);
        
        // 从对象扫描
        // List<ToolSpecification> specs = ToolSpecifications.toolSpecificationsFrom(new MathTools());

3)方式三@Tool + @P + @ToolMemoryId

  • 特点:最简洁
  • 适用场景:适用大部分场景,是开发最常用的工具函数定义方式
  • 注解参数说明
    • @Tool:定义工具/函数
      • name:名称,要具体
      • value:描述,要清晰
    • @P:对工具/方法的参数进行说明
      • value :参数说明。必填项。
      • required :参数是否为必填项,默认为 true 。可选字段。
    • @ToolMemoryId:如果 AI Service 方法中有使用 @MemoryId 注解的参数,那么你也可以在 @Tool 方法的参数上使用 @ToolMemoryId。这样,AI Service 方法提供的值会自动传递给 @Tool 方法。这在多用户/多会话记忆的场景中非常有用,可以区分不同用户或对话
public class WeatherTool {

    /**
     * @Tool 标记工具
     * name:工具名
     * description:工具描述(给AI看)
     * @P 对参数进行说明(必填)
     * @ToolMemoryId 绑定记忆ID
     */
    @Tool(
        name = "getWeather",
        description = "获取指定城市的实时天气"
    )
    public String getWeather(
            // 参数说明
            @P(value = "要查询的城市名称,例如:北京", required = true) String city,
            
            // 记忆ID(多会话隔离)
            @ToolMemoryId String memoryId
    ) {
        return city + ":晴天,25℃";
    }
}

工具函数的执行

工具函数是给 LLM 用的,不是给人用的,所以通常而言工具函数无需我们手动去调用,只需要申明给 LLM,LLM 在处理问题时,发现这个问题可能需要用到某一个工具函数,就会主动去调用。

这就好比一个人去上班,老板给他提供一个工具箱,他在干活,干活过程中遇到一个问题,比如需要拧掉十字螺丝,这时候他就会去找十字螺丝刀,而不是要老板强迫(命令)他用某一个工具

  • 方式 一可手动可自动

    下面我们演示一下如何手动执行

            // 定义参数
            Map<String, Object> params = new HashMap<>();
            params.put("a", 10);
            params.put("b", 20);
            // 手动调用工具函数,需要指定工具名和参数
            Object result = executor.execute("add", params);
     		// 查看工具函数执行结果
            System.out.println("手动执行结果:" + result);
    
  • 方式二、三:AI 自动执行

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐