当前位置:AIGC资讯 > AIGC > 正文

使用langchain4j调用大模型写个聊天助手

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用完了。请无视小程序本身的功能。

总结

更新时间 2024-08-21