clearwind

clearwind

首页
分类
登录 →
clearwind

clearwind

首页 分类
登录
  1. 首页
  2. 🚀AI
  3. LongChain4J核心组件

LongChain4J核心组件

0
  • 🚀AI
  • 发布于 2024-07-10
  • 76 次阅读
clearwind
clearwind

AiService

利用AI服务组件来完成包括创建对象、调用其方法并传递参数,通过@SystemMmessage注解和@V注解来指定系统提示词和变量,以及运用这些元素来控制输出内容。如何扩展功能,如自定义输出长度。

public class _02_AiService {
    // 定义一个接口Writer
    interface Writer {
        String write(String title);
    }

    public static void main(String[] args) {
        // 创建代理对象时,传入一个提前定义了的ChatLanguageModel
        ChatLanguageModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 利用定义AiService来创建一个Writer代理对象,调用write()方法来写文章
        Writer writer = AiServices.create(Writer.class, model);
    }
}
String content = writer.write("我最爱的人");
System.out.println(content);

执行代码结果为:

是我的家人,他们是我生命中最重要的人,无论发生什么事情,他们都会一直支持和爱护着我。他们给予我无限的爱和关怀,让我感到无比幸福和幸运。我愿意为他们奉献一切,尽我所能地去照顾和呵护他们。他们是我生命中最珍贵的存在,我永远都会珍惜和爱护他们。

@SystemMessage预设系统消息

使用@SystemMessage注解告诉大模型让它先扮演一名作家,然后再回答我的问题。

@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
String write(String title);

public class _02_AiService {

    interface Writer {

        // 定义@SystemMessage,并描述系统提示词
        @SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
        String write(String title);

        static Writer create(){
            ChatLanguageModel model = OpenAiChatModel.builder()
                    .baseUrl("http://langchain4j.dev/demo/openai/v1")
                    .apiKey("demo")
                    .build();

            return AiServices.create(Writer.class, model);
        }
        
    }

    public static void main(String[] args) {
        Writer writer = Writer.create();
        String content = writer.write("我最爱的人");
        System.out.println(content);
    }
}

那么当我们调用write()方法时,LangChain4j就会自动组合SystemMessage和用户输入的标题,然后发送给大模型,这样大模型就知道自己是一名作家了。

运行以上代码的结果就变为了:

在我生命中,最爱的人是我母亲。她是我生命中最重要的人,也是我永远的依靠和支持。母亲那温暖的微笑,总是能给我无限的力量和勇气。

我记得小时候,母亲总是在我生病时守在我身边,用温柔的手轻轻拍着我的背,给我端来热腾腾的粥汤。她总是用她的爱和关心包裹着我,让我感受到无比的安全和幸福。

母亲是一个坚强而又温柔的女人,她用她的辛勤劳动支撑起这个家庭,用她的慈爱呵护着我们。我常常想,如果没有母亲,我将会是一个怎样的人呢?母亲是我生命中的灯塔,指引着我前行的方向。

无论我遇到什么困难和挑战,母亲总是在我身边默默支持着我。她的爱如同一股暖流,贯穿着我的整个生命。我爱你,母亲,永远都爱你。

@Moderate和ModerationModel安全机制

@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
@Moderate
String write(String title);

我们在write()方法上加了@Moderate注解,那么当调用write()方法时,会调用两次大模型:
1首先是配置的ModerationModel,如果没有配置则创建代理对象都不会成功,ModerationModel会对方法的输入进行审核,看是否涉及敏感、不安全的内容。
2然后才是配置的ChatLanguageModel

配置ModerationModel的方式如下:

interface Writer {

	@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")
	@Moderate
	String write(String title);

