跳到主要内容

AI 服务

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

我们希望你专注于业务逻辑,而不是底层实现细节。 因此,LangChain4j 目前有两个高层概念可以帮助做到这一点:AI 服务(AI Services)和链(Chains)。

链(Chains,遗留)

链(Chains)的概念源自 Python 版 LangChain(在引入 LCEL 之前)。 其思路是为每个常见用例(如聊天机器人、RAG 等)提供一个 Chain。 链将多个底层组件组合在一起,并编排它们之间的交互。 它们的主要问题是:一旦你需要自定义某些行为,就会显得过于僵硬。 LangChain4j 目前只实现了两个链(ConversationalChainConversationalRetrievalChain), 我们暂时不打算再增加更多。

AI 服务

我们提出另一种为 Java 量身定制的方案,称为 AI 服务(AI Services)。 其思路是:将与 LLM 及其他组件交互的复杂性隐藏在一个简单的 API 背后。

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

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

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

它们还支持更高级的功能:

  • 聊天记忆
  • 工具
  • RAG

AI 服务既可以用来构建支持来回交互的有状态聊天机器人, 也可以用来自动化那些每次对 LLM 的调用彼此独立的流程。

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

最简单的 AI 服务

首先,我们定义一个带有单个方法 chat 的接口,该方法接受一个 String 作为输入并返回一个 String

interface Assistant {

String chat(String userMessage);
}

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

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

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

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

QuarkusSpring Boot 应用中, 自动配置会负责创建 Assistant bean。 这意味着你不需要调用 AiServices.create(...),只需在需要的地方注入/自动装配 Assistant 即可。

现在我们可以使用 Assistant

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 中提供指令来实现。

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

系统消息提供者

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

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

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

系统消息转换器

系统消息转换器允许你在每次调用时动态修改系统消息, 时机是在它从 @SystemMessagesystemMessageProvider 解析之后,但在 chatRequestTransformer 运行之前。 当你需要向系统消息追加或前置内容,而不论其最初如何配置时,这会很有用。

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 的双参数重载:

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

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 的提示词模板,该变量引用唯一的方法参数。

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

interface Friend {

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

请注意,在与 Quarkus 或 Spring Boot 一起使用 LangChain4j 时,不必使用 @V。 只有在 Java 编译时 启用 -parameters 选项时,才需要此注解。

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

以编程方式重写 ChatRequest

在某些情况下,在将 ChatRequest 发送给 LLM 之前对其进行修改会很有用。例如,可能需要向用户消息追加一些额外上下文,或根据某些外部条件修改系统消息。

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

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

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

ChatRequestParameters

另一个自由度是可以按每次调用配置参数(例如 temperature、toolsChoice、最大 token 数等)。例如,你可能希望某些请求更“有创意”(更高的 temperature),而另一些请求更确定性(更低的 temperature)。

为此,你可以创建一个还接受 ChatRequestParameters 类型参数(或任何提供商特定类型,如 OpenAiChatRequestParameters)的 AI 服务方法。这会告诉 LangChain4j 在每次调用时接受并合并这些参数。

备注

请注意,ChatRequestParameters 中指定的 toolSpecificationsresponseFormat 将覆盖由 AI 服务生成的那些。

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

interface AssistantWithChatParams {

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

构建 AI 服务:

java

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

使用任意每次调用的参数来调用它:

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

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

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

有效 AI 服务方法的示例

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

UserMessage
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
SystemMessageUserMessage
@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 服务方法可以接受一个或多个 ContentList<Content> 参数:

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 生成的输出解析为所需类型

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

  • TokenUsage —— AI 服务调用期间使用的 token 总数。如果 AI 服务对 LLM 进行了多次调用(例如因为执行了工具),它会汇总所有调用的 token 用量。
  • 来源 —— RAG 检索期间获取的 Content
  • AI 服务调用期间执行的所有工具(包括请求和结果)
  • 最终聊天响应的 FinishReason
  • 所有中间的 ChatResponse
  • 最终的 ChatResponse

示例:

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

结构化输出

如果你希望从 LLM 接收结构化输出(例如复杂的 Java 对象, 而不是 String 中的非结构化文本), 可以将 AI 服务方法的返回类型从 String 改为其他类型。

备注

关于结构化输出的更多信息可见此处

几个示例:

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

Enum 作为返回类型

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

以 POJO 作为返回类型

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 进行响应。

备注

请注意,JSON 模式与工具/函数调用是相似的功能, 但它们有不同的 API,并用于不同的目的。

当你 始终 需要 LLM 以结构化格式(有效 JSON)返回响应时,JSON 模式很有用。 此外,通常不需要状态/记忆,因此与 LLM 的每次交互彼此独立。 例如,你可能希望从文本中提取信息,比如该文本中提到的人员列表, 或将自由形式的产品评论转换为带有 String productNameSentiment sentimentList<String> claimedProblems 等字段的结构化表单。

另一方面,当 LLM 应当能够执行某些操作时,工具/函数很有用 (例如:查询数据库、搜索网络、取消用户预订等)。 在这种情况下,会向 LLM 提供带有其期望 JSON schema 的工具列表,并由它自主决定 是否调用其中任何一个以满足用户请求。

此前,函数调用常被用于结构化数据提取, 但现在我们有了更适合此目的的 JSON 模式功能。

以下是如何启用 JSON 模式:

