基本概念与依赖导入

模型开发和常规开发层级对比

Prompt:好比UI层,进行用户交互
Langchain4j, Spring AI:好比Controller,调用AI大模型
各类AI大模型:好比Service,提供人工智能服务
向量数据库:好比Mapper,进行数据存储

在这里插入图片描述

大模型调用三件套

API Key:sk-xxx自己的API key
模型名 :qwen-plus、deepseek-v4-pro
调用地址:使用sdk调用时需要配置的base_url

导入依赖

导入 Lanchain4j 依赖

导入 BOM 包管理版本

参考文档

<properties>  
    <!-- langchain4j -->  
    <langchain4j.version>1.0.1</langchain4j.version>  
</properties>
 
<dependencyManagement>  
	<dependencies>
        <!--langchain4j的依赖清单,加载BOM后所有langchain4j版本号可以被统一管理起来  
        https://docs.langchain4j.dev/get-started        -->        
        <dependency>  
            <groupId>dev.langchain4j</groupId>  
            <artifactId>langchain4j-bom</artifactId>  
            <version>${langchain4j.version}</version>  
            <type>pom</type>  
            <scope>import</scope>  
		</dependency>
	<dependencies>
</dependencyManagement>
 
导入原生依赖包
  • 导入 Low-Level API 和 High-Level API 的依赖包
<!--langchain4j-open-ai 基础-->  
<dependency>  
    <groupId>dev.langchain4j</groupId>  
    <artifactId>langchain4j-open-ai</artifactId>  
</dependency>  
<!--langchain4j 高阶-->  
<dependency>  
    <groupId>dev.langchain4j</groupId>  
    <artifactId>langchain4j</artifactId>  
</dependency>
导入SpringBoot 集成依赖包

参考文档: SprintBoot 集成

  • 导入和 SpringBoot 的整合包
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
    <version>1.0.0-beta3</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-spring-boot-starter</artifactId>
    <version>1.0.0-beta3</version>
</dependency>

第三方集成 (通义千问为例)

参考文档:通义千问

导入 BOM 包管理版本
<properties>  
    <!--langchain4j-community 引入阿里云百炼平台依赖管理清单-->  
    <langchain4j-community.version>1.0.1-beta6</langchain4j-community.version>  
</properties>
 
<dependencyManagement>  
	<dependencies>
		<!--引入阿里云百炼平台依赖管理清单  
		https://docs.langchain4j.dev/integrations/language-models/dashscope  
		-->  
		 <dependency>  
			 <groupId>dev.langchain4j</groupId>  
			 <artifactId>langchain4j-community-bom</artifactId>  
			 <version>${langchain4j-community.version}</version>  
			 <type>pom</type>  
			 <scope>import</scope>  
		 </dependency>
	</dependencies>  
</dependencyManagement>
 
导入 SpringBoot 集成依赖包
<dependency>  
	<groupId>dev.langchain4j</groupId>  
	<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>  
</dependency>

创建 ChatModel

APIKey 要从环境变量中获取。

设置方法:百炼平台文档

@Bean  
public ChatModel chatModelQwen()  
{  
    System.out.println("ALIQWEN_API: "+System.getenv("ALIQWEN_API"));  
    return OpenAiChatModel.builder()  
                    .apiKey(System.getenv("ALIQWEN_API"))  
                    .modelName("qwen-plus")  
                    .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")  
            .build();  
}

创建 AI Service

  • 和 SpringBoot 整合:声明式创建 AI Service
// ChatAssiatant.java
@AiService  
public interface ChatAssistant {  
    String chat(String prompt);  
}
// Controller.java
@RestController  
public class DeclarativeAIServiceController {  
    @Resource  
    private ChatAssistant chatAssistant;    
  
    @GetMapping(value = "/lc4j/boot/chatAssistant")  
    public String deepseekAI(@RequestParam(value = "prompt", defaultValue = "你是谁") String prompt)  
    {  
        return chatAssistant.chat(prompt);   
    }  
}

@AIService(wiringMode = EXPLICIT, chatModel = "chatModelQwen") 可以指定改 AI Service 使用的是 chatModelQwen 模型。

  • 原生整合:手动创建 AI Service
// ChatAssistant.java
public interface ChatAssistant {  
    String chat(String prompt);  
}
// Config.java
	@Bean(name = "qwen")
    public ChatModel chatModelQwen()
    {
        return OpenAiChatModel.builder()
                    .apiKey(System.getenv("aliQwen-api"))
                    .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-api"))
                        .modelName("deepseek-chat")
                        //.modelName("deepseek-reasoner")
                        .baseUrl("https://api.deepseek.com/v1")
                .build();
    }

@Bean  
public ChatAssistant chatAssistant(@Qualifier("qwen") ChatModel chatModelQwen) {  
    return AiServices.create(ChatAssistant.class, chatModelQwen);  
}
// Controller.java
@RestController  
@Slf4j  
public class HighApiController {  
    @Resource  
    private ChatAssistant chatAssistant;  
  
