一、前言

本系列仅做个人笔记使用,绝大部分内容基于 LangChain4j 官网 ,内容个人做了一定修改,可能存在错漏,一切以官网为准。


本系列使用 LangChain4j 版本:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-bom</artifactId>
            <version>1.8.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

本系列完整代码地址 :langchain4j-hwl


【LangChain4j 01】【基本使用】 中我们介绍了low-level API(以ChatModel为核心)和 high-level API(以AI Services为代表)的概念。本篇来介绍 high-level API

LangChain4j 中目前有两个 high-level API 可以帮助实现这一点:AI Services 和 Chains。Chains 官方已经不推荐使用,这里不再介绍。

二、AI Services

AI Services 采用与 Spring Data JPA、Retrofit 一致的 “声明式接口 + 框架代理” 模式,开发者仅需定义接口(比如声明 “接收用户消息并返回回答” 的方法),无需写接口的实现类。LangChain4j 会自动生成代理对象,这个代理会暗中处理底层工作(如将用户文本转为 LLM 能识别的格式、调用 LLM、解析返回结果等),开发者直接用代理对象调用接口方法即可。

AI Service 的构造有两种方式 :

  • 通过 AiServices 建造者模式构建。
  • 在 Spring 容器中通过 @AiService 注解构建。

下面我们具体来看

1. AiServices 构造器模式

  1. 创建一个 ChatAssistant 接口

    public interface ChatAssistant {
        String chat(String userMessage);
    }
    
  2. 通过 AiServices 反射创建示例并注入到容器中

        @Bean
        public ChatAssistant chatAssistant(ChatModel chatModel) {
            return AiServices.builder(ChatAssistant.class)
                    .chatModel(chatModel)
                    .build();
        }
    
  3. 容器中可注入直接使用

    @Resource
    private ChatAssistant chatAssistant;

    @Override
    public String chat(String userMessage) {
        return chatAssistant.chat(userMessage);
    }

2. @AiService 注解模式

  1. langchain4j 提供了 Spring Boot 的集成包,可以直接通过如下方式引入

       <dependency>
           <groupId>dev.langchain4j</groupId>
           <artifactId>langchain4j-spring-boot-starter</artifactId>
       </dependency>
    
  2. 直接在 ChatAssistant 上添加 @AiService 注解,无需通过 AiServices 再进行反射创建,如下:

    @AiService
    public interface ChatAssistant {
        String chat(String userMessage);
    }
    

三、进阶使用

1. SystemMessage

与 ChatModel 中的 SystemMessage 作用相同, Ai Services 可以通过如下方式来实现 SystemMessage 功能。

  1. AiServices 构造器模式 : 通过 systemMessageProvider 提供程序动态定义,可以根据 chatMemoryId 生成不同的 SystemMessage

    FriendChatAssistant01 friend = AiServices.builder(FriendChatAssistant01.class)
        .chatModel(model)
        // TODO : 可以基于不同的 chatMemoryId  提供不同的SystemMessage
        .systemMessageProvider(chatMemoryId -> "你是一个抬杠大师,你要一直反驳我说的话")
        .build();
    
  2. @SystemMessage 注解 : 直接在指定方法上添加注解即可。

    @AiService
    public interface FriendChatAssistant01 {
        @SystemMessage("你是一个抬杠大师,你要一直反驳我说的话")
        String chat(String userMessage);
    }
    

    在这个示例中,我们添加了带有我们想要使用的系统提示模板的@SystemMessage注解。这将在后台转换为SystemMessage,并与UserMessage一起发送给大语言模型。

    @SystemMessage 还可以从资源中加载提示模板:@SystemMessage(fromResource = “my-prompt-template.txt”)

  3. @UserMessage 注解 :当 LLM 不支持 SystemMessage(部分旧模型),或需将指令与用户输入结合时,用此注解将指令作为用户输入的一部分传递。

    @AiService
    public interface FriendChatAssistant02 {
        @UserMessage("你是一个抬杠大师,你要一直反驳我说的话。{{userMessage}}")
        String chat(@V("userMessage") String userMessage);
    }
    

    这里需要注意:

    • 可以使用 @V 为变量取别名。
    • 当使用 @UserMessage 替代 @SystemMessage 时,提示模板中的{{it}}会自动引用方法的唯一参数,简化了单参数场景的模板编写。
    • @UserMessage 也可以从资源中加载提示模板:@UserMessage(fromResource = "my-prompt-template.txt")