  • 对于 OpenAI:

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

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

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

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

从 JSON schema:

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

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

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

从 JSON schema:

GoogleAiGeminiChatModel.builder()
...
.responseFormat(ResponseFormat.builder()
.type(JSON)
.jsonSchema(JsonSchema.builder()...build())
.build())
.build();
  • 对于 Mistral AI:
MistralAiChatModel.builder()
...
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
.strictJsonSchema(true)
.build();
  • 对于 Ollama:
OllamaChatModel.builder()
...
.responseFormat(JSON)
.build();
  • 对于其他模型提供商:如果底层模型提供商不支持 JSON 模式, 提示工程是你最好的选择。另外,尝试降低 temperature 以获得更高的确定性。

更多示例

流式输出

当使用 TokenStream 返回类型时,AI 服务可以逐 token 流式返回响应


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>)

例如:

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。 为此,请导入 langchain4j-reactor 模块:

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.18.1-beta28</version>
</dependency>
interface Assistant {

Flux<String> chat(String message);
}

流式示例

聊天记忆

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

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

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

该问题的解决方案是使用 ChatMemoryProvider


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 会为每个记忆 ID 提供两个不同的 ChatMemory 实例。

以这种方式使用 ChatMemory 时,同样重要的是淘汰不再需要的对话记忆,以避免内存泄漏。为了使 AI 服务内部使用的聊天记忆可访问,定义它的接口只需扩展 ChatMemoryAccess 即可。


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

这样既可以访问单个对话的 ChatMemory 实例,也可以在对话结束时将其清除。

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);
备注

请注意,如果 AI 服务方法没有带 @MemoryId 注解的参数, 则 ChatMemoryProvider 中的 memoryId 值将默认为字符串 "default"

备注

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

工具(函数调用)

可以为 AI 服务配置 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)
.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


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

ContentRetriever contentRetriever = new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

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

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

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 何时调用搜索。

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,而是将检索逻辑注册为工具。

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 服务时配置自动审核:

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

示例

串联多个 AI 服务

你的 LLM 驱动应用逻辑越复杂, 就越有必要像软件开发中的常见做法一样,将其拆分为更小的部分。

例如,把大量指令塞进系统提示词以覆盖所有可能场景, 很容易出错且效率低下。如果指令太多,LLM 可能会忽略其中一些。 此外,指令呈现的顺序也很重要,这使过程更具挑战性。

这一原则同样适用于工具、RAG,以及 temperaturemaxTokens 等模型参数。

你的聊天机器人很可能并不需要在任何时候都了解你拥有的每一个工具。 例如,当用户只是向聊天机器人打招呼或说再见时, 让 LLM 访问几十或上百个工具既昂贵,有时甚至危险 (每次 LLM 调用中包含的每个工具都会消耗大量 token), 并可能导致意外结果(LLM 可能产生幻觉,或被操纵以使用非预期输入调用工具)。

关于 RAG:同样,有时需要向 LLM 提供一些上下文, 但并非总是如此,因为这会带来额外成本(更多上下文 = 更多 token), 并增加响应时间(更多上下文 = 更高延迟)。

关于模型参数:在某些情况下,你可能需要 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)生成响应。

以下是如何将此任务分解为 2 个独立的 AI 服务:

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 GreetingExpertChatBot,并单独测试 MilesOfSmiles。 此外,我可以分别对 GreetingExpertChatBot 进行集成测试。 我可以分别评估它们,并为每个子任务找到最合适的参数, 或者从长远来看,甚至为每个特定子任务微调一个小型专用模型。

测试

相关教程