    @GetMapping(value = "/highapi/highapi")  
    public String highApi(@RequestParam(value = "prompt", defaultValue = "你是谁") String prompt)  
    {  
        return chatAssistant.chat(prompt);  
    }  
}

模型参数

// Config.java
@Configuration  
public class LLMConfig  
{  
    @Bean(name = "qwen")  
    public ChatModel chatModelQwen()  
    {  
        return OpenAiChatModel.builder()  
                    .apiKey(System.getenv("ALIQWEN_API"))  
                    .modelName("qwen-plus")  
                    .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")  
                .logRequests(true) // 日志级别设置为debug才有效  
                .logResponses(true)// 日志级别设置为debug才有效  
                .listeners(List.of(new TestChatModelListener()))  // 事件监听
                .maxRetries(2) // 最大重试次数
                .timeout(Duration.ofSeconds(5)) // 请求大模型的超时时间
                .build();  
    }  
}
// Linstener.java
@Slf4j  
public class TestChatModelListener implements ChatModelListener  
{  
    @Override  
    public void onRequest(ChatModelRequestContext requestContext)  
    {  
  
        // onRequest配置的k:v键值对,在onResponse阶段可以获得,上下文传递参数好用  
        String uuidValue = IdUtil.simpleUUID();  
        requestContext.attributes().put("TraceID",uuidValue);  
        log.info("请求参数requestContext:{}", requestContext+"\t"+uuidValue);  
    }  
  
    @Override  
    public void onResponse(ChatModelResponseContext responseContext)  
    {  
        Object object = responseContext.attributes().get("TraceID");  
  
        log.info("返回结果responseContext:{}", object);  
    }  
  
    @Override  
    public void onError(ChatModelErrorContext errorContext)  
    {  
        log.error("请求异常ChatModelErrorContext:{}", errorContext);  
    }  
}

图片生产模型

使用 qwen-vl-max 模型

// Config.java
@Configuration  
public class LLMConfig  
{  
    @Bean  
    public ChatModel ImageModel() {  
        return OpenAiChatModel.builder()  
                .apiKey(System.getenv("ALIQWEN_API"))  
                //qwen-vl-max 是一个多模态大模型,支持图片和文本的结合输入,适用于视觉-语言任务。  
                .modelName("qwen-vl-max")  //deepseek-v4-flash-vision-exp
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")  
                .build();  
    }
}

发送用户消息:

  • 图片转 Base 64 编码
  • 构造请求消息 UserMessage :包含文字和转码后的图片
  • 调用 .chat 接口
  • 打印请求结果
// ImageModeController.java
@RestController  
@Slf4j  
public class ImageModelController  
{  
    @Autowired  
    private ChatModel chatModel;  
  
    @Value("classpath:static/images/mi.jpg")  
    private Resource resource;//import org.springframework.core.io.Resource;  
  
    /**  
    * @Description: 通过Base64编码将图片转化为字符串  
     *              结合ImageContent和TextContent形成UserMessage一起发送到模型进行处理。  
    * @Auther: zzyybs@126.com  
     *     *测试地址:http://localhost:9006/image/call  
    */    @GetMapping(value = "/image/call")  
    public String readImageContent() throws IOException  
    {  
        String result = null;  
  
        //第一步,图片转码:通过Base64编码将图片转化为字符串  
        byte[] byteArray = resource.getContentAsByteArray();  
        String base64Data = Base64.getEncoder().encodeToString(byteArray);  
  
        //第二步,提示词指定:结合ImageContent和TextContent一起发送到模型进行处理。  
        UserMessage userMessage = UserMessage.from(  
                TextContent.from("从下面图片种获取来源网站名称,股价走势和5月30号股价"),  
                ImageContent.from(base64Data, "image/jpg")  
        );  
        //第三步,API调用:使用OpenAiChatModel来构建请求,并通过chat()方法调用模型。  
        //请求内容包括文本提示和图片,模型会根据输入返回分析结果。  
        ChatResponse chatResponse = chatModel.chat(userMessage);  
  
        //第四步,解析与输出:从ChatResponse中获取AI大模型的回复,打印出处理后的结果。  
        result = chatResponse.aiMessage().text();  
  
        //后台打印  
        System.out.println(result);  
  
        //返回前台  
        return result;  
    }  
}

图片生成

@RestController
public class WanxImageModelController {
    @Resource
    private WanxImageModel wanxImageModel;

    @GetMapping(value = "/image/create2")
    public String createImageContent2(){
        Response<Image> imageResponse = wanxImageModel.generate("美女");

        System.out.println(imageResponse.content().url());
        return imageResponse.content().url().toString();
    }

}


