[笔记] SpringBoot + LangChain4j: 构建 AI Agent 技术总结(上篇)

1,120 阅读18分钟

创建 SpringBoot 项目

创建 Maven 项目

  • properties 中定义依赖(以及编译器、编码)的版本号变量,方便统一维护
<properties>  
    <maven.compiler.source>17</maven.compiler.source>  
    <maven.compiler.target>17</maven.compiler.target>  
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>  
  
    <spring-boot.version>3.2.6</spring-boot.version>  
    <knife4j.version>4.3.0</knife4j.version>  
    <mybatis-plus.version>3.5.11</mybatis-plus.version>  
</properties>
  • dependencyManagement 中导入了 Spring Boot 的 BOM,当 Maven 碰到 Spring Boot 相关依赖就会使用指定的版本,但不会自动把这些依赖下载到项目里
<dependencyManagement>  
    <dependencies>  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-dependencies</artifactId>  
            <version>${spring-boot.version}</version>  
            <type>pom</type>  
            <scope>import</scope>  
        </dependency>  
    </dependencies>  
</dependencyManagement>
  • dependencies 中列出了项目真正要用的依赖,Maven 才会根据这里的坐标(和从 BOM 继承来的版本)去下载并加入到 classpath
<dependencies>  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
    <dependency>        
	    <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-test</artifactId>  
        <scope>test</scope>  
    </dependency>  
    <dependency>        
	    <groupId>com.github.xiaoymin</groupId>  
        <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>  
        <version>${knife4j.version}</version>  
    </dependency>  
</dependencies>
  • 在 resource 中进行添加文件取名为 application.yml 或是 application.properties,进行设置项目启动所要占用的端口号
# Web端口号  
server:  
  port: 8080
  • 随后在主程序上进行添加注解,进行启动项目
@SpringBootApplication  
public class Main {  
    public static void main(String[] args) {  
        SpringApplication.run(Main.class, args);  
    }  
}

对接大语言模型

  • 想一个问题,现在让你使用代码去对接各大主流的 LLM 并且实现流畅的切换,你会怎么去做?
伪代码:
1. 查找官网或者社区提供的 Java SDK 依赖
2. 查看 Api 文档
3. 编写代码,配置连接信息
4. 根据官网规范,发送请求
5. 接收返回内容,解析内容
6. 处理可能的错误
  • 这还是对接一个 LLM 的情况下,如果是2个、3个....呢?除了这些重复的代码之外,最大的成本是你需要去学习新的“规则”
  • 如果使用 1LangChain4j 的话,会怎么样呢?
伪代码:
1. 引用 LangChain4j 的依赖
2. 根据 LangChain4j 提供的统一方法进行配置连接信息
3. 使用 LangChain4j 提供的方法进行发送请求(底层自动完成发送和接收)
4. 获得 LLM 的响应内容

LangChain4j

  • 参考链接:docs.langchain4j.dev/get-started
  • 通过下面的图可以看到,引入依赖的时候一个是带 open-ai 的,一个是不带的。
    • 可以简单的理解他们是属于这样的架构:
      • langchain4j-open-ai 依赖于 langchain4j-core
      • 而 langchain4j 是一个聚合模块,用来管理所有的子模块

LangChain4j依赖的说明.png

  • 在官方文档中也为了我们提供了 BOM,我们进行改造一下吧。在 dependencyManagement 中添加
<dependencyManagement>    
        <dependency>            
	        <groupId>dev.langchain4j</groupId>  
            <artifactId>langchain4j-bom</artifactId>  
            <version>${langchain4j.version}</version>  
            <type>pom</type>  
            <scope>import</scope>  
        </dependency>  
    </dependencies>  
</dependencyManagement>
  • 随后在 properties 中进行定义版本号
<properties>  
    <langchain4j.version>1.0.0-beta3</langchain4j.version>  
</properties>
  • 在 dependencies 中引用
    <dependencies>  
<!--        引入langChain4j 的 open-ai 依赖-->  
        <dependency>  
            <groupId>dev.langchain4j</groupId>  
            <artifactId>langchain4j-open-ai</artifactId>  
        </dependency>  
    </dependencies>
  • 随后就可以进行接入大模型了,可以先试用一下 LangChain4j 提供的测试大模型
