 AI Services
AI Services
  # AI Service
到目前为止,我们已经涵盖了如 ChatLanguageModel、ChatMessage、ChatMemory 等底层组件的内容。在这种层次上工作非常灵活,能够让你拥有完全的自由,但也会迫使你编写大量的样板代码。
由于 LLM 驱动的应用程序通常不仅需要单一组件,而是多个组件协同工作(例如,Prompt 模板、对话记忆、LLM、输出解析器、RAG 组件:嵌入模型和存储等),并且常常涉及多次交互,因此对这些组件进行编排变得非常繁琐。
我们希望你专注于业务逻辑,而不是底层实现细节。因此,LangChain4j 提供了两个高级概念来帮助实现这一目标:AI Service和链(Chains)。
# 链(Chains)(遗留概念)
链的概念来源于 Python 的 LangChain(LCEL 引入之前)。其理念是为每个常见用例创建一个 Chain,例如聊天机器人、RAG 等。链将多个底层组件组合在一起,并编排它们之间的交互。
主要问题在于,如果你需要定制某些内容,这种方式会显得过于僵化。LangChain4j 仅实现了两个链(ConversationalChain 和 ConversationalRetrievalChain),目前没有计划增加更多链。
# AI Service
我们提出了另一种名为 AI Service的解决方案,专为 Java 设计。其理念是将与 LLM 和其他组件交互的复杂性隐藏在一个简单的 API 背后。
这种方法与 Spring Data JPA 或 Retrofit 非常相似:你可以声明性地定义一个包含所需 API 的接口,LangChain4j 提供一个对象(代理)来实现该接口。你可以将 AI Service视为应用程序服务层的一个组件,它提供 AI 服务,因此得名。
AI Service处理最常见的操作:
- 格式化 LLM 的输入
- 解析 LLM 的输出
此外,它还支持更高级的功能:
- 对话记忆
- 工具
- RAG
AI Service既可以用于构建支持多轮交互的有状态聊天机器人,也可以用于自动化每次调用 LLM 都是独立的流程。
让我们看看最简单的 AI Service示例,然后再探索更复杂的例子。
# 最简单的 AI Service
首先,我们定义一个包含单一方法 chat 的接口,该方法接受一个 String 类型的输入并返回一个 String 类型的输出。
interface Assistant {
    String chat(String userMessage);
}
2
3
4
接着,我们创建底层组件,这些组件将在 AI Service的底层使用。在本例中,我们只需要 ChatLanguageModel:
ChatLanguageModel model = OpenAiChatModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .modelName(GPT_4_O_MINI)
    .build();
2
3
4
最后,我们使用 AiServices 类来创建我们的 AI Service实例:
Assistant assistant = AiServices.create(Assistant.class, model);
在 Quarkus (opens new window) 和 Spring Boot 应用程序中,自动配置会创建一个 Assistant bean。
这意味着你无需调用 AiServices.create(...),可以直接在需要的地方注入/自动装配 Assistant。
现在我们可以使用 Assistant:
String answer = assistant.chat("Hello");
System.out.println(answer); // Hello, how can I help you?
2
# 它是如何工作的?
你需要向 AiServices 提供你的接口的 Class 以及底层组件,AiServices 会创建一个实现该接口的代理对象。目前,这使用了反射技术,但我们也在考虑其他替代方案。
这个代理对象会处理所有输入和输出的转换。在本例中,输入是一个 String,但我们使用的 ChatLanguageModel 接受的是 ChatMessage。因此,AiService 会自动将其转换为 UserMessage 并调用 ChatLanguageModel。由于 chat 方法的输出类型是 String,ChatLanguageModel 返回的 AiMessage 会在返回 chat 方法之前转换为 String。
# Quarkus 应用中的 AI Service
LangChain4j Quarkus 扩展 (opens new window) 极大地简化了在 Quarkus 应用中使用 AI Service的过程。
更多信息请参考 此处 (opens new window)。
# Spring Boot 应用中的 AI Service
LangChain4j Spring Boot 启动器 极大地简化了在 Spring Boot 应用中使用 AI Service的过程。
# @SystemMessage
接下来,我们来看一个更复杂的示例。我们将强制 LLM 使用俚语进行回复 😉
这通常是通过在 SystemMessage 中提供指令实现的:
interface Friend {
    @SystemMessage("You are a good friend of mine. Answer using slang.")
    String chat(String userMessage);
}
Friend friend = AiServices.create(Friend.class, model);
String answer = friend.chat("Hello"); // Hey! What's up?
2
3
4
5
6
7
8
9
在此示例中,我们添加了 @SystemMessage 注解以及我们想要使用的系统提示模板。这将在后台转换为 SystemMessage 并与 UserMessage 一起发送到 LLM。
@SystemMessage 还可以从资源中加载提示模板:@SystemMessage(fromResource = "my-prompt-template.txt")
# 系统消息提供器
系统消息也可以通过系统消息提供器动态定义:
Friend friend = AiServices.builder(Friend.class)
    .chatLanguageModel(model)
    .systemMessageProvider(chatMemoryId -> "You are a good friend of mine. Answer using slang.")
    .build();