//config
	@Bean
    public WanxImageModel wanxImageModel(){
        return WanxImageModel.builder()
                .apiKey(System.getenv("ALIQWEN_API"))
                .modelName("wanx2.1-t2i-turbo")
                .build();
    }

流式输出

流式输出
参考文档:

响应流式输出
AI Services Flux

前置准备

导入响应式编程依赖包:

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-reactor</artifactId>
    <version>1.0.0-beta3</version>
</dependency>

配置文件设置字符集:

# 设置响应的字符编码,避免流式返回输出乱码  
server.servlet.encoding.charset=utf-8  
server.servlet.encoding.enabled=true  
server.servlet.encoding.force=true

StreamingChatResponseHandler 处理流式事件:

public interface StreamingChatResponseHandler {
 
    void onPartialResponse(String partialResponse);
 
    void onCompleteResponse(ChatResponse completeResponse);
 
    void onError(Throwable error);
}

通过实现 StreamingChatResponseHandler,可以为以下事件定义操作:

  • 当生成下一个部分响应时:调用 onPartialResponse(String partialResponse)。可以在标记可用时立即将其发送到 UI。
  • 当 LLM 完成生成时:调用 onCompleteResponse(ChatResponse completeResponse)ChatResponse 对象包含完整的响应(AiMessage)以及 ChatResponseMetadata
  • 当发生错误时:调用 onError(Throwable error)

原生使用

  • LanguageModel -> StreamingLanguageModel
  • ChatLanguageModel -> StreamingChatLanguageModel
// Config.java
@Configuration  
public class LLMConfig  
{
	// 流式对话接口 StreamingChatModel  
	@Bean  
	public StreamingChatModel streamingChatModel(){  
	    return OpenAiStreamingChatModel.builder()  
	                .apiKey(System.getenv("ALIQWEN_API"))  
	                .modelName("qwen-plus")  
	                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")  
	            .build();  
	}
}
 
// Controller.java
@RestController  
@Slf4j  
public class StreamingChatModelController  
{  
    @Resource //直接使用 low-level LLM API    
    private StreamingChatModel streamingChatLanguageModel;  
  
    // http://localhost:9007/chatstream/chat?prompt=杭州有什么好吃的  
    @GetMapping(value = "/chatstream/chat")  
    public Flux<String> chat(@RequestParam("prompt") String prompt)  
    {  
        System.out.println("---come in chat");  
  
        return Flux.create(emitter -> {  
            streamingChatLanguageModel.chat(prompt, new StreamingChatResponseHandler()  
            {  
                @Override  
                public void onPartialResponse(String partialResponse)  
                {  
                    emitter.next(partialResponse);  
                }  
  
                @Override  
                public void onCompleteResponse(ChatResponse completeResponse)  
                {  
                    emitter.complete();  
                }  
  
                @Override  
                public void onError(Throwable throwable)  
                {  
                    emitter.error(throwable);  
                }  
            });  
        });  
    }
	
	//该方式只会在后端输出
	@GetMapping(value = "/chatstream/chat2")
    public void chat2(@RequestParam(value = "prompt", defaultValue = "北京有什么好吃") String prompt){
        System.out.println("---come in chat2");

        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) {
                throwable.printStackTrace();
            }
        });
    }

AI Services 使用

  • 接口签名为:Flux<String> chatFlux(String prompt);
  • 传递 streamingChatModel 创建
// ChatAssistant.java
public interface ChatAssistant  
{  
    String chat(String prompt);  
  
    Flux<String> chatFlux(String prompt);  
}
 
// Config.java
@Configuration
public class LLMConfig  {
	@Bean  
	public ChatAssistant chatAssistant(StreamingChatModel streamingChatModel){  
	    return AiServices.create(ChatAssistant.class, streamingChatModel);  
	}
}

@RestController
public class StreamingChatModelController {
	@GetMapping(value = "/chatstream/chat3")
    public Flux<String> chat3(@RequestParam(value = "prompt", defaultValue = "南京有什么好吃") String prompt){
        System.out.println("---come in chat3");
        return chatAssistant.chatFlux(prompt);
    }
}

ChatMemory

参考文档:聊天记忆

聊天记忆含义

LangChain4j 提供的聊天记忆的抽象容器,用于简化手动维护和管理聊天消息的繁琐工作。

记忆 vs 历史的区别:

  • 历史:保持用户和AI之间所有消息的完整记录,是用户在UI中看到的实际对话内容
  • 记忆:保存部分信息呈现给LLM,使其表现得"记住"对话,可以通过淘汰、总结、修改消息等方式处理历史

主要功能特性

  • 淘汰策略:自动管理消息数量以适应LLM上下文窗口限制
    • MessageWindowChatMemory(消息计数):保留最近的N条消息
    • TokenWindowChatMemoryTokenizer 计数):保留最近的N个 Token
  • 持久化支持:可将聊天消息存储到持久化存储(ChatMemoryStore)中
  • SystemMessage特殊处理:系统消息的专门管理机制
  • 工具消息特殊处理:避免孤立工具执行结果消息导致的问题