	static Writer create() {
		ChatLanguageModel model = OpenAiChatModel.builder()
			.baseUrl("http://langchain4j.dev/demo/openai/v1")
			.apiKey("demo")
			.build();

		ModerationModel moderationModel = OpenAiModerationModel.builder()
			.baseUrl("http://langchain4j.dev/demo/openai/v1")
			.apiKey("demo")
			.build();

		return AiServices.builder(Writer.class)
			.chatLanguageModel(model)
			.moderationModel(moderationModel)
			.build();
	}

}

虽然ChatLanguageModel和ModerationModel都是OpenAi,但是你可以理解为OpenAiModerationModel在安全方面更近专业。

ChatMemory

ChatMemory是LangChain4j提供的用来存储历史对话的组件,并且还支持窗口限制、淘汰机制、持久化机制等等扩展功能。

public class _01_HelloWorld {

    public static void main(String[] args) {

        ChatLanguageModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();


        UserMessage userMessage1 = UserMessage.userMessage("你好,我是周瑜");
        Response<AiMessage> response1 = model.generate(userMessage1);
        AiMessage aiMessage1 = response1.content(); // 大模型的第一次响应
        System.out.println(aiMessage1.text());
        System.out.println("----");

        // 下面一行代码是重点
        Response<AiMessage> response2 = model.generate(userMessage1, aiMessage1, UserMessage.userMessage("我叫什么"));
        AiMessage aiMessage2 = response2.content(); // 大模型的第二次响应
        System.out.println(aiMessage2.text());

    }
}

这种实现方式太过麻烦了,我们用ChatMemory来优化,注意ChatMemory需要基于AiService来使用:

1.ChatMemory的使用场景

public class _03_ChatMemory {

    // 取名大师,通过talk()方法来和大师进行交流
    interface NamingMaster {

        String talk(String desc);
    }


    public static void main(String[] args) {

        ChatLanguageModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 用来存储历史对话记录
        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("换一个"));

    }
}

岳霖 (Yuè Lín)
---
岳华 (Yuè Huá)

实现类MessageWindowChatMemory

而这两个实现类内部都有一个ChatMemoryStore属性,ChatMemoryStore也是一个接口,默认有一个InMemoryChatMemoryStore实现类,该类的实现比较简单:

public class InMemoryChatMemoryStore implements ChatMemoryStore {
    
	private final Map<Object, List<ChatMessage>> messagesByMemoryId = new ConcurrentHashMap<>();

    public InMemoryChatMemoryStore() {}

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        return messagesByMemoryId.computeIfAbsent(memoryId, ignored -> new ArrayList<>());
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        messagesByMemoryId.put(memoryId, messages);
    }

    @Override
    public void deleteMessages(Object memoryId) {
        messagesByMemoryId.remove(memoryId);
    }
}

本质上就是一个ConcurrentHashMap

static class PersistentChatMemoryStore implements ChatMemoryStore {

	private final DB db = DBMaker.fileDB("chat-memory.db").transactionEnable().make();
	private final Map<String, String> map = db.hashMap("messages", STRING, STRING).createOrOpen();

	@Override
	public List<ChatMessage> getMessages(Object memoryId) {
		String json = map.get((String) memoryId);
		return messagesFromJson(json);
	}

	@Override
	public void updateMessages(Object memoryId, List<ChatMessage> messages) {
		String json = messagesToJson(messages);
		map.put((String) memoryId, json);
		db.commit();
	}

	@Override
	public void deleteMessages(Object memoryId) {
		map.remove((String) memoryId);
		db.commit();
	}
}

需要添加依赖:

<dependency>
	<groupId>org.mapdb</groupId>
	<artifactId>mapdb</artifactId>
	<version>3.0.9</version>
	<exclusions>
		<exclusion>
			<groupId>org.jetbrains.kotlin</groupId>
			<artifactId>kotlin-stdlib</artifactId>
		</exclusion>
	</exclusions>
</dependency>

这样我们就可以自己定义ChatMemory从而实现持久化了:

ChatMemory chatMemory = MessageWindowChatMemory.builder()
	.chatMemoryStore(new PersistentChatMemoryStore())
	.maxMessages(10)
	.build();

