Skip to content
LangChain4j 文档版本 1.22.0 源快照 3900c0e7 更新于 2026-10-14

AI 服务

到目前为止,我们一直在讲低层组件,例如 ChatModel、ChatMessage、ChatMemory 等。在这一层工作非常灵活、给你完全的自由,但也迫使你写大量样板代码。由于由 LLM 驱动的应用通常需要的不只是单个组件,而是多个组件协同工作(例如提示词模板、聊天记忆、LLM、输出解析器、RAG 组件:嵌入模型与存储),且常常涉及多次交互,编排它们全都变得更加繁琐。

我们希望你专注于业务逻辑,而非低层实现细节。因此,LangChain4j 目前有两个高层概念能帮忙:AI 服务与 Chains。

Chains(遗留)

Chains 的概念源自 Python 的 LangChain(在引入 LCEL 之前)。其思路是为每个常见用例(例如聊天机器人、RAG 等)准备一个 Chain。Chains 组合多个低层组件并编排它们之间的交互。它们的主要问题是:若你需要定制某些东西,它们过于僵化。LangChain4j 只实现了两个 Chains(ConversationalChain 与 ConversationalRetrievalChain),且我们目前不打算再加更多。

AI 服务

我们提出另一个名为 AI 服务的方案,专为 Java 定制。其思路是把与 LLM 及其他组件交互的复杂性隐藏在一个简单的 API 之后。

这种方式非常类似于 Spring Data JPA 或 Retrofit:你以声明方式定义一个带期望 API 的接口,LangChain4j 提供一个实现该接口的对象(代理)。你可以把 AI 服务看作你应用中服务层的一个组件。它提供 AI 服务。故名。

AI 服务处理最常见的操作:

  • 为 LLM 格式化输入
  • 解析来自 LLM 的输出

它们还支持更高级的特性:

  • 聊天记忆
  • 工具
  • RAG

AI 服务既可用来构建促进来回交互的、有状态的聊天机器人,也可用来自动化那些每次对 LLM 的调用都相互隔离的流程。

让我们看看最简单的 AI 服务。之后我们再探索更复杂的示例。

最简单的 AI 服务

首先,我们定义一个带单个方法 chat 的接口,它以 String 作为输入、返回 String。

java
interface Assistant {

    String chat(String userMessage);
}

然后,我们创建我们的低层组件。这些组件会在我们 AI 服务的底层被使用。在本例中,我们只需要 ChatModel:

java
ChatModel model = OpenAiChatModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .modelName(GPT_4_O_MINI)
    .build();

最后,我们可以用 AiServices 类创建我们 AI 服务的一个实例:

java
Assistant assistant = AiServices.create(Assistant.class, model);

NOTE

在 Quarkus 与 Spring Boot 应用中,自动配置会处理创建一个 Assistant bean。这意味着你无需调用 AiServices.create(...),只需在需要处简单地注入/自动装配 Assistant。

现在我们可以使用 Assistant:

java
String answer = assistant.chat("Hello");
System.out.println(answer); // Hello, how can I help you?

它是如何工作的?

你把接口的 Class 连同低层组件一起提供给 AiServices,AiServices 会创建一个实现该接口的代理对象。目前它使用反射,但我们也在考虑其他替代方案。这个代理对象处理输入与输出的所有转换。在本例中,输入是单个 String,但我们用的 ChatModel 以 ChatMessage 作为输入。于是 AiService 会自动把它转换为一个 UserMessage 并调用 ChatModel。由于 chat 方法的输出类型是 String,在 ChatModel 返回 AiMessage 之后,它会被转换为一个 String 再从 chat 方法返回。

Quarkus 应用中的 AI 服务

LangChain4j Quarkus 扩展极大地简化了在 Quarkus 应用中使用 AI 服务。

更多信息可在这里找到。

Spring Boot 应用中的 AI 服务

LangChain4j Spring Boot starter极大地简化了在 Spring Boot 应用中使用 AI 服务。

@SystemMessage

现在,让我们看一个更复杂的示例。我们会用俚语 😉 强制 LLM 回复。

这通常通过在 SystemMessage 中提供指令来达成。

java
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?

在本例中,我们添加了一个带我们所要使用的系统提示词模板的 @SystemMessage 注解。它会在幕后被转换为一个 SystemMessage,并连同 UserMessage 一起发送给 LLM。