@Test  
public void testGPTDemo() {  
  
    OpenAiChatModel model = OpenAiChatModel.builder()  
            .baseUrl("http://langchain4j.dev/demo/openai/v1")  
            .apiKey("demo")  
            .modelName("gpt-4o-mini")  
            .build();  
  
    String answer = model.chat("你好,你是谁?");  
    System.out.println(answer);  
}
  • 这样子或许方便,但是因为我们是 SpringBoot 项目,那么自然而然就要将这个对象交给 Spring 容器进行管理,所以接下来使用 Spring Boot 启动器进行使用 LangChain4j

SpringBoot 中的 LangChain4j

<dependency>  
    <groupId>dev.langchain4j</groupId>  
    <artifactId>langchain4j-open-ai</artifactId>  
</dependency>
  • 替换成
<dependency>  
    <groupId>dev.langchain4j</groupId>  
    <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>  
</dependency>
  • 随后在你的 yml 文件或是 properties 文件中添加
langchain4j:  
  open-ai:  
    chat-model:  
      base-url: http://langchain4j.dev/demo/openai/v1  
      api-key: demo  
      model-name: gpt-4o-mini  
      # 是否记录请求和响应  
      log-requests: true  
      log-responses: true  
  
# 将系统日志设置为debug级别  
logging:  
  level:  
    root: debug
  • 测试案例
@Autowired  
private OpenAiChatModel openAiChatModel;  
@Test  
public void SpringBootTest() {  
    String answer = openAiChatModel.chat("你好,你是谁");  
    System.out.println(answer);  
}

接入其他大模型

接入DeepSeek

DeepSeekApiKey配置.png

  • 填写完毕之后,重启一下 idea 避免无法读取到系统变量的数据,然后只需要替换 yml 文件中的数据,就可以了。代码不需要改变,因为 DeepSeek 和 ChatGPT 使用的是同一套 OpenAi 标准
# Web端口号  
server:  
  port: 8080  
  
langchain4j:  
  open-ai:  
    chat-model:  
      base-url: https://api.deepseek.com  
      api-key: ${DEEP_SEEK_API_KEY}  
      model-name: deepseek-chat  
      # 是否记录请求和响应  
      log-requests: true  
      log-responses: true  
  
# 将系统日志设置为debug级别  
logging:  
  level:  
    root: debug

接入Ollama

  • 本地部署就不赘述了
  • 在 pom 文件中添加 ollama 的 springboot 依赖
<!--        引入langChain4j 的 ollama 依赖-->  
        <dependency>  
            <groupId>dev.langchain4j</groupId>  
            <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>  
        </dependency>
  • 在 yml 文件中配置信息
langchain4j:  
  # ollama  
  ollama:  
    chat-model:  
      base-url: http://localhost:11434  
      model-name: deepseek-r1:7b  
      temperature: 0.8  
      timeout: PT60s
  • 代码只需要注意引入的是 ollamaChatModel 对象类型即可
@Autowired  
private OllamaChatModel ollamaChatModel;  
@Test  
public void OllamaTest() {  
    String answer = ollamaChatModel.chat("你好,你是谁");  
    System.out.println(answer);  
}

接入阿里百炼

<dependencyManagement>
        <!--引入百炼依赖管理清单-->  
        <dependency>  
            <groupId>dev.langchain4j</groupId>  
            <artifactId>langchain4j-community-bom</artifactId>  
            <version>${langchain4j.version}</version>  
            <type>pom</type>  
            <scope>import</scope>  
        </dependency>  
    </dependencies>  
</dependencyManagement>

<dependencies>
<!--        引入langChain4j 的 阿里百炼 依赖-->  
    <dependency>  
        <groupId>dev.langchain4j</groupId>  
        <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>  
    </dependency>  
<dependencies>
  • 配置 application.yml 文件
langchain4j:  
  # 阿里百炼  
  community:  
    dashscope:  
      chat-model:  
        api-key: ${BAI_LIAN_API_KEY}  
        model-name: qwen-max
  • 调用 Qwen
@Autowired  
private QwenChatModel qwenChatModel;  
@Test  
public void QwenChatTest(){  
    String answer = qwenChatModel.chat("你好,你是谁");  
    System.out.println(answer);  
}
  • 在测试一个阿里百炼的文生图的功能
