LangChain4j Prompt提示词工程
未完待续
引言
之前,使用Spring AI对接大模型实现了对话机器人的功能:Spring AI实现一个简单的对话机器人,spring-boot与langchain4j整合可以实现同样的功能。
spring-boot与langchain4j整合,可以采用集成底层API(popular integrations)的方式,也有集成高层API(declarative AI Services)的方式,这里先后使用底层和高层API进行集成和测试。
1.底层API实现对话
引入spring-boot 3.5.4,langchain4j-bom。截至目前,官网上langchain4j-bom的最高版本是1.8.0,均需要jdk17+
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.8.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>以对接OpenAI及支持该协议的大模型为例,添加底层API依赖langchain4j-open-ai-spring-boot-starter
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
</dependency>
<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>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>1.1 阻塞式ChatModel
使用OpenAI协议对接DeepSeek大模型,更多详细的模型参数介绍见:https://docs.langchain4j.dev/tutorials/model-parameters
langchain4j:
open-ai:
chat-model:
base-url: https://api.deepseek.com
api-key: ${OPEN_API_KEY}
model-name: deepseek-reasoner
log-requests: true
log-responses: true
return-thinking: true
server:
port: 8080
logging:
level:
dev.langchain4j: debug #需要设置日志级别有些配置项不支持填写在配置文件,因此还可以通过配置类进行配置
package org.example.config;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LangChainConfig {
public ChatModel chatModel() {
return OpenAiChatModel.builder()
.baseUrl("https://api.deepseek.com")
.apiKey(System.getProperty("OPEN_API_KEY"))
.modelName("deepseek-reasoner")
.maxRetries(3)
.logRequests(true)
.logResponses(true)
.returnThinking(true)
.build();
}
}然后可以直接使用ChatModel实现Prompt对话,并返回消耗的Token数,ChatModel是一种阻塞式的API,需要等待大模型回复完成将结果一次性返回
package org.example.controller;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.output.TokenUsage;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("chat")
@Slf4j
public class ChatController {
@Resource
private ChatModel chatModel;
@GetMapping("chat")
public String chat(String msg) {
List<ChatMessage> messages = Arrays.asList(
SystemMessage.from("你是一个数学老师,用简单易懂的方式解释数学概念。"),
UserMessage.from(msg)
);
ChatResponse chatResponse = chatModel.chat(messages);
TokenUsage tokenUsage = chatResponse.tokenUsage();
log.info("token usage: {}", tokenUsage);
return chatResponse.aiMessage().text();
}
}
1.2 流式StreamingChatModel
StreamingChatModel是一种非阻塞式的API,不需要等待大模型回复完成将结果一次性返回,而是实时返回大模型生成的片段,直到全部返回。
pom.xml中新增支持流式返回的依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
</dependency>配置文件application.yml需要新增流式的streaming-chat-model配置
langchain4j:
open-ai:
streaming-chat-model:
base-url: https://api.deepseek.com
api-key: ${OPEN_API_KEY}
model-name: deepseek-reasoner
log-requests: true
log-responses: true
return-thinking: true同样可以通过配置类进行配置
package org.example.config;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LangChainConfig {
@Bean
public StreamingChatModel chatModel() {
return OpenAiStreamingChatModel.builder()
.baseUrl("https://api.deepseek.com")
.apiKey(System.getProperty("OPEN_API_KEY"))
.modelName("deepseek-reasoner")
.logRequests(true)
.logResponses(true)
.returnThinking(true)
.build();
}
}流式API是由StreamingChatModel类来实现,在web环境下,需要配合Spring的Flux来使用,在下面方法回调触发时调用相应的Flux的方法,像Spring AI那样将Flux对象返回。
onPartialResponse实时返回大模型生成的片段,调用sink.next()实时输出到浏览器onPartialThinking实时返回大模型推理过程,调用sink.next()实时输出到浏览器onCompleteResponse大模型生成完成,调用sink.complete()结束流的输出,还可以对消耗的token进行统计onError出错,记录错误信息,调用sink.complete()结束流的输出
package org.example.controller;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.*;
import dev.langchain4j.model.output.TokenUsage;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("chat")
@Slf4j
public class StreamController {
@Resource
private StreamingChatModel streamingChatModel;
@GetMapping(value = "streaming", produces = "text/html; charset=utf-8")
public Flux<String> streaming(String msg) {
List<ChatMessage> messages = Arrays.asList(
SystemMessage.from("你是一个数学老师,用简单易懂的方式解释数学概念。"),
UserMessage.from(msg)
);
return Flux.create(sink -> {
streamingChatModel.chat(messages, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {
sink.next(partialResponse.text());
}
@Override
public void onPartialThinking(PartialThinking partialThinking) {
sink.next("<span style='color:red;'>" + partialThinking.text() + "</span>");
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
TokenUsage tokenUsage = completeResponse.tokenUsage();
log.info("token usage: {}", tokenUsage);
sink.complete();
}
@Override
public void onError(Throwable error) {
error.printStackTrace();
sink.complete();
}
});
});
}
}2.高层API实现对话
使用高层API,需要在底层API基础上,额外引入这个依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>2.1 阻塞式对话
新建一个接口,将调用大模型的方法声明在里面,方法的第一个参数默认就是UserMessage
package org.example.ai;
public interface AiAssistant {
String chat(String prompt);
}langchain4j提供了一些消息注解对高级API接口内方法进行设定
@SystemMessage指明系统提示词,可以从类路径下读取文本文件@UserMessage预先指明用户提示词的固定部分,也可以从类路径下读取文本文件,会和后续调用方法时传入的用户提示词进行拼接替换,因此需要通过{{it}}的固定写法对用户传入的提示词进行占位,如果不想写成{{it}},则需要@V注解更换展位的字符
package org.example.ai;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import reactor.core.publisher.Flux;
public interface AiAssistant {
// 系统提示词
@SystemMessage("你是一个数学老师,用简单易懂的方式解释数学概念。")
// @SystemMessage(fromResource = "1.txt") 基于工程类路径查找
Flux<String> teacher(String prompt);
// 用户提示词
@UserMessage("你是一个数学老师,用简单易懂的方式解释数学概念。{{it}}")
//@UserMessage(fromResource = "1.txt") 基于工程类路径查找
Flux<String> check(String prompt);
@UserMessage("你是一个数学老师,用简单易懂的方式解释数学概念。{{msg}}")
Flux<String> chat3(@V("msg") String prompt);
}配置类中,通过AiServices类将刚刚定义的AiAssistant注入容器,并注入之前定义好的ChatModel对象到AiAssistant
package org.example.config;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;
import org.example.ai.AiAssistant;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LangChainConfig {
@Bean
public AiAssistant aiAssistant(ChatModel chatModel) {
return AiServices.builder(AiAssistant.class)
.chatModel(chatModel)
.build();
}
}
然后直接注入AiAssistant到对应类,并调用方法即可
package org.example.controller;
import jakarta.annotation.Resource;
import org.example.ai.AiAssistant;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("high-chat")
public class HighChatController {
@Resource
private AiAssistant aiAssistant;
@GetMapping("chat")
public String chat(String msg) {
return aiAssistant.chat(msg);
}
}实际上,高层API可以使用接口类加注解的方式进行配置,通过@AiService注解标注为操作大模型的接口类,会直接被实例化,无需在配置类中再去通过AiServices.builder进行实例化
package org.example.ai;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
@AiService(
//如需手动配置模型,需要设置属性:AiServiceWiringMode.EXPLICIT
wiringMode = AiServiceWiringMode.EXPLICIT,
//如需手动配置模型,要指定具体使用哪个模型,例如:chatModel = "deepseek"
chatModel = "chatModel"
)
public interface AiAssistant {
String chat(String prompt);
}2.2 流式对话
- 同底层API的流式一样,也要引入langchain4j-reactor依赖
- 同样需要先将一个StreamingChatModel的对象注入容器
@AiService注解中大模型属性名使用streamingChatModel,然后调用StreamAssistant的方法即可,Controller中可以直接将Flux对象返回
package org.example.ai;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
import reactor.core.publisher.Flux;
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,
streamingChatModel = "streamingChatModel"
)
public interface StreamAssistant {
Flux<String> chat(String prompt);
}@Resource
private StreamAssistant streamAssistant;
@GetMapping(value = "chat", produces = "text/html; charset=utf-8")
public Flux<String> chat(String msg) {
return streamAssistant.chat(msg);
}3.对话记忆ChatMemory
关于会话记忆的概念等,已经在:Spring AI实现一个简单的对话机器人一文中讲到。
先明确langchain4j中的两个概念,记忆和历史
历史(History) 历史记录会完整保存用户与人工智能之间的所有消息。历史记录就是用户在用户界面中看到的内容,它代表了实际发生过的所有对话。
记忆(Memory) 保留一些信息,这些信息会呈现给LLM,使其表现得好像“记住”了对话。记忆与历史记录截然不同。根据所使用的内存算法,它可以以各种方式修改历史记录:例如,删除一些消息、汇总多条消息、汇总单个消息、移除消息中不重要的细节、向消息中注入额外信息(用于RAG算法)或指令(用于结构化输出)等等。
langchain4j目前仅提供记忆管理,不提供历史记录管理。如需要保留完整的历史记录,要手动操作。
langchain4j通过ChatMemory实现记忆缓存,因为一段长对话含有的信息很多,如果不加以修剪,会产生很多冗余,甚至超过一次对话的Token大小限制,因此langchain4j对ChatMemory设计了两种实现:
- MessageWindowChatMemory 一个比较简单的实现,作为一个滑动窗口,只保留最近的N多个记录
- TokenWindowChatMemory 保留最近的N多个Token,通过TokenCountEstimator计算会话的令牌数
3.1 底层API实现对话记忆
这里以MessageWindowChatMemory为例,配置类中新增配置
package org.example.config;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import dev.langchain4j.store.memory.chat.InMemoryChatMemoryStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LangChainConfig {
/**
* 采用内存存储
*/
@Bean
public ChatMemoryStore chatMemoryStore() {
return new InMemoryChatMemoryStore();
}
/**
* ChatMemoryProvider类,每次根据不同对话ID生成专属的ChatMemory对象
*/
@Bean
public ChatMemoryProvider chatMemoryProvider () {
return new ChatMemoryProvider() {
@Override
public ChatMemory get(Object id) {
return MessageWindowChatMemory.builder()
.id(id)
.maxMessages(1000)
.chatMemoryStore( chatMemoryStore() )
.build();
}
};
}
}存储会话采用的InMemoryChatMemoryStore仅仅将会话保存到内存,用于测试,实际场景应该将会话持久化保存到数据库中,因此实际项目中需要自行实现一个ChatMemoryStore接口的实现类来保存会话内容
Controller中,注入ChatMemoryProvider对象,将和大模型的对话改造升级为支持记忆的
每次对话,将用户提问和大模型回答都进行保存,关联到同一个会话ID
package org.example.controller;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.*;
import dev.langchain4j.model.output.TokenUsage;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("memory-chat")
@Slf4j
public class MemoryController {
@Resource
private StreamingChatModel streamingChatModel;
@Resource
private ChatMemoryProvider chatMemoryProvider;
@GetMapping(value = "streaming", produces = "text/html; charset=utf-8")
public Flux<String> streaming(String msg, String msgId) {
// 将问题保存到当前对话记忆
ChatMemory chatMemory = chatMemoryProvider.get(msgId);
chatMemory.add(UserMessage.from(msg));
return Flux.create(sink -> {
streamingChatModel.chat(chatMemory.messages(), new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {
sink.next(partialResponse.text());
}
@Override
public void onPartialThinking(PartialThinking partialThinking) {
sink.next("<span style='color:red;'>" + partialThinking.text() + "</span>");
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
TokenUsage tokenUsage = completeResponse.tokenUsage();
log.info("token usage: {}", tokenUsage);
// 大模型回答完毕,将大模型的回答也添加进当前对话记忆
AiMessage aiMessage = completeResponse.aiMessage();
chatMemory.add(aiMessage);
sink.complete();
}
@Override
public void onError(Throwable error) {
error.printStackTrace();
sink.complete();
}
});
});
}
}3.2 高层API实现对话记忆
高层API实现对话记忆,首先接口类的方法要标注一个消息ID@MemoryId String msgId,其次接口方法如果不止一个参数则需要将用户提示词通过@UserMessage注解标注。然后@AiService注解通过属性chatMemoryProvider = "chatMemoryProvider"关联我们之前在配置类声明的chatMemoryProvider对象
package org.example.ai;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
import reactor.core.publisher.Flux;
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,
streamingChatModel = "streamingChatModel",
chatMemoryProvider = "chatMemoryProvider"
)
public interface StreamAssistant {
@SystemMessage("你是一个数学老师,用简单易懂的方式解释数学概念。")
Flux<String> chat(@UserMessage String prompt, @MemoryId String msgId);
}4.监听
langchain4j允许对大模型调用和返回等进行监控(Observability),并适时触发回调方法,详见:https://docs.langchain4j.dev/tutorials/observability/
新建一个监听器,名字叫MyChatModelListener,实现langchain4j的ChatModelListener接口方法,在发送请求前,得到响应后以及调用出错时触发回调去执行一些代码,而且支持在之间传输自定义的属性,因此可以生成一个traceId供我们调试某次对话。
package org.example.listener;
import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
import dev.langchain4j.model.chat.listener.ChatModelListener;
import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Slf4j
public class MyChatModelListener implements ChatModelListener {
@Override
public void onRequest(ChatModelRequestContext requestContext) {
String traceId = UUID.randomUUID().toString();
requestContext.attributes().put("traceId", traceId);
log.info("********** 请求参数 {} {} ********", traceId, requestContext);
}
@Override
public void onResponse(ChatModelResponseContext responseContext) {
Object traceId = responseContext.attributes().get("traceId");
log.info("********** 响应结果 {} {} ********", traceId, responseContext);
}
@Override
public void onError(ChatModelErrorContext errorContext) {
log.info("********** 请求异常 {} ********", errorContext );
}
}
配置类中其他地方都不变,只需要新配置一个listeners属性即可
@Resource
private ChatModelListener chatModelListener;
@Bean
public ChatModel streamingChatModel() {
return OpenAiChatModel.builder()
.baseUrl("https://api.deepseek.com/")
.apiKey(System.getProperty("OPEN_API_KEY"))
.modelName("deepseek-reasoner")
.logRequests(true)
.logResponses(true)
.returnThinking(true)
.listeners(Collections.singletonList(chatModelListener))
.build();
}如果采用yml配置大模型,MyChatModelListener注入容器后会自动生效,无需在yml中配置,也无法配置
该功能在未来版本可能会有变化
"如果文章对您有帮助,可以请作者喝杯咖啡吗?"
微信支付
支付宝