这里我们仍然利用的是MessageWindowChatMemory,只是修改了chatMemoryStore属性,同样我们也可以修改TokenWindowChatMemory,这里就不再重复演示了。

那么MessageWindowChatMemory除开可以存储ChatMessage之外,还有什么特殊的吗?

我们直接看它的add()方法实现:

@Override
public void add(ChatMessage message) {

	// 从ChatMemoryStore获取当前所存储的ChatMessage
	List<ChatMessage> messages = messages();

	// 如果待添加的是SystemMessage
	if (message instanceof SystemMessage) {
		Optional<SystemMessage> systemMessage = findSystemMessage(messages);
		if (systemMessage.isPresent()) {
			// 如果存在相同的SystemMessage,则什么都不做,直接返回
			if (systemMessage.get().equals(message)) {
				return; // do not add the same system message
			} else {
				messages.remove(systemMessage.get()); // need to replace existing system message
			}
		}
	}

	// 添加
	messages.add(message);

	// 如果超过了maxMessages限制,则会淘汰List最前面的,也就是最旧的ChatMessage
	// 注意,SystemMessage不会被淘汰
	ensureCapacity(messages, maxMessages);

	// 将改变了的List更新到ChatMemoryStore中
	store.updateMessages(id, messages);
}

从以上源码可以看出MessageWindowChatMemory有淘汰机制,可以设置maxMessages,超过maxMessages会淘汰最旧的ChatMessage,SystemMessage不会被淘汰。

TokenWindowChatMemory

TokenWindowChatMemory和MessageWindowChatMemory类似,区别在于计算容量的方式不一样,MessageWindowChatMemory直接取的是List<ChatMessage>的大小,而TokenWindowChatMemory会利用指定的Tokenizer对List<ChatMessage>对应的Token数进行估算,然后和设置的maxTokens进行比较,超过maxTokens也会进行淘汰,也是淘汰最旧的ChatMessage。

Tokenizer是一个接口,默认提供了OpenAiTokenizer实现类,是用来估算一条ChatMessage对应多少个Token的,很多大模型的API都是按使用的Token数来收费的,所以在对成本比较敏感时,建议使用TokenWindowChatMemory来对一个会话使用的总Token数进行控制。

独立ChatMemory

public static void main(String[] args) {

	ChatLanguageModel model = OpenAiChatModel.builder()
		.baseUrl("http://langchain4j.dev/demo/openai/v1")
		.apiKey("demo")
		.build();

	ChatMemory chatMemory = MessageWindowChatMemory.builder()
		.chatMemoryStore(new PersistentChatMemoryStore())
		.maxMessages(10)
		.build();

	NamingMaster namingMaster = AiServices.builder(NamingMaster.class)
		.chatLanguageModel(model)
		.chatMemory(chatMemory)
		.build();

	System.out.println(namingMaster.talk("帮我取一个很有中国文化内涵的男孩名字,给我一个你觉得最好的就行了"));
	System.out.println("---");
	System.out.println(namingMaster.talk("换一个"));

}

以上代码有什么问题吗?如果只有一个用户用是没问题的,那如果有多个用户用呢?

比如NamingMaster代理对象被多个用户同时使用,那么这多个用户使用的是同一个ChatMemory,那就会出现这多个用户的对话记录混杂在了一起,这肯定是有问题的,所以需要有一种机制能够使得每个用户对应一个ChatMemory。

所以MessageWindowChatMemory和TokenWindowChatMemory其实都还有一个id属性,而具体的id值则有用于使用时动态传入。

我们改造一下AiServices中设置ChatMemory的方式:

NamingMaster namingMaster = AiServices.builder(NamingMaster.class)
	.chatLanguageModel(model)
	.chatMemoryProvider(userId -> MessageWindowChatMemory.withMaxMessages(10))
	.build();

