AIService(人工智能服务)

什么是AIService

其实在前面我们在阅读langchain4j的文档中,就看到了这个AIService,只是没有提到:

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">AiService</font>是一个注解,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AiService {
AiServiceWiringMode wiringMode() default AiServiceWiringMode.AUTOMATIC;
String chatModel() default ""; //绑定聊天模型
String streamingChatModel() default "";
String chatMemory() default ""; //绑定聊天记忆
String chatMemoryProvider() default ""; //绑定聊天记忆隔离和持久化
String contentRetriever() default ""; //绑定内容检索器
String retrievalAugmentor() default "";
String moderationModel() default "";
String[] tools() default {}; //绑定工具
}

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">@AiService</font>注解,用于标记一个接口,使其被框架(如 <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">langchain4j</font>)自动处理,生成AI服务的实现:

  • 动态代理:框架会基于该接口生成代理类,处理方法调用(如 <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">chat(String message)</font>)。
  • 依赖注入:标记的接口会被 Spring 容器管理,允许通过 <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">@Autowired</font> 或其他方式注入。
  • AI 功能集成:注解会将接口与 AI 模型(如 OpenAI 或其他语言模型)绑定,自动处理请求和响应。

举个例子,这里我们定义了一个名为Assistant的接口,然后使用<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">@AiService</font>注解标记它,这样这个接口就会被自动处理,生成AI服务的实现:

1
2
3
4
5
6
@AiService
//如果你有很多AI模型实例,可用自定义绑定哪个模型
//@AiService(AiServiceWiringMode.EXPLICIT, chatModel = "ollamaChatModel")
public interface Assistant {
String chat(String message);
}

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">AiService</font>使用面向接口和动态代理的方式来完成程序的编写,以更灵活的实现高级功能。

链 Chain(旧版)

链的概念源自Python中的LangChain,其理念是针对每个常见的用例都设置一条链,比如聊天机器人、检索增强生成(RAG)等。链将多个底层组件组合起来,并协调它们之间的交互。链存在的主要问题是不灵活。

AIService的作用

在LangChain4j中,我们可以使用AIService来完成复杂操作,它的底层组件将由AIService进行组装。

AIService可处理最常见的操作,如为大语言模型格式化输入内容、解析大语言模型的输出结果,同时还支持更高级的功能,如聊天记忆(Chat Memory)、工具(Tools)和检索增强生成(RAG)。

创建AIService

点击 这里,查看与SpringBoot的集成,这是langchain4j的高级功能:

第一步,在pom文件中新增如下依赖:

1
2
3
4
5
<!--langchain4j高级功能-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>

第二步,新建一个名为assistant的包,并在该包内定义一个名为Assistant的接口,里面定义一个chat方法:

1
2
3
public interface Assistant {
String chat(String message);
}

第三步,新建一个名为controller的包,并在该包内定义一个名为TestController的类,里面定义一个testChat方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private OllamaChatModel ollamaChatModel;

@GetMapping("/testChat")
public String testChat(){
//创建AIService
Assistant assistant = AiServices.create(Assistant.class, ollamaChatModel);
//调用service的接口
return assistant.chat("你好,你是谁?");
}
}

这我们通过注入OllamaChatModel对象,然后调用AiServices的create方法,传入Assistant类以及OllamaChatModel对象,这样让返回的Assistant具备Ai功能。

第四步,启动项目,访问如下接口:

1
http://localhost:8080/test/testChat

页面显示内容如下:

实际上我们可以直接在自定义的Assistant接口上添加@AiService注解,由于我们在配置文件中同时配置了多个大语言模型,所以需要在这里明确指定(EXPLICIT)模型的beanName,即此处为ollamaChatModel:

1
2
3
4
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "ollamaChatModel")
public interface Assistant {
String chat(String message);
}

然后我们修改TestController类中的代码为如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/test")
public class TestController {

@Autowired
private Assistant assistant;

@GetMapping("/testChat")
public String testChat(){
//调用service的接口
return assistant.chat("你好,你是谁?");
}
}

重启项目,访问之前的接口,也是能得到之前的结果,毫无疑问,第二种方式更简单方便。

工作原理

实际上AiService会组装Assistant接口以及其他组件,并使用反射机制创建一个实现Assistant接口的代理对象。这个代理对象会处理输入和输出的所有转换工作,即代理对象的作用是输入转换和输出转换

在上述例子中,chat方法的输入是一个字符串,但是大模型需要一个UserMessage对象。所以,代理对象将这个字符串转换为UserMessage,并调用聊天语言模型。chat方法的输出类型也是字符串,但是大模型返回的是 AiMessage对象,代理对象会将其转换为字符串。

ChatMemory(聊天记忆)

点击 这里,阅读langchain4j关于chat memory相关的部分介绍:

测试对话是否有记忆

在TestController类中新增一个名为testChatMemory的方法,代码如下:

1
2
3
4
5
6
@GetMapping("/testChatMemory")
public String testChatMemory(){
String answer = assistant.chat("我是张三!");
log.info(answer);
return assistant.chat("我是谁?");
}

启动项目,访问如下接口:

1
http://localhost:8080/test/testChatMemory

页面显示内容如下:

由此可见,当前的接入方式,大模型是没有记忆的。

控制台输出如下信息:

1
你好!我是一个由深度求索公司独立开发的智能助手DeepSeek-R1,如果你有任何任何问题,我会尽我所能为您提供帮助。

使用ChatMemory实现聊天记忆

ChatMemory是一个接口,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface ChatMemory {
//ID
Object id();

//添加消息
void add(ChatMessage var1);

//消息集合,存储历史消息
List<ChatMessage> messages();

//清空消息
void clear();
}