2
3
4
正如你所见,你可以基于聊天记忆 ID(用户或对话)提供不同的系统消息。
# @UserMessage
现在,假设我们使用的模型不支持系统消息,或者我们只是想使用 @UserMessage 来实现此目的。
interface Friend {
    @UserMessage("You are a good friend of mine. Answer using slang. {{it}}")
    String chat(String userMessage);
}
Friend friend = AiServices.create(Friend.class, model);
String answer = friend.chat("Hello"); // Hey! What's shakin'?
2
3
4
5
6
7
8
9
我们用 @UserMessage 注解替换了 @SystemMessage,并指定了一个包含变量 it 的提示模板,该变量引用了唯一的方法参数。
也可以为 String userMessage 添加 @V 注解,并为提示模板变量分配一个自定义名称:
interface Friend {
    @UserMessage("You are a good friend of mine. Answer using slang. {{message}}")
    String chat(@V("message") String userMessage);
}
2
3
4
5
请注意,使用 @V 并非在使用 LangChain4j 和 Quarkus 或 Spring Boot 时的必要操作。
仅当在 Java 编译时未启用 -parameters 选项时才需要此注解。
@UserMessage 还可以从资源中加载提示模板:@UserMessage(fromResource = "my-prompt-template.txt")
# 有效的 AI Service方法示例
以下是一些有效的 AI Service方法示例。
String chat(String userMessage);
String chat(@UserMessage String userMessage);
String chat(@UserMessage String userMessage, @V("country") String country); // userMessage 包含 "{{country}}" 模板变量
@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); // 仅在 Quarkus 和 Spring Boot 应用中有效
@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 包含 "{{country}}" 模板变量
@SystemMessage("Given a name of a country, {{answerInstructions}}")
String chat(@V("answerInstructions") String answerInstructions, @UserMessage String userMessage, @V("country") String country); // userMessage 包含 "{{country}}" 模板变量
@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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 多模态
AI Service目前不支持多模态,请使用低级 API。
# 结构化输出
更多关于结构化输出的信息,请参考此处。
如果您希望从 LLM 接收结构化输出,可以将 AI Service方法的返回类型从 String 更改为其他类型。目前,AI Service支持以下返回类型:
- String
- AiMessage
- 自定义 POJO
- Enum或- List<Enum>或- Set<Enum>,适用于文本分类(例如情感、用户意图等)
- boolean/- Boolean,适用于“是”或“否”问题
- byte/- short/- int/- BigInteger/- long/- float/- double/- BigDecimal
- Date/- LocalDate/- LocalTime/- LocalDateTime
- List<String>/- Set<String>,适用于列表形式的回答
- Map<K, V>
- Result<T>,如果需要访问- TokenUsage、- FinishReason、来源(在 RAG 中检索到的- Content)以及执行的工具,除此之外还有- T,可以是上述任意类型。例如:- Result<String>,- Result<MyCustomPojo>
除 String、AiMessage 和 Map<K, V> 外,AI Service会自动在 UserMessage 的末尾追加指示 LLM 应如何响应的说明。返回方法之前,AI Service会将 LLM 的输出解析为所需类型。
启用日志记录可以观察追加的说明。
某些 LLM 提供商(例如 OpenAI 和 Google Gemini)允许指定 JSON schema 作为所需输出的格式。如果支持并启用了此功能,将不会在 UserMessage 末尾追加自由格式的文本说明。此时,JSON schema 会根据您的 POJO 自动生成并传递给 LLM,从而确保其遵循该 JSON schema。
让我们看几个例子。
# 返回类型为 boolean
 interface SentimentAnalyzer {
    @UserMessage("Does {{it}} has a positive sentiment?")
    boolean isPositive(String text);
}
SentimentAnalyzer sentimentAnalyzer = AiServices.create(SentimentAnalyzer.class, model);
boolean positive = sentimentAnalyzer.isPositive("It's wonderful!");
// true
2
3
4
5
6
7
8
9
10
11
# 返回类型为 Enum
 enum Priority {
    
    @Description("Critical issues such as payment gateway failures or security breaches.")
    CRITICAL,
    
    @Description("High-priority issues like major feature malfunctions or widespread outages.")
    HIGH,
    
    @Description("Low-priority issues such as minor bugs or cosmetic problems.")
    LOW
}
interface PriorityAnalyzer {
    