代码示例

在创建 AiService 时,再添加一个 chatMemoryProvider 即可。

// LLMConfig.java
// MessageWindowChatMemory示例
@Bean(name = "chatMessageWindowChatMemory")  
public ChatMemoryAssistant chatMessageWindowChatMemory(ChatModel chatModel)  
{  
  
    return AiServices.builder(ChatMemoryAssistant.class)  
            .chatModel(chatModel)  
            //按照memoryId对应创建了一个chatMemory  
            .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(100))  
            .build();  
}  
  
// TokenWindowChatMemory示例(需要Tokenizer计数)
@Bean(name = "chatTokenWindowChatMemory")  
public ChatMemoryAssistant chatTokenWindowChatMemory(ChatModel chatModel)  
{
    TokenCountEstimator openAiTokenCountEstimator = new OpenAiTokenCountEstimator("gpt-4");  
  
    return AiServices.builder(ChatMemoryAssistant.class)  
            .chatModel(chatModel)  
            .chatMemoryProvider(memoryId -> TokenWindowChatMemory.withMaxTokens(1000,openAiTokenCountEstimator))  
            .build();  
}

聊天记忆持久化

用 redis 为例。

1.实现 ChatMemoryStore 接口。注意需要用 messagesToJsonmessagesToJson 进行序列化和反序列化。

// RedisChatMemoryStore.java
@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 retValue = redisTemplate.opsForValue().get(CHAT_MEMORY_PREFIX + memoryId);
 
        return  ChatMessageDeserializer.messagesFromJson(retValue);
    }
 
    @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);
    }
}

2.创建 ChatMemoryProvider,添加 ChatMemoryStore

// LLMConfig.java
// ...
@Bean  
public ChatPersistenceAssistant chatMemoryAssistant(ChatModel chatModel)  
{  
 
	ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()  
			.id(memoryId)  
			.maxMessages(1000)  
			.chatMemoryStore(redisChatMemoryStore)  
			.build();  
 
//        TokenCountEstimator openAiTokenCountEstimator = new OpenAiTokenCountEstimator("gpt-4");  
//        ChatMemoryProvider chatMemoryProvider1 = memoryId -> TokenWindowChatMemory.builder()  
//                .id(memoryId)  
//                .maxTokens(1000, openAiTokenCountEstimator)  
//                .chatMemoryStore(redisChatMemoryStore)  
//                .build();  
 
	return AiServices.builder(ChatPersistenceAssistant.class)  
			.chatModel(chatModel)  
			.chatMemoryProvider(chatMemoryProvider)  
			.build();  
}
// ...

提示词工程

消息类型

消息类型 说明
SystemMessage【人设】 系统提示词,一次只能保留一条,添加后始终保留
UserMessage【用户消息】 用户消息,代表用户输入
AiMessage【大模型回复】 AI 消息,代表模型的回应
ToolExecutionResultMessage【工具调用】 工具执行结果消息,ToolExecutionRequestMessage的返回结果

三种提示词构建方式

@SystemMessage+@UserMessage+@V

单独设置系统提示词,用 @V 填充用户消息的占位符。

// LawAssistant.java
public interface LawAssistant  
{  
    // @SystemMessage+@UserMessage+@V  
    @SystemMessage("你是一位专业的中国法律顾问,只回答与中国法律相关的问题。" +  
            "输出限制:对于其他领域的问题禁止回答,直接返回'抱歉,我只能回答中国法律相关的问题。'")  
    @UserMessage("请回答以下法律问题:{{question}},字数控制在{{length}}以内")  
    String chat(@V("question") String question, @V("length") int length);
}

带着@StructuredPrompt 的业务实体类

单独设置系统提示词,用业务类填充用户消息的占位符。

// LawPrompt.java
@Data  
@StructuredPrompt("根据中国{{legal}}法律,解答以下问题:{{question}}")  
public class LawPrompt  
{  
    private String legal;  
    private String question;  
}
 
// LawAssistant.java
public interface LawAssistant  
{  
    // LawPrompt:@StructuredPrompt的业务实体类,比如
    @SystemMessage("你是一位专业的中国法律顾问,只回答与中国法律相关的问题。" +  
            "输出限制:对于其他领域的问题禁止回答,直接返回'抱歉,我只能回答中国法律相关的问题。'")  
    String chat(LawPrompt lawPrompt);  
}

PromptTemplate

设置一个提示词模板,通过填充占位符形成不同提示词。

//1 构造PromptTemplate模板  
PromptTemplate template = PromptTemplate.from("你是一个{{it}}助手,{{question}}怎么办");  
//2 由PromptTemplate生成Prompt  
Prompt prompt = template.apply(Map.of("it",role,"question",question));  
//3 Prompt提示词变成UserMessage  
UserMessage userMessage = prompt.toUserMessage();  

