主页 > 开源代码  > 

第5章:在LangChain中如何使用AIServices

第5章:在LangChain中如何使用AIServices

这篇文章详细介绍了 LangChain4j 中的 AI Services 概念,展示了如何通过高层次的抽象来简化与大语言模型(LLM)的交互。AI Services 的核心思想是隐藏底层复杂性,让开发者专注于业务逻辑,同时支持聊天记忆、工具调用和 RAG 等高级功能。通过示例和代码片段,文章展示了如何定义和使用 AI Services,以及如何将它们组合起来构建复杂的 LLM 驱动的应用程

AI Services | LangChain4j 引言

到目前为止,我们已经介绍了低层次的组件,例如 ChatLanguageModel、ChatMessage 和 ChatMemory 等。在这一层次上工作非常灵活,给你完全的自由,但这也迫使你编写大量的样板代码(boilerplate code)。由于基于 LLM 的应用程序通常不仅需要单个组件,而是多个组件协同工作(例如,提示词模板、聊天记忆、LLM、输出解析器、RAG 组件:嵌入模型和存储),并且通常涉及多次交互,因此协调它们变得更加繁琐。

解决方案

我们希望你专注于业务逻辑,而不是底层实现细节。因此,LangChain4j 提出了两个高层次的概念来帮助实现这一点:AI Services 和 Chains。

Chains(已废弃) Chains 的概念源自 Python 的 LangChain(在引入 LCEL 之前)。其想法是为每个常见用例提供一个 Chain,例如聊天机器人、RAG 等。Chains 结合了多个低层次组件,并协调它们之间的交互。然而,它们的主要问题是,如果你需要自定义某些内容,它们会显得过于僵化。LangChain4j 目前只实现了两个 Chains(ConversationalChain 和 ConversationalRetrievalChain),并且目前不计划添加更多。AI Services 我们提出了另一种解决方案,称为 AI Services,专为 Java 设计。其想法是将与 LLM 和其他组件交互的复杂性隐藏在一个简单的 API 后面。 这种方法类似于 Spring Data JPA 或 Retrofit:你声明性地定义一个接口,指定所需的 API,而 LangChain4j 提供一个实现该接口的对象(代理)。你可以将 AI Service 视为应用程序服务层的一个组件,它提供 AI 服务,因此得名。

AI Services 处理最常见的操作:

为 LLM 格式化输入。解析 LLM 的输出。 它们还支持更高级的功能:聊天记忆(Chat Memory)。工具(Tools)。RAG(Retrieval-Augmented Generation,检索增强生成)。

AI Services 可以用于构建支持来回交互的状态化聊天机器人,也可以用于自动化每个 LLM 调用都是独立的流程。

AI Service初探 最简单的 AI Service 示例

首先,我们定义一个接口,其中包含一个名为 chat 的方法,该方法接受一个 String 类型的输入并返回一个 String 类型的输出:

interface Assistant { String chat(String userMessage); }

然后,我们创建低层次组件。这些组件将在 AI Service 的底层使用。在这个例子中,我们只需要 ChatLanguageModel:

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

最后,我们使用 AiServices 类创建 AI Service 的实例:

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

注意:在 Quarkus 和 Spring Boot 应用程序中,自动配置会处理 Assistant 的创建。这意味着你不需要调用 AiServices.create(…),只需在需要的地方注入/自动装配 Assistant 即可。 现在我们可以使用 Assistant:

String answer = assistant.chat("Hello"); System.out.println(answer); // 输出:Hello, how can I help you? 工作原理

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

在 Quarkus 和 Spring Boot 应用程序中使用 AI Services

LangChain4j 提供了 Quarkus 扩展和 Spring Boot 启动器,极大地简化了在这些框架中使用 AI Services 的过程。

@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") 系统消息提供者(System Message Provider)

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

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

你可以根据聊天记忆 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'?

我们将 @SystemMessage 替换为 @UserMessage,并指定了一个包含变量 it 的提示模板,该变量指向方法的唯一参数。 你也可以使用 @V 注解为提示模板变量指定自定义名称:

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

注意:在使用 LangChain4j 的 Quarkus 或 Spring Boot 应用程序中,@V 注解不是必需的。只有在 Java 编译时未启用 -parameters 选项时,才需要使用它。 @UserMessage 也可以从资源文件中加载提示模板:

@UserMessage(fromResource = "my-prompt-template.txt") 有效的 AI Service 方法示例

以下是一些有效的 AI Service 方法示例:

使用 UserMessage 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 和 UserMessage @SystemMessage("Given a name of a country, answer with a name of its capital") String chat(String userMessage); @SystemMessage("Given a name of a country, answer with a name of its 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 its capital") String chat(@UserMessage String userMessage, @V("country") String country); // userMessage 包含 "{{country}}" 模板变量 @SystemMessage("Given a name of a country, answer with a name of its 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 its 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 its 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); 多模态(Multimodality)