    @UserMessage("Analyze the priority of the following issue: {{it}}")
    Priority analyzePriority(String issueDescription);
}
PriorityAnalyzer priorityAnalyzer = AiServices.create(PriorityAnalyzer.class, model);
Priority priority = priorityAnalyzer.analyzePriority("The main payment gateway is down, and customers cannot process transactions.");
// CRITICAL
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Description 注解是可选的。当枚举名称不够直观时,建议使用此注解。
# POJO 作为返回类型
class Person {
    @Description("个人的名字") // 可以添加可选描述,帮助 LLM 更好地理解
    String firstName;
    String lastName;
    LocalDate birthDate;
    Address address;
}
@Description("地址") // 可以添加可选描述,帮助 LLM 更好地理解
class Address {
    String street;
    Integer streetNumber;
    String city;
}
interface PersonExtractor {
    @UserMessage("从 {{it}} 提取个人信息")
    Person extractPersonFrom(String text);
}
PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, model);
String text = """
            1968年,在独立日逐渐消逝的回声中,
            一个名叫 John 的孩子在宁静的夜空下出生。
            这个新生儿以 Doe 为姓,开启了一段新的人生旅程。
            他诞生于 345 Whispering Pines 大道,
            一条位于 Springfield 心脏地带的安静街道,
            这里回荡着郊区梦想与憧憬的温柔旋律。
            """;