@Test  
public void TestDashScopeWanx(){  
  
    WanxImageModel wanxImageModel = WanxImageModel.builder()  
            .modelName("wanx2.1-t2i-plus")  
            .apiKey(System.getenv("BAI_LIAN_API_KEY"))  
            .build();  
  
    Response<Image> imageResponse = wanxImageModel.generate(prompt 自己想);  
    System.out.println(imageResponse.content().url());  
}

功能接口

  • 想一个问题,这个时候我们是怎么和用户进行交互的?
伪代码:
1. 创建 ChatModel 对象
2. 手动创建用户消息的 prompt
3. 调用 chat 方法,进行将消息发送给 LLM
4. 接收返回值,手动提取回复的内容
5. 输出给用户
  • 看起来好像可以,但是每次和用户交互那么我们都需要进行创建消息对象->调用 chat 方法-> 提取回复....
  • 如果有了 AiService 会怎么样?
伪代码:
1. 定义接口,一个方法代表一个功能
2. 使用 AiServices.create() 或是 Spring Boot 的 @Aiservice 创建接口实例
3. 直接调用方法进行和用户交互(只需要传入参数,底层会自动帮你封装成对应的 prompt,当然你也可以进行自定义prompt)
  • 注意 LangChain4j 会根据你的接口自动帮你去实现一个对应的类,你只需要专注于业务即可
  • 其实上面的内容,我感觉跟正常调用 chat 方法区别并不是很大,所以...再看一个稍微复杂点的情况,不使用 AiService 的情况下
伪代码:
1. 创建 ChatModel 对象
2. 接收用户传递的内容:"张经理"、"下周会议议程"、"正式且简洁"
3. 根据用户传递的内容构建 prompt:"请写一封邮件给 " + 张经理 + ",主题是关于 " + 下周会议议程 + ",语气要" + 正式且简洁 + "。\n邮件正文:"
4. 调用 chat 方法发送请求等操作......
  • 使用 AiService 的情况下
伪代码:
1. 定义接口和方法(接收多个参数的方法)
2. 获取接口的实例
3. 接收用户的传递内容
4. 调用方法,传递内容,接收内容......
  • 另外在 chat 方法中,我们传入的是字符串类型,但是大模型其实接收的是 UserMessage 类型,并且返回的也是 AiMessage 类型

使用AiService

<!--        引入langChain4j 的高级功能依赖-->  
<dependencies>
    <dependency>  
        <groupId>dev.langchain4j</groupId>  
        <artifactId>langchain4j-spring-boot-starter</artifactId>  
    </dependency>  
</dependencies>
  • 第一种写法:自己手动创建对象
public interface Assistant {  
    String chat(String userMessage);  
}
@SpringBootTest  
public class AiServiceTest {  
  
    @Autowired  
    private QwenChatModel qwenChatModel;  
  
    @Test  
    public void testChat(){  
        Assistant assistant = AiServices.create(Assistant.class, qwenChatModel);  
        String answer = assistant.chat("你是谁");  
        System.out.println(answer);  
  
    }  
  
}
  • 第二种写法:springboot 写法
@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel")  
public interface Assistant {  
  
    String chat(String userMessage);  
  
}
@SpringBootTest  
public class AiServiceTest {  
	@Autowired  
	private Assistant assistant;  
	  
	@Test  
	public void SpringBootTest(){  
	    String answer = assistant.chat("你是谁");  
	    System.out.println(answer);  
	}
}

会话管理

  • 上面的两个操作让我们可以无缝衔接各大主流的 LLM,以及可以对交互的内容进行预设 prompt,但是有一点小小的bug
    • 每一轮的记忆是不关联的!所以我们得解决这个问题
    • 如果这个时候有两个用户进行使用,要如何区分会话?
  • 先看一下简易解决方式的伪代码,用来解决每一轮会话消息不关联的问题
1. 创建 ChatModel 的对象
2. 手动创建字符串为 UserMessage 对象
3. chat 方法发送消息,并且获取返回内容 AiMessage 对象
4. 手动创建第二次的字符串为 UserMessage 对象
5. chat 方法传入 第一次的 userMessage 以及 AiMessage 还有第二次的 UserMessage
6. 得到返回内容
  • 实现代码