2. 多模态

AI Service 也可以实现 ChatModel 中的多模态功能 :通过接受一个或多个 ContentList<Content>参数即可,如下是一些写法的示例:

  1. @UserMessage 写法示例

    String chat(String userMessage);
    
    String chat(@UserMessage String userMessage);
    
    String chat(@UserMessage String userMessage, @V("country") String country); // userMessage contains "{{country}}" template variable
    
    String chat(@UserMessage String userMessage, @UserMessage Content content); // content can be one of: TextContent, ImageContent, AudioContent, VideoContent, PdfFileContent
    
    String chat(@UserMessage String userMessage, @UserMessage ImageContent image); // second argument can be one of: TextContent, ImageContent, AudioContent, VideoContent, PdfFileContent
    
    String chat(@UserMessage String userMessage, @UserMessage List<Content> contents);
    
    String chat(@UserMessage String userMessage, @UserMessage List<ImageContent> images);
    
    @UserMessage("What is the capital of Germany?")
    String chat();
    
    @UserMessage("What is the capital of {{it}}?")
    String chat(String country);
    
    @UserMessage("What is the capital of {{country}}?")
    String chat(@V("country") String country);
    
    @UserMessage("What is the {{something}} of {{country}}?")
    String chat(@V("something") String something, @V("country") String country);
    
    @UserMessage("What is the capital of {{country}}?")
    String chat(String country); // this works only in Quarkus and Spring Boot applications
    
  2. @SystemMessage 和 @UserMessage 写法示例

    @SystemMessage("Given a name of a country, answer with a name of it's capital")
    String chat(String userMessage);
    
    @SystemMessage("Given a name of a country, answer with a name of it's capital")
    String chat(@UserMessage String userMessage);
    
    @SystemMessage("Given a name of a country, {{answerInstructions}}")
    String chat(@V("answerInstructions") String answerInstructions, @UserMessage String userMessage);
    
    @SystemMessage("Given a name of a country, answer with a name of it's capital")
    String chat(@UserMessage String userMessage, @V("country") String country); // userMessage contains "{{country}}" template variable
    
    @SystemMessage("Given a name of a country, {{answerInstructions}}")
    String chat(@V("answerInstructions") String answerInstructions, @UserMessage String userMessage, @V("country") String country); // userMessage contains "{{country}}" template variable
    
    @SystemMessage("Given a name of a country, answer with a name of it's capital")
    @UserMessage("Germany")
    String chat();
    
    @SystemMessage("Given a name of a country, {{answerInstructions}}")
    @UserMessage("Germany")
    String chat(@V("answerInstructions") String answerInstructions);
    
    @SystemMessage("Given a name of a country, answer with a name of it's capital")
    @UserMessage("{{it}}")
    String chat(String country);
    
    @SystemMessage("Given a name of a country, answer with a name of it's capital")
    @UserMessage("{{country}}")
    String chat(@V("country") String country);
    
    @SystemMessage("Given a name of a country, {{answerInstructions}}")
    @UserMessage("{{country}}")
    String chat(@V("answerInstructions") String answerInstructions, @V("country") String country);
    

3. Return Types

Ai Services 提供了两种返回类型 :

  • String类型:最直接的返回方式,大语言模型(LLM)生成的原始文本会“无处理、无解析”地直接返回,适合只需原始文本输出的场景(如简单对话回复)。

  • 结构化输出:若需要规整的输出格式(而非纯文本),可指定结构化类型(如boolean、枚举Enum、自定义POJO等)。此时AI服务会自动将LLM的文本输出解析为目标类型(例如把“这是正面情绪”解析为boolean true),减少手动处理成本。

3.1 String类型