目前,AI Services 不支持多模态功能,需要使用基础的套件和 API 实现。

结构化输出(Structured Outputs)

如果你希望从 LLM 中获取结构化输出,可以将 AI Service 方法的返回类型从 String 改为其他类型。目前,AI Services 支持以下返回类型:

StringAiMessage任意自定义 POJO(Plain Old Java Object)任意 Enum 或 List 或 Set(用于对文本进行分类,例如情感分析、用户意图等)boolean/Boolean(用于获取“是”或“否”的回答)byte/short/int/BigInteger/long/float/double/BigDecimalDate/LocalDate/LocalTime/LocalDateTimeList/Set(用于以项目符号列表的形式返回答案)Map<K, V>Result(如果需要访问 TokenUsage、FinishReason、来源(RAG 中检索到的内容)和执行的工具,除了 T,T 可以是上述任意类型。例如:Result、Result)

除非返回类型是 String、AiMessage 或 Map<K, V>,AI Service 会自动在 UserMessage 的末尾附加指示 LLM 应该如何响应的指令。在方法返回之前,AI Service 会将 LLM 的输出解析为所需的类型。 你可以通过启用日志记录来观察附加的指令。

注意:某些 LLM 提供商(例如 OpenAI 和 Google Gemini)允许为期望的输出指定 JSON 模式。如果此功能被支持且启用,自由格式的文本指令不会被附加到 UserMessage 的末尾。在这种情况下,将从你的 POJO 自动生成 JSON 模式并传递给 LLM,从而确保 LLM 遵循该 JSON 模式。

现在,让我们来看一些示例。

1. 返回类型为 boolean 的示例 interface SentimentAnalyzer { @UserMessage("Does {{it}} have a positive sentiment?") boolean isPositive(String text); } SentimentAnalyzer sentimentAnalyzer = AiServices.create(SentimentAnalyzer.class, model); boolean positive = sentimentAnalyzer.isPositive("It's wonderful!"); // 输出:true 2. 返回类型为 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

注意:@Description 注解是可选的。当枚举名称不够直观时,建议使用它来帮助 LLM 更好地理解。