@Autowired  
private QwenChatModel qwenChatModel; 

@Test  
public void QwenChatTest(){  
    UserMessage userMessage = UserMessage.userMessage("我是 蚯蚓无牙");  
    ChatResponse chat = qwenChatModel.chat(userMessage);  
    AiMessage aiMessage = chat.aiMessage();  
  
    System.out.println(aiMessage.text());  
  
    UserMessage userMessage2 = UserMessage.userMessage("我是谁?");  
    ChatResponse chat2 = qwenChatModel.chat(
	    Arrays.asList(userMessage, aiMessage, userMessage2)
    );  
  
    System.out.println(chat2.aiMessage().text());  
}
  • 可以看出弊端就是,随着对话轮数的增加,手动管理消息列表会变得非常繁琐、容易出错,并且难以应对多用户并发等复杂场景
  • 这个时候可以借用 LangChain4j 的 2MessageWindowChatMemory 来解决这个痛点了

实现上下文记忆

  • 伪代码
1. 创建 MessageWindowChatMemory 的实例对象,并且设置最大会话数
2. 使用 AiServices.builder 方法创建 AiService 实例
	a. 注入自定义的接口类和 ChatModel 对象
	b. 额外注入 ChatMemory 接口的实现类对象(例如 MessageWindowChatMemory 的实例),为其配置会话记忆
3. 发送消息
  • 实现代码
@Test  
public void QwenChatTest(){  
  
    MessageWindowChatMemory  chatMemory = MessageWindowChatMemory.withMaxMessages(10);  
  
    Assistant assitant = AiServices.builder(Assistant.class)  
            .chatLanguageModel(qwenChatModel)  
            .chatMemory(chatMemory)  
            .build();  
  
    System.out.println(assitant.chat("我是蚯蚓无牙"));  
  
    System.out.println(assitant.chat("我是谁"));  
}
  • 虽然上面的代码也成功实现了效果,但是我们是 Spring Boot 程序,而上面的代码并没有将 Assistant 实例放到 Spring 容器中,所以我们还是需要改造一下
  • 另外你是否好奇 withMaxMessages(10) 代表了什么? 它代表了你本次会话最大的会话次数,一旦超过那么就会形成数据结构中的 FIFO
  • 好了,接下来将 Assistant 实例放到 Spring 容器中,先创建一个配置类
@Configuration  
public class MemoryChatAssistantConfig {  
  
    @Bean  
    public ChatMemory chatMemory() {  
        return MessageWindowChatMemory.withMaxMessages(10);  
    }  
  
}
  • 通过上面的代码不难看出 MessageWindowChatMemory 一定是 ChatMemory 的实现类,那么这个 ChatMemory 接口是什么?
  • 这个 ChatMemory 是一个临时会话记忆,它会让你的大模型不至于说完马上就忘记
@AiService(  
        wiringMode = EXPLICIT,  
        chatModel = "qwenChatModel",  
        chatMemory = "chatMemory"  
  
)  
public interface MemoryChatAssistant {  
  
    String chat(String userMessage);  
  
}
@Autowired  
MemoryChatAssistant memoryChatAssistant;  
@Test  
public void QwenChatTest(){  
  
    System.out.println(memoryChatAssistant.chat("我是蚯蚓无牙"));  
  
    System.out.println(memoryChatAssistant.chat("我是谁"));  
}

实现会话隔离

  • 现在虽然解决了聊天记忆的问题,但是还有一个聊天隔离的问题。这个问题也是非常的好解决,只需要添加一个注解 @MemoryId 即可
@AiService(  
        wiringMode = EXPLICIT,  
        chatModel = "qwenChatModel",  
        chatMemoryProvider = "chatMemoryProvider"  
  
)  
public interface SeparateChatAssistant {  
  
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);  
  
}
@Configuration  
public class SeparateChatAssistantConfig {  
  
    @Bean  
    public ChatMemoryProvider chatMemoryProvider(){  
        return memoryId -> MessageWindowChatMemory.builder().id(memoryId).maxMessages(10).build();  
    }  
  
}
@Autowired  
SeparateChatAssistant separateChatAssistant;  
@Test  
public void SeparateChatTest(){  
  
    System.out.println(separateChatAssistant.chat(1,"我是蚯蚓无牙"));  
  
    System.out.println(separateChatAssistant.chat(1,"我是谁"));  
  
    System.out.println(separateChatAssistant.chat(2, "我是谁"));  
}

