LangChain4j是一款基于Java的高效、灵活的AI大模型应用框架,专为简化Java应用程序与LLMs(大语言模型)的集成而设计。它提供统一API和模块化设计,支持多种LLM提供商和嵌入模型,以及丰富的工具箱,如AI服务和RAG(检索增强生成)。LangChain4j通过简化集成过程,降低开发成本,助力开发者快速构建和部署AI应用。langchain4j还提供了openAI部分接口免费测试的能力,可以在没有key的情况下学习使用大模型。
UI代码在文末
1、导入相关包
<!-- openai包 -->
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai</artifactId><version>0.32.0</version>
</dependency>
<!-- 高级工具包,如ai service -->
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j</artifactId><version>0.32.0</version>
</dependency>
<!-- 日志工具 -->
<dependency><groupId>org.tinylog</groupId><artifactId>tinylog-impl</artifactId><version>2.6.2</version>
</dependency>
<dependency><groupId>org.tinylog</groupId><artifactId>slf4j-tinylog</artifactId><version>2.6.2</version>
</dependency>
langchain4j是一个还在完善的库,最新版本请查看官网: Get Started | LangChain4j
jdk版本最好使用17,因为它还有一个基于 Spring Boot 3.2 版本,它最低支持jdk17
2、helloword入门
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
....public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.withApiKey("demo");String answer = model.generate("你是谁");System.out.println("answer:" + answer);
}
控制台会输出:
answer:我是一个智能助手,可以回答您的问题并提供帮助。您有什么需要我帮忙的吗?
代码里的 demo
是langchain4j给开发者免费使用可调用openai模型(gpt-3.5-turbo)的key,不要在生产环境使用哦。
如果引入的是智普包,那么使用ZhipuAiChatModel替换即可
如果你有自己的key,代码如下配置:
public static void main(String[] args) {// 替换成你自己的URL和key就行ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();String answer = model.generate("你是谁,我是小飞");System.out.println("answer:" + answer);String answer2 = model.generate("请问我是谁");System.out.println("answer2:" + answer2);}
控制台输出:
answer:很高兴认识你,小飞。我是一个智能助手,可以回答你的问题和提供帮助。有什么我可以帮助你的吗?
answer2:很抱歉,我无法知道您是谁,因为我是一个虚拟助手,无法直接获取您的身份信息。您可以介绍一下自己,让我更了解您吗?
为什么会这样呢?
由于大模型是无状态的,你要让它知道上下文信息,则需要把你们的会话历史记录也发给它,否则每次都是一次新的会话。langchain4j可以使用ChatMemory管理会话。
2、AiServices和ChatMessage
AiServices 封装了与 LLM 交互的复杂性,使得开发者能够以更自然、更面向对象的方式来与 LLMs 进行交互。
ChatMessage是对话中的一个消息的抽象表示,它包含了4个实现类:UserMessage(用户消息)、AiMessage(大模型回复消息)、SystemMessage(系统消息,如应用的角色和能力)、ToolExecutionResultMessage(用于调用本地应用,可扩展大模型能力)
package org.example;import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.*;public class Test {interface Writer{@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇{{num}}字以内的作文")String write(@UserMessage String text, @V("num") int num);}public static void main(String[] args) {ChatLanguageModel chatLanguageModel = OpenAiChatModel.builder() .baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();// 使用代理模式创建一个作家对象Writer writer = AiServices.create(Writer.class, chatLanguageModel);String content = writer.write("我的爸爸", 100);System.out.println(content);}
}
控制台输出:
我的爸爸是我生命中最重要的人。他不仅是我生活中的导师,还是我永远的支持者。每当我遇到困难时,他总是在我身边给予我鼓励和指导。他教会我坚强,教会我勇敢,也教会我如何面对生活的挑战。他是我心中的英雄,是我无法取代的存在。我爱我的爸爸,他是我生命中最坚定的依靠,也是我永远的骄傲。
3、使用ChatMemory管理上下文
3.1 两种ChatMemory的使用方式
ChatMemory主要用于管理和维护聊天过程中的消息记忆,使得大型语言模型(LLM)能够模拟记住对话上下文的能力
LangChain4j 提供了两种 ChatMemory 的实现方式,如MessageWindowChatMemory 和 TokenWindowChatMemory 。这些实现方式可以根据不同的需求进行选择:
- MessageWindowChatMemory:保留最新的 N 条消息并删除旧消息。由于每条消息可以包含不同数量的令牌,因此这种实现方式主要用于快速原型设计。
- TokenWindowChatMemory:保留最新的 N 个令牌,并根据配置的token删除较旧的消息,能够更精确地控制上下文窗口的大小。
package org.example;import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.*;public class Test2 {interface NamingMaster {String talk(String desc);}public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().apiKey("demo").baseUrl("http://langchain4j.dev/demo/openai/v1").build();// 最多保存10条会话ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);NamingMaster namingMaster = AiServices.builder(NamingMaster.class).chatLanguageModel(model).chatMemory(chatMemory).build();System.out.println(namingMaster.talk("帮我取一个很有中国文化内涵的女孩名字"));System.out.println("-----------------");System.out.println(namingMaster.talk("换一个"));}
}
控制台输出:
岚儿 (Lánér)
-----------------
芷若 (Zhǐruò)
由于使用了ChatMemory管理会话,大模型就理解了 “换一个” 的含义。
如果想指定最大的token数,如下:
// 设置最大token数为1000
ChatMemory chatMemory = TokenWindowChatMemory.withMaxTokens(1000, new OpenAiTokenizer("gpt-3.5-turbo"));
token的计算是与模型有关的,所以这里要指定当前使用的是哪个模型。
3.2 memoryId的使用
如果应用需要多个人使用,每个人都要有自己的会话,否则上下文就串了,可以使用memoryId来做区分。
package org.example;import dev.langchain4j.memory.chat.TokenWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiTokenizer;
import dev.langchain4j.service.*;public class Test3 {interface NamingMaster {String talk(@MemoryId String userId, @UserMessage String desc);}public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().apiKey("demo").baseUrl("http://langchain4j.dev/demo/openai/v1").build();NamingMaster namingMaster = AiServices.builder(NamingMaster.class).chatLanguageModel(model).chatMemoryProvider(uerId -> TokenWindowChatMemory.builder().id(uerId).maxTokens(1000, new OpenAiTokenizer("gpt-3.5-turbo")).build()).build();// 未设置memoryId 默认为 defaultSystem.out.println(namingMaster.talk("user1", "帮我取一个很有中国文化内涵的女孩名字"));System.out.println("-----------------");System.out.println(namingMaster.talk("user2", "换一个"));}}
控制台输出:
岚娜 (Lan Na)
-----------------
换一个什么?请问你需要什么样的帮助呢?
可以看到user2给大模型发消息: “换一个” ,大模型当做了一个新的会话了,这样就与user1隔离了。
4、大模型与外部系统交互
当大模型需要调用外部工具来获取特定信息或执行特定任务时,需要使用Tool
,它提供了这些工具的必要信息,使得大模型能够理解工具,然后正确地调用它 。
使用场景
例如,在构建聊天机器人或智能助手时,大模型可能需要调用天气API来获取实时天气信息,或者调用数据库查询API来获取用户数据。在这些情况下,Tool
可以帮助大模型准确地描述和调用这些外部工具。
package org.example;import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.*;import java.time.LocalDateTime;public class Test4 {interface Assistant {String chat(String userMessage);}static class MyTools {@Tool("获取当前日期")public static String dateUtil(String onUse) {return LocalDateTime.now().toString();}}public static void main(String[] args) {// 上面测试的key在此处不支持ChatLanguageModel model = OpenAiChatModel.builder().apiKey("your key").baseUrl("your base url").build();Assistant assistant = AiServices.builder(Assistant.class).chatLanguageModel(model).tools(new MyTools()).chatMemory(MessageWindowChatMemory.withMaxMessages(10)).build();String date = assistant.chat("获取今天的日期");System.out.println(date);}
}
gpt-3.5-turbo本身是无法获取今天的日期的
控制台输出:
今天:今天的日期是2024年7月11日。
2024-07-11 18:20:24 [main] dev.langchain4j.agent.tool.DefaultToolExecutor.execute()
DEBUG: About to execute ToolExecutionRequest { id = “call_BjJCXo78e6NeRrMrScp0A3Ip”, name = “dateUtil”, arguments = “{“arg0”:“after tomorrow”}” } for memoryId default
2024-07-11 18:20:24 [main] dev.langchain4j.agent.tool.DefaultToolExecutor.execute()
DEBUG: Tool execution result: 2024-07-11T18:20:24.931279900
后天:后天的日期是2024年7月13日。
注意 工具 相关的 demo
key不支持,包括Streaming和生成图片同样也不支持,需要申请自己的key。
其他工具相关demo或者其他api请移步官方demo:langchain4j demo
5、其他国内大模型
国内的智谱 https://www.zhipuai.cn/
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-zhipu-ai</artifactId><version>0.32.0</version>
</dependency>
国内的百度
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-qianfan</artifactId><version>0.32.0</version>
</dependency>
查看官方其他支持大模型: Language Models | LangChain4j
6、写个应用
接下来,使用以上知识来做一个聊天助手:
6.1 配置spring boot相关包和配置文件
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai-spring-boot-starter</artifactId><version>0.32.0</version>
</dependency>
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-spring-boot-starter</artifactId><version>0.32.0</version>
</dependency>
application.properties
langchain4j.open-ai.chat-model.api-key=填写你的key
langchain4j.open-ai.chat-model.base-url=填写你的URL
# langchain4j.open-ai.streaming-chat-model.api-key=server.port=9000
server.servlet.context-path=/ai
6.2 控制器ChatController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.Map;@RestController
public class ChatController {ChatService chatService;@AutowiredChatController(ChatService chatService) {this.chatService = chatService;}@PostMapping("/reply")public String replyAssistant(@RequestBody Map<String, String> map) {String uid = map.get("uid");String msg = map.get("msg");System.out.println("userId:" + uid + " msg:" + msg);String result = chatService.speak(uid, msg);System.out.println(result);return result;}
}
6.3 接口ChatMaster
小应用体验好不好,除了大模型本身的能力外,关键在于提示词,通过优化提示词可以让大模型程序更智能。提示词我常用如下4种优化方式:
1、给大模型一个角色。比如你想让大模型生成孩子能理解的答案,可以告诉大模型它是一位幼儿园老师;
2、给大模型一个参考示例。如果你需要的格式是有要求的,比如每行需要有emoji表情包,那你可以把一个完成的示例发给大模型,让它参考输出;
3、让大模型一步一步解答。由于大模型是根据前面的提示词生成后面的提示词,对于逻辑比较复杂的问题时,往往出错,这个时候让它一步一步解答会有更好的结果;
4、告诉大模型它是提示词优化专家,让它帮你优化。
最后你要描述清楚你的问题,根据输出的结果不断调整提示词
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;public interface ChatMaster {// 以下提示词仅供参考@SystemMessage("你是一位恋爱大师,名叫贝贝。和你对话的是一位女孩,请根据上下文进行分析,然后以男生的角度进行回话。风格要幽默、有趣、体贴、温柔,适当扩展话题,让对话轻松愉快")String speak(@MemoryId String userId, @UserMessage String desc);
}
6.4 服务类ChatService
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.service.AiServices;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class ChatService {private final ChatLanguageModel chatLanguageModel;@AutowiredChatService(ChatLanguageModel chatLanguageModel) {this.chatLanguageModel = chatLanguageModel;}private ChatMaster chatMaster;@PostConstructpublic void init() {chatMaster = AiServices.builder(ChatMaster.class).chatLanguageModel(chatLanguageModel)
// .chatMemoryProvider(uerId -> MessageWindowChatMemory.builder().id(uerId).maxMessages(20).build()).chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)).build();}public String speak(String userId, String desc) {return chatMaster.speak(userId, desc);}
}
6.5 界面部分
前端vue3,使用uniapp开发,使用组件来显示头像,依赖了uni-ui,可以使用图片代替
<template><view class="ai-container"><view class="main"><view v-for="(message, index) in messages" :class="['record-item',index%2===1?'reverse':'']"><view class="portrait"><!-- <image v-if="index%2===0" src="../../static/img/icon-chatgpt.png"></image> --><uni-icons v-if="index%2===0" type="chat-filled" size="30" color="#fff"></uni-icons><uni-icons v-else type="person-filled" size="30" color="#fff"></uni-icons></view><view class="content" @longpress="handleLongPress(message)">{{message}}</view></view></view><view class="foot-box"><view class="text-box"><textarea v-model="userMessage" placeholder="请输入女生给你的回复" /></view><view class="send-btn-box"><button @click="sendMsg" class="uni-btn uni-btn-mini" type="primary" size="mini":loading="loading">发送</button></view></view></view>
</template><script setup>import {ref} from 'vue'const messages = ref(['我是一个帮你回复消息的AI,请把她回复给你的消息发给我,我帮你回复!长按可复制消息'])let uid = generateRandomString(8)let userMessage = ref('')let loading = ref(false)// 调用后台接口的函数 function sendMsg() {if(loading.value){return}let userMessageVal = userMessage.valueuserMessage.value = ''if (!userMessageVal) {uni.showToast({title: '请输入你的回复',icon: 'none'})return}loading.value = truemessages.value = [...messages.value, userMessageVal]uni.request({url: 'https://xxx.xxx.cn/ai/reply',method: 'POST',data: {uid: uid,msg: userMessageVal},success: (res) => {loading.value = falseconsole.log('res:', res)if (res.statusCode === 200) {const aiMessage = res.data// 将新数据添加到数组中 messages.value = [...messages.value, aiMessage]} else {console.error('数据加载失败', res);}},fail: (err) => {console.error('请求失败:', err);}});}function handleLongPress(textToCopy) {uni.setClipboardData({data: textToCopy,success: () => {uni.showToast({title: '复制成功',icon: 'success'});},fail: () => {uni.showToast({title: '复制失败',icon: 'none'});}});}function generateRandomString(length) {let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';let result = '';const charactersLength = characters.length;for (let i = 0; i < length; i++) {result += characters.charAt(Math.floor(Math.random() * charactersLength));}return result;}
</script><script>export default {data() {return {}},methods: {}}
</script><style lang="scss">.ai-container {display: flex;flex-direction: column;background-color: #fafafa;height: 100vh;/* #ifdef H5 */height: calc(100vh - 44px);/* #endif */.main {flex: 1;overflow-y: auto;padding: 10px;margin-top: 5px;.record-item {display: flex;margin-bottom: 15px;&.reverse {flex-direction: row-reverse;.content,.portrait {background-color: $theme-color-rgba;}}.portrait {display: flex;justify-content: center;align-items: center;width: 40px;min-width: 40px;height: 40px;background-color: #e8e8e8;image {width: 30px;height: 30px;}}.content {display: flex;align-items: center;margin: 0 10px;padding: 5px 10px;font-size: 14px;background-color: white;border-radius: 5px;}}}.foot-box {display: flex;background-color: white;height: 120px;.text-box {display: flex;flex: 1;textarea {margin: 10px 0px 10px 15px;padding: 10px;height: 80px;width: 100%;font-size: 14px;background-color: #fafafa;border-radius: 5px;}}.send-btn-box {display: flex;align-items: center;justify-content: center;.uni-btn {margin: 0 15px;padding: 0;width: 60px;}}}}
</style>
购买的key没用完,可以扫码微微体验一下,返回不了消息就是key用完了。请无视小程序本身的功能。