3. 返回类型为 POJO 的示例 class Person { @Description("first name of a person") // 可选描述,帮助 LLM 更好地理解 String firstName; String lastName; LocalDate birthDate; Address address; } @Description("an address") // 可选描述,帮助 LLM 更好地理解 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 模式(JSON Mode)

当提取自定义 POJO(实际上是 JSON,然后解析为 POJO)时,建议在模型配置中启用“JSON 模式”。这样,LLM 将被强制以有效的 JSON 格式响应。

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

JSON 模式:当你总是需要 LLM 以结构化格式(有效的 JSON)响应时非常有用。此外,通常不要状态/记忆,因此每次与 LLM 的交互都是独立的。例如,你可能希望从文本中提取信息,例如文本中提到的人的列表,或者将自由格式的产品评论转换为具有字段(如 String productName、Sentiment sentiment、List 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): OpenAiChatModel.builder() ... .responseFormat("json_object") .build(); Azure OpenAI AzureOpenAiChatModel.builder() ... .responseFormat(new ChatCompletionsJsonResponseFormat()) .build(); Vertex AI Gemini VertexAiGeminiChatModel.builder() ... .responseMimeType("application/json") .build();

或者通过指定一个 Java 类的显式模式:

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

或者通过指定一个 JSON 模式:

GoogleAiGeminiChatModel.builder() ... .responseFormat(ResponseFormat.builder() .type(JSON) .jsonSchema(JsonSchema.builder()...build()) .build()) .build(); Mistral AI MistralAiChatModel.builder() ... .responseFormat(MistralAiResponseFormatType.JSON_OBJECT) .build(); Ollama OllamaChatModel.builder() ... .responseFormat(JSON) .build(); 其他模型提供商

如果底层模型提供商不支持 JSON 模式,提示工程(Prompt Engineering)是你的最佳选择。此外,尝试降低 temperature 参数以获得更确定性的结果。

流式响应(Streaming)

AI Service 可以通过使用 TokenStream 返回类型逐个流式传输响应令牌:

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(); 使用 Flux

你也可以使用 Flux 替代 TokenStream。为此,请导入 langchain4j-reactor 模块:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-reactor</artifactId> <version>1.0.0-beta1</version> </dependency>

代码示例

interface Assistant { Flux<String> chat(String message); } 聊天记忆(Chat Memory)

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

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

在这种情况下,相同的 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");

在这种情况下,ChatMemoryProvider 将为每个内存 ID 提供两个不同的 ChatMemory 实例。

注意:

如果 AI Service 方法没有带有 @MemoryId 注解的参数,则 ChatMemoryProvider 中的 memoryId 默认值为字符串 “default”。目前,AI Service 不支持对同一个 @MemoryId 的并发调用,因为这可能导致 ChatMemory 被破坏。AI Service 目前没有实现任何机制来防止对同一个 @MemoryId 的并发调用。 工具(Tools)

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

在这种情况下,LLM 将请求执行 add(1, 2) 和 multiply(3, 4) 方法,然后才提供最终答案。LangChain4j 将自动执行这些方法。

关于工具的更多信息可以参考 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();

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

RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder() .queryTransformer(...) .queryRouter(...) .contentAggregator(...) .contentInjector(...) .executor(...) .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .retrievalAugmentor(retrievalAugmentor) .build();

关于 RAG 的更多信息可以参考 LangChain4j 文档。

自动审核(Auto-Moderation)

(示例略)

链接多个 AI Services

随着你的 LLM 驱动应用程序的逻辑变得越来越复杂,将其分解为更小的部分变得至关重要,这在软件开发中是一种常见的实践。

例如,将大量指令塞入系统提示中以涵盖所有可能的场景,容易出错且效率低下。如果指令过多,LLM 可能会忽略一些。此外,指令的呈现顺序也很重要,这使得整个过程更加复杂。

这一原则也适用于工具、RAG 和模型参数(例如 temperature、maxTokens 等)。 你的聊天机器人可能并不需要在所有情况下都了解你所有的工具。例如,当用户仅仅是问候聊天机器人或说再见时,让 LLM 访问数十个甚至数百个工具(每个工具都会消耗大量的 token)是成本高昂的,有时甚至是危险的,可能会导致意外的结果(LLM 可能会幻觉或被操纵以调用工具并输入意外的内容)。

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

关于模型参数:在某些情况下,你可能需要 LLM 高度确定性,因此你会设置较低的 temperature。在其他情况下,你可能会选择较高的 temperature,依此类推。

要点是,更小且更具体的组件更容易、更便宜开发、测试、维护和理解。

另一个需要考虑的方面是两个极端:

你是否希望你的应用程序高度确定性,其中应用程序控制流程,LLM 只是其中一个组件?或者你希望 LLM 完全自主并驱动你的应用程序?

或许根据情况,两者都有?所有这些选项都可以通过将你的应用程序分解为更小、更易于管理的部分来实现。

AI Services 可以作为常规(确定性)软件组件使用,并与其他组件结合:

你可以依次调用一个 AI Service(即链式调用)。你可以使用确定性和基于 LLM 的 if/else 语句(AI Services 可以返回 boolean)。你可以使用确定性和基于 LLM 的 switch 语句(AI Services 可以返回 enum)。你可以使用确定性和基于 LLM 的 for/while 循环(AI Services 可以返回 int 和其他数值类型)。你可以模拟 AI Service(因为它是一个接口)以进行单元测试。你可以单独集成测试每个 AI Service。你可以分别评估每个 AI Service 并找到每个子任务的最优参数。 等等。 让我们考虑一个简单的例子。我想为我的公司构建一个聊天机器人。如果用户问候聊天机器人,我希望它用预定义的问候语回答,而不依赖 LLM 生成问候语。如果用户提问,我希望 LLM 使用公司的内部知识库生成回答(即 RAG)。 以下是如何将此任务分解为两个独立的 AI Services: 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 MilesOfSmiles(GreetingExpert greetingExpert, ChatBot chatBot) { this.greetingExpert = greetingExpert; this.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 ...

注意我们如何使用较便宜的 Llama2 来完成简单的问候识别任务,而使用更昂贵的 GPT-4(带有内容检索器,即 RAG)来完成更复杂的任务。

这是一个非常简单且有些幼稚的例子,但希望它能说明这个想法。 现在,我可以分别模拟 GreetingExpert 和 ChatBot,并独立测试 MilesOfSmiles。此外,我还可以分别集成测试 GreetingExpert 和 ChatBot,分别评估它们,并为每个子任务找到最优化的参数,甚至在长期内为每个特定任务微调一个小的专用模型。

总结

这篇文章详细介绍了 LangChain4j 中的 AI Services 概念,展示了如何通过高层次的抽象来简化与大语言模型(LLM)的交互。AI Services 的核心思想是隐藏底层复杂性,让开发者专注于业务逻辑,同时支持聊天记忆、工具调用和 RAG 等高级功能。通过示例和代码片段,文章展示了如何定义和使用 AI Services,以及如何将它们组合起来构建复杂的 LLM 驱动的应用程序。

标签:

第5章:在LangChain中如何使用AIServices由讯客互联开源代码栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“第5章:在LangChain中如何使用AIServices