如何切换存储会话的存储方式

  • 想要搞懂这个问题,那就需要先知道它是存储在什么位置的

存储位置.png

  • 通过代码可以看到都是存储在 MessageWindowChatMemory 中的 ChatMemoryStore 类型属性中
  • 而这个 ChatMemoryStore 是有两个实现类的
    • InMemoryChatMemoryStore
    • SingleSlotChatMemoryStore

ChatMemoryStore的两个实现类.png

  • 而我们默认的存储方式在 SingleSlotChatMemoryStore,这个 SingleSlotChatMemoryStore 的存储方式是通过 List<ChatMessage> 进行存储的
  • 而 InMemoryChatMemoryStore 的存储方式是 Map<Object, List<ChatMessage>>

两个实现类的存储方式.png

  • 如果我们想要切换存储方式的话,也很简单
@Configuration  
public class SeparateChatAssistantConfig {  
  
    @Bean  
    public ChatMemoryProvider chatMemoryProvider(){  
        return memoryId -> MessageWindowChatMemory.builder()  
                .id(memoryId)  
                .maxMessages(10)  
                .chatMemoryStore(new InMemoryChatMemoryStore())  
                .build();  
    }  
  
}
  • 甚至我们可以自己去实现这个接口,然后构造我们的实现类

会话记忆VS会话仓库

  • 上面一共提到了两种会话记忆,一种是 ChatMemory 一种是 ChatMemoryStore,那么这两者的关系是什么?
    • 关系很简单,你要知道 ChatMemory 无论如何它是短期的,而真正存储记忆的是 ChatMemoryStore。
    • 而 ChatMemoryStore 被调用的时机除了在每次对话的时候,就是在读取历史消息的时候。
  • 如果是这样的话,其实还有一个疑问:既然会话记忆都是真正存储在 ChatMemoryStore 中的,那还要 ChatMemory 干嘛?
    • 职责:ChatMemory 只负责指定条数内的会话记忆,而 ChatMemoryStore 除了当前会话还可以跨会话
    • 性能:ChatMemory 负责本次会话的上下文,速度快。而 ChatMemoryStore 负责持久化的存储
    • 解耦合:方便让你自定义存储的方式吧....

持久化聊天记忆

  • 现在我们的会话都是存储在内存当中的,也就是说如果哪天我们的程序一不小心关闭了那么这些内容都会丢失。所以我们需要将它存储到硬盘当中。
  • 这边我们的存储数据库选择的是 MongoDB,为什么不选择 SQL 型数据库?
    • 因为 Ai 的数据通常具备下面的特征:非结构化或是结构化、数据结构多变、高并发高读写
    • 那为什么不选择 Redis? 因为 Redis 的数据是在内存中、数据结构简单、不擅长复杂查询

整合MongoDB

  • 引入依赖
<!-- Spring Boot Starter Data MongoDB -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-data-mongodb</artifactId>  
        </dependency>
  • 设置 MongoDB 的配置信息
spring:  
  data:  
    mongodb:  
      uri: mongodb://localhost:27017/chat_memory_db
  • 注意啊,chat_memory_db 会自动帮你创建,所以你不用自己去创建
  • 先看一下伪代码,梳理一下逻辑
1. 需要一个实体类去承接我们程序和存储数据中产生的交互
2. 需要创建一个类去实现 ChatMemoryStore 进行自定义存储
	1. 需要引入 MongoTemplate 
	2. 实现接口的三个方法
  • 创建实体类
@Data  
@AllArgsConstructor  
@NoArgsConstructor  
@Document("chat_messages")  
public class ChatMessages {  
    //唯一标识,映射到 MongoDB 文档的 _id 字段  
    @Id  
    private ObjectId messageId;  
  
    // 聊天会话id  
    private String memoryId;  
  
    //存储当前聊天记录列表的json字符串  
    private String content;  
}
  • 创建 MongoChatMemoryStore
public class MongoChatMemoryStore implements ChatMemoryStore {  
  
    @Override  
    public List<ChatMessage> getMessages(Object o) {  
        return List.of();  
    }  
  