使用案例

@RestController  
@Slf4j  
public class ChatPromptController  
{  
    @Resource  
    private LawAssistant lawAssistant;  
    @Resource  
    private ChatModel chatModel;  
  
    // http://localhost:9009/chatprompt/test1  
    @GetMapping(value = "/chatprompt/test1")  
    public String test1()  
    {  
        String chat = lawAssistant.chat("什么是知识产权?",2000);  
        System.out.println(chat);  
  
        String chat2 = lawAssistant.chat("什么是java?",2000);  
        System.out.println(chat2);  
 
                return "success : "+ DateUtil.now()+"<br> \n\n chat: "+chat+"<br> \n\n chat2: "+chat2;  
    }  
  
    @GetMapping(value = "/chatprompt/test2")  
    public String test2()  
    {  
        LawPrompt prompt = new LawPrompt();  
  
        prompt.setLegal("知识产权");  
        prompt.setQuestion("TRIPS协议?");  
  
        String chat = lawAssistant.chat(prompt);  
  
        System.out.println(chat);  
  
        return "success : "+ DateUtil.now()+"<br> \n\n chat: "+chat;  
    }  
  
  
	@GetMapping(value = "/chatprompt/test3")  
    public String test3()  
    {  
        // 看看源码,单个参数时 PromptTemplate 构造使用 it 属性作为默认占位符  
        String role = "财务会计";  
        String question = "人民币大写";  
  
        //1 构造PromptTemplate模板  
        PromptTemplate template = PromptTemplate.from("你是一个{{it}}助手,{{question}}怎么办");  
        //2 由PromptTemplate生成Prompt  
        Prompt prompt = template.apply(Map.of("it",role,"question",question));  
        //3 Prompt提示词变成UserMessage  
        UserMessage userMessage = prompt.toUserMessage();  
        //4 调用大模型  
        ChatResponse chatResponse = chatModel.chat(userMessage);  
  
        //4.1 后台打印  
        System.out.println(chatResponse.aiMessage().text());  
        //4.2 前台返回  
        return "success : "+ DateUtil.now()+"<br> \n\n chat: "+chatResponse.aiMessage().text();  
    }  
}

持久化之 ChatMemoryStore

用redis来存储

# ==========config redis===============
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.database=0
spring.data.redis.connect-timeout=3s
spring.data.redis.timeout=2s
@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.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

@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 retValue = redisTemplate.opsForValue().get(CHAT_MEMORY_PREFIX + memoryId);
        return ChatMessageDeserializer.messagesFromJson(retValue);
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> list) {
        redisTemplate.opsForValue()
                .set(CHAT_MEMORY_PREFIX + memoryId,ChatMessageSerializer.messagesToJson(list));
    }

    @Override
    public void deleteMessages(Object memoryId) {
        redisTemplate.delete(CHAT_MEMORY_PREFIX + memoryId);
    }
}

@Configuration
public class LLMConfig {
    @Resource
    private RedisChatMemoryStore redisChatMemoryStore;

    @Bean
    public ChatModel chatModel(){
        return OpenAiChatModel.builder()
                .apiKey(System.getenv("ALIQWEN_API"))
                .modelName("qwen-long")
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
                .build();
    }

    @Bean
    public ChatPersistenceAssistant chatPersistenceAssistant(ChatModel chatModel){
        ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(1000)
                .chatMemoryStore(redisChatMemoryStore)
                .build();

        return AiServices.builder(ChatPersistenceAssistant.class)
                .chatModel(chatModel)
                .chatMemoryProvider(chatMemoryProvider)
                .build();
    }
}

public interface ChatPersistenceAssistant {
    String chat(@MemoryId Long userId, @UserMessage String messgae);
}

@RestController
public class ChatPersistenceController {
    @Resource
    private ChatPersistenceAssistant chatPersistenceAssistant;

    @GetMapping(value = "/chatpersistence/redis")
    public String testChatPersistence(){
        chatPersistenceAssistant.chat(1L, "你好!我的名字是redis");
        chatPersistenceAssistant.chat(2L, "你好!我的名字是nacos");

        String chat = chatPersistenceAssistant.chat(1L, "我的名字是什么");
        System.out.println(chat);

        chat = chatPersistenceAssistant.chat(2L, "我的名字是什么");
        System.out.println(chat);
        return "testChatPersistence success : "+ DateUtil.now();
    }
}

Function Call

基本概念