这个ChatMemory接口有两个实现类:MessageWindowChatMemory和TokenWindowChatMemory,区别如下:

(1)MessageWindowChatMemory:基于消息数量的滑动窗口,保留最近的 N 条消息;

(2)TokenWindowChatMemory:基于token数量的滑动窗口,保留最近的 Ntoken

接下来我们尝试使用MessageWindowChatMemory来实现聊天记忆。回到TestController类中,新增一个名为testChatMemoryV2的方法,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/testChatMemoryV2")
public String testChatMemoryV2(){
//创建Memory
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
//创建AIService
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(ollamaChatModel)
.chatMemory(chatMemory)
.build();
//调用service的接口
String answer = assistant.chat("我是张三");
log.info(answer);
return assistant.chat("我是谁");
}

启动项目,访问如下接口:

1
http://localhost:8080/test/testChatMemoryV2

页面显示内容如下:

说明大模型目前是具备聊天记忆功能了。

使用AIService实现聊天记忆

创建记忆对话智能体

当AIService由多个组件(如大模型,聊天记忆等)组成时,我们就可以称它为智能体。

在assistant包内定义一个名为MemoryChatAssistant的接口,里面的代码如下:

1
2
3
4
5
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "ollamaChatModel", chatMemory = "chatMemory")
public interface MemoryChatAssistant {
String chat(String message);
}

由于此处的chatMemory属性使用的是chatMemory,因此需要提供一个名为chatMemory的Bean对象,可在配置类中提供。

配置ChatMemory

新建一个名为config的包,并在该包内定义一个名为MemoryChatAssistantConfig的配置类,里面的代码如下:

1
2
3
4
5
6
7
8
@Configuration
public class MemoryChatAssistantConfig {
@Bean
ChatMemory chatMemory() {
//设置聊天记忆记录的message数量
return MessageWindowChatMemory.withMaxMessages(10);
}
}

聊天测试

回到TestController类中,新增一个名为testChatMemoryV3的方法,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
@Autowired
private MemoryChatAssistant memoryChatAssistant;

@GetMapping("/testChatMemoryV3")
public String testChatMemoryV3(){
String answer1 = memoryChatAssistant.chat("我是张三");
log.info(answer1);
String answer2 = memoryChatAssistant.chat("我是谁");
log.info(answer2);
return answer2;
}

启动项目,访问如下接口:

1
http://localhost:8080/test/testChatMemoryV3

页面显示内容如下:

说明大模型目前是具备聊天记忆功能了。

隔离聊天记忆

所谓的隔离聊天记忆,就是为每个用户的新聊天或者不同的用户区分聊天记忆。

实际上我们之前使用的ChatMemory是单一会话内存,而ChatMemoryProvider是多会话内存,因此我们需要使用ChatMemoryProvider给每个用户或会话提供独立的ChatMemory实例,并根据提供的memoryId(通常是用户 ID 或会话 ID)返回对应的 ChatMemory 实例。

  • <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">@MemoryId</font> 注解用于标识方法参数,该参数的值将作为 <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">memoryId</font> 传递给 <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">chatMemoryProvider</font>,以获取对应的<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">ChatMemory</font>实例。
  • <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">@UserMessage</font>注解用于标识方法参数,该参数的值将作为用户消息发送给大语言模型(LLM)。

创建记忆隔离对话智能体

在assistant包内定义一个名为SeparateChatAssistant的接口,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "ollamaChatModel", chatMemoryProvider = "chatMemoryProvider")
public interface SeparateChatAssistant {
/**
* 分离聊天记录
* @param memoryId 聊天id
* @param userMessage 用户消息
*/
String chat(@MemoryId int memoryId , @UserMessage String userMessage);
}

由于此处的chatMemoryProvider属性使用的是chatMemoryProvider,因此需要提供一个名为chatMemoryProvider的Bean对象,可在配置类中提供。

配置ChatMemoryProvider

在config包内定义一个名为SeparateChatAssistantConfig的配置类,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class SeparateChatAssistantConfig {
@Bean
ChatMemoryProvider chatMemoryProvider(){
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(10)
.build();
}
}

可以看到这个ChatMemoryProvider是一个函数式接口,定义了如何根据memoryId(通常是用户 ID 或会话 ID)提供对应的ChatMemory实例:

1
2
3
4
@FunctionalInterface
public interface ChatMemoryProvider {
ChatMemory get(Object var1);
}

这样每当有新的对话请求时,LangChain4j会调用chatMemoryProviderget方法,传入当前的memoryId,以获取对应的ChatMemory实例。

这意味着,对于每个不同的memoryId,都会有一个独立的对话记忆实例,确保多用户或多会话场景下的对话上下文不会混淆。

测试对话助手

接下来我们将尝试使用两个不同的memoryId,来测试聊天记忆的隔离效果。

回到TestController类中,新增一个名为testChatMemoryV4的方法,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
private SeparateChatAssistant separateChatAssistant;

/**
* 隔离聊天记忆
*/
@GetMapping("/testChatMemoryV4")
public String testChatMemoryV4(){
String answer1 = separateChatAssistant.chat(1,"我是张三");
log.info("answer1:{}",answer1);
String answer2 = separateChatAssistant.chat(1,"我是谁");
log.info("answer2:{}",answer2);
String answer3 = separateChatAssistant.chat(2,"我是谁");
log.info("answer3:{}",answer3);
return answer3;
}

启动项目,访问如下接口:

1
http://localhost:8080/test/testChatMemoryV4

页面显示内容如下:

说明大模型目前是具备隔离聊天记忆功能了。