String类型返回的定义只需要将 AI Service 的返回类型定义为 String 即可。

@AiService
public interface ChatAssistant {
    String chat(String userMessage);
}

3.2 结构化输出

结构化输出的内容可以参考 【LangChain4j 06】【Structured Outputs】 一文。

3.3 增强返回类型

在此之上,AI Services 还提供了一种增强返回类型 :用 dev.langchain4j.service.Result<T> 包装以获取元数据。
若需要了解AI服务调用的“附加信息”(而非仅关注输出结果),可将上述任意基础类型包装进Result<T>T为基础类型)。通过Result<T>能获取5类关键元数据:

  • TokenUsage:调用过程中消耗的令牌总数(多次调用LLM时会自动求和,便于成本控制);
  • Sources:若使用了RAG(检索增强生成),会返回检索到的相关内容(可追溯回答依据);
  • Tool 执行记录:调用期间执行的所有工具(含工具请求参数和执行结果);
  • FinishReason:LLM最终回复的“结束原因”(如正常完成、达到令牌上限等);
  • 中间ChatResponse:调用过程中产生的所有中间响应(便于调试或跟踪流程)

简单示例如下:

@AiService
public interface FriendChatAssistant03 {
    @UserMessage("你是一个抬杠大师,你要一直反驳我说的话。{{userMessage}}")
    Result<List<String>> chat(@V("userMessage") String userMessage);
}
...

    @Override
    public List<String> chatWithResult(String userMessage) {
        Result<List<String>> result = friendChatAssistant03.chat(userMessage);
        // 获取额外的属性信息
        List<String> outline = result.content();
        TokenUsage tokenUsage = result.tokenUsage();
        List<Content> sources = result.sources();
        List<ToolExecution> toolExecutions = result.toolExecutions();
        FinishReason finishReason = result.finishReason();
        return outline;
    }

4. 流式响应

AI Service 中流式响应基础实现有下面几种方式 :

  • 通过定义返回类型为TokenStream的接口方法,实现逐AI服务的逐令牌流式输出
  • 使用Flux构建支持流式传输的聊天模型

简单示例如下:

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
		// 指定流式传输模型
        streamingChatModel = "aliQwenStreamingChatModel")
public interface FriendChatAssistant04 extends ChatMemoryAccess {
	// 通过 Flux 直接接收流式响应
    @SystemMessage("你是一个抬杠大师,你要一直反驳我说的话。")
    Flux<String> chat01(String userMessage);
	
	// 通过 TokenStream 来接收流式响应
    @SystemMessage("你是一个抬杠大师,你要一直反驳我说的话。")
    TokenStream chat02(String userMessage);
}

...

    @Override
    public Flux<String> chatWithStreaming01(String userMessage) {
        return friendChatAssistant04.chat01(userMessage);
    }

    @Override
    public Flux<String> chatWithStreaming02(String userMessage) {
        TokenStream chat = friendChatAssistant04.chat02(userMessage);
        return Flux.create(fluxSink ->
                chat.onPartialResponse(fluxSink::next)
                        .onCompleteResponse(chatResponse -> fluxSink.complete())
                        .onError(fluxSink::error)
                        .start());
    }

需要注意 :如果需要返回 Flux<String> 则需要先引入 langchain4j-reactor,如下:

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-reactor</artifactId>
</dependency>

在上面的示例中,使用 TokenStream 的方式接收流式响应时,我们可以实现 TokenStream 中的众多回调方法,各个方法的作用如下:

  • onPartialResponse:处理部分响应内容(逐段输出)
  • onPartialThinking:处理思考过程信息
  • onRetrieved:处理检索到的内容
  • beforeToolExecution/onToolExecuted:处理工具调用前后的事件
  • onCompleteResponse/onError:处理完成或错误状态
  • 通过onPartialResponseWithContextonPartialThinkingWithContext回调,可在特定条件下调用StreamingHandle.cancel()取消流传输,取消后会关闭连接,不再接收后续回调。如下:
     tokenStream
         .onPartialResponseWithContext((PartialResponse partialResponse, PartialResponseContext context) -> {
             process(partialResponse);
             if (shouldCancel()) {
                 context.streamingHandle().cancel();
             }
         })
         .onCompleteResponse((ChatResponse response) -> futureResponse.complete(response))
         .onError((Throwable error) -> futureResponse.completeExceptionally(error))
         .start();
    