大模型本身不调用工具,而是决策使用哪个工具。
工具调用一般会有两轮对话,以调用工具计算两数之和为例:

  • 1.定义工具: 我们将手动创建一个 ToolSpecification 来描述一个名为 calculator 的工具,它有一个 add 方法。
  • 2.首次请求: 用户提问 “What is the sum of 5 and 7?”。我们将此问题连同工具规范一起发送给 LLM。
  • 3.处理模型响应: LLM 不会直接回答,而是返回一个 ToolExecutionRequest,要求我们执行 add 方法,并提供参数 {"a": 5, "b": 7}
  • 4.执行工具: 我们的 Java 代码将解析这个请求,调用本地的 add 方法,得到结果 12
  • 5.二次请求: 我们将工具的执行结果 12 封装成 ToolExecutionResultMessage,连同之前的对话历史一起再次发送给 LLM。
  • 6.最终答案: LLM 收到工具的计算结果后,会用自然语言生成最终的答案,例如 “The sum of 5 and 7 is 12.”。

调用工具的两种方式

调用工具需要告知大模型三个参数:

1.工具名称
2.工具描述
3.工具参数
本地需要有模型的执行代码。

低阶 API:ChatModel

  • ToolSpecification :描述工具,包含名称,描述,参数
  • ToolExecutor :工具的业务逻辑
  • AiServices.tools() 挂载工具
@Configuration
public class LLMConfig {
    @Bean
    public ChatModel chatModel(){
        return OpenAiChatModel.builder()
                .apiKey(System.getenv("ALIQWEN_API"))
                .modelName("qwen-plus")
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
                .build();
    }

    @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 = (toolExecutionRequest, memoryId)->{
            System.out.println(toolExecutionRequest.id());
            System.out.println(toolExecutionRequest.name());
            String arguments1 = toolExecutionRequest.arguments();
            System.out.println("arguments1****》 " + arguments1);
            return "开具成功";
        };
        Map<ToolSpecification, ToolExecutor> tools =new HashMap<>();
        tools.put(toolSpecification, toolExecutor);

        return AiServices.builder(FunctionAssistant.class)
                .chatModel(chatModel)
                .tools(tools)// Tools (Function Calling)
                .build();
    }
}
public interface FunctionAssistant {
    String chat(String message);
}
@RestController
public class ChatFunctionCallingController {
    @Resource
    private FunctionAssistant functionAssistant;

    @GetMapping(value = "/chatfunction/test1")
    public String test1()
    {
        String chat = functionAssistant.chat("开张发票,公司:尚硅谷教育科技有限公司 税号:atguigu533 金额:668.12");

        System.out.println(chat);
        return "success : "+ DateUtil.now() + "\t"+chat;
    }
}

高阶 API:AIService

  • @Tool@P 注解:描述工具
  • 具体的类方法:工具的业务逻辑
  • AiServices.tools() 挂载工具
// InvoiceHandler.java
@Slf4j  
public class InvoiceHandler {  
  
	@Tool(name = "开票助手", value = "根据用户提交的开票信息进行开票")
    public String handle(@P("公司名称") String companyName,  
                         @P("税号") String dutyNumber,  
                         @P("金额保留两位有效数字") String amount) throws Exception  
    {  
        log.info("companyName =>>>> {} dutyNumber =>>>> {} amount =>>>> {}", companyName, dutyNumber, amount);  
        //----------------------------------
        // 这块写自己的业务逻辑,调用redis/rabbitmq/kafka/mybatis/顺丰单据/医疗化验报告/支付接口等第3方
        //----------------------------------
        
        return "开票成功";  
    }  
}




// LLMConfig.java
	@Bean
    public FunctionAssistant FunctionAssistantfunctionAssistant(ChatModel chatModel)
    {
        return AiServices.builder(FunctionAssistant.class)
                    .chatModel(chatModel)
                    .tools(new InvoiceHandler())
                .build();
    }

向量数据库

基本概念

在这里插入图片描述

  • 嵌入模型(向量模型):数据和用户请求通过嵌入模型转换为向量
  • 向量数据库:数据向量存储在向量数据库中
  • 相似度查询:用户请求也转换为向量,和向量数据库中的向量进行匹配,根据相似度高低返回结果

配置通义向量模型

获取模型

在这里插入图片描述
模型名称为:text-embedding-v3

配置模型

和配置聊天模型一致,只不过接口变为 EmbeddingModel

//LLMConfig.java
 @Configuration
public class LLMConfig {
	@Bean  
	public EmbeddingModel embeddingModel()  
	{  
	    return OpenAiEmbeddingModel.builder()  
	                .apiKey(System.getenv("ALIQWEN_API"))  
	                .modelName("text-embedding-v3")  
	                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")  
	            .build();  
	}
}

使用模型

文本向量化测试,看看形成向量后的文本。

  • embeddingModel.embed() :把数据或用户请求转化为向量
// Controller.java
 
@GetMapping(value = "/embedding/embed")  
public String embed()  
{  
    String prompt = """  
               咏鸡  
            鸡鸣破晓光,红冠映朝阳金羽披霞彩,昂首步高岗。  
            """;  
    Response<Embedding> embeddingResponse = embeddingModel.embed(prompt);  
  
    System.out.println(embeddingResponse);  
  
    return embeddingResponse.content().toString();  
}