以上代码表示,NamingMaster代理对象对应的ChatMemory并不是固定的,会根据设置的ChatMemoryProvider来提供,而ChatMemoryProvider是一个Lambda表达式,意思是每个不同的userId对应不同的ChatMemory对象。

同时,我们也需要改造talk()方法来支持动态传入userId:

interface NamingMaster {

	String talk(@MemoryId String userId, @UserMessage String desc);
}

完整代码:

public class _03_ChatMemory {

    interface NamingMaster {

        String talk(@MemoryId String userId, @UserMessage String desc);
    }


    public static void main(String[] args) {

        ChatLanguageModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

		// 以下有问题
        // NamingMaster namingMaster = AiServices.builder(NamingMaster.class)
        //         .chatLanguageModel(model)
        //         .chatMemoryProvider(userId -> MessageWindowChatMemory.withMaxMessages(10))
        //         .build();

		// 这才是正确的写法
		NamingMaster namingMaster = AiServices.builder(NamingMaster.class)
				.chatLanguageModel(model)
                .chatMemoryProvider(userId -> TokenWindowChatMemory.builder().id(userId).maxTokens(10000,new OpenAiTokenizer()).build())
                .build();

        System.out.println(namingMaster.talk("1", "帮我取一个很有中国文化内涵的男孩名字,给我一个你觉得最好的就行了"));
        System.out.println("---");
        System.out.println(namingMaster.talk("2", "换一个"));

    }

    static class PersistentChatMemoryStore implements ChatMemoryStore {

        private final DB db = DBMaker.fileDB("chat-memory.db").transactionEnable().make();
        private final Map<String, String> map = db.hashMap("messages", STRING, STRING).createOrOpen();

        @Override
        public List<ChatMessage> getMessages(Object memoryId) {
            String json = map.get((String) memoryId);
            return messagesFromJson(json);
        }

        @Override
        public void updateMessages(Object memoryId, List<ChatMessage> messages) {
            String json = messagesToJson(messages);
            map.put((String) memoryId, json);
            db.commit();
        }

        @Override
        public void deleteMessages(Object memoryId) {
            map.remove((String) memoryId);
            db.commit();
        }
    }
}

由于以上代码传入的userId不同,所以代码执行结果为:

玉山 (Yushan)
---
好的,请问您想要换成什么样的内容呢?

这就表示,两个不同的用户使用的是独立的ChatMemory。

源码分析

AiService

代理对象的创建流程

创建代理对象是通过AiServices.create(Writer.class, model)进行的,由于AiServices是一个抽象类,源码中有一个默认的子类DefaultAiServices,核心实现源码都在DefaultAiServices中。

DefaultAiServices的build方法就是用来创建指定接口的代理对象:

public T build() {

	// 验证是否配置了ChatLanguageModel
	performBasicValidation();

	// 验证接口中是否有方法上加了@Moderate,但是又没有配置ModerationModel
	for (Method method : context.aiServiceClass.getMethods()) {
		if (method.isAnnotationPresent(Moderate.class) && context.moderationModel == null) {
			throw illegalConfiguration("The @Moderate annotation is present, but the moderationModel is not set up. " +
									   "Please ensure a valid moderationModel is configured before using the @Moderate annotation.");
		}
	}

	// JDK动态代理创建代理对象
	Object proxyInstance = Proxy.newProxyInstance(
		context.aiServiceClass.getClassLoader(),
		new Class<?>[]{context.aiServiceClass},
		new InvocationHandler() {

			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
				// ...
			}

		});

	return (T) proxyInstance;
}

可以发现,其实就是用的JDK动态代理机制创建的代理对象,只不过在创建代理对象之前有两步验证:

1. 验证是否配置了ChatLanguageModel:这一步不难理解,如果代理对象没有配置ChatLanguageModel,那就利用不上大模型的能力了

2. 验证接口中是否有方法上加了@Moderate,但是又没有配置ModerationModel