@SystemMessage 也能从资源加载一个提示词模板:@SystemMessage(fromResource = "my-prompt-template.txt")

@SystemMessage 也可以在 AI 服务接口上声明,那种情况下它适用于该 AI 服务暴露的所有方法,包括继承而来的方法:

java
@SystemMessage("You are a good friend of mine. Answer using slang.")
interface Friend {

    String chat(String userMessage);

    String chatAgain(String userMessage);
}

在方法上声明的 @SystemMessage 优先于在接口上声明的那个。仅在一个父接口上声明的 @SystemMessage 不会被子 AI 服务接口继承。

系统消息提供者

系统消息也可以用系统消息提供者动态定义:

java
Friend friend = AiServices.builder(Friend.class)
    .chatModel(model)
    .systemMessageProvider(chatMemoryId -> "You are a good friend of mine. Answer using slang.")
    .build();

如你所见,你可以基于一个聊天记忆 ID(用户或会话)提供不同的系统消息。

系统消息转换器

系统消息转换器让你能在每次调用时动态修改系统消息,时机是在它从 @SystemMessage 或 systemMessageProvider 解析之后、但在 chatRequestTransformer 运行之前。当你需要无论系统消息原本如何配置都要向其追加或前置内容时,这很有用。

java
Friend friend = AiServices.builder(Friend.class)
    .chatModel(model)
    .systemMessageProvider(chatMemoryId -> "You are a good friend of mine. Answer using slang.")
    .systemMessageTransformer(systemMessage -> systemMessage + " Today's date is " + LocalDate.now() + ".")
    .build();

若没有配置任何系统消息,转换器会接收到 null。

当你还需要访问调用上下文(例如方法名或其参数)时,使用接受一个 InvocationContext 的双参数重载:

java
Friend friend = AiServices.builder(Friend.class)
    .chatModel(model)
    .systemMessageProvider(chatMemoryId -> "You are a good friend of mine. Answer using slang.")
    .systemMessageTransformer((systemMessage, context) ->
            systemMessage + " Tenant: " + context.invocationParameters().get("tenant") + ".")
    .build();

@UserMessage

现在,假设我们用的模型不支持系统消息,或者我们只是想用 UserMessage 来达到那个目的。

java
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'?

我们把 @SystemMessage 注解替换成了 @UserMessage,并指定了一个含有变量 it 的提示词模板,it 引用那唯一的方法参数。

也可以给 String userMessage 加上 @V 注解,并为提示词模板变量指定一个自定义名称:

java
interface Friend {

    @UserMessage("You are a good friend of mine. Answer using slang. {{message}}")
    String chat(@V("message") String userMessage);
}

NOTE

请注意,当在 Quarkus 或 Spring Boot 中使用 LangChain4j 时,无需使用 @V。仅当 Java 编译期间 未 启用 -parameters 选项时,才需要该注解。

@UserMessage 也能从资源加载一个提示词模板:@UserMessage(fromResource = "my-prompt-template.txt")

以编程方式改写 ChatRequest

在某些情况下,在 ChatRequest 被发送给 LLM 之前修改它会很有用。例如,可能需要在用户消息中追加一些额外上下文,或基于某些外部条件修改系统消息。

可以通过给 AI 服务配置一个实现该转换、将应用于 ChatRequest 的 UnaryOperator<ChatRequest> 来做到这一点:

java
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .chatRequestTransformer(transformingFunction)  // Configures the transformation function to be applied to the ChatRequest
    .build();

若实现所需的 ChatRequest 转换还需要访问 ChatMemory,也可以给 chatRequestTransformer 方法配置一个 BiFunction<ChatRequest, Object, ChatRequest>,其中传给该函数的第二个参数是记忆 ID。

ChatRequestParameters

另一重自由度是可以逐次调用地配置参数(例如 temperature、toolsChoice、maximum tokens 等)。例如,你可能希望某些请求更"有创造力"(更高 temperature),而另一些更确定(更低 temperature)。

要做到这一点,你可以创建一个额外接受 ChatRequestParameters 类型(或任何提供者专有类型,如 OpenAiChatRequestParameters)参数的 AI 服务方法。这告诉 LangChain4j 在每次调用时接受并合并这些参数。

