封面来源:由博主个人绘制,如需使用请联系博主。
参考链接:尚硅谷 LangChain4j 实战
LangChain4J 官网:LangChain4j
源码仓库:mofan212/langchain4j-demo
1. 开工前的准备
1.1 前置约定
以阿里百炼平台(通义千问)为主,并辅以 DeepSeek 模型。
涉及的所有调用均基于 OpenAI 协议标准或者 SpringBoot 官方推荐整合规则, 实现一致的接口设计与规范,确保多模型切换的便利性,提供高度可扩展的开发支持。
1.2 三件套准备
阿里百炼平台:大模型服务平台百炼控制台
三件套是什么?
获得 API-KEY
获得模型名
获得 base_url 开发地址
获得 API-KEY
登录控制台后,创建自己的 API-KEY:
获得模型名
「模型名」是模型对应的 Code,而不是模型名称:
获得 base_url 开发地址
2. 入门案例
2.1 Hello World
父工程内需要的配置与依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 <properties > <java.version > 21</java.version > <maven.compiler.source > 21</maven.compiler.source > <maven.compiler.target > 21</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > <spring-boot.version > 3.5.0</spring-boot.version > <langchain4j.version > 1.0.1</langchain4j.version > <langchain4j-community.version > 1.0.1-beta6</langchain4j-community.version > <maven-deploy-plugin.version > 3.1.1</maven-deploy-plugin.version > <flatten-maven-plugin.version > 1.3.0</flatten-maven-plugin.version > <maven-compiler-plugin.version > 3.8.1</maven-compiler-plugin.version > </properties > <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 > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-bom</artifactId > <version > ${langchain4j.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-community-bom</artifactId > <version > ${langchain4j-community.version}</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <version > ${spring-boot.version}</version > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-deploy-plugin</artifactId > <version > ${maven-deploy-plugin.version}</version > <configuration > <skip > true</skip > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > ${maven-compiler-plugin.version}</version > <configuration > <release > ${java.version}</release > <compilerArgs > <compilerArg > -parameters</compilerArg > </compilerArgs > </configuration > </plugin > <plugin > <groupId > org.codehaus.mojo</groupId > <artifactId > flatten-maven-plugin</artifactId > <version > ${flatten-maven-plugin.version}</version > <inherited > true</inherited > <executions > <execution > <id > flatten</id > <phase > process-resources</phase > <goals > <goal > flatten</goal > </goals > <configuration > <updatePomFile > true</updatePomFile > <flattenMode > ossrh</flattenMode > <pomElements > <distributionManagement > remove</distributionManagement > <dependencyManagement > remove</dependencyManagement > <repositories > remove</repositories > <scm > keep</scm > <url > keep</url > <organization > resolve</organization > </pomElements > </configuration > </execution > <execution > <id > flatten.clean</id > <phase > clean</phase > <goals > <goal > clean</goal > </goals > </execution > </executions > </plugin > </plugins > </build > <repositories > <repository > <id > spring-milestones</id > <name > Spring Milestones</name > <url > https://repo.spring.io/milestone</url > <snapshots > <enabled > false</enabled > </snapshots > </repository > <repository > <id > spring-snapshots</id > <name > Spring Snapshots</name > <url > https://repo.spring.io/snapshot</url > <releases > <enabled > false</enabled > </releases > </repository > <repository > <id > aliyunmaven</id > <name > aliyun</name > <url > https://maven.aliyun.com/repository/public</url > </repository > </repositories > <pluginRepositories > <pluginRepository > <id > public</id > <name > aliyun nexus</name > <url > https://maven.aliyun.com/repository/public</url > <releases > <enabled > true</enabled > </releases > <snapshots > <enabled > false</enabled > </snapshots > </pluginRepository > </pluginRepositories >
子工程内引入以下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-open-ai</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies >
API-Key 配置到环境变量
复制在阿里云百炼中配置的 API-Key,将其配置到系统环境变量中,名称任意,比如:
本文使用 AliQwen_Key
作为环境变量名称。
配置完环境变量后,务必重新 IDEA,避免运行时无法成功读取到环境变量!
创建配置类,设置 API-Key、模型名称和调用地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class LLMConfig { @Bean public ChatModel chatModelQwen () { return OpenAiChatModel.builder() .apiKey(System.getenv("AliQwen_Key" )) .modelName("qwen-plus" ) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1" ) .build(); } }
使用 ChatModel#chat()
方法调用大模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j @RestController public class HelloLangChain4JController { @Autowired private ChatModel chatModel; @GetMapping("/langchain4j/hello") public String hello (@RequestParam(value = "question", defaultValue = "你是谁") String question) { return chatModel.chat(question); } }
启动服务,浏览器访问 http://localhost:9001/langchain4j/hello
:
2.2 多模型共存
如果需要配置多个模型,只需在配置类中定义不同名称、ChatModel
类型的 Bean 即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Configuration public class LLMConfig { @Bean(name = "qwen") public ChatModel chatModelQwen () { return OpenAiChatModel.builder() .apiKey(System.getenv("AliQwen_Key" )) .modelName("qwen-plus" ) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1" ) .build(); } @Bean(name = "deepseek") public ChatModel chatModelDeepSeek () { return OpenAiChatModel.builder() .apiKey(System.getenv("DeepSeek_Key" )) .modelName("deepseek-chat" ) .baseUrl("https://api.deepseek.com/v1" ) .build(); } }
按需注入对应的 Bean 即可完成调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Slf4j @RestController public class MultiModelController { @Resource(name = "qwen") private ChatModel chatModelQwen; @Resource(name = "deepseek") private ChatModel chatModelDeepSeek; @GetMapping("/multi-model/qwen") public String qwenCall (@RequestParam(value = "prompt", defaultValue = "你是谁") String prompt) { return chatModelQwen.chat(prompt); } @GetMapping("/multi-model/deepseek") public String deepseekCall (@RequestParam(value = "prompt", defaultValue = "你是谁") String prompt) { return chatModelDeepSeek.chat(prompt); } }
3. 整合 SpringBoot
官方介绍:Spring Boot Integration
LangChain4J 有 两种抽象级别 :
低阶。在这个级别,你可以最自由地访问所有底层组件,比如 ChatModel
、UserMessage
、AiMessage
、EmbeddingStore
、Embedding
等等。这些是由 LLM 驱动的应用程序的「原语」(primitives)。你可以完全控制如何组合它们,但需要编写更多的胶水代码。
高阶。在这个级别,你可以使用高级 API(如 AI Services )与 LLM 进行交互,从而隐藏所有复杂性和样板代码。你仍可以以声明式的方式对模型进行微调。
导入以下两个依赖,分别对应低级和高级抽象级别的使用:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-open-ai-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-spring-boot-starter</artifactId > </dependency >
可以在 .yaml
文件中配置大模型的必要信息(也可以使用配置类):
1 2 3 4 5 6 langchain4j: open-ai: chat-model: api-key: ${AliQwen_Key} model-name: qwen-plus base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
使用低级抽象级别
和先前的使用方式不变,注入 ChatModel
即可:
1 2 3 4 5 6 7 8 9 10 @RestController public class PopularIntegrationController { @Resource private ChatModel chatModel; @GetMapping("/lc4j/boot/chat") public String chat (@RequestParam(value = "prompt", defaultValue = "你是谁") String prompt) { return chatModel.chat(prompt); } }
使用高级抽象级别
使用声明式 AI Service 时,需要定义一个被 @AiService
注解标记的接口:
1 2 3 4 @AiService public interface ChatAssistant { String chat (String userMessage) ; }
然后注入并使用即可:
1 2 3 4 5 6 7 8 9 10 @RestController public class DeclarativeAIServiceController { @Resource private ChatAssistant chatAssistant; @GetMapping("/lc4j/boot/declarative") public String declarative (@RequestParam(value = "prompt", defaultValue = "who are you?") String prompt) { return chatAssistant.chat(prompt); } }
4. 低阶和高阶 API
4.1 低阶 API
官方介绍:Chat and Language Models
LLM 目前有两种 API 类型:
LanguageModel
:它们的 API 非常简单,接收一个字符串入参,并返回一个字符串出参。这种 API 目前已经过时,取而代之的是 chat API(接下来的第二章 API 类型);
ChatModel
:接收多种 ChatMessage
作为入参,并返回一个 AiMessage
作为出参。ChatMessage
通常包含文本,但一些 LLM 也支持其他模态的输入(图片,音频等),比如 OpenAI 的 gpt-4o-mini
和谷歌的 gemini-1.5-pro
。
LangChain4J 不再拓展对 LanguageModel
的支持,因此在所有新特新中,都将使用 ChatModel
。
ChatModel
是 LangChain4J 中与 LLM 进行交互的低阶 API,提供了强大的功能和灵活性。
除 ChatModel
外,UserMessage
、AiMessage
、EmbeddingStore
、Embedding
等也属于低阶 API。
4.2 高阶 API
官方介绍:AI Services
使用低阶 API 可以自由组合各个组件,但需要编写更多的胶水代码。由 LLM 驱动的应用程序不仅需要单个组件,通常需要多个组件相互协同工作,编排它们也变得更加繁琐。
为了更专注于业务逻辑,而不是低阶实现细节,为此 LangChain4J 提供了 AI Services 和 Chains 两个高级概念。其中的 Chain 被官方认定为过时的,而 AI Services 是专为 Java 量身定制的解决方案。
使用高阶 API 时,需要自行定义接口,调用 AiServices
类中的方法进行实现,不仅减少了代码的复杂性,也可以进行灵活的微调(见文档:Simplest AI Service )。
使用示例
定义接口:
1 2 3 public interface ChatAssistant { String chat (String userMessage) ; }
定义配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class LLMConfig { @Bean(name = "qwen") public ChatModel chatModelQwen () { return OpenAiChatModel.builder() .apiKey(System.getenv("AliQwen_Key" )) .modelName("qwen-plus" ) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1" ) .build(); } @Bean public ChatAssistant chatAssistant (@Qualifier("qwen") ChatModel chatModelQwen) { return AiServices.create(ChatAssistant.class, chatModelQwen); } }
最后使用:
1 2 3 4 5 6 7 8 9 10 @RestController public class HighApiController { @Resource private ChatAssistant chatAssistant; @GetMapping("/highapi/highapi") public String highApi (@RequestParam(value = "prompt", defaultValue = "你是谁") String prompt) { return chatAssistant.chat(prompt); } }
在 Web 开发中,定义一个 Service 后,需要定义其对应的实现类。现在使用高阶 API 定义接口后,不用再定义实现类,LangChain4J 会帮你完成。
注意与前文使用案例的区分,创建 Assistant
接口后,并没有使用 @AiService
标记它,因为 @AiService
是与 SpringBoot 集成所需要的。
工作原理
官方介绍:How does it work?
简单来说,将接口的 Class
对象和低阶组件(ChatModel
)传给 AiServices
后,会使用反射创建接口的代理对象。
4.3 Token
参考 DeepSeek API 文档中的 介绍 :
token 是模型用来表示自然语言文本的基本单位,也是我们的计费单元,可以直观的理解为「字」或「词」;通常 1 个中文词语、1 个英文单词、1 个数字或 1 个符号计为 1 个 token。
一般情况下模型中 token 和字数的换算比例大致如下:
1 个英文字符 ≈ 0.3 个 token。
1 个中文字符 ≈ 0.6 个 token。
但因为不同模型的分词不同,所以换算比例也存在差异,每一次实际处理 token 数量以模型返回为准,您可以从返回结果的 usage
中查看。
Token 一词在 Web 开发中也很常见。在 Web 开发中,token 通常指的是用于认证和授权的一种加密字符串。它被用来确保用户身份的安全验证,比如 JWT。这类 token 由服务器生成,并发给客户端保存(比如存储在浏览器的本地存储或 Cookie 中),之后每次请求都需要携带这个 token 来证明用户的身份。
使用低阶 API 获取 token 用量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Resource(name = "deepseek") private ChatModel chatModelDeepSeek;@GetMapping("/lowapi/api02") public String getTokenUsage (@RequestParam(value = "prompt", defaultValue = "你是谁") String prompt) { ChatResponse response = chatModelDeepSeek.chat(UserMessage.from(prompt)); String result = response.aiMessage().text(); System.out.println("调用大模型的返回结果: " + result); TokenUsage tokenUsage = response.tokenUsage(); System.out.println("本次调用消耗的 token: " + tokenUsage); return result + "\t\n" + tokenUsage; }
5. 模型参数
官方介绍:Model Parameters
在与 SpringBoot 进行整合时,可以在配置文件中配置模型参数,参考:Spring Boot
日志
官方介绍:Logging
在配置类中添加 ChatModel
类型的 Bean 时调用 logRequests(true)
和 logResponses(true)
方法:
1 2 3 4 5 OpenAiChatModel.builder() .logRequests(true ) .logResponses(true ) .build();
然后添加日志依赖,比如:
1 2 3 4 5 <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > ${logback.version}</version > </dependency >
不需要在配置文件里设置日志级别为 DEBUG。
监听
官方介绍:Observability
整合了 SpringBoot 时,可以在配置类中进行监听的配置:Observability | Spring Boot Integration
实现 ChatModelListener
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Slf4j public class TestChatModelListener implements ChatModelListener { @Override public void onRequest (ChatModelRequestContext requestContext) { String uuid = UUID.randomUUID().toString(); requestContext.attributes().put("TraceId" , uuid); log.info("请求参数 requestContext: {}" , requestContext + "\t" + uuid); } @Override public void onResponse (ChatModelResponseContext responseContext) { Object traceId = responseContext.attributes().get("TraceId" ); log.info("返回结果 responseContext: {}" , responseContext + "\t" + traceId); } @Override public void onError (ChatModelErrorContext errorContext) { log.error("请求异常 errorContext: {}" , errorContext); } }
构造 ChatModel
时,添加监听器:
1 2 3 4 OpenAiChatModel.builder() .listeners(List.of(new TestChatModelListener ())) .build();
以 ChatModel#chat()
方法与大模型进行交互时,且存在监听器时,整个流程如下:
flowchart TD
A("ChatModelListener#onRequest()")
B("ChatModel#chat()")
C{"是否出现异常"}
D("ChatModelListener#onResponse()")
E("ChatModelListener#onError()")
A --> B --> C
C -- 是 --> E
C -- 否 --> D
重试
默认重试 3 次。
可以在构造 ChatModel
时调用 maxRetries()
方法进行配置,在与 SpringBoot 进行整合时,也可以在配置文件中以下方形式进行配置:
1 2 3 4 langchain4j: open-ai: chat-model: max-retries: 3
超时
向大模型发送请求时,如果在制定时间内没有收到响应,该请求会被终端,并提示超时。
同样的,既可以在构造 ChatModel
时调用对应方法进行配置:
1 2 3 4 OpenAiChatModel.builder() .timeout(Duration.ofSeconds(2 )) .build();
与 SpringBoot 进行整合时,也可以在配置文件中以下方形式配置:
1 2 3 4 langchain4j: open-ai: chat-model: timeout: 2s
6. 多模态视觉理解
官方介绍:Multimodality
UserMessage
不仅仅可以包含文本,还可以包含其他类型的内容。UserMessage
包含一个 List<Content> contents
。
Content
是一个接口,有下列实现:
TextContent
ImageContent
AudioContent
VideoContent
PdfFileContent
可以从 这个表 中查看哪些 LLM 提供商提供了哪些输入模式。
6.1 解读图片
本节选用 通义千问VL-Max 作为使用的模型。
相应地需要调整配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class LLMConfig { @Bean(name = "qwen") public ChatModel chatModelQwen () { return OpenAiChatModel.builder() .apiKey(System.getenv("AliQwen_Key" )) .modelName("qwen-vl-max" ) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1" ) .build(); } }
需要将图片转换成 Base64 字符串作为提示词:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @RestController public class ImageModelController { @Resource private ChatModel chatModel; @Value("classpath:static/images/mi.jpg") private org.springframework.core.io.Resource resource; @SneakyThrows @GetMapping("image/call") public String readImageContent () { byte [] byteArray = resource.getContentAsByteArray(); String base64Data = Base64.getEncoder().encodeToString(byteArray); UserMessage userMessage = UserMessage.from( TextContent.from("从下面图片中获取来源网站名称、股价走势和 5 月 30 日的股价" ), ImageContent.from(base64Data, "image/jpg" ) ); ChatResponse response = chatModel.chat(userMessage); return response.aiMessage().text(); } }
6.2 接入三方平台
以接入 DashScope(Qwen) 为例,按 官网 说明,父工程新增 Maven 配置(最开始其实已经导入):
1 2 3 4 5 6 7 8 9 <dependencyManagement > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-community-bom</artifactId > <version > ${langchain4j-community.version}</version > <type > pom</type > <scope > import</scope > </dependency > </dependencyManagement >
在子工程导入接入 DashScope 需要的依赖:
1 2 3 4 <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-community-dashscope-spring-boot-starter</artifactId > </dependency >
使用 wanx2.1-t2i-turbo
模型完成文生图:
1 2 3 4 5 6 7 8 9 10 11 @Bean public WanxImageModel wanxImageModel () { return WanxImageModel.builder() .apiKey(System.getenv("AliQwen_Key" )) .modelName("wanx2.1-t2i-turbo" ) .build(); }
7. 流式输出
7.1 基本概念
流式输出(StreamingOutput)是一种逐步返回大模型生成结果的技术,允许服务器将响应内容分批次实时传输给客户端,而不是等待全部内容生成完毕后再一次性返回。
这种机制能显著提升用户体验,尤其适用于大模型响应较慢的场景(如生成长文本或复杂推理结果)。
官方介绍:Response Streaming
LLM 一次生成一个 token,因此大多数 LLM 提供商都提供了逐个 token 流式输出的方式,而不是等待整个文本生成完成后才输出。这极大地改善了用户的体验,用户无需再等待未知的时间,可以立即开始阅读 LLM 的输出内容。
与 ChatModel
对应,其流式输出接口是 StreamingChatModel
,它接收一个 StreamingChatResponseHandler
接口的实现作为参数。
1 2 3 4 5 6 7 public interface StreamingChatResponseHandler { void onPartialResponse (String partialResponse) ; void onCompleteResponse (ChatResponse completeResponse) ; void onError (Throwable error) ; }
通过实现 StreamingChatResponseHandler
接口,可以为下列事件定义操作:
当下一部分响应生成时,将调用 onPartialResponse()
方法,这部分响应可以包含一个和多个 token,你可以将这些 token 直接发送给 UI;
当 LLM 完成内容生成时,将调用 onCompleteResponse()
方法。ChatResponse
对象包含完整的响应(AiMessage
)以及响应元数据(ChatResponseMetadata
);
当发生错误时,onError()
方法将被执行。
与 ChatModel
一样,StreamingChatModel
只是流式输出的低阶 API,使用高阶 API AI Service 时,将 TokenStream
作为返回类型就可以实现流式输出。
除此之外,还可以使用 Flux<String>
作为返回类型,但这时需要额外引入 Maven 坐标:
1 2 3 4 5 <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-reactor</artifactId > <version > ${langchain4j.version}</version > </dependency >
7.2 编码实战
Maven 中引入 langchain4j-reactor
,在配置文件中添加以下信息:
1 2 3 4 5 6 7 server: servlet: encoding: charset: utf-8 enabled: true force: true
定义 AI Service 接口:
1 2 3 public interface ChatAssistant { Flux<String> chatFlux (String prompt) ; }
在配置类中完成对 StreamingChatModel
的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class LLMConfig { @Bean public StreamingChatModel streamingChatModel () { return OpenAiStreamingChatModel.builder() .apiKey(System.getenv("AliQwen_Key" )) .modelName("qwen-plus" ) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1" ) .build(); } @Bean public ChatAssistant chatAssistant (StreamingChatModel streamingChatModel) { return AiServices.create(ChatAssistant.class, streamingChatModel); } }
最后是一份简单的食用指南:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Slf4j @RestController public class StreamChatModelController { @Resource private StreamingChatModel streamingChatModel; @Resource private ChatAssistant chatAssistant; @GetMapping("/chat-stream/chat") public void simpleChat (@RequestParam(value = "prompt", defaultValue = "成都有什么吃的?") String prompt) { streamingChatModel.chat(prompt, new StreamingChatResponseHandler () { @Override public void onPartialResponse (String s) { System.out.println(s); } @Override public void onCompleteResponse (ChatResponse chatResponse) { System.out.println("-- response over: " + chatResponse); } @Override public void onError (Throwable throwable) { log.error(throwable.getMessage()); } }); } @GetMapping("/chat-stream/chat-flux") public Flux<String> chatFlux (@RequestParam(value = "prompt", defaultValue = "重庆有什么好吃的?") String prompt) { return Flux.create(i -> streamingChatModel.chat(prompt, new StreamingChatResponseHandler () { @Override public void onPartialResponse (String s) { i.next(s); } @Override public void onCompleteResponse (ChatResponse chatResponse) { i.complete(); } @Override public void onError (Throwable throwable) { i.error(throwable); } })); } @GetMapping("/chat-stream/high-level-chat-flux") public Flux<String> highLevelChatFlux (@RequestParam(value = "prompt", defaultValue = "浙江有什么好吃的?") String prompt) { return chatAssistant.chatFlux(prompt); } }
8. Chat Memory
官方介绍:Chat Memory
Chat Memory 是聊天系统中的重要组件,用于存储和管理对话的上下文信息。它的主要作用是让 AI 助手能够「记住」之前的对话内容,从而提供连贯和个性化的回复。
手动维护和管理 ChatMessage
非常麻烦,因此 LangChain4J 提供了一个 ChatMemory
抽象以及多种开箱即用的实现。ChatMemory
既可以作为独立的底层组件使用,也可以作为 AI Service 等高级组件的一部分使用。
ChatMemory
作为 ChatMessage
的容器(由 List
支持),并具有以下附加功能:
驱逐策略
持久性
SystemMessage
的特殊处理
tool messages
的特殊处理
8.1 Memory vs History
「memory」和「history」是既相似但又不同的概念:
「history」完整地保存了用户和 AI 之间的所有信息。「history」是用户在 UI 中看到的内容,代表实际说过的话。
「memory」会保留一些信息,这些信息被呈现给 LLM,使其表现得好像「记住」了对话。「memory」和「history」截然不同。根据使用的不同「memory」算法,可以以各种方式修改「history」:驱逐一些消息、汇总多条消息、汇总单独的消息、删除消息中不重要的细节、向消息中注入额外的信息(如 RAG)或指令(如结构化输出)等等。
LangChain4J 目前只提供「memory」,不提供「history」,如果需要保存整个「history」,请手动操作。
8.2 驱逐策略
出于以下几个原因,驱逐策略是必要的:
适应 LLM 的上下文窗口。LLM 一次可以处理的 token 数量是有限的,某些时候可能会超过这个限制。在这种情况下,一些消息应该被驱逐。通常情况下,最旧的消息会被驱逐,但如果需要,也可以实现更复杂的算法。
控制成本。每个 token 都是有成本的,这使得每次调用 LLM 的成本逐渐增加,删除不必要的消息可以降低成本。
控制延迟。发送给 LLM 的 token 越多,处理它们的时间就越长。
目前 LangChain4J 提供了两种开箱即用的实现:
其中简单的是 MessageWindowChatMemory
,它作为一个滑动窗口,保留 N
条最新的消息,删除不再适用的旧消息。
更为复杂的是 TokenWindowChatMemory
,它也作为一个滑动窗口,但在保留 N
条最新的消息时,根据需要删除旧消息。消息是不可分割的。如果一条消息不再适用,它会被完全驱逐。TokenWindowChatMemory
需要 TokenCountEstimator
来计算每个 ChatMessage
中的 token。
8.3 编码实现
定义 AI Service 接口 ChatMemoryAssistant
:
1 2 3 public interface ChatMemoryAssistant { String chatWithChatMemory (@MemoryId Long userId, @UserMessage String prompt) ; }
注意标注 @MemoryId
和 @UserMessage
。
这次选用 qwen-long
模型,编写配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Configuration public class LLMConfig { @Bean public ChatModel streamingChatModel () { return OpenAiChatModel.builder() .apiKey(System.getenv("AliQwen_Key" )) .modelName("qwen-long" ) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1" ) .build(); } @Bean(name = "chatMessageWindowChatMemory") public ChatMemoryAssistant chatMessageWindowChatMemory (ChatModel chatModel) { return AiServices.builder(ChatMemoryAssistant.class) .chatModel(chatModel) .chatMemoryProvider(i -> MessageWindowChatMemory.builder() .id(i) .maxMessages(100 ) .build()) .build(); } @Bean(name = "chatTokenWindowChatMemory") public ChatMemoryAssistant chatTokenWindowChatMemory (ChatModel chatModel) { return AiServices.builder(ChatMemoryAssistant.class) .chatModel(chatModel) .chatMemoryProvider(i -> TokenWindowChatMemory.builder() .id(i) .maxTokens(1000 , new OpenAiTokenCountEstimator ("gpt-4" )) .build()) .build(); } }
在使用时注入对应的 AI Service 接口即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Resource(name = "chatMessageWindowChatMemory") private ChatMemoryAssistant chatMessageWindowChatMemory;@Resource(name = "chatTokenWindowChatMemory") private ChatMemoryAssistant chatTokenWindowChatMemory;@GetMapping("/chat-memory/message-window") public String chatMessageWindow () { chatMessageWindowChatMemory.chatWithChatMemory(1L , "你好,我的名字是张三" ); String ans01 = chatMessageWindowChatMemory.chatWithChatMemory(1L , "我的名字是什么?" ); System.out.println("ans01 的返回结果是: " + ans01); chatMessageWindowChatMemory.chatWithChatMemory(100L , "你好,我的名字是李四" ); String ans02 = chatMessageWindowChatMemory.chatWithChatMemory(100L , "我的名字是什么?" ); System.out.println("ans02 的返回结果是: " + ans02); return "chatMessageWindow success: " + LocalDateTime.now() + "<br />\n\n ans01: " + ans01 + "<br />\n\n ans02: " + ans02; } @GetMapping("/chat-memory/token-window") public String chatTokenWindow () { chatTokenWindowChatMemory.chatWithChatMemory(1L , "你好,我的名字是张三" ); String ans01 = chatTokenWindowChatMemory.chatWithChatMemory(1L , "我的名字是什么?" ); System.out.println("ans01 的返回结果是: " + ans01); chatTokenWindowChatMemory.chatWithChatMemory(100L , "你好,我的名字是李四" ); String ans02 = chatTokenWindowChatMemory.chatWithChatMemory(100L , "我的名字是什么?" ); System.out.println("ans02 的返回结果是: " + ans02); return "chatMessageWindow success: " + LocalDateTime.now() + "<br />\n\n ans01: " + ans01 + "<br />\n\n ans02: " + ans02; }
9. 提示词工程
9.1 多种 ChatMessage
官方介绍:Types of ChatMessage
ChatMessage
是 LangChain4J 提供的一个接口,内置了 5 种默认实现,这在 ChatMessageType
枚举中也能发现:
1 2 3 4 5 6 7 public enum ChatMessageType { SYSTEM(SystemMessage.class), USER(UserMessage.class), AI(AiMessage.class), TOOL_EXECUTION_RESULT(ToolExecutionResultMessage.class), CUSTOM(CustomMessage.class); }
根据官网的描述:
UserMessage
:来自用户的消息。这里的用户可以是应用程序的终端用户(人类),也可以是应用程序本身。根据 LLM 支持的多模态,UserMessage
可以只包含文本,也可以包含其他模态(比如图片、音频、视频、PDF)。
AiMessage
:AI 生成的消息,通常是对 UserMessage
的响应。在先前的使用案例中,调用 ChatModel
中接收 UserMessage
的 chat()
方法时,会返回一个 ChatResponse
对象,其内部就存在一个 AiMessage
对象。AiMessage
可以包含文本响应(String
)和执行工具(Tool)的请求(ToolExecutionRequest
),关于 Tool 将在后续介绍。
ToolExecutionResultMessage
:ToolExecutionRequest
的结果。
SystemMessage
:来自系统的消息。开发人员通常会定义该消息的内容。开发人员需要在这里写下 LLM 在本次对话中的角色、应该如何表现、以什么风格回答等指示。LLM 被训练成相比于其他类型的消息,更关注 SystemMessage
,所以要小心,最好不要让终端用户自由定义或向 SystemMessage
注入一些内容。通常情况下,SystemMessage
位于对话的开头。
CustomMessage
:包含任意属性的自定义消息。此消息只能在支持它的 ChatModel
实现上使用,目前只有 Ollama。
有这么多种 ChatMessage
,如何在对话中将它们结合起来呢?
在最简单的情况下,我们可以在 chat
方法中提供一个单一的 UserMessage
实例。这与 ChatModel#chat(String)
很类似,只不过 chat(ChatMessage)
不再返回字符串,而是 ChatResponse
。ChatResponse
内部不仅包含 AiMessage
,还有 ChatResponseMetadata
。ChatResponseMetadata
是封装了响应的元数据,包括 TokenUsage
,它统计了输入(提供给 generate
方法的所有 ChatMessage
)包含多少 token、输出(在 AiMessage
)生成了多少 token,以及 token 总量。你可以根据这些信息计算调用 LLM 的成本。ChatResponseMetadata
还包含一个 FinishReason
枚举,定义了停止生成的各种原因。通常情况下,如果 LLM 自行决定停止生成,枚举值为 FinishReason.STOP
。
根据内容的不同,有多种创建 UserMessage
的方式。最简单的是 new UserMessage("Hi")
或 UserMessage.from("Hi")
。
9.2 提示词的演化
提示词的演化分为三个历程:
flowchart TD
A("简单纯文本提问")
B("占位符(Prompt Template)")
C("多角色消息")
A --> B --> C
最初的提示词(Prompt)只是简单的文本字符串,后续引入占位符 {{it}}
以动态插入内容,如今将消息分为不同角色(如用户、助手、系统等),设置功能边界,增强交互的复杂性和上下文感知能力。
9.3 编码实现
定义一个模板类 LawPrompt
,以便后续以面向对象的方式定义提示词:
1 2 3 4 5 6 7 @Getter @Setter @StructuredPrompt("根据中国{{legal}}法律,解答以下问题: {{question}}") public class LawPrompt { private String legal; private String question; }
新建 AI Service 接口 LawAssistant
,使用 @SystemMessage
、@UserMessage
和 @V
注解定义各种提示词:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface LawAssistant { @SystemMessage(""" 你是一位专业的中国法律顾问,只回答与中国法律相关的问题。 输出限制:对于其他领域的问题禁止回答,直接返回'抱歉,我只能回答中国法律相关的问题'。 """) @UserMessage("请回答以下法律问题:{{question}},字数控制在 {{length}} 以内。") String chat (@V("question") String question, @V("length") int length) ; @SystemMessage(""" 你是一位专业的中国法律顾问,只回答与中国法律相关的问题。 输出限制:对于其他领域的问题禁止回答,直接返回'抱歉,我只能回答中国法律相关的问题'。 """) String chat (LawPrompt lawPrompt) ; }
在配置类中创建 Bean:
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration public class LLMConfig { @Bean public ChatModel streamingChatModel () { } @Bean public LawAssistant lawAssistant (ChatModel chatModel) { return AiServices.create(LawAssistant.class, chatModel); } }
最后就是「酣畅淋漓」的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @RestController public class ChatPromptController { @Resource private LawAssistant lawAssistant; @Resource private ChatModel chatModel; @GetMapping("/chat-prompt/test1") public String test1 () { String chat1 = lawAssistant.chat("什么是知识产权?" , 2000 ); System.out.println(chat1); String chat2 = lawAssistant.chat("什么是 Java?" , 2000 ); System.out.println(chat2); String chat3 = lawAssistant.chat("介绍下西瓜和芒果?" , 2000 ); System.out.println(chat3); String chat4 = lawAssistant.chat("飞机发动机原理?" , 2000 ); System.out.println(chat4); return "success : " + LocalDateTime.now() + "<br /> \n\n chat1: " + chat1 + "<br /> \n\n chat2: " + chat2 + "<br /> \n\n chat3: " + chat3 + "<br /> \n\n chat4: " + chat4; } @GetMapping("/chat-prompt/test2") public String test2 () { LawPrompt lawPrompt = new LawPrompt (); lawPrompt.setLegal("知识产权" ); lawPrompt.setQuestion("TRIPS协议" ); String chat = lawAssistant.chat(lawPrompt); return "success : " + LocalDateTime.now() + "<br /> \n\n chat: " + chat; } @GetMapping("/chat-prompt/test3") public String test3 () { String role = "外科医生" ; String question = "牙疼" ; PromptTemplate template = PromptTemplate.from("你是一个{{it}}助手,{{question}}怎么办?" ); Prompt prompt = template.apply(Map.of("it" , role, "question" , question)); UserMessage userMessage = prompt.toUserMessage(); ChatResponse response = chatModel.chat(userMessage); String text = response.aiMessage().text(); return "success : " + LocalDateTime.now() + "<br /> \n\n chat: " + text; } }
10. 持久化
10.1 官方介绍
官方介绍:Persistence
默认情况下,ChatMemory
将 ChatMessage
存储在内存中。
如果需要持久化,可以自定义 ChatMemoryStore
实现,将 ChatMessage
存储到选择的持久化存储区中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class PersistentChatMemoryStore implements ChatMemoryStore { @Override public List<ChatMessage> getMessages (Object memoryId) { } @Override public void updateMessages (Object memoryId, List<ChatMessage> messages) { } @Override public void deleteMessages (Object memoryId) { } } ChatMemory chatMemory = MessageWindowChatMemory.builder() .id("12345" ) .maxMessages(10 ) .chatMemoryStore(new PersistentChatMemoryStore ()) .build();
每当新的 ChatMessage
被添加到 ChatMemory
中时,就会调用 updateMessages()
方法。在每次与 LLM 交互时,这通常会发生两次:一次是添加新的 UserMessage
,另一次是添加新的 AiMessage
。updateMessages()
方法将更新与给定 memory ID 相关的所有信息。ChatMessage
既可以单独存储(例如每条信息对应一条记录、一行、一个对象),也可以一起存储(例如整个 ChatMemory
对应一条记录、一行、一个对象)。
请注意,从 ChatMemory
中驱逐的消息也会从 ChatMemoryStore
中驱逐。当消息被驱逐时,updateMessages()
方法会被调用,且传入的消息列表中不包含被驱逐的消息。
每当 ChatMemory
的用户请求所有消息时,就会调用 getMessages()
方法。在每次与 LLM 交互时,这通常会发生一次。参数 memoryId
对象的值与创建 ChatMemory
时指定的 id 相对应,它被用于区分多个用户或多个对话。getMessages()
方法会返回与给定 memory id 相关联的所有信息。
每当调用 ChatMemory.clear()
时,就会调用 deleteMessages()
方法。如果不使用这个功能,可以将此方法留空。
10.2 编码实现
将客户和大模型的对话回答保存进 Redis,进行持久化记忆留存。
额外引入 Redis 依赖:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
新建 AI Service 接口:
1 2 3 public interface ChatPersistenceAssistant { String chat (@MemoryId Long memoryId, @UserMessage String message) ; }
由于需要将消息持久化到 Redis 中,配置 Redis Key-Value 的序列化策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate <>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer ()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer ()); redisTemplate.setHashKeySerializer(new StringRedisSerializer ()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer ()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
自定义 ChatMemoryStore
实现类,定义持久化策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Component public class RedisChatMemoryStore implements ChatMemoryStore { public static final String CHAT_MEMORY_PREFIX = "CHAT_MEMORY:" ; @Resource private RedisTemplate<String, String> redisTemplate; @Override public List<ChatMessage> getMessages (Object memoryId) { String value = redisTemplate.opsForValue().get(CHAT_MEMORY_PREFIX + memoryId); return ChatMessageDeserializer.messagesFromJson(value); } @Override public void updateMessages (Object memoryId, List<ChatMessage> messages) { redisTemplate.opsForValue() .set(CHAT_MEMORY_PREFIX + memoryId, ChatMessageSerializer.messagesToJson(messages)); } @Override public void deleteMessages (Object memoryId) { redisTemplate.delete(CHAT_MEMORY_PREFIX + memoryId); } }
定义 LLM 配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Configuration public class LLMConfig { @Resource private RedisChatMemoryStore redisChatMemoryStore; @Bean public ChatModel chatModel () { } @Bean public ChatPersistenceAssistant chatPersistenceAssistant (ChatModel chatModel) { ChatMemoryProvider provider = memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(1000 ) .chatMemoryStore(redisChatMemoryStore) .build(); return AiServices.builder(ChatPersistenceAssistant.class) .chatModel(chatModel) .chatMemoryProvider(provider) .build(); } }
最后又是「酣畅淋漓」的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RestController public class ChatPersistenceController { @Resource private ChatPersistenceAssistant chatPersistenceAssistant; @GetMapping("/chat-persistence/redis") public String test () { chatPersistenceAssistant.chat(1L , "你好,我的名字是 redis" ); chatPersistenceAssistant.chat(2L , "你好,我的名字是 nacos" ); String chat1 = chatPersistenceAssistant.chat(1L , "我的名字是什么?" ); System.out.println(chat1); String chat2 = chatPersistenceAssistant.chat(2L , "我的名字是什么?" ); System.out.println(chat2); return "success: " + LocalDateTime.now(); } }
11.1 官方介绍
官方介绍:Tools (Function Calling)
一些 LLM (并非所有)除了生成文本外,还可以触发操作。
有一个被称为「tools(工具)」或「function calling(函数调用)」的概念。它允许 LLM 在必要时调用一个或多个由开发者定义、可用的工具。这个工具可以是任何东西:网络搜索、调用外部 API 或执行特定的代码等等。LLM 自身并不能调用工具,而是在响应中表达调用特定工具的意图(不是以纯文本的形式回答)。作为开发者,我们应该用提供的参数执行这个工具,并报告工具的执行结果。
例如,我们知道 LLM 自身并不擅长数学计算。如果你的用例偶尔涉及到数学计算,你可能需要为 LLM 提供一个「数学工具」。通过在向 LLM 的请求中声明一个或多个工具,LLM 可以决定调用一个它认为合适的工具。给定一个数学问题和一系列「数学工具」,LLM 为了正确地回答问题,它应该首先调用提供的数学工具。
简单来说,就是给 LLM 配一个调用的外部工具类。
LLM 不仅仅是文本生成的能手,它们还能触发并调用第三方函数,比如微信查询、快递单号查询等等。使用 Tools,可以将 LLM 的智能与外部工具或 API 无缝衔接。注意,LLM 本身并不执行函数,它只指示应该如何调用函数。
那咋使用呢?
LangChain4J 提供了关于使用 tools 的两个抽象级别:
低阶:使用 ChatModel
和 ToolSpecification
高阶:使用 AI Service 和带有 @Tools
注解的 Java 方法
11.2 低阶 API
官方介绍:Low Level Tool API
定义 AI Service 接口:
1 2 3 public interface FunctionAssistant { String chat (String message) ; }
在配置类中手动指定工具说明、执行工具的业务逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Configuration public class LLMConfig { @Bean public ChatModel chatModel () { } @Bean public FunctionAssistant functionAssistant (ChatModel chatModel) { ToolSpecification toolSpecification = ToolSpecification.builder() .name("开具发票助手" ) .description("根据用户提供的开票信息,开具发票" ) .parameters( JsonObjectSchema.builder() .addStringProperty("companyName" , "公司名称" ) .addStringProperty("dutyNumber" , "税号序列" ) .addStringProperty("amount" , "开票金额,保留两位有效数字" ) .build() ) .build(); ToolExecutor toolExecutor = (req, memoryId) -> { System.out.println(req.id()); System.out.println(req.name()); String args = req.arguments(); System.out.println("args: " + args); return "开具成功" ; }; return AiServices.builder(FunctionAssistant.class) .chatModel(chatModel) .tools(Map.of(toolSpecification, toolExecutor)) .build(); } }
最后是「紧张刺激」的测试:
1 2 3 4 5 6 7 8 9 10 11 12 @RestController public class ChatFunctionCallingController { @Resource private FunctionAssistant functionAssistant; @GetMapping("/chat-function/test") public String test () { String chat = functionAssistant.chat("开张发票,公司:啥都有小卖部 税号:everything666 金额:888" ); System.out.println(chat); return "success: " + LocalDateTime.now() + "<br />" + chat; } }
11.3 高阶 API
使用 @Tools
注解可以更方便地集成函数调用,只需用 @Tools
标记 Java 方法,LangChain4J 就可以自动地将其转换为 ToolSpecification
。
需求:查询某地实时的天气预报。
不再手动创建 ToolSpecification
对象,而是使用 @Tool
和 @P
注解完成。
新增 WeatherAssistant
:
1 2 3 public interface WeatherAssistant { String chat (String message) ; }
定义 WeatherTools
类,用于获取某地的实时天气预报:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class WeatherTools { private static final String BASE_URL = "https://cn.apihz.cn/api/tianqi/tqyb.php?id=88888888&key=88888888&sheng=%s&place=%s" ; @Tool("返回给定省、市的天气预报") public String getWeather (@P("省") String sheng, @P("市") String place) { return getRealWeather(sheng, place); } private String getRealWeather (String sheng, String place) { String url = String.format(BASE_URL, sheng, place); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Content-Type" , "application/json" ) .GET() .build(); String weather = null ; try (HttpClient client = HttpClient .newBuilder() .connectTimeout(Duration.ofMillis(500 )) .build()) { HttpResponse<String> resp = client.send(request, HttpResponse.BodyHandlers.ofString()); weather = resp.body(); } catch (IOException | InterruptedException e) { throw new RuntimeException (e); } return weather; } }
创建 WeatherAssistant
AI Service 时指定可使用的工具:
1 2 3 4 5 6 7 8 9 10 11 12 @Bean public WeatherTools weatherService () { return new WeatherTools (); } @Bean public WeatherAssistant weatherAssistant (ChatModel chatModel, WeatherTools weatherTools) { return AiServices.builder(WeatherAssistant.class) .chatModel(chatModel) .tools(weatherTools) .build(); }
「震撼人心」的调用:
1 2 3 4 5 6 7 @Resource private WeatherAssistant weatherAssistant;@GetMapping("/chat-function/weather") public String getWeather () { return weatherAssistant.chat("今天成都的天气怎么样?" ); }
12. 向量数据库
12.1 相关概念
Vector
Vector,向量(数学概念)、矢量(物理概念)。
向量是用于表示具有大小和方向的量。
向量可以在不同的维度空间中定义,最常见的是二维和三维空间中的向量,但理论上也可以有更高维的向量。例如在二维平面上的一个向量可以写作 ( x , y ) (x, y) ( x , y ) ,这里 x x x 和 y y y 分别表示该向量沿两个坐标轴方向上的分量;而在三维空间里,则会有一个额外的 z z z 坐标,即 ( x , y , z ) (x,y,z) ( x , y , z ) 。
向量化
向量化是将数据转化为向量形式的过程,使得计算机能够更有效地处理和理解复杂信息。
维度
维度,Dimensions。
在 x − y x-y x − y 坐标系中,每个数值向量都有 x x x 和 y y y 坐标(或者在多维系统中的 x x x 、y y y 、z z z …)。x x x 和 y y y 是这个向量空间的轴,成为维度。
对于想要表示为向量的一些非数值实体,需要先决定其维度,并为每个实体在每个维度分配一个值。
例如在一个交通工具数据集中,可以定义四个维度:
轮子数量
是否有发动机
是否可以在地上开动
最大乘客数
然后可以将一些交通工具表示为:
item
number of wheels
has an engine
moves on land
max occupants
car
4
yes
yes
5
bicycle
2
no
yes
1
tricycle
3
no
yes
1
motorcycle
2
yes
yes
2
sailboat
0
no
no
20
ship
0
yes
no
1000
可以使用向量 ( 4 , y e s , y e s , 5 ) (4, yes, yes, 5) ( 4 , yes , yes , 5 ) 来表示汽车 car,如果将 yes 设置为 1,no 设置为 0,可以进一步简化为 ( 4 , 1 , 1 , 5 ) (4, 1, 1, 5) ( 4 , 1 , 1 , 5 ) 。
向量的每个维度代表数据的不同特性,维度越多对事物的描述越精准。
如何确定最相似?
在 x − y x-y x − y 坐标系中的每个向量都有一个长度和方向。例如下图中的 p p p 和 a a a 指向了相同的方法,但长度不同。p p p 和 b b b 指向相反的方向,但拥有相似的长度。此外还有 c c c ,长度比 p p p 短一点,方向也不相同,但很接近。
那么哪一个最接近 p p p 呢?
如果「相似」仅仅意味着指向相似的方向,那么 a a a 最接近 p p p ;如果「相似」仅仅意为着相似的长度,那么 b b b 最接近 p p p 。
由于向量通常用于描述语义,仅仅看长度无法满足需求。
大多数相似度的测量要么仅依赖于方向,要么同时考虑方向和大小。
12.2 Embedding Model
官方介绍:Embedding Model
EmbeddingModel
接口代表一种特殊类型的模型,可以将文本转换为 Embedding。
目前支持的 Embedding Model:Embedding Models
使用方式:
EmbeddingModel.embed(String)
嵌入给定文本
EmbeddingModel.embed(TextSegment)
嵌入给定的 TextSegment
EmbeddingModel.embedAll(List<TextSegment>)
嵌入所有给定的 TextSegment
EmbeddingModel.dimension()
返回该模型产生的 Embedding 的维度
什么是 Embedding?
Embedding,嵌入,其工作原理是将文本、图形和视频转换为向量(Vectors)的浮点数数组。这些向量旨在捕捉文本、图像和视频的含义。嵌入数组的长度成为向量的维度(Dimensionality)。
嵌入模型(EmbeddingModel)是嵌入过程中采用的模型。当前 EmbeddingModel 的接口主要用于将文本转换为数值向量,接口的设计主要围绕这两个目标展开:
可移植性:该接口确保在各种嵌入模型之间的轻松适配。它允许开发者在不同的嵌入技术或模型之间切换,所需的代码更改最小化,这一设计与 Spring 模块化和互换性的理念一致。
简单性:嵌入模型简化了文本转换为嵌入的过程。通过提供如 embed(String text)
和 embed(Document document)
这样简单的方法,去除处理原始文本数据和嵌入算法的复杂性。这个设计选择使开发者(尤其是那些初次接触 AI 的开发者),更容易在他们的应用程序中使用嵌入,而无需深入了解其底层机制。
12.3 Embedding Store
官方介绍:Embedding Store
EmbeddingStore
接口表示 Embedding 的存储空间,也成为向量数据库。它允许存储和搜索相似的 Embedding。
目前支持的 Embedding Store:Comparison table of all supported Embedding Stores
EmbeddingStore
可以单独存储 Embedding,也可以与相应的 TextSegment
一起存储:
它只能按 ID 存储 Embedding。原始嵌入数据可以存储在其他地方,并使用 ID 进行关联。
它可以同时存储 Embedding 和已嵌入的原始数据(通常是 TextSegment
)。
Vector Store
向量存储(Vector Store)是一种用于存储和检索高维向量数据的数据库或存储解决方案,它特别适用于处理那些经过嵌入模型转换后的数据。在 Vector Store 中,查询与传统关系型数据库不一样,它们执行相似性搜索,而不是精准匹配。当给定一个向量作为查询时,Vector Store 返回与查询向量「相似」的向量。
Vector Store 用于将您的数据与 AI 模型集成。在使用它们时的第一步是将您的数据加载到矢量数据库中。当用户将查询发送到 AI 模型时,首先检索一组相似文档。这些文档作为用户问题的上下文,并与用户的查询一起发送到 AI 模型。这种技术也被称为检索增强生成(Retrieval Augmented Generation,RAG)。
EmbeddingSearchRequest
官方介绍:EmbeddingSearchRequest
EmbeddingSearchRequest
表示在 EmbeddingStore
中进行搜索的请求。它具有以下属性:
Embedding queryEmbedding
:用作参考的 Embedding
int maxResults
:返回结果的最大数量。这是一个可选参数。默认值 3
double minScore
:最小分数,范围 0 到 1(包括 1)。只有得分不小于 minScore
的 Embedding 才会被返回。这也是一个可选参数。默认值 0
Filter filter
:用于搜索 Metadata
的过滤器。只有 Metadata
符合该 Filter
的 TextSegment
才会被返回。
12.4 特征点总结
向量数据库能够:
捕捉复杂的词汇关系(语义相似性、同义词、多义词)
超越传统词袋模型的简单计数方式
动态嵌入模型(如 BERT)可根据上下文生成不同的词向量
向量嵌入为现代搜索和检索增强生成(RAG)应用程序提供支持
即:将文本映射到高维空间中的点,使语义相似的文本在这个空间中距离接近。
12.5 编码实现
本次选用 Qdrant 作为使用的向量数据库。
Qdrant 是一个高性能的向量数据库,用于存储 Embedding 并进行快速的向量搜索。
安装 Qdrant:
1 docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
其中:
6333
端口用于 HTTP API,浏览器 Web 界面
6334
端口用于 gRPC API
在浏览器中访问 http://localhost:6333/
,如果出现类似以下信息,则证明启动成功:
1 2 3 4 5 { "title" : "qdrant - vector search engine" , "version" : "1.15.0" , "commit" : "137b6c1e4a91320e4658987d47f235078fb0ab11" }
访问 http://localhost:6333/dashboard#/collections
时能够进入 Qdrant 的后台。
工程中引入 Qdrant 相关依赖:
1 2 3 4 <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-qdrant</artifactId > </dependency >
更换模型为 text-embedding-v3
并在配置类完成 Qdrant 的相关配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Configuration public class LLMConfig { @Bean public EmbeddingModel embeddingModel () { return OpenAiEmbeddingModel.builder() .apiKey(System.getenv("AliQwen_Key" )) .modelName("text-embedding-v3" ) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1" ) .build(); } @Bean public QdrantClient qdrantClient () { QdrantGrpcClient.Builder builder = QdrantGrpcClient.newBuilder("127.0.0.1" , 6334 , false ); return new QdrantClient (builder.build()); } @Bean public EmbeddingStore<TextSegment> embeddingStore () { return QdrantEmbeddingStore.builder() .host("127.0.0.1" ) .port(6334 ) .collectionName("test-qdrant" ) .build(); } }
先测试下文本向量化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Resource private EmbeddingModel embeddingModel;@GetMapping("/embedding/embed") public String embed () { String prompt = """ 咏鸡 鸡鸣破晓光, 红冠映朝阳。 金羽披霞彩, 昂首步高岗。 """ ; Response<Embedding> embeddingResponse = embeddingModel.embed(prompt); return embeddingResponse.content().toString(); }
存储向量时,需要先创建数据库实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Resource private QdrantClient qdrantClient;@GetMapping(value = "/embedding/create-collection") public void createCollection () { var vectorParams = Collections.VectorParams.newBuilder() .setDistance(Collections.Distance.Cosine) .setSize(1024 ) .build(); qdrantClient.createCollectionAsync("test-qdrant" , vectorParams); }
向向量数据库中添加信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @GetMapping(value = "/embedding/add") public String add () { String prompt = """ 咏鸡 鸡鸣破晓光, 红冠映朝阳。 金羽披霞彩, 昂首步高岗。 """ ; TextSegment segment = TextSegment.from(prompt); segment.metadata().put("author" , "mofan" ); Embedding embedding = embeddingModel.embed(segment).content(); return embeddingStore.add(embedding, segment); }
最后再测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @GetMapping(value = "/embedding/query1") public String query1 () { Embedding queryEmbedding = embeddingModel.embed("咏鸡说的是什么" ).content(); EmbeddingSearchRequest req = EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(1 ) .build(); EmbeddingSearchResult<TextSegment> result = embeddingStore.search(req); return result.matches().getFirst().embedded().text(); } @GetMapping(value = "/embedding/query2") public String query2 () { Embedding queryEmbedding = embeddingModel.embed("咏鸡" ).content(); EmbeddingSearchRequest req = EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .filter(MetadataFilterBuilder.metadataKey("author" ).isEqualTo("mofan212" )) .maxResults(1 ) .build(); EmbeddingSearchResult<TextSegment> result = embeddingStore.search(req); List<EmbeddingMatch<TextSegment>> list = result.matches(); if (list.isEmpty()) { return "未查询到指定内容" ; } return list.getFirst().embedded().text(); }
13. RAG
13.1 相关概念
官方介绍:RAG
LLM 的知识仅限于训练数据。如果想要让 LLM 了解特定领域的知识或专有数据,可以:
使用 RAG
使用您的数据微调大模型
结合 RAG 和微调
什么是 RAG
RAG,Retrieval-Augmented Generation,检索增强生成。
简单来说,RAG 是在将提示词发送给 LLM 前,从给定数据中找到相关信息并注入到提示词中的方式。这样 LLM 就能够获取到相关信息,并使用这些信息进行回复,从而降低出现「幻觉」的概率。
可以使用各种信息检索方式找到相关信息,最常用的方法有:
全文(关键词)检索
向量(矢量)检索
混合检索(全文 + 向量)
前面提到的「幻觉」又是什么呢?
RAG 给 LLM 装上了「实时百科大脑」,为了让 LLM 获取足够的上下文,以便获取到更广泛的信息源,通过先查资料再回答的机制,让 LLM 摆脱「知识遗忘和幻觉回复」的困境。
13.2 RAG Stages
官方介绍:RAG Stages
RAG 过程分为两个明确的阶段:索引和检索。LangChain4j 为这两个阶段都提供了工具。
Indexing(索引)
官方介绍:Indexing
在索引阶段会对给定文档进行预处理,以便在检索阶段进行高效地搜索。
这一过程会根据使用的信息检索方式发生变化。对于向量搜索,这通常包括清理文档、用附加数据和元数据补充文档、将文档分割成更小的片段(分块)、嵌入(embedding)这些片段,最后将它们存储到向量数据库中。
索引阶段通常是离线进行的,这意味着终端用户无需等待索引完成。例如,可以每周末定时对公司内部文档进行一次索引操作。负责检索的代码可以是一个只处理索引任务的单个应用程序。
然而在某些情况下,终端用户可能希望上传自己的定制文档,以便 LLM 能够访问这些文件。在这种情况下,索引应该在线进行,并成为主应用程序的一部分。
下面是索引阶段的简化流程图:
Retrieval(检索)
官方介绍:Retrieval
检索阶段通常是在线进行的,当用户提交一个问题时,将使用索引的文档给出答案。
这一过程会根据使用的信息检索方式发生变化。对于向量搜索,这通常包括嵌入(embedding)用户的查询(问题),并在向量数据库中执行相似性搜索,最后将相关片段(原始文档中的片段)注入到提示词中并发送给 LLM。
13.3 核心 API
官方介绍:Core RAG APIs
Embedding Store Ingestor
官方介绍:Embedding Store Ingestor
LangChain4j 中存在名为 EmbeddingStoreIngestor
的类,其内部有 5 个成员变量:
DocumentTransformer documentTransformer
:文档转换
DocumentSplitter documentSplitter
:文档分割
TextSegmentTransformer textSegmentTransformer
:转换单个文本段(用于标准化或清理)
EmbeddingModel embeddingModel
:文本段向量化
EmbeddingStore<TextSegment> embeddingStore
:存储生成的嵌入向量及其对应的文本段
Document Loader
官方介绍:Document Loader
可以根据一个 String
创建一个 Document
对象,但更简单的方式是通过 LangChain4j 中内置的文档加载器创建,比如:
FileSystemDocumentLoader
:从文档系统中加载
UrlDocumentLoader
:从 URL 中加载
AmazonS3DocumentLoader
:从 Amazon S3 中加载
AzureBlobStorageDocumentLoader
:从 Azure BLOB 中加载
GitHubDocumentLoader
:从 GitHub 中加载
TencentCosDocumentLoader
:从腾讯云中加载
Document Parser
官方介绍:Document Parser
Document
代表各种格式的文件,比如 PDF、DOC、TXT 等等。为了解析这些格式,LangChain4j 提供了 DocumentParser
接口及多种实现:
TextDocumentParser
:解析纯文本格式的文档,比如 TXT、HTML、MD 等等
ApachePdfBoxDocumentParser
:解析 PDF
ApachePoiDocumentParser
:解析 MS Office 格式的文档,比如 DOC、DOCX、PPT、PPTX、XLS、XLSX 等等
ApacheTikaDocumentParser
:自动检测并解析几乎所有格式的文档 😲
Document Transformer
官方介绍:Document Transformer
对文档执行各种转换,包括清理、过滤、增强或总结。
没有通用的解决方案,需要根据需求自行实现 DocumentTransformer
。
Document Splitter
官方介绍:Document Splitter
LangChain4j 提供了 DocumentSplitter
接口,及多种开箱即用的实现:
DocumentByParagraphSplitter
:按段落拆分
DocumentBySentenceSplitter
:按句子拆分
DocumentByWordSplitter
:按单词拆分
DocumentByCharacterSplitter
:按字符拆分
DocumentByRegexSplitter
:按正则拆分
构建 RAG 一般步骤
加载文档:使用适当的 DocumentLoader
和 DocumentParser
加载文档
转换文档:使用 DocumentTransformer
清理或增强文档(可选)
拆分文档:使用 DocumentSplitter
将文档拆分为更小的片段(可选)
嵌入文档:使用 EmbeddingModel
将文档片段转换为嵌入向量
存储嵌入:使用 EmbeddingStoreIngestor
存储嵌入向量
检索相关内容:根据用户查询,从 EmbeddingStore
检索最相关的文档片段
生成响应:将检索到的相关内容与用户查询一起提供给语言模型,生成最终响应
13.4 简单实现
本节只做最简单的 Easy RAG 演示。
引入依赖:
1 2 3 4 <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-easy-rag</artifactId > </dependency >
不使用向量数据库,简单起见,在内存中存储 Embeddding:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Configuration public class LLMConfig { @Bean public ChatModel chatModel () { return OpenAiChatModel.builder() .apiKey(System.getenv("AliQwen_Key" )) .modelName("qwen-plus" ) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1" ) .build(); } @Bean public InMemoryEmbeddingStore<TextSegment> embeddingStore () { return new InMemoryEmbeddingStore <>(); } @Bean public ChatAssistant assistant (ChatModel chatModel, EmbeddingStore<TextSegment> embeddingStore) { return AiServices.builder(ChatAssistant.class) .chatModel(chatModel) .chatMemory(MessageWindowChatMemory.withMaxMessages(50 )) .contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore)) .build(); } }
简单测试下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @RestController public class RAGController { @Resource private InMemoryEmbeddingStore<TextSegment> embeddingStore; @Resource private ChatAssistant chatAssistant; @GetMapping("/rag/add") public String test () throws MalformedURLException { URI uri = URI.create("https://mofan212.github.io/mine/" ); Document document = UrlDocumentLoader.load( uri.toURL(), new ApacheTikaDocumentParser () ); EmbeddingStoreIngestor.ingest(document, embeddingStore); return chatAssistant.chat("如何联系默烦?" ); } }
14. MCP
14.1 相关概念
LangChain4j 官方介绍:Model Context Protocol (MCP)
MCP 官方介绍:Introduction - Model Context Protocol
MCP,Model Context Protocol,模型上下文协议。
MCP 是一种开放协议,它规范了应用程序向 LLM 提供上下文的方式。把 MCP 想象成 AI 应用程序的 USB-C 接口,就像 USB-C 接口提供了将设备连接到各种外设和配件,MCP 也提供了将 AI 模型连接到不同数据源和工具(Tool)的标准化方式。
MCP 协议规定了两种传输类型,这在 LangChain4j 中都得到了支持:
HTTP
:客户端请求 SSE 通道接收来自服务器的事件,然后通过 HTTP POST 请求发送命令。
stdio
:客户端可以将 MCP 服务器作为本地子进程运行,并通过标准输入/输出与之直接通信。
特新
SSE
STDIO
传输协议
HTTP(长连接)
操作系统级文件描述符
方向
服务器到客户端(单向传输)
双向流(stdin,stdout)
保持连接
长连接(Connection: keep-alive)
不保证长时间打开,取决于进程生命周期
数据格式
文本流(EventStream 格式)
原始字节流
异常处理
可通过 HTTP 状态码或重连机制
进程退出或管道断裂
MCP 可以类比于 OpenFeign,前者用于大模型之间的通讯,后者用于微服务之间的通讯。
MCP 是比 Tools(Function Calling) 更高一级的抽象,也是实现智能体 Agent 的基础。
访问 MCP.so 选择调用的 MCP Server。
14.2 通用架构
官方链接:General architecture
MCP 遵循客户端-服务器架构,其中主机应用程序可以连接到多个服务器:
flowchart LR
subgraph "Your Computer"
Host["Host with MCP Client<br/>(Claude, IDEs, Tools)"]
S1["MCP Server A"]
S2["MCP Server B"]
D1[("Local<br/>Data Source A")]
Host <-->|"MCP Protocol"| S1
Host <-->|"MCP Protocol"| S2
S1 <--> D1
end
subgraph "Internet"
S3["MCP Server C"]
D2[("Remote<br/>Service B")]
D3[("Remote<br/>Service C")]
S2 <-->|"Web APIs"| D2
S3 <-->|"Web APIs"| D3
end
Host <-->|"MCP Protocol"| S3
MCP Hosts(MCP 主机):发起请求的 AI 应用程序,比如聊天机器人、AI 驱动的 IDE 等
MCP Clients(MCP 客户端):在主机程序内部,与 MCP 服务器保持 1:1 的链接
MCP Servers(MCP 服务器):为 MCP 客户端提供上下文、工具和提示信息
Local Data Sources(本地数据源):本地计算机中可供 MCP 服务器安全访问的资源,如文件、数据库
Remote Services(远程服务):MCP 服务器可以连接到的远程资源,如通过 API 提供的数据
14.3 简单使用
选择 Baidu Map 作为本次 MCP Server。
预先安装 Node.js。
进入 百度地图开发平台 ,创建应用,获取「访问应用 (AK)」并将其配置到系统环境变量,可以参考百度地图 MCP Server 的介绍,同样以 BAIDU_MAP_API_KEY
作为环境变量名称。
配置完环境变量后,记得重启 IDEA!
引入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-reactor</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-community-dashscope-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-mcp</artifactId > </dependency >
修改配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 server: port: 9994 servlet: encoding: charset: UTF-8 enabled: true force: true spring: application: name: langchain4j-14chat-mcp langchain4j: community: dashscope: streaming-chat-model: api-key: ${AliQwen_Key} model-name: qwen-plus chat-model: api-key: ${AliQwen_Key} model-name: qwen-plus
创建 AI Service 接口:
1 2 3 public interface McpService { Flux<String> chat (String question) ; }
先尝试在 cmd 中执行 npx -y @baidumap/mcp-server-baidu-map
命令,查看 MCP 服务端是否能够正常启动。如果不能正常启动,需要查看错误信息并解决。我在此遇到问题,错误信息是「请求 npm 淘宝源下的 baidu map mcp server 失败,原因是证书过期」,于是我将 npm 镜像源又切回默认,然后再尝试执行,提示 Baidu Map MCP Server running on stdio
,正常启动 MCP 服务端。
激动人心的测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @RestController public class McpCallServerController { @Resource private StreamingChatModel streamingChatModel; @GetMapping("/mcp/chat") public Flux<String> chat (@RequestParam("question") String question) throws Exception { McpTransport transport = new StdioMcpTransport .Builder() .command(List.of("cmd" , "/c" , "npx" , "-y" , "@baidumap/mcp-server-baidu-map" )) .environment(Map.of("BAIDU_MAP_API_KEY" , System.getenv("BAIDU_MAP_API_KEY" ))) .build(); McpClient mcpClient = new DefaultMcpClient .Builder() .transport(transport) .build(); ToolProvider toolProvider = McpToolProvider.builder() .mcpClients(mcpClient) .build(); McpService mcpService = AiServices.builder(McpService.class) .streamingChatModel(streamingChatModel) .toolProvider(toolProvider) .build(); try { return mcpService.chat(question); } finally { mcpClient.close(); } } }