Person person = personExtractor.extractPersonFrom(text);
System.out.println(person); // Person { firstName = "John", lastName = "Doe", birthDate = 1968-07-04, address = Address { ... } }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# JSON 模式
在提取自定义 POJO(实际上是 JSON,随后会被解析为 POJO)时,建议在模型配置中启用“JSON 模式”。 这样,LLM 会被强制返回有效的 JSON。
请注意,JSON 模式和工具/函数调用是类似的功能,但它们的 API 不同且适用于不同的场景。
JSON 模式适用于当你 始终 需要 LLM 以结构化格式(有效的 JSON)响应的场景。 此外,这种模式通常不需要状态/记忆,因此与 LLM 的每次交互都是独立的。 例如,你可能希望从文本中提取信息,如提及的人员列表, 或者将自由格式的产品评论转换为包含字段的结构化形式: String productName、Sentiment sentiment、List<String> claimedProblems 等。
另一方面,当需要 LLM 执行某些操作时(例如查询数据库、搜索网页、取消用户预订等), 工具/函数调用是更合适的选择。 在这种情况下,会向 LLM 提供一组工具及其预期的 JSON 模式,LLM 自主决定是否调用它们以满足用户请求。
以前,函数调用经常用于结构化数据提取, 但现在有了更适合此目的的 JSON 模式功能。
以下是启用 JSON 模式的方法:
- 对于 OpenAI: - 对于支持 结构化输出的新模型(例如 - gpt-4o-mini、- gpt-4o-2024-08-06):
 - OpenAiChatModel.builder() 
 ...
 .responseFormat("json_schema")
 .strictJsonSchema(true)
 .build();- 更多细节参见 此处。 - 对于旧模型(例如 gpt-3.5-turbo、gpt-4): ```java OpenAiChatModel.builder() ... .responseFormat("json_object") .build();1
 2
 3
 4
 5
 6
 7
 8
 9
 10
- 对于 Azure OpenAI: 
AzureOpenAiChatModel.builder()
    ...
    .responseFormat(new ChatCompletionsJsonResponseFormat())
    .build();
2
3
4
- 对于 Vertex AI Gemini:
VertexAiGeminiChatModel.builder()
    ...
    .responseMimeType("application/json")
    .build();
2
3
4
或者通过从 Java 类中指定显式模式:
VertexAiGeminiChatModel.builder()
    ...
    .responseSchema(SchemaHelper.fromClass(Person.class))
    .build();
2
3
4
从 JSON 模式中指定:
VertexAiGeminiChatModel.builder()
    ...
    .responseSchema(Schema.builder()...build())
    .build();
2
3
4
- 对于 Google AI Gemini:
GoogleAiGeminiChatModel.builder()
    ...
    .responseFormat(ResponseFormat.JSON)
    .build();
2
3
4
或者通过从 Java 类中指定显式模式:
GoogleAiGeminiChatModel.builder()
    ...
    .responseFormat(ResponseFormat.builder()
        .type(JSON)
        .jsonSchema(JsonSchemas.jsonSchemaFrom(Person.class).get())
        .build())
    .build();
2
3
4
5
6
7
从 JSON 模式中指定:
GoogleAiGeminiChatModel.builder()
    ...
    .responseFormat(ResponseFormat.builder()
        .type(JSON)
        .jsonSchema(JsonSchema.builder()...build())
        .build())
    .build();
2
3
4
5
6
7
- 对于 Mistral AI:
MistralAiChatModel.builder()
    ...
    .responseFormat(MistralAiResponseFormatType.JSON_OBJECT)
    .build();
2
3
4
- 对于 Ollama:
OllamaChatModel.builder()
    ...
    .responseFormat(JSON)
    .build();
2
3
4
- 对于其他模型提供商:如果底层模型提供商不支持 JSON 模式, 可以尝试通过提示工程实现。此外,尝试降低 temperature参数以增加确定性。
# 流式响应
在使用 TokenStream 返回类型时,AI Service可以逐字流式响应:
interface Assistant {
    TokenStream chat(String message);
}
StreamingChatLanguageModel model = OpenAiStreamingChatModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .modelName(GPT_4_O_MINI)
    .build();
Assistant assistant = AiServices.create(Assistant.class, model);
TokenStream tokenStream = assistant.chat("Tell me a joke");
tokenStream.onNext((String token) -> System.out.println(token))
    .onRetrieved((List<Content> contents) -> System.out.println(contents))
    .onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
    .onComplete((Response<AiMessage> response) -> System.out.println(response))
    .onError((Throwable error) -> error.printStackTrace())
    .start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Flux
你也可以使用 Flux<String> 替代 TokenStream。
为此,请引入 langchain4j-reactor 模块:
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-reactor</artifactId>
    <version>1.0.0-alpha1</version>
</dependency>
interface Assistant {
  Flux<String> chat(String message);
}
2
3
4
5
6
7
8
9
# 会话记忆
AI Service可以使用会话记忆来“记住”先前的交互:
Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(model)
    .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
    .build();
2
3
4
在这种情况下,相同的 ChatMemory 实例将用于 AI Service的所有调用。 但是,如果有多个用户,这种方法将无法使用, 因为每个用户需要自己的 ChatMemory 实例来维护他们的独立会话。
解决此问题的方法是使用 ChatMemoryProvider:
interface Assistant  {
    String chat(@MemoryId int memoryId, @UserMessage String message);
}
Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(model)
    .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
    .build();
String answerToKlaus = assistant.chat(1, "Hello, my name is Klaus");
String answerToFrancine = assistant.chat(2, "Hello, my name is Francine");
2
3
4
5
6
7
8
9
10
11
在这种情况下,由 ChatMemoryProvider 提供的两个不同的 ChatMemory 实例将分别用于每个 memoryId。
请注意,如果 AI Service方法没有带有 @MemoryId 注解的参数, 则 ChatMemoryProvider 中 memoryId 的值默认为字符串 "default"。
请注意,不应为相同的 @MemoryId 并发调用 AI Service, 因为这可能会导致 ChatMemory 损坏。 目前,AI Service未实现任何机制来防止相同 @MemoryId 的并发调用。
- 单一 ChatMemory 示例 (opens new window)
- 为每个用户提供 ChatMemory 示例 (opens new window)
- 单一持久化 ChatMemory 示例 (opens new window)
- 为每个用户提供持久化 ChatMemory 示例 (opens new window)
# 工具(函数调用)
AI Service可以配置工具,以便 LLM 使用它们:
class Tools {
    
    @Tool
    int add(int a, int b) {
        return a + b;
    }
    @Tool
    int multiply(int a, int b) {
        return a * b;
    }
}
Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(model)
    .tools(new Tools())
    .build();
String answer = assistant.chat("What is 1+2 and 3*4?");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在这种情况下,LLM 会请求执行 add(1, 2) 和 multiply(3, 4) 方法, 然后再提供最终答案。 LangChain4j 将自动执行这些方法。
关于工具的更多详情,请参见这里。
# RAG
AI Service可以通过配置 ContentRetriever 启用简单 RAG功能:
EmbeddingStore embeddingStore = ...
EmbeddingModel embeddingModel = ...
ContentRetriever contentRetriever = new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(model)
    .contentRetriever(contentRetriever)
    .build();
2
3
4
5
6
7
8
9
通过配置 RetrievalAugmentor 可以提供更多的灵活性,从而实现高级 RAG功能,如查询转换、重排序等:
RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
        .queryTransformer(...)
        .queryRouter(...)
        .contentAggregator(...)
        .contentInjector(...)
        .executor(...)
        .build();
Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(model)
    .retrievalAugmentor(retrievalAugmentor)
    .build();
2
3
4
5
6
7
8
9
10
11
12
更多关于 RAG 的细节可以参考这里。
更多 RAG 示例可以参考这里 (opens new window)。
# 自动化内容审核 (Auto-Moderation)
# 多 AI Service的链式调用
随着基于 LLM 的应用逻辑复杂性增加,将其分解为更小的部分至关重要,这与软件开发的常见实践一致。
例如,将大量指令塞入系统提示以覆盖所有可能的场景可能导致错误和效率低下。如果指令过多,LLM 可能会忽略某些指令。此外,指令的呈现顺序也会影响效果,增加了操作难度。
这一原则同样适用于工具、RAG,以及模型参数(如 temperature 和 maxTokens)。
# 分解的好处:
- 易于开发、测试、维护和理解。
- 提高成本效率和响应速度。
以下是一些实现方式:
- 将一个 AI Service调用另一个(即链式调用)。
- 使用确定性和 LLM 驱动的条件语句(如 if/else或switch)。
- 使用确定性和 LLM 驱动的循环(如 for/while)。
- 在单元测试中模拟 AI Service。
- 独立进行 AI Service的集成测试。
- 单独优化每个 AI Service的参数。
# 示例
构建一个公司的聊天机器人,根据场景调用不同服务:
interface GreetingExpert {
    @UserMessage("Is the following text a greeting? Text: {{it}}")
    boolean isGreeting(String text);
}
interface ChatBot {
    @SystemMessage("You are a polite chatbot of a company called Miles of Smiles.")
    String reply(String userMessage);
}
class MilesOfSmiles {
    private final GreetingExpert greetingExpert;
    private final ChatBot chatBot;
    public String handle(String userMessage) {
        if (greetingExpert.isGreeting(userMessage)) {
            return "Greetings from Miles of Smiles! How can I make your day better?";
        } else {
            return chatBot.reply(userMessage);
        }
    }
}
GreetingExpert greetingExpert = AiServices.create(GreetingExpert.class, llama2);
ChatBot chatBot = AiServices.builder(ChatBot.class)
    .chatLanguageModel(gpt4)
    .contentRetriever(milesOfSmilesContentRetriever)
    .build();
MilesOfSmiles milesOfSmiles = new MilesOfSmiles(greetingExpert, chatBot);
String greeting = milesOfSmiles.handle("Hello");
System.out.println(greeting); // Greetings from Miles of Smiles! How can I make your day better?
String answer = milesOfSmiles.handle("Which services do you provide?");
System.out.println(answer); // At Miles of Smiles, we provide a wide range of services ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
此示例展示了:
- 使用低成本模型(如 Llama2)处理简单任务。
- 使用高成本模型(如 GPT-4)处理复杂任务。
通过这种分解,可以:
- 独立测试各部分。
- 优化每个子任务的参数。
- 长期内为特定任务微调小型模型。