配置 Qdrant 向量数据库

安装 Qdrant

添加依赖:项目 pom.xml 文件中添加:

<!--qdrant-->  
<dependency>  
    <groupId>dev.langchain4j</groupId>  
    <artifactId>langchain4j-qdrant</artifactId>  
</dependency>

Docker 部署 qdrant:docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant

  • 6333 端口:前端端口,访问 http://localhost:6333/dashboard#/welcome 即可看到前端界面
  • 6334 端口:gRpc 调用端口

配置 Qdrant

// LLMConfig.java
 
// Qdrant客户端:创建Collection
@Bean  
public QdrantClient qdrantClient() {  
    QdrantGrpcClient.Builder grpcClientBuilder =  
            QdrantGrpcClient.newBuilder("127.0.0.1", 6334, false);  
    return new QdrantClient(grpcClientBuilder.build());  
}  
 
// Qdrant存储器:连接到具体的Collction
@Bean  
public EmbeddingStore<TextSegment> embeddingStore() {  
    return QdrantEmbeddingStore.builder()  
            .host("127.0.0.1")  
            .port(6334)  
            .collectionName("test-qdrant")  
            .build();  
}

使用 Qdrant

创建 Qdrant Collection(相当于在 Mysql 中创建一个数据库):qdrantClient

// Controller.java
 
@GetMapping(value = "/embedding/createCollection")  
public void createCollection()  
{  
    var vectorParams = Collections.VectorParams.newBuilder()  
            .setDistance(Collections.Distance.Cosine)  
            .setSize(1024)  
            .build();  
    qdrantClient.createCollectionAsync("test-qdrant", vectorParams);  
}

往 Collection 中添加数据:embeddingStore.add
在 Collection 中查询数据:embeddingStore.search

// Controller.java
 
// 往向量数据库新增文本记录  
@GetMapping(value = "/embedding/add")  
public String add()  
{  
    String prompt = """  
            咏鸡  
            鸡鸣破晓光,红冠映朝阳。金羽披霞彩,昂首步高岗。  
            """;  
    TextSegment segment1 = TextSegment.from(prompt);  
    segment1.metadata().put("author", "zzyy");  
    Embedding embedding1 = embeddingModel.embed(segment1).content();  
    String result = embeddingStore.add(embedding1, segment1);  
  
    System.out.println(result);  
  
    return result;  
}  
  
@GetMapping(value = "/embedding/query1")  
public void query1(){  
    Embedding queryEmbedding = embeddingModel.embed("咏鸡说的是什么").content();  
    EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()  
            .queryEmbedding(queryEmbedding)  
            .maxResults(1)  
            .build();  
    EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(embeddingSearchRequest);  
    System.out.println(searchResult.matches().get(0).embedded().text());  
}  

测试结果

// Controller.java
@Resource  
InMemoryEmbeddingStore<TextSegment> embeddingStore;  
  
@Resource  
ChatAssistant chatAssistant;  
  
// http://localhost:9013/rag/add  
@GetMapping(value = "/rag/add")  
public String testAdd() throws FileNotFoundException  
{  
    //Document document = FileSystemDocumentLoader.loadDocument("/Users/iven/Documents/Research/03 Line/AI开发/Langchain4j-4-Function Call.md");  
    FileInputStream fileInputStream = new FileInputStream("/Users/iven/Documents/Research/03 Line/AI开发/Langchain4j-4-Function Call.md");  
    Document document = new ApacheTikaDocumentParser().parse(fileInputStream);  
  
    EmbeddingStoreIngestor.ingest(document, embeddingStore);  
  
    String result = chatAssistant.chat("文档内容是什么");  
  
    System.out.println(result);  
  
    return result;  
}

在这里插入图片描述

RAG

基本概念

RAG(检索增强生成,Retrieval-Augmented Generation)是通过外挂知识库,让大模型具备垂类知识的一种技术,扩展知识边界,并避免“幻觉”问题。

主要步骤有:

  • 预处理(蓝线部分):将数据通过分词器切分为片段,然后通过嵌入模型转换为向量,存储到向量数据库中
  • 检索(红线部分):通过外部文档库搜索相关文本片段,或通过向量数据库进行相似度查询,先检索出与用户问题相关的内容。
  • 生成(红线部分):将检索到的相关内容与用户原始问题一同输入到大语言模型中,由模型进行理解和综合,生成更准确、包含事实依据的回答。

在这里插入图片描述

简单示例

添加依赖

<dependency>  
    <groupId>dev.langchain4j</groupId>  
    <artifactId>langchain4j-easy-rag</artifactId>  
</dependency>

添加向量数据库(这里方便起见使用内存向量数据库)