NOTE

请注意,在 ChatRequestParameters 中指定的 toolSpecifications 与 responseFormat 会覆盖 AI 服务生成的那些。

定义你的接口,带第二个参数:

java
interface AssistantWithChatParams {

    String chat(@UserMessage String userMessage, ChatRequestParameters params);
}

构建 AI 服务:

java
AssistantWithChatParams assistant = AiServices.builder(AssistantWithChatParams.class)
    .chatModel(openAiChatModel)  // or whichever model
    .build();

用它调用,带任意逐次调用参数:

java
ChatRequestParameters customParams = ChatRequestParameters.builder()
    .temperature(0.85)
    .build();

String answer = assistant.chat("Hi there!", customParams);

作为参数传给 AI 服务方法的 ChatRequestParameters 也会被传播到上一节讨论的 chatRequestTransformer,因此在那里如有必要也能访问并修改它。

有效 AI 服务方法的示例

下面是一些有效的 AI 服务方法示例。

`UserMessage`
java
String chat(String userMessage);

String chat(@UserMessage String userMessage);

String chat(@UserMessage String userMessage, ChatRequestParameters parameters);

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` and `UserMessage`
java
@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);

多模态

除文本内容外,或替代文本内容,AI 服务方法可以接受一个或多个 Content 或 List<Content> 参数:

java
String chat(@UserMessage String userMessage, @UserMessage Content content);

String chat(@UserMessage String userMessage, @UserMessage ImageContent image);

String chat(@UserMessage String userMessage, @UserMessage ImageContent image, @UserMessage AudioContent audio);

String chat(@UserMessage String userMessage, @UserMessage List<Content> contents);

String chat(@UserMessage String userMessage, @UserMessage List<ImageContent> images);

String chat(Content content);

String chat(AudioContent content);

String chat(List<Content> contents);

String chat(List<AudioContent> contents);

String chat(@UserMessage Content content1, @UserMessage Content content2);

String chat(@UserMessage AudioContent audio, @UserMessage ImageContent image);

AI 服务会按参数声明的顺序把所有内容放入最终的 UserMessage。

关于各可用内容类型的更多细节,请查看 Content API。

返回类型

AI 服务方法可以返回以下类型之一:

  • String —— 那种情况下,LLM 生成的输出不经任何处理/解析即返回
  • 任何被 结构化输出 支持的类型 —— 那种情况下 AI 服务会在返回前把 LLM 生成的输出解析为一个期望类型
  • TokenStream —— 在其生成之际接收响应
  • CompletableFuture<T> / CompletionStage<T> 或 Flow.Publisher<...> —— 运行该交互 而不阻塞调用线程(实验性)

任何类型都可以额外包装进一个 Result<T>,以获取关于 AI 服务调用的额外元数据:

  • TokenUsage —— 在 AI 服务调用期间使用的词元总数。若 AI 服务对 LLM 做了多次调用, (例如因为执行了工具),它会累加所有调用的词元用量。
  • Sources —— 在 RAG 检索期间取回的 Content
  • 在 AI 服务调用期间执行的所有工具(包括请求与结果)
  • 最终聊天响应的 FinishReason
  • 所有中间的 ChatResponse
  • 最终的 ChatResponse

一个示例:

java
interface Assistant {
    
    @UserMessage("Generate an outline for the article on the following topic: {{it}}")
    Result<List<String>> generateOutlineFor(String topic);
}

Result<List<String>> result = assistant.generateOutlineFor("Java");

List<String> outline = result.content();
TokenUsage tokenUsage = result.tokenUsage();
List<Content> sources = result.sources();
List<ToolExecution> toolExecutions = result.toolExecutions();
FinishReason finishReason = result.finishReason();

NOTE

Result<T> 中的 T 是 LLM 产出的内容,所以它必须是 LLM 实际能生成的类型:一个 String、一个枚举、一个 POJO、这些类型的集合等。LangChain4j 自身的类型,如 ChatResponse、ChatMessage、TextSegment、Embedding 或 TokenUsage,不能用作 T(也不能用作 List<T>/Set<T> 返回类型的元素):创建时 AI 服务会以 IllegalConfigurationException 失败。最终 ChatResponse 携带的一切在 Result<T> 本身都已可获取,例如:

java
ChatResponse chatResponse = result.finalResponse();

结构化输出

若你想从 LLM 接收结构化输出(例如一个复杂 Java 对象,而非 String 内的非结构化文本),你可以把 AI 服务方法的返回类型从 String 改为其他类型。

NOTE

关于结构化输出的更多信息可在这里找到。

一些示例:

boolean 作为返回类型

java
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

Enum 作为返回类型

java
enum Priority {
    CRITICAL, HIGH, 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

TIP

对于是/否问题与固定选项之间的选择(如上面那些示例),你也可以考虑决策服务,它使用决策模型而非聊天模型:它们能为每个答案返回一个概率,且用专门的决策模型通常更快、更便宜。

POJO 作为返回类型

java
class Person {

    @Description("first name of a person") // you can add an optional description to help an LLM have a better understanding
    String firstName;
    String lastName;
    LocalDate birthDate;
    Address address;
}

@Description("an address") // you can add an optional description to help an LLM have a better understanding
class Address {
    String street;
    Integer streetNumber;
    String city;
}

interface PersonExtractor {

    @UserMessage("Extract information about a person from {{it}}")
    Person extractPersonFrom(String text);
}

PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, model);

String text = """
            In 1968, amidst the fading echoes of Independence Day,
            a child named John arrived under the calm evening sky.
            This newborn, bearing the surname Doe, marked the start of a new journey.
            He was welcomed into the world at 345 Whispering Pines Avenue
            a quaint street nestled in the heart of Springfield
            an abode that echoed with the gentle hum of suburban dreams and aspirations.
            """;

Person person = personExtractor.extractPersonFrom(text);

System.out.println(person); // Person { firstName = "John", lastName = "Doe", birthDate = 1968-07-04, address = Address { ... } }

JSON 模式

当抽取自定义 POJO(实际上是 JSON,随后被解析为 POJO)时,建议在模型配置中启用"JSON 模式"。这样 LLM 会被强制以合法 JSON 响应。

NOTE

请注意,JSON 模式与工具/函数调用是相似的特性,但有不同的 API、用于不同的目的。

JSON 模式适用于你_总是_需要 LLM 以结构化格式(合法 JSON)响应的场景。此外,通常不需要状态/记忆,因此每次与 LLM 的交互都相互独立。例如,你可能想从一段文本中抽取信息,比如该文本中提到的人名单,或把一个自由格式的产品评论转换为带 String productName、Sentiment sentiment、List<String> claimedProblems 等字段的结构化表单。

另一方面,工具/函数适用于 LLM 需要执行某些动作的场景(例如查数据库、搜网页、取消用户的预订等)。那种情况下,一份工具列表及其期望的 JSON schema 会被提供给 LLM,由它自主决定是否调用其中任何一个来满足用户请求。

早前,函数调用常用于结构化数据抽取,但现在我们有了 JSON 模式特性,它更适合这个目的。

以下是启用 JSON 模式的方式:

  • 对于 OpenAI:

    • 对支持结构化输出的较新模型(例如 gpt-4o-mini、gpt-4o-2024-08-06):
      java
      OpenAiChatModel.builder()
          ...
          .supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
          .strictJsonSchema(true)
          .build();
      更多细节见这里。
    • 对较旧模型(例如 gpt-3.5-turbo、gpt-4):
      java
      OpenAiChatModel.builder()
          ...
          .responseFormat("json_object")
          .build();
  • 对于 Azure OpenAI:

java
AzureOpenAiChatModel.builder()
    ...
    .responseFormat(new ChatCompletionsJsonResponseFormat())
    .build();
  • 对于 Vertex AI Gemini:
java
VertexAiGeminiChatModel.builder()
    ...
    .responseMimeType("application/json")
    .build();

或通过从 Java 类指定一个显式 schema:

java
VertexAiGeminiChatModel.builder()
    ...
    .responseSchema(SchemaHelper.fromClass(Person.class))
    .build();

从一个 JSON schema:

java
VertexAiGeminiChatModel.builder()
    ...
    .responseSchema(Schema.builder()...build())
    .build();
  • 对于 Google AI Gemini:
java
GoogleAiGeminiChatModel.builder()
    ...
    .responseFormat(ResponseFormat.JSON)
    .build();

或通过从 Java 类指定一个显式 schema:

java
GoogleAiGeminiChatModel.builder()
    ...
    .responseFormat(ResponseFormat.builder()
        .type(JSON)
        .jsonSchema(JsonSchemas.jsonSchemaFrom(Person.class).get())
        .build())
    .build();

从一个 JSON schema:

java
GoogleAiGeminiChatModel.builder()
    ...
    .responseFormat(ResponseFormat.builder()
        .type(JSON)
        .jsonSchema(JsonSchema.builder()...build())
        .build())
    .build();
  • 对于 Mistral AI:
java
MistralAiChatModel.builder()
    ...
    .supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
    .strictJsonSchema(true)
    .build();
  • 对于 Ollama:
java
OllamaChatModel.builder()
    ...
    .responseFormat(JSON)
    .build();
  • 对于其他模型提供者:若底层模型提供者不支持 JSON 模式, 提示词工程是你最好的办法。另外,试着调低 temperature 以获得更多确定性。

更多示例

流式传输

当使用 TokenStream 返回类型时,AI 服务可以流式响应逐词元。Flow.Publisher 返回类型流式传输同样的交互而不阻塞调用线程——见非阻塞与响应式。

java
interface Assistant {

    TokenStream chat(String message);
}

StreamingChatModel 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");

CompletableFuture<ChatResponse> futureResponse = new CompletableFuture<>();

tokenStream
    .onPartialResponse((String partialResponse) -> System.out.println(partialResponse))
    .onPartialThinking((PartialThinking partialThinking) -> System.out.println(partialThinking))
    .onRetrieved((List<Content> contents) -> System.out.println(contents))
    .onIntermediateResponse((ChatResponse intermediateResponse) -> System.out.println(intermediateResponse))
     // This will be invoked every time a new partial tool call (usually containing a single token of the tool's arguments) is available.
    .onPartialToolCall((PartialToolCall partialToolCall) -> System.out.println(partialToolCall))
     // This will be invoked right before a tool is executed. BeforeToolExecution contains ToolExecutionRequest (e.g. tool name, tool arguments, etc.)
    .beforeToolExecution((BeforeToolExecution beforeToolExecution) -> System.out.println(beforeToolExecution))
     // This will be invoked right after a tool is executed. ToolExecution contains ToolExecutionRequest and tool execution result.
    .onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
     // This will be invoked for raw provider streaming events that are not already exposed via the typed callbacks above (e.g. server-tool lifecycle events). See the "Unmapped Raw Events" section of Response Streaming.
    .onUnmappedRawEvent((Object rawEvent) -> System.out.println(rawEvent))
    .onCompleteResponse((ChatResponse response) -> futureResponse.complete(response))
    .onError((Throwable error) -> futureResponse.completeExceptionally(error))
    .start();

futureResponse.join(); // Blocks the main thread until the streaming process (running in another thread) is complete

流式取消

若你想取消流式,可以从以下回调之一做到:

  • onPartialResponseWithContext(BiConsumer<PartialResponse, PartialResponseContext>)
  • onPartialThinkingWithContext(BiConsumer<PartialThinking, PartialThinkingContext>)

例如:

java
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();

当 StreamingHandle.cancel() 被调用时,LangChain4j 会关闭连接并停止流式。一旦 StreamingHandle.cancel() 被调用,TokenStream 不会再收到任何回调。

Flux

你也可以用 Flux<String> 代替 TokenStream。(Flow.Publisher<String> 无需额外模块就给你同样的流——见非阻塞与响应式。)为此,请导入 langchain4j-reactor 模块:

xml
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-reactor</artifactId>
    <version>1.22.0-beta32</version>
</dependency>
java
interface Assistant {

  Flux<String> chat(String message);
}

Flux<String> 由 TokenStream 支撑,因此它对每个提供者都有效。同一模块还带有非阻塞模式的 Reactor 绑定——Mono<T> 与 Flux<AiServiceStreamingEvent>——它们只对已实现它们的提供者有效。

流式示例

聊天记忆

AI 服务可以使用聊天记忆来"记住"之前的交互:

java
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
    .build();

在这种场景中,同一个 ChatMemory 实例会被用于 AI 服务的所有调用。然而,如果你有多个用户,这种方式行不通,因为每个用户都需要自己的 ChatMemory 实例来维护各自的会话。

这个问题的解决方案是使用 ChatMemoryProvider:

java
interface Assistant  {
    String chat(@MemoryId int memoryId, @UserMessage String message);
}

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(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");

在这种场景中,ChatMemoryProvider 会提供两个不同的 ChatMemory 实例,每个记忆 ID 一个。

以这种方式使用 ChatMemory 时,淘汰不再需要的会话的记忆也很重要,以避免内存泄漏。要让 AI 服务内部使用的聊天记忆可访问,只需定义它的接口继承 ChatMemoryAccess 即可。

java
interface Assistant extends ChatMemoryAccess {
    String chat(@MemoryId int memoryId, @UserMessage String message);
}

这既能访问单次会话的 ChatMemory 实例,也能在会话结束时把它清除掉。

java
String answerToKlaus = assistant.chat(1, "Hello, my name is Klaus");
String answerToFrancine = assistant.chat(2, "Hello, my name is Francine");

List<ChatMessage> messagesWithKlaus = assistant.getChatMemory(1).messages();
boolean chatMemoryWithFrancineEvicted = assistant.evictChatMemory(2);

NOTE

请注意,若某个 AI 服务方法没有带 @MemoryId 注解的参数,ChatMemoryProvider 中 memoryId 的值会默认为字符串 "default"。

NOTE

请注意,不应对同一个 @MemoryId 并发调用 AI 服务,因为这可能导致 ChatMemory 损坏。目前,AI 服务没有实现任何机制来阻止对同一个 @MemoryId 的并发调用。

工具(函数调用)

AI 服务可以配置供 LLM 使用的工具:

java
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)
    .chatModel(model)
    .tools(new Tools())
    .build();

String answer = assistant.chat("What is 1+2 and 3*4?");

在这种场景中,LLM 会请求先执行 add(1, 2) 与 multiply(3, 4) 方法,再给出最终答案。LangChain4j 会自动执行这些方法。

关于工具的更多细节可在这里找到。

RAG

AI 服务可以配置一个 ContentRetriever,以启用朴素 RAG:

java
EmbeddingStore embeddingStore  = ...
EmbeddingModel embeddingModel = ...

ContentRetriever contentRetriever = new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .contentRetriever(contentRetriever)
    .build();

配置一个 RetrievalAugmentor 提供更进一步的灵活性,启用高级 RAG能力,例如查询转换、重排序等:

java
RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
        .queryTransformer(...)
        .queryRouter(...)
        .contentAggregator(...)
        .contentInjector(...)
        .executor(...)
        .build();

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .retrievalAugmentor(retrievalAugmentor)
    .build();

RAG 作为工具

默认情况下,每个用户查询都会执行内容检索。或者,检索也可被当作一种类工具能力,只在模型判定需要额外上下文时才被调用。用这种方式,检索仍是 RAG 流水线的一部分,但被有条件地执行,避免为简单查询做不必要的搜索。

要实现这一点,你可以把一个 ContentRetriever 封装进一个 @Tool 并注册到 AiServices。这让 LLM 能基于该工具的描述自主决定是否触发检索。

1. 定义检索工具

创建一个包装你的 ContentRetriever 的类。
@Tool 的描述至关重要,它告知 LLM 何时调用搜索。

java
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.query.Query;

import java.util.stream.Collectors;

static class SearchTool {

    private final ContentRetriever contentRetriever;

    SearchTool(ContentRetriever contentRetriever) {
        this.contentRetriever = contentRetriever;
    }

    @Tool("Search for technical information about LangChain4j and RAG configurations")
    public String search(String query) {
        // This logic is only executed when the LLM determines retrieval is necessary
        return contentRetriever.retrieve(new Query(query)).stream()
                .map(content -> content.textSegment().text())
                .collect(Collectors.joining("\n\n"));
    }
}

2. 把工具注册到 AiServices

不用全局的 RetrievalAugmentor,而是把检索逻辑注册为一个工具。

java
Assistant assistant = AiServices.builder(Assistant.class)
        .chatModel(model)
        .tools(new SearchTool(contentRetriever))
        .build();

3. 预期行为

LLM 会拿用户意图与工具描述做比对,以决定是否执行搜索。

场景 A —— 一般对话

  • 输入:
    Hello, how are you today?

  • 行为:
    LLM 直接以其内部知识作答,不调用该工具。

场景 B —— 技术问题

  • 输入:
    How do I configure a ContentRetriever?

  • 行为:
    LLM 识别出技术意图,调用 search(),并基于检索到的文档生成响应。

这种方式让检索能作为一个按需能力运作,类似一个工具,而非每个查询都必须走的一步。

关于 RAG 的更多细节可在这里找到。

更多 RAG 示例可在这里找到。

自动审核

AI 服务可以自动执行内容审核。当检测到不当内容时,会抛出一个 ModerationException,它含有原始的 Moderation 对象。该对象包含关于被标记内容的信息,例如被标记的具体文本。

自动审核可以在构建 AI 服务时配置:

java
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .moderationModel(moderationModel)  // Configures moderation  model
    .build();

示例

链式组合多个 AI 服务

你的 LLM 驱动应用的逻辑越复杂,把它拆解成更小的部分就越关键——这在软件开发中是通行做法。

例如,往系统提示词里塞一大堆指令以顾及所有可能场景,既易错又低效。指令太多时,LLM 可能会忽略其中一些。此外,指令呈现的先后顺序也有影响,让这个过程更加棘手。

这一原则同样适用于工具、RAG,以及像 temperature、maxTokens 这样的模型参数。

你的聊天机器人多半不需要在任何时候都知晓你拥有的所有工具。例如,当用户只是打招呼或道别时,让 LLM 访问几十甚至几百个工具既昂贵有时甚至危险(每个工具被包含进 LLM 调用都会消耗大量词元),并可能招致非预期结果(LLM 会产生幻觉,或被诱导以非预期输入调用某个工具)。

关于 RAG:类似地,有时需要给 LLM 提供某些上下文,但并非总是,因为它带来额外成本(更多上下文 = 更多词元),并拉长响应时间(更多上下文 = 更高延迟)。

关于模型参数:某些情形下你可能需要 LLM 高度确定,于是你会设置很低的 temperature;另一些情形你可能选更高的 temperature,以此类推。

重点是,更小、更专门的组件更容易也更便宜地开发、测试、维护与理解。

另一个要考虑的方面涉及两个极端:

  • 你希望你的应用高度确定, 由应用控制流程、LLM 只是其中一个组件?
  • 还是你希望 LLM 拥有完全自主权并驱动你的应用?

又或视情况而定、混合两者?当你把应用分解为更小、更易管理的部分时,所有这些选项都可行。

AI 服务可以当作常规(确定性)软件组件使用,也能与其组合:

  • 你可以在一个 AI 服务之后再调用另一个(即链式组合)。
  • 你可以使用确定性的与 LLM 驱动的 if/else 语句(AI 服务能返回 boolean)。
  • 你可以使用确定性的与 LLM 驱动的 switch 语句(AI 服务能返回 enum)。
  • 你可以使用确定性的与 LLM 驱动的 for/while 循环(AI 服务能返回 int 及其他数值类型)。
  • 你可以在单元测试中 mock 一个 AI 服务(因为它是一个接口)。
  • 你可以对每个 AI 服务做独立集成测试。
  • 你可以对每个 AI 服务分别评估并寻找最优参数。
  • 等等

来看一个简单例子。我想为公司构建一个聊天机器人。若用户打招呼,我希望它用预定义的问候语回复,不依赖 LLM 生成问候。若用户提问,我希望 LLM 用公司内部知识库生成回复(即 RAG)。

这个任务可以如此拆解为两个独立的 AI 服务:

java
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)
    .chatModel(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 ...

注意我们在"识别文本是否为问候"这个简单任务上用了更便宜的 Llama2,而在更复杂的任务上用了更昂贵的、带内容检索器(RAG)的 GPT-4。

这是一个非常简单、略显朴素的示例,但希望能说明这个思路。

现在,我可以 mock GreetingExpert 与 ChatBot 两者,并对 MilesOfSmiles 做独立测试;也可以分别对 GreetingExpert 与 ChatBot 做集成测试;可以分别评估两者并为每个子任务找到最优参数;长远来看,甚至可以为每个具体子任务微调一个专门的小模型。

测试

相关教程

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。