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管理会话。
3、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);
}
}
控制台输出:
我的爸爸是我生命中最重要的人。他不仅是我生活中的导师,还是我永远的支持者。每当我遇到困难时,他总是在我身边给予我鼓励和指导。他教会我坚强,教会我勇敢,也教会我如何面对生活的挑战。他是我心中的英雄,是我无法取代的存在。我爱我的爸爸,他是我生命中最坚定的依靠,也是我永远的骄傲。
4、使用ChatMemory管理上下文
4.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的计算是与模型有关的,所以这里要指定当前使用的是哪个模型。
4.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 默认为 default
System.out.println(namingMaster.talk("user1", "帮我取一个很有中国文化内涵的女孩名字"));
System.out.println("-----------------");
System.out.println(namingMaster.talk("user2", "换一个"));
}
}
控制台输出:
岚娜 (Lan Na)
-----------------
换一个什么?请问你需要什么样的帮助呢?
可以看到user2给大模型发消息: “换一个” ,大模型当做了一个新的会话了,这样就与user1隔离了。
5、大模型与外部系统交互
当大模型需要调用外部工具来获取特定信息或执行特定任务时,需要使用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);
String date2 = assistant.chat("获取后天的日期");
System.out.println(date2);
}
}
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
6、生成图片
使用ImageModel调用调用大模型可以生成图片。
package org.example;
import dev.langchain4j.data.image.Image;
import dev.langchain4j.model.image.ImageModel;
import dev.langchain4j.model.openai.OpenAiImageModel;
import dev.langchain4j.model.output.Response;
public class Test5
{
public static void main(String[] args) {
ImageModel model = OpenAiImageModel.builder().baseUrl("your base url").apiKey("your key").build();
Response<Image> response = model.generate("小狗在森林里觅食");
System.out.println(response.content().url());
}
}
会返回一个临时链接,也可返回base64(response.content().base64Data())
7、其他国内大模型
国内的智谱 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
8、写个应用
接下来,使用以上知识来做一个聊天助手:
8.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
8.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;
@Autowired
ChatController(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;
}
}
8.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);
}
8.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;
@Autowired
ChatService(ChatLanguageModel chatLanguageModel) {
this.chatLanguageModel = chatLanguageModel;
}
private ChatMaster chatMaster;
@PostConstruct
public 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);
}
}
8.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="https://blog.csdn.net/dan_seek/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 @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" 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.value
userMessage.value = ''
if (!userMessageVal) {
uni.showToast({
title: '请输入你的回复',
icon: 'none'
})
return
}
loading.value = true
messages.value = [...messages.value, userMessageVal]
uni.request({
url: 'https://xxx.xxx.cn/ai/reply',
method: 'POST',
data: {
uid: uid,
msg: userMessageVal
},
success: (res) => {
loading.value = false
console.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用完了。请无视小程序本身的功能。
总结