    @Override  
    public void updateMessages(Object o, List<ChatMessage> list) {  
  
    }  
    @Override  
    public void deleteMessages(Object o) {  
  
    }}
  • 伪代码
1. 不管是增删改查,那么都需要操作存储数据的 MongoDB
	a. 自动注入 MongoDBTemplate 的实例对象交给 Spring 容器管理
2. 而想要将 MongoDB 的实例对象交给 Spring 容器,那么得确保你的类也是被容器所管理的
	a. 在类名上添加 @Component
3. 伪代码:查询
	a. 使用 MongoDB 提供的 Criteria 进行构建查询条件
	b. 使用 MongoDB 提供的 Query 进行组装出真正的查询语句
	c. 使用 MongoDBTemplate 的实例对象进行执行语句,并且规定返回的类型是我们定义的类类型
	d. 如果我们是第一次查询的话,那么会返回空指针异常,所以需要判断一下
		i. 如果有数据,那么就把它转换成 ChatMessage 类型
		ii. 如果没有数据,那么就返回空集合
  • 代码实现查询效果
@Component  
public class MongoChatMemoryStore implements ChatMemoryStore {  
  
    @Autowired  
    private MongoTemplate mongoTemplate;  
  
    @Override  
    public List<ChatMessage> getMessages(Object memoryId) {  
  
        Criteria criteria = Criteria.where("memoryId").is(memoryId);  
        Query query = new Query(criteria);  
  
        ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);  
  
        if (chatMessages != null){  
            String content = chatMessages.getContent();  
            return ChatMessageDeserializer.messagesFromJson(content);  
        }  
  
//        return List.of();  
        return new LinkedList<>();  
    }
}
  • 这边我把继承来的 List.of() 进行注释重写了,因为 List.of() 创建的是不可变的列表对象
  • 接下来看一下新增、更新的方法伪代码
1. 使用 Criteria 定义规则,然后使用 Query 进行组装完整的查询语句
2. 使用 Update 组装完整的更新语句
	a. 并且将 ChatMessage 类型的 list 集合进行转换成 json 格式存储
3. 使用 MongoDBTemplate 进行执行语句
  • 代码实现新增、更新代码
@Override  
public void updateMessages(Object memoryId, List<ChatMessage> list) {  
    Criteria criteria = Criteria.where("memoryId").is(memoryId);  
    Query query = new Query(criteria);  
      
    Update update = new Update();  
    update.set("content", ChatMessageSerializer.messagesToJson(list));  
  
    mongoTemplate.upsert(query, update, ChatMessages.class);  
}
  • 最后看一下删除的伪代码
1. 使用 Criteria 定义规则,使用 Query 进行组装完整的查询语句
2. 使用 MongoDBTemplate 进行执行语句
  • 实现代码
@Override  
public void deleteMessages(Object memoryId) {  
    Criteria criteria = Criteria.where("memoryId").is(memoryId);  
    Query query = new Query(criteria);  
    mongoTemplate.remove(query, ChatMessages.class);  
}
  • 最后更换一下底层的存储方式
@Configuration  
public class SeparateChatAssistantConfig {  
    @Autowired  
    private MongoChatMemoryStore mongoChatMemoryStore;  
      
  
    @Bean  
    public ChatMemoryProvider chatMemoryProvider(){  
        return memoryId -> MessageWindowChatMemory.builder()  
                .id(memoryId)  
                .maxMessages(10)  
//                .chatMemoryStore(new InMemoryChatMemoryStore())  
                .chatMemoryStore(mongoChatMemoryStore)  
                .build();  
    }  
  
}
  • 然后去看一下 MongoDB 的数据,成功

MongoDB数据.png

提示词

系统提示词

  • 想让一款 Agent 程序落地到实际应用,最好的方法就是垂直于领域,而这样的话我们就需要给大语言模型设置它的身份,明确它的能力范围
  • 直接在接口的方法上使用 @SystemMessage 注解即可
@AiService(  
        wiringMode = EXPLICIT,  
        chatModel = "qwenChatModel",  
        chatMemoryProvider = "chatMemoryProvider"  
  
)  
public interface SeparateChatAssistant {  
   