// LLMConfig.java
@Bean  
public InMemoryEmbeddingStore<TextSegment> embeddingStore() {  
    return new InMemoryEmbeddingStore<>();  
}

构建 AIService

// ChatAssictant.java
public interface ChatAssistant {  
  
    /**  
     * 聊天  
     *  
     * @param message 消息  
     * @return {@link String }  
     */    
     String chat(String message);  
}
 
// LLMConfig.java
@Configuration
public class LLMConfig {
	@Bean
	    public ChatModel chatModel() {
	        return OpenAiChatModel.builder()
	                    .apiKey(System.getenv("aliQwen-api"))
	                    .modelName("qwen-plus")
	                    .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
	                .build();
	    }
	
	
	
	
    /**
     * 需要预处理文档并将其存储在专门的嵌入存储(也称为矢量数据库)中。当用户提出问题时,这对于快速找到相关信息是必要的。
     * 我们可以使用我们支持的 15 多个嵌入存储中的任何一个,但为了简单起见,我们将使用内存中的嵌入存储:
     *
     * https://docs.langchain4j.dev/integrations/embedding-stores/in-memory
     *
     * @return
     */
    @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();  
	}
}


@RestController
@Slf4j
public class RAGController{
    @Resource
    InMemoryEmbeddingStore<TextSegment> embeddingStore;

    @Resource
    ChatAssistant chatAssistant;

    // http://localhost:9013/rag/add
    @GetMapping(value = "/rag/add")
    public String testAdd() throws FileNotFoundException{
        //Document document = FileSystemDocumentLoader.loadDocument("D:\\44\\alibaba-java.docx");

        FileInputStream fileInputStream = new FileInputStream("D:\\44\\alibaba-java.docx");
        Document document = new ApacheTikaDocumentParser().parse(fileInputStream);

        EmbeddingStoreIngestor.ingest(document, embeddingStore);

        String result = chatAssistant.chat("错误码00000和A0001分别是什么");

        System.out.println(result);

        return result;
    }
}


MCP

基本概念

MCP 让模型可以调用远程或本地的各种工具与资源,如果模型是大脑,MCP 则赋予了大脑手脚,使它可以与外界世界进行交互。
大模型版的openFeign,用于大模型之间的通讯

MCP协议指定了两种传输类型:

  • HTTP:客户端请求一个 SSE 通道来接收来自服务器的事件,然后通过 HTTP POST 请求发送命令。
  • stdio:客户端可以将 MCP 服务器作为本地子进程运行,并通过标准输入/输出直接与其通信。
    用户与 MCP 和大模型的交互方式如下图所示(出处):

在这里插入图片描述
在这里插入图片描述

使用示例

  1. 本地启动 mcp 服务端(stdio 方式)
  2. 构建 mcp 客户端
  3. 构建工具集
  4. 挂载到 AIService
  5. 发送请求返回结果(流式返回)
//Controller.java
@Autowired  
private StreamingChatModel streamingChatModel;  
  
  
@GetMapping("/mcp/chat")  
public Flux<String> chat(@RequestParam("question") String question) throws Exception  
{  
    // 1.本地启动mcp服务端(stdio)  
    String baiduApiKey = System.getenv("BAIDU_MAP_API_KEY");  
    if (baiduApiKey == null) {  
        throw new IllegalArgumentException("请设置环境变量 BAIDU_MAP_API_KEY");  
    }  
    System.out.println("使用的百度地图API Key: " + baiduApiKey);  
  
    McpTransport transport = new StdioMcpTransport.Builder()  
            .command(List.of("npx", "-y", "@baidumap/mcp-server-baidu-map"))  
            .environment(Map.of("BAIDU_MAP_API_KEY", baiduApiKey))  
            .build();  
  
    // 2.构建McpClient客户端  
    McpClient mcpClient = new DefaultMcpClient.Builder()  
            .transport(transport)  
            .build();  
  
    // 3.创建工具集和原生的FunctionCalling类似  
    ToolProvider toolProvider = McpToolProvider.builder()  
            .mcpClients(mcpClient)  
            .build();  
  
    // 4.通过AiServivces给我们自定义接口McpService构建实现类并将工具集和大模型赋值给AiService  
    McpService mcpService = AiServices.builder(McpService.class)  
            .streamingChatModel(streamingChatModel)  
            .toolProvider(toolProvider)  
            .build();  
  
  
    // 5.调用我们定义的HighApi接口,通过大模型对百度mcpserver调用  
    Flux<String> result = mcpService.chat(question);  
  
    // 在流式响应完成后关闭 MCP 客户端  
    return result.doFinally(signalType -> {  
        try {  
            mcpClient.close();  
        } catch (Exception e) {  
            // 记录关闭异常但不影响响应  
            System.err.println("Error closing MCP client: " + e.getMessage());  
        }  
    });  
}

返回结果
在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