代理对象的方法执行流程

在invoke()方法的源码中有这么两行代码:

Optional<SystemMessage> systemMessage = prepareSystemMessage(method, args);
UserMessage userMessage = prepareUserMessage(method, args);

分别调用了prepareSystemMessage()和prepareUserMessage()两个方法,而入参都是代理对象当前正在执行的方法和参数。

在看prepareSystemMessage()方法之前,我们需要再了解一个跟@SystemMessage有关的功能,前面我们是这么定义SystemMessage的:

@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇200字以内的作文")

其中200是固定的,但是作为一名作家不可能永远只能写200字以内的作文,而这个字数应该都用户来指定,也就是说200应该得是个变量,那么我们可以这么做:

@SystemMessage("请扮演一名作家,根据输入的文章题目写一篇{{num}}字以内的作文")
String write(@UserMessage String title, @V("num") int num);

其中{num}就是变量,该变量的值由用户在调用write方法时指定

String content = writer.write("我最爱的人", 300);

知prepareSystemMessage()方法的实现:

private Optional<SystemMessage> prepareSystemMessage(Method method, Object[] args) {

	// 得到当前正在执行的方法参数
	Parameter[] parameters = method.getParameters();

	// 解析方法参数前定义的@V注解,@V的value为Map的key,对应的参数值为Map的value
	Map<String, Object> variables = getPromptTemplateVariables(args, parameters);

	// 解析方法上的@SystemMessage注解
	dev.langchain4j.service.SystemMessage annotation = method.getAnnotation(dev.langchain4j.service.SystemMessage.class);
	if (annotation != null) {

		// 拼接多个SystemMessage注解
		String systemMessageTemplate = String.join(annotation.delimiter(), annotation.value());
		if (systemMessageTemplate.isEmpty()) {
			throw illegalConfiguration("@SystemMessage's template cannot be empty");
		}

		// 填充变量
		Prompt prompt = PromptTemplate.from(systemMessageTemplate).apply(variables);
		
		// 返回最终的SystemMessage对象
		return Optional.of(prompt.toSystemMessage());
	}

	return Optional.empty();
}

从源码@SystemMessage注解的value属性是一个String[]:

@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface SystemMessage {
	
    String[] value();

    String delimiter() default "\n";
}

表示如果系统提示词比较长,可以写成多个String,不过最后会使用delimiter的值将这多个String拼接为一个SystemMessage,并且在拼接完以后会根据@V的值填充SystemMessage中的变量,从而得到最终的SystemMessage。

prepareUserMessage()方法:

// 如果有多个参数,获取加了@UserMessage注解参数的值作为UserMessage
for (int i = 0; i < parameters.length; i++) {
	if (parameters[i].isAnnotationPresent(dev.langchain4j.service.UserMessage.class)) {
		String text = toString(args[i]);
		if (userName != null) {
			return userMessage(userName, text);
		} else {
			return userMessage(text);
		}
	}
}

// 如果只有一个参数,则直接使用该参数值作为UserMessage
if (args.length == 1) {
	String text = toString(args[0]);
	if (userName != null) {
		return userMessage(userName, text);
	} else {
		return userMessage(text);
	}
}

这样就得到了最终的SystemMessage和UserMessage,那么如何将他们组装在一起呢?

List<ChatMessage> messages;
if (context.hasChatMemory()) {
	messages = context.chatMemory(memoryId).messages();
} else {
	
	messages = new ArrayList<>();
	
	// 添加SystemMessage
	systemMessage.ifPresent(messages::add);

	// 添加UserMessage
	messages.add(userMessage);
}

按顺序将SystemMessage和UserMessage添加到一个List<ChatMessage>中,后续只要将这个List<ChatMessage>传入给ChatLanguageModel的generate()方法。

AiServices整合ChatMemory源码分析

AiServices中是如何利用ChatMemory来实现对话历史记录的。

