【LangChain4j 02】【AI Services】
本文介绍了LangChain4j框架中AI Services的高级API使用方法。主要内容包括: 两种构建AI Service的方式:通过AiServices建造者模式和@AiService注解模式 使用SystemMessage的三种方法:构造器模式、@SystemMessage注解以及@UserMessage注解 多模态功能的实现,展示如何通过@UserMessage注解接收不同类型的Cont
文章目录
一、前言
本系列仅做个人笔记使用,绝大部分内容基于 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 构造器模式
-
创建一个 ChatAssistant 接口
public interface ChatAssistant { String chat(String userMessage); } -
通过 AiServices 反射创建示例并注入到容器中
@Bean public ChatAssistant chatAssistant(ChatModel chatModel) { return AiServices.builder(ChatAssistant.class) .chatModel(chatModel) .build(); } -
容器中可注入直接使用
@Resource
private ChatAssistant chatAssistant;
@Override
public String chat(String userMessage) {
return chatAssistant.chat(userMessage);
}
2. @AiService 注解模式
-
langchain4j 提供了 Spring Boot 的集成包,可以直接通过如下方式引入
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> </dependency> -
直接在 ChatAssistant 上添加
@AiService注解,无需通过 AiServices 再进行反射创建,如下:@AiService public interface ChatAssistant { String chat(String userMessage); }
三、进阶使用
1. SystemMessage
与 ChatModel 中的 SystemMessage 作用相同, Ai Services 可以通过如下方式来实现 SystemMessage 功能。
-
AiServices 构造器模式 : 通过
systemMessageProvider提供程序动态定义,可以根据 chatMemoryId 生成不同的 SystemMessageFriendChatAssistant01 friend = AiServices.builder(FriendChatAssistant01.class) .chatModel(model) // TODO : 可以基于不同的 chatMemoryId 提供不同的SystemMessage .systemMessageProvider(chatMemoryId -> "你是一个抬杠大师,你要一直反驳我说的话") .build(); -
@SystemMessage 注解 : 直接在指定方法上添加注解即可。
@AiService public interface FriendChatAssistant01 { @SystemMessage("你是一个抬杠大师,你要一直反驳我说的话") String chat(String userMessage); }在这个示例中,我们添加了带有我们想要使用的系统提示模板的@SystemMessage注解。这将在后台转换为SystemMessage,并与UserMessage一起发送给大语言模型。
@SystemMessage 还可以从资源中加载提示模板:@SystemMessage(fromResource = “my-prompt-template.txt”)
-
@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 中的多模态功能 :通过接受一个或多个 Content 或List<Content>参数即可,如下是一些写法的示例:
-
@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 -
@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:处理完成或错误状态- 通过
onPartialResponseWithContext或onPartialThinkingWithContext回调,可在特定条件下调用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 的方式。
- @AiService 方式 : 直接指定 chatMemoryProvider 属性即可(需要指定
wiringMode为AiServiceWiringMode.EXPLICI)
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
streamingChatModel = "aliQwenStreamingChatModel",
chatMemoryProvider = "chatMemoryProvider")
public interface FriendChatAssistant04 extends ChatMemoryAccess {
@SystemMessage("你是一个抬杠大师,你要一直反驳我说的话。")
Flux<String> chat(String userMessage);
}
- 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/else、switch、循环等逻辑; - 测试优化:因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);
}
四、参考内容
更多推荐


所有评论(0)