5. Chat Memory

这里简单介绍使用 AiService 方式使用 ChatMemory 的方式。

  1. @AiService 方式 : 直接指定 chatMemoryProvider 属性即可(需要指定 wiringModeAiServiceWiringMode.EXPLICI
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        streamingChatModel = "aliQwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProvider")
public interface FriendChatAssistant04 extends ChatMemoryAccess {
    @SystemMessage("你是一个抬杠大师,你要一直反驳我说的话。")
    Flux<String> chat(String userMessage);
}
  1. AiServices#builder 方式
    @Bean
    public FriendChatAssistant04 friendChatAssistant04(ChatMemoryProvider chatMemoryProvider) {
        return AiServices.builder(FriendChatAssistant04.class)
                .chatModel(openAiChatModel())
                .chatMemoryProvider(chatMemoryProvider::get)
                .build();
    }

6. Tools

详参【LangChain4j 04】【Tools (Function Calling)】

7. RAG

详参【LangChain4j 05】【RAG】

8. Auto-Moderation(自动审核)

Auto-Moderation(自动审核)的作用是让AI服务在处理用户输入(或生成输出)时,自动检测不当内容(如违规、不适当文本),避免违规内容流转,减少人工审核成本。

当AI服务检测到不当内容时,不会正常返回结果,而是主动抛出ModerationException(审核异常),通过“异常抛出”的方式中断常规流程,提醒开发者处理违规场景。

ModerationException中包含的Moderation对象是关键——它会记录被标记内容的具体信息(比如哪段文本触发了审核、违规类型等),方便开发者定位问题、后续追溯或进一步处理(如提示用户修改内容)。


简单示例如下:

    @Bean
    public ModerationAssistant moderationAssistant() {
        OpenAiModerationModel moderationModel = OpenAiModerationModel.builder()
                .apiKey("demo")
                .modelName(TEXT_MODERATION_LATEST)
                .build();

        return AiServices.builder(ModerationAssistant.class)
                .chatModel(openAiChatModel())
                .moderationModel(moderationModel)
                .build();
    }

9. ChatRequest 重写

ChatRequest 是传递给 LLM 的请求体(包含用户消息、系统消息等核心内容)。在实际场景中,常需根据外部条件调整它——比如给用户提问附加“当前用户所在地区”“历史对话中的关键信息”等上下文,或动态修改系统消息(如对新用户用引导式系统提示,对老用户用简洁式提示),从而让 LLM 生成更精准的响应。


可以通过 AiServices#chatRequestTransformer 的两个重载方法来完成 ChatRequest 重写,简单示例如下:

    @Bean
    public RewritingChatAssistant rewritingChatAssistant() {
        return AiServices.builder(RewritingChatAssistant.class)
                .chatModel(openAiChatModel())
                // 编辑重写逻辑
                .chatRequestTransformer((chatRequest, memoryId) -> {
                    log.info("memoryId: {}, chatRequest: {}", memoryId, chatRequest);
                    // 添加额外信息
                    List<ChatMessage> messages = new ArrayList<>(List.of(UserMessage.from("张三跟王五是亲兄弟")));
                    messages.addAll(chatRequest.messages());
                    return ChatRequest.builder()
                            .messages(messages).build();
                })
                .build();
    }

10. Chaining multiple AI Services

Chaining multiple AI Services 即 将多个AI服务串联起来。

LLM应用逻辑越复杂,越要像传统软件开发(如模块化设计)那样拆分为小型组件。这是因为“大一统”的设计会带来多重问题,而拆解能解决这些痛点。