DefaultAiServices中的代理对象中的invoke()方法中:

Object memoryId = memoryId(method, args).orElse(DEFAULT);

memoryId()方法其实就是解析方法参数中加了@MemoryId注解的参数值,仅接着就会执行:

if (context.hasChatMemory()) {
	// 根据memoryId获取或创建ChatMemory
	ChatMemory chatMemory = context.chatMemory(memoryId);

	// 将SystemMessage、UserMessage添加到ChatMemory中
	systemMessage.ifPresent(chatMemory::add);
	chatMemory.add(userMessage);
}

这里的context为AiServiceContext,它内部有一个chatMemories属性,类型为Map<Object, ChatMemory> ,就是专门用来存储memoryId和ChatMemory对象之间的映射关系的。

以上代码只是新增一条UserMessage,而传入给大模型的得是所有的对话历史,所以后续会执行:

List<ChatMessage> messages;
if (context.hasChatMemory()) {
	messages = context.chatMemory(memoryId).messages();
} else {
	messages = new ArrayList<>();
	systemMessage.ifPresent(messages::add);
	messages.add(userMessage);
}

根据memoryId把对应的ChatMemory中存储的所有ChatMessage获取出来,然后传入给大模型。

标签: #LangChain4j 5 #Java 6 #AI 7
相关文章
简述 Transformer 训练计算过程(刷新渲染)

简述 Transformer 训练计算过程(刷新渲染) 2025-01-11 23:54

Step1-定义数据集 用于创建 ChatGPT 的数据集为 570 GB。假设数据集为一下内容: 白日依山尽,黄河入海流。 马无夜草不肥,人无横财不富。 天行健,君子以自强不息,地势坤,君子以厚德载物。 Step2-计算词汇量

基于Deepseek的AI试题问答

基于Deepseek的AI试题问答 2025-02-28 09:46

需求 项目目标‌ 构建一个基于大模型微调的AI试题问答系统,支持数学、历史、英语等多学科试题的智能解析、答案生成及知识点关联,适配考试场景的自动评分与错题分析功能‌。 核心功能需求‌ ‌试题交互与解析‌:支持选择、填空、判断、问答等题型交互,自动生成试题解析(含解题步骤与知识点标注)‌。 ‌智能查询

基于 internlm2 和 LangChain 搭建你的知识库

基于 internlm2 和 LangChain 搭建你的知识库 2025-02-27 14:25

环境配置 internlm2 模型部署 创建虚拟环境 conda create -n deepseek_rag python=3.10 -y conda activate deepseek_rag 并在环境中安装运行 demo 所需要的依赖 # 升级pip python -m pip install

xtuner微调大模型

xtuner微调大模型 2025-02-26 09:31

构建环境 # 创建虚拟环境 conda create --name xtuner-env python=3.10 -y conda activate xtuner-env # 安装xtuner git clone https://github.com/InternLM/xtuner.git cd

Llama-Factory 微调全过程

Llama-Factory 微调全过程 2025-01-13 22:28

数据集 数据集下载:通过ModelScope获取原始数据集https://modelscope.cn/datasets/w10442005/ruozhiba_qa/summary git clone https://www.modelscope.cn/datasets/w10442005/ruozh

矩阵分解 2025-01-11 15:48

矩阵分解是一种通过将较大的矩阵分解为多个小矩阵已降低计算复杂度的技术,在模型训练微调上,通常用于简化模型、提高训练效率。矩阵分解有多种形式,一下是几种常见的模型微调权重分解方法: 奇异值分解 将矩阵分解为三个矩阵乘积的方法: W=U \Sigma V^{T} 其中: W是原始权重矩阵。 U和V是正交

目录
  • clearwind
  • 微信小程序

导航菜单

  • 首页
  • 分类
Copyright © 2024 your company All Rights Reserved. Powered by clearwind.
皖ICP备19023482号