    @SystemMessage("你是我的网友,网名叫做Rum_0m,喜欢男生,你的网名简称是0m,今天是{{current_date}}")  
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);  
  
}
  • 这个 current_date 是 LangChain4j 的占位符,用来获取当前的日期和时间的
  • 可以看一下,现在 MongoDB 中的存储数据是什么样子的

系统提示词.png

  • 可以看到系统提示词只有一条(并不代表只发送这一次!),就好像我们在和 Ai 进行演戏一样,系统提示词就是背景,虽然只会出现这一次。但是这个系统提示词却是贯穿全部,并且会出现到开始的地方
  • 那如果这个时候,我们修改了系统提示词,会怎么样?
@AiService(  
        wiringMode = EXPLICIT,  
        chatModel = "qwenChatModel",  
        chatMemoryProvider = "chatMemoryProvider"  
  
)  
public interface SeparateChatAssistant {  
   
    @SystemMessage("你是我的网友,网名叫做Rum_0m,不喜欢女生,你的网名简称是0m,今天是{{current_date}}")  
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);  
  
}
  • 看效果

修改系统提示词.png

  • 你会发现,大模型自动的遗忘了系统提示词前面的对话。同时 idea 也会给你一个警告

修改系统提示词带来的警告.png

  • 简单的理解:系统提示词应该在第一条,但是你没有遵守这个规范,因此将会忽略系统提示词前面的消息
  • 实际的业务场景中,prompt 是非常长的,所以我们可以写在文件中再进行引用
@AiService(  
        wiringMode = EXPLICIT,  
        chatModel = "qwenChatModel",  
        chatMemoryProvider = "chatMemoryProvider"  
  
)  
public interface SeparateChatAssistant {  
  
    @SystemMessage(fromResource = "my-prompt-test.txt")  
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);  
  
}
  • 文件放在 resource 文件中
你是我的网友,网名叫做Rum_0m,喜欢男生,你的网名简称是0m  
今天是{{current_date}}

用户提示词

  • 用户提示词,只能用在一个参数的情况下
@AiService(  
        wiringMode = EXPLICIT,  
        chatModel = "qwenChatModel",  
        chatMemory = "chatMemory"  
  
)  
public interface MemoryChatAssistant {  
  
    @UserMessage("你是我的网上好友,{{it}}")  
    String chat(String userMessage);  
  
}
  • 调试代码进行查看

用户提示词.png

自定义占位符

  • 前面,我们了解到了系统内部的占位符 {{current_date}} 和 {{it}} ,那么我们如何自定义自己的占位符?
  • 或是说用户提示词只可以用在一个参数的时候,那如果这个时候我们有多个参数又想要使用用户提示词呢?
@SystemMessage("你是我的好朋友。请用粤语回答我的问题。{{message}}")  
String chat2(@MemoryId int memoryId,@UserMessage @V("message") String userMessage);
  • 当然这个注解也是可以用到外部文件的
@SystemMessage(fromResource = "my-prompt-test2.txt")  
String chat3(  
        @MemoryId int memoryId,  
        @UserMessage String userMessage,  
        @V("username") String username,  
        @V("age") int age  
);
  • 外部文件
你是我的好朋友,我是{{username}},我的年龄是{{age}},请用粤语回答问题,回答问题的时候适当添加表情  
符号。  
今天是 {{current_date}}。

调用工具

  • 大模型不适合数学运算,所以我们可以提供一个数学计算的工具给它,这样当我们提出数学问题的时候,大模型就会判断是否使用

Function Calling 函数调用

  • Function Calling 也叫做 Tools 工具
  • 先理一下逻辑
1. 在 tools 包中创建 CalculatorTools 类
2. 在类上添加 @Component 注解,交给 Spring 容器管理
3. 使用 @Tool 注解标记方法
4. 将工具类配置到大语言模型中
5. 和大模型对话,让大模型自动调用
  • 代码实现,CalculatorTools 类
package com.s3cd.java.ai.langchain4j.tools;  
  
import dev.langchain4j.agent.tool.Tool;  
import org.springframework.stereotype.Component;  
  
@Component  
public class CalculatorTools {  
  
    @Tool  
    double sum(double a, double b) {  
        System.out.println("调用加法运算");  
        return a + b;  
    }  
    @Tool  
    double squareRoot(double x) {  
        System.out.println("调用平方根运算");  
        return Math.sqrt(x);  
    }  
  
}
  • 配置到模型中
