跳转至

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:
      chat-model:
        model-name: qwen-max
        api-key: 

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

image-20251009105453769

RAG

LangChain4J 提供了检索增强能力,通过匹配向量化的外部文件与向量化的 user text 来增强 LLM 输入

使用方式

基本的思路分为两步:

  1. 将开发者提供的外部文档进行向量化,存储到内存 / 向量数据库中,供后续检索。对于预先给定的外部文档,这个工作可以在 Bean 初始化时进行(如下例)
  2. 配置 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();
  }
}

流程分析

image-20251010121626184