LangChain4j
笔记主要聚焦于在 SpringBoot 框架下使用 LangChain4J
ChatModel¶
ChatModel 作为 LangChain4j 提供的直接和模型交互的底层,最基本地提供了一个 chat 方法,入参可以为SystemMessage 和 UserMessage,返回 ChatResponse
模型配置¶
首先引入 spring-boot-starter 依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
<version>1.1.0-beta7</version>
</dependency>
在 application.yml 中配置使用的模型
langchain4j.community.dashscope.* 是被 langchain4j 的 SpringBoot Starter 识别的(它内部有对应的 @ConfigurationProperties 自动装配)
该 Starter 会读取配置前缀 langchain4j.community.dashscope.chat-model下的属性,随后创建一个 ChatModel bean
AiServices¶
AI Services 作为 LangChain4J 的一种基本的开发模式,基于 AiServices 类提供了一套高度抽象的 API,本质上就是在进行 LLM 输入和输出的检查和转换
基本使用¶
引入 LangChain4j 的基本依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.1.0</version>
</dependency>
随后可以创建一个 interface 来声明一些基本的方法,特别地,可以通过注解来规定 system prompt
public interface AiChatService {
@SystemMessage(fromResource = "prompt.txt") // 文件在 /src/resources
String chat(String userMessage);
}
为了将接口类型的对象放到 bean 中,供其他类进行依赖注入,可以创建一个配置类,用 @Bean 来将相关方法的返回值(一个动态代理对象)放到 bean 中:
@Configuration
public class AiChatServiceFactory {
@Resource
private ChatModel chatModel;
@Bean
public AiChatService aiCodeHelperService() {
return AiServices.create(AiCodeHelperService.class, qwenChatModel);
}
}
动态代理¶
上述代码中的 AiServicces.create 底层调用的 build 方法会拦截 chat 方法(事实上是传入的接口的所有方法),特别地,当方法上有 @SystemMessage 注解或者参数为单一的 String 类型时,参数会被封装为 UserMessage 发送给 LLM
所以,即使不实现接口方法,由于 LangChain4j 本身提供的 build 方法返回了一个运行时生成的动态代理对象,所以后续通过依赖注入调用 chat 方法也可以直接和模型通信并拿到返回内容
ChatMemory¶
LangChain4j 提供了一个会话记忆类 MessagWindowChatMemory,封装了用户级会话上下文历史管理功能
基本的实现思路是使用一个 map 来记录 uid 和 context 的映射,在后一条对话的请求信息中携带历史 context
基本使用¶
MessagWindowChatMemory 作为滑动窗口运行,保留最新的 N 条消息(注意到 LLM 可以接受的上下文的 token 数量是有限的)
我们提供的 interface 中的 chat 方法需要以注解的形式再接收一个 memoryId 来隔离不同用户之间的会话:
public interface AiChatService {
@SystemMessage(fromResource = "prompt.txt")
String chat(@MemoryId int memoryId, @UserMessage String msg);
}
相应地,配置类的放置方法需要进行修改,即提供一个接受 memoryId 为参数的创建内存的方法:
@Configuration
public class AiChatServiceFactory {
@Resource
private ChatModel chatModel; // qwen
@Bean
public AiChatService aiChatService() {
return AiServices.builder(AiChatService.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(20))
.build();
}
}
持久化存储¶
默认情况下,memory 存储在内存中
如果需要进行持久化,可以实现 ChatMemoryStore 接口,重写三个方法,以 id 为单位来存储 Memory
class PersistentChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object memoryId) {
// TODO: Implement getting all messages from the persistent store by memory ID.
// ChatMessageDeserializer.messageFromJson(String) and
// ChatMessageDeserializer.messagesFromJson(String) helper methods can be used to
// easily deserialize chat messages from JSON.
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
// TODO: Implement updating all messages in the persistent store by memory ID.
// ChatMessageSerializer.messageToJson(ChatMessage) and
// ChatMessageSerializer.messagesToJson(List<ChatMessage>) helper methods can be used to
// easily serialize chat messages into JSON.
}
@Override
public void deleteMessages(Object memoryId) {
// TODO: Implement deleting all messages in the persistent store by memory ID.
}
}
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.id("12345")
.maxMessages(10)
.chatMemoryStore(new PersistentChatMemoryStore())
.build();
存储结构¶
每个 memoryId 下主要存储的内容为 UserMessage 和 AiMessage,均为 String text
RAG¶
LangChain4J 提供了检索增强能力,通过匹配向量化的外部文件与向量化的 user text 来增强 LLM 输入
使用方式¶
基本的思路分为两步:
- 将开发者提供的外部文档进行向量化,存储到内存 / 向量数据库中,供后续检索。对于预先给定的外部文档,这个工作可以在 Bean 初始化时进行(如下例)
- 配置 embedding 模型,包括用于外部文档和用户提问两个 text 的模型。需要严格保证两个模型生成的向量维度相同,一般使用同一种模型
首先在 application.yml 中增加对于 embedding 模型的配置:
spring:
application:
name: AIChat-LC4J
langchain4j:
community:
dashscope:
chat-model:
model-name: qwen-max
api-key:
embedding-model:
model-name: text-embedding-v4
api-key:
随后编写自动装配相关逻辑。特别地,
- 在外部文档分割时,可以自定义文档分段的最大字符数和重叠字符数
- 在 Embedding 创建时(EmbeddingStoreIngestor),可以加入元数据等再向量化
- 在 Retriever 创建时(EmbeddingStoreContentReriever),可以增加阈值过滤、metadata 过滤、关键字筛选等
@Configuration
public class RagConfig {
@Resource
private EmbeddingModel embeddingModel;
@Bean
public ContentRetriever contentRetriever(){
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
// 读文件进内存
List<Document> docs = FileSystemDocumentLoader.loadDocuments("src/main/resources/rag");
// 定义分割器
DocumentByParagraphSplitter documentByParagraphSplitter = new DocumentByParagraphSplitter(1000, 200);
// 建造 ingestor
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(documentByParagraphSplitter)
.textSegmentTransformer(segment -> {
return TextSegment.from(segment.metadata().getString("file_name") + "\n" + segment.text(), segment.metadata());
})
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
// 将文件转为分块的向量存到内存中
ingestor.ingest(docs);
return EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
// .minScore(0.8) // 相似度阈值
.maxResults(3)
.build();
}
}
上述过程除了在项目的初始化阶段执行实际的外部文档向量化操作,还返回了后续用于检索对比的 ContentRetriever
随后,需要在 AiServices 中注册 ContentRetriever,从而真正启用 RAG
@Configuration
public class AiChatServiceFactory {
@Resource
private ChatModel chatModel; // qwen
@Resource
private ContentRetriever contentRetriever;
@Bean
public AiChatService aiChatService() {
return AiServices.builder(AiChatService.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(20))
.contentRetriever(contentRetriever)
.build();
}
}
这样最后动态代理得到的对象的相关方法就拥有了 RAG 功能
在业务层进行调用时,还可以拿到引用的外部文本的位置信息等元数据:
这里通过动态代理生成的代理对象还提供了格式化输出的功能,使 chat 方法可以拿到 Result 对象
void chatWthRag() {
Result<String> result = aiChatService.chat(100, "我想知道有哪些常见的面试题");
System.out.println(result.content());
System.out.println(result.sources());
}
流程分析¶
在调用了实际的 chat 方法(见上)时,在使用 Inmemory 向量数据库时,核心的调用就是 LangChain4J 的 InmemoryEmbeddingStore 类重写的 EmbeddingStore 接口的 search 方法:
Embedded 原始对象类型,Embedding 浮点向量
@Override
public EmbeddingSearchResult<Embedded> search(EmbeddingSearchRequest embeddingSearchRequest) {
Comparator<EmbeddingMatch<Embedded>> comparator = comparingDouble(EmbeddingMatch::score);
PriorityQueue<EmbeddingMatch<Embedded>> matches = new PriorityQueue<>(comparator);
Filter filter = embeddingSearchRequest.filter();
for (Entry<Embedded> entry : entries) {
if (filter != null && entry.embedded instanceof TextSegment) {
Metadata metadata = ((TextSegment) entry.embedded).metadata();
if (!filter.test(metadata)) {
continue;
}
}
double cosineSimilarity = CosineSimilarity.between(entry.embedding, embeddingSearchRequest.queryEmbedding());
double score = RelevanceScore.fromCosineSimilarity(cosineSimilarity);
if (score >= embeddingSearchRequest.minScore()) {
matches.add(new EmbeddingMatch<>(score, entry.id, entry.embedding, entry.embedded));
if (matches.size() > embeddingSearchRequest.maxResults()) {
matches.poll();
}
}
}
List<EmbeddingMatch<Embedded>> result = new ArrayList<>(matches);
result.sort(comparator);
Collections.reverse(result);
return new EmbeddingSearchResult<>(result);
}
此方法目的是截取有序的余弦相似度最高(方向的相似度,忽略长度,点积除以长度积)的一组外部文本库中的向量
至此,在拿到目标的外部文本数据之后,再加上同一个 memoryId 下的前 N 条历史信息,全部放到一起作为一次 LLM api 调用时的 UserMessage,然后结合 SystemMessage 得到 ChatRequest
Function Call¶
LLM 通过选择需要调用的方法,以及参数,来调用服务端提供的方法
基本使用¶
首先定义 Tools
- @Tool 中,通过 value 定义针对方法提示语
- 在定义 tool func 的参数时,通过 @P 的 value 定义针对参数的提示语
public class CalculateTool {
@Tool(name = "multiplier", value = "used for multiply two numbers")
public int multiplier(@P(value = "number1") int number1, @P(value = "number2") int number2) {
return number1 * number2;
}
}
随后需要将这个工具注册到功能链中:
@Configuration
public class AiChatServiceFactory {
@Resource
private ChatModel chatModel; // qwen
@Resource
private ContentRetriever contentRetriever;
@Bean
public AiChatService aiChatService() {
return AiServices.builder(AiChatService.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(20))
.contentRetriever(contentRetriever)
.tools(new CalculateTool())
.build();
}
}
流程分析¶
在以计算器作为 tool 的例子中,在业务层发起一次 chat 调用时,涉及到两次与模型的通信
- 首先是在第一次请求时,带上 tools 列表以及 @Tools 注解中的工具提示,在向 LLM 提问的同时展示可用的工具名与参数列表,此时得到的 response1 实际上为 toolExecutionRequest,即 LLM 对于工具的选择,以及构造的参数
- 随后是工具执行完成后,再将执行的结果返回给 LLM,此时 LLM 选择直接返回 text 结果
可以注意到,Response1 中的 AiMessage 被 LangChain4J 的适配器解析为 ToolExecutionRequest 类型,LLM 返回了需要调用的方法及参数
Request 1:
- messages:
- UserMessage:
- text: What is the square root of 475695037565?
- tools:
- sum(double a, double b): Sums 2 given numbers
- squareRoot(double x): Returns a square root of a given number
Response 1:
- AiMessage:
- toolExecutionRequests:
- squareRoot(475695037565)
... here we are executing the squareRoot method with the "475695037565" argument and getting "689706.486532" as a result ...
Request 2:
- messages:
- UserMessage:
- text: What is the square root of 475695037565?
- AiMessage:
- toolExecutionRequests:
- squareRoot(475695037565)
- ToolExecutionResultMessage:
- text: 689706.486532
Response 2:
- AiMessage:
- text: The square root of 475695037565 is 689706.486532.
MCP¶
LangChain4J 提供了 MCP Client 的 sdk,作为 LLM 和 MCP Server 的中间层
基本使用¶
首先引入 MCP Client 的依赖
<!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j-mcp -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>1.1.0-beta7</version>
</dependency>
随后在 yml 中配置 MCP Server,这里使用 BigModel 部署的 web search server
spring:
application:
name: AIChat-LC4J
langchain4j:
community:
dashscope:
chat-model:
model-name: qwen-max
api-key:
embedding-model:
model-name: text-embedding-v4
api-key:
bigmodel:
api-key:
随后定义 McpToolProvider,本质上就是配置传输层的 McpTransport
由于 BigModel 的 server 用的是 sse 协议,所以这里设置的是 sseUrl
@Configuration
public class McpConfig {
@Value("${bigmodel.api-key}") // 从 springboot environment 取值注入
private String apiKey;
@Bean
public McpToolProvider mcpToolProvider() {
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("https://open.bigmodel.cn/api/mcp/web_search/sse?Authorization=" + apiKey)
.logRequests(true)
.logResponses(true)
.build();
McpClient mcpClientBigModelSearch = new DefaultMcpClient.Builder()
.key("AiChatLC4J-MCPClient")
.transport(transport)
.build();
return new McpToolProvider.Builder()
.mcpClients(mcpClientBigModelSearch)
.build();
}
}
最后在要生成的动态代理对象中注册 toolProvider
@Configuration
public class AiChatServiceFactory {
@Resource
private ChatModel chatModel; // qwen
@Resource
private ContentRetriever contentRetriever;
@Autowired(required = false)
private McpToolProvider mcpToolProvider;
@Bean
public AiChatService aiChatService() {
return AiServices.builder(AiChatService.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(20))
.contentRetriever(contentRetriever)
.tools(new CalculateTool())
.toolProvider(mcpToolProvider)
.build();
}
}