@AiService(  
        wiringMode = EXPLICIT,  
        chatModel = "qwenChatModel",  
        chatMemoryProvider = "chatMemoryProvider",  
        tools = "calculatorTools"  
  
)  
public interface SeparateChatAssistant {  
  
    @SystemMessage(fromResource = "my-prompt-test.txt")  
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);  
  
    @SystemMessage("你是我的好朋友。请用粤语回答我的问题。{{message}}")  
    String chat2(@MemoryId int memoryId,@UserMessage @V("message") String userMessage);  
  
    @SystemMessage(fromResource = "my-prompt-test2.txt")  
    String chat3(  
            @MemoryId int memoryId,  
            @UserMessage String userMessage,  
            @V("username") String username,  
            @V("age") int age  
    );  
}
  • 和大模型对话,让大模型自己调用
@SpringBootTest  
public class ToolsTest {  
  
    @Autowired  
    private SeparateChatAssistant separateChatAssistant;  
  
    @Test  
    public void testCalculatorTools() {  
        System.out.println(separateChatAssistant.chat(11,"1+1 和 6878797754242的平方根是多少?"));  
    }  
  
}
  • 可以看到这些工具都是大模型进行调用的

工具的调用.png

其他注解

  • 想过一个问题没有,大模型是怎么知道在哪一步调用哪个工具的?
  • 其实是通过方法的名字进行判断的,而有的时候我们的方法名可能没办法很好的表达出我们的逻辑,那么可以借用 @Tool 注解中的 name 和 value
@Tool(name = "加法运算",value = "计算a和b两个数的相加结果")  
double sum(double a, double b) {  
    System.out.println("调用加法运算");  
    return a + b;  
}
  • 而如果,我们想要给参数添加说明的话使用注解 @P,它也有两个参数添加说明的是 value,还有一个是代表着这个参数是否必须 required
@Tool(name = "加法运算",value = "计算a和b两个数的相加结果")  
double sum(  
        @P(value = "加数1",required = true) double a,  
        @P(value = "加数2")double b) {  
    System.out.println("调用加法运算");  
    return a + b;  
}
  • 那如果说我们需要在调用工具的时候也传入我们的聊天记忆 id 的话,那么就可以使用@MemoryId 这个注解
@Tool(name = "加法运算",value = "计算a和b两个数的相加结果")  
double sum(  
        @MemoryId int memoryId,  
        @P(value = "加数1",required = true) double a,  
        @P(value = "加数2")double b) {  
    System.out.println("调用加法运算");  
    return a + b;  
}

Footnotes

  1. LangChain4j:

    • 官网地址:docs.langchain4j.dev/
    • 它是专门为 Java 开发者设计的一个库,它的核心作用就是让开发者可以更加轻松、简单的对接市面上的大语言模型,让我们不需要从零开始去对接每一个模型的Api
    • 核心功能:
      • 连接不同的 LLM:它可以方便地接入各种主流的 LLM 提供商(比如 OpenAI, Google, Anthropic 等),让你可以在不同模型之间切换,而无需修改太多代码
      • 创建“链”(Chains):这是 LangChain 的核心概念之一。你可以把一系列操作(比如:先提取信息,然后用提取的信息问LLM,再把LLM的回答进行格式化)串联起来,形成一个工作流
      • 管理记忆(Memory):在对话应用中,记住之前的对话非常重要。LangChain4j 提供了管理对话历史的机制,让 LLM 知道上下文
      • 使用嵌入(Embeddings)和向量存储(Vector Stores):这允许你的应用理解文本的“含义”,并能从大量文本中快速找到相关信息,比如构建一个基于文档问答的机器人
      • 调用工具(Tools):你可以给 LLM 提供一些“工具”,比如搜索网页的功能、调用计算器的功能、查询数据库的功能等,让 LLM 不仅仅局限于它自己的训练数据
  2. MessageWindowChatMemory:

    • 是 LangChain4j 中一个非常常用的会话记忆 ChatMemory 接口的实现类
    • 它用来记住你和大语言模型之间的对话历史,但是它只保留“窗口”内的最新消息
    • 当我们设置为10时,它会记住我们和LLM最近消息的10条记录(UserMessage 和 AiMessage 都算!)