“大一统”设计的3类核心问题:

  • 系统提示(Prompt)层面:若把所有场景的指令都塞进系统提示,LLM可能遗漏部分指令,且指令顺序会影响效果,易出错、效率低;
  • 工具调用层面:聊天机器人无需时刻加载所有工具(如用户仅打招呼时,加载数百个工具会消耗大量Token(成本高),还可能因LLM幻觉/误操作导致意外调用(风险高));
  • RAG与模型参数层面:RAG并非万能——无需上下文时强行提供,会增加Token成本与响应延迟;模型参数(如temperature)需按需调整(需确定性时设低,需创造力时设高),统一参数无法适配所有场景。

拆解的核心优势使得更小组件更易落地与维护

小型、专一的组件能解决“大一统”的痛点,具体优势包括:开发更简单、测试更易(可单独验证)、维护成本更低、逻辑更易理解,同时能针对性优化(如为不同组件匹配最优LLM参数)。


拆解后可根据场景,灵活选择应用的控制逻辑:

  • 极端1:应用主导(高度确定)——LLM仅作为组件之一,如用固定话术回应问候,无需LLM生成;
  • 极端2:LLM主导(高度自主)——让LLM驱动流程,如复杂问题的推理;
  • 混合模式:结合两者,如用LLM判断“是否为问候语”(返回boolean),再决定用固定话术还是LLM生成回答。

LangChain4j的“AI Services”(接口化定义LLM服务)是拆解落地的关键载体,它可与传统软件组件融合,实现:

  • 流程串联:多个AI Services链式调用(如先判断意图,再调用RAG回答);
  • 逻辑控制:用AI Services的返回值(boolean/enum/int)实现if/elseswitch、循环等逻辑;
  • 测试优化:因AI Services是接口,可模拟(Mock)进行单元测试,也可单独做集成测试、单独优化参数。

简单示例如下:

一个三国野史聊天机器人,如果判断是问候会回复固定消息,其他内容则通过 SgChatBot (加载了本地知识库内容)回答。

public interface GreetingExpert {
    /**
     * 是否是问候
     *
     * @param text 输入的文本
     * @return 是否是问候
     */
    @SystemMessage("判断用户输入是否是问候语")
    boolean isGreeting(String text);
}

public interface SgChatBot {

    /**
     * 聊天
     *
     * @param userMessage 用户输入的消息
     * @return 助手回复
     */
    @SystemMessage("你是一个三国野史的聊天机器人,礼貌回答客户问题, 回答消息前先说明自身身份")
    String chat(String userMessage);
}


public class SgChatBotDelegate {

    /**
     * 问候专家
     */
    private final GreetingExpert greetingExpert;

    /**
     * 聊天机器人
     */
    private final SgChatBot chatBot;

    public SgChatBotDelegate(GreetingExpert greetingExpert, SgChatBot chatBot) {
        this.greetingExpert = greetingExpert;
        this.chatBot = chatBot;
    }


    /**
     * 聊天
     *
     * @param userMessage 用户输入的消息
     * @return 助手回复
     */
    public String chat(String userMessage) {
        if (greetingExpert.isGreeting(userMessage)) {
            return "你好!我是三国野史机器人";
        }
        return chatBot.chat(userMessage);
    }


}


...

    @Bean
    public SgChatBotDelegate sgChatBotDelegate(ChatModel chatModel) {
        // TODO : 可以指定更加便宜的模型来进行 GreetingExpert 的构建
        GreetingExpert greetingExpert = AiServices.builder(GreetingExpert.class)
                .chatModel(chatModel)
                .build();
		
        List<Document> documents = ClassPathDocumentLoader.loadDocuments("static/knowledge");
        // 构建一个内存中的向量数据库(EmbeddingStore),用于存储文档的向量表示。
        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
        EmbeddingStoreIngestor.ingest(documents, embeddingStore);

        SgChatBot chatBotAssistant = AiServices.builder(SgChatBot.class)
                .chatModel(chatModel)
//                .systemMessageProvider(o -> "你是一个三国野史的聊天机器人,回答消息前先说明自身身份")
                .contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
                .build();

        return new SgChatBotDelegate(greetingExpert, chatBotAssistant);
    }



四、参考内容

  1. LangChain4j 官网
  2. 豆包
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