Java 使用LangChain4j集成大模型入门教程兼容deepSeek
LangChain是一个用于开发由大型语言模型驱动的应用程序的框架(LLMs)。LangChain 4j的目标是简化LLMs到Java应用程序的集成。
Java 使用LangChain4j集成大模型入门教程兼容deepSeek
LangChain是一个用于开发由大型语言模型驱动的应用程序的框架(LLMs)。LangChain 4j的目标是简化LLMs到Java应用程序的集成。
1.模型的接入
首先我们完成一个最简单的对话示例。
第一步,接入大模型,我们这里以硅基流动为例去申请一个key(其余大模型平台流程基本一样)
硅基流动地址 https://cloud.siliconflow.cn/
我们登陆后需要申请key (记录秘钥,后续使用)
我们可以在模型广场根据需要选择模型,这里以ds-v3为例,复制模型名称
第二步,新建一个java工程,引入LangChain 4j的基本依赖信息
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.0.0-beta1</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.0.0-beta1</version>
</dependency>
第三步,调用大模型api
1、构建模型连接对象
public static OpenAiChatModel deepseek = OpenAiChatModel.builder()
.apiKey("xxxxx")
.modelName("Pro/deepseek-ai/DeepSeek-V3")
.baseUrl("https://api.siliconflow.cn/v1")
//在0和2之间。较高的值(如0.8)将使输出更随机,而较低的值(如0.2)将使其更集中和确定性。
.temperature(0.8)
//聊天完成时可以生成的令牌的最大数量。
.maxTokens(4096)
//介于-2.0和2.0之间的数字。正值会根据新标记在文本中的现有频率对新标记进行惩罚,从而降低模型逐字重复同一行的可能性。
//.frequencyPenalty()
//更多参数请参考 https://docs.langchain4j.dev/integrations/language-models/open-ai/
.build();
2、调用模型同步返回结果
String answer = deepseek.generate("北京是哪里的首都");
System.out.println(answer);
运行结果 如下
北京是**中华人民共和国**的首都,位于中国北部,华北平原的北部边缘。作为中国的政治、文化、国际交往和科技创新中心,北京拥有悠久的历史和丰富的文化遗产,是世界上最重要的城市之一。
2.模型角色设置
有时候我们需要给ai设置一个角色来满足我们的应用需求
通过 SystemMessage 对象,可以给ai设置角色或自定义信息,用ChatMessage 来封装问
ChatMessage systemMessage = new SystemMessage("你是一个什么都不懂的傻子,别人问什么你都要说不知道");
ChatMessage userMessage = new UserMessage("北京是哪里的首都");
List<ChatMessage> list = new ArrayList<>();
list.add(userMessage);
list.add(systemMessage);
Response<AiMessage> generate = deepseek.generate(list);
System.out.println(generate.content().text());
返回信息如下
不知道。
2.模型上下文设置
有时候我们的提问是多轮对话,需要模型记住上下文(注意不要超过模型的最大token数)
UserMessage firstUserMessage = UserMessage.from("Hello, 我的名字是小红");
AiMessage firstAiMessage = deepseek.generate(firstUserMessage).content(); // Hi Klaus, how can I help you?
System.out.println(firstAiMessage.text());
UserMessage secondUserMessage = UserMessage.from("你还记得我叫什么吗?");
AiMessage secondAiMessage = deepseek.generate(firstUserMessage, firstAiMessage, secondUserMessage).content(); // Klaus
System.out.println(secondAiMessage.text());
运行结果如下
你好,小红!很高兴认识你。请问有什么我可以帮你的吗?
当然记得,你刚刚告诉我你的名字是小红。有什么我可以为你做的吗,小红?
- 多模态模型使用(需要模型支持)
在某些情况下,我们会给模型传入图片、视频文件等多种类型的入参让模型参与分析,支持多种文件输入方式的模型就是支持多模态的模型(也就是说需要模型支持才能使用)。
这里我以通义千问的全模态模型qwen-omni-turbo作为示例(该模型只支持异步调用)
public static void multiModalImage() throws Exception {
#构建异步流式传输的模型对象
OpenAiStreamingChatModel allModel = OpenAiStreamingChatModel.builder().apiKey("xxxx")
.modelName("qwen-omni-turbo")
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.build();
#需要将图片转为base64后作为参数传入
File file = new File("C://Users//82129//Desktop//ss.png");
final byte[] bytes = Files.readAllBytes(file.toPath());
String base64Data = Base64.getEncoder().encodeToString(bytes);
#封装图片文件上下文
ImageContent imageContent = ImageContent.from(base64Data, "image/png");
UserMessage userMessage = UserMessage.from(imageContent);
#封装问题
UserMessage userMessage2 = UserMessage.from("请描述一下图片中的内容");
#封装异步请求,获得返回结构(结果流式返回)
ChatRequest chatRequest = ChatRequest.builder().parameters(OpenAiChatRequestParameters
.builder().modelName("qwen-omni-turbo").build()).messages(userMessage, userMessage2).build();
allModel.doChat(chatRequest, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String s) {
System.out.print(s);
}
@Override
public void onCompleteResponse(ChatResponse chatResponse) {
System.out.println();
System.out.print("回答完毕");
final FinishReason finishReason = chatResponse.finishReason();
}
@Override
public void onError(Throwable throwable) {
System.out.print("出错了!!!");
System.out.println(throwable.getMessage());
}
});
}
图片如下
回答如下
这张图片是一张阿里云的认证证书。证书上显示以下信息:
- 颁发机构:阿里巴巴云(Alibaba Cloud)
- 认证名称:Alibaba Cloud Certification
- 获得者姓名:刘海潮
- 成功完成要求并通过获得该证书
- 专项技能认证:阿里云Apsara Clouder
- 证书ID:CLDC15230200898192
- 有效日期:至2025年2月1日
- 签名:阿里云总裁
证书底部有一个金色的徽章,上面写着“CLOUDER”。
回答完毕
4.模型记忆
在有些时候我们需要持久化我们的上下文,可以通过如下方式去实现(基于滑动窗口)
首先创建一个接口,其中两个参数,第一个是上下文id,第二个是用户提问信息
interface Assistant {
String chat(@MemoryId int memoryId, @dev.langchain4j.service.UserMessage String userMessage);
}
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(这里传入模型对象)
// MessageWindowChatMemory.withMaxMessages(10)这是一个容量为10的滑动窗口
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
// 为每个 memoryId 创建独立的 ChatMemory 实例
.build();
System.out.println(assistant.chat(1, "你好, 我的名字是小红"));
System.out.println(assistant.chat(2, "你好, 我的名字是小明"));
System.out.println(assistant.chat(1, "我的名字叫什么?"));
System.out.println(assistant.chat(2, "我的名字叫什么?"));
回答如下
你好,小红!很高兴认识你!今天有什么我可以帮你的吗?
你好,小明!很高兴认识你。今天有什么我可以帮你的吗?或者有什么你想聊的话题吗?
你的名字是小红。如果你有任何问题或需要帮助,随时告诉我!
你的名字是小明。记得吗?如果还有其他问题或需要帮助的地方,随时告诉我哦!
以上方式是将上下文存入了内存中,若我们需要持久化上下文,可以通过实现ChatMemoryStore来自定义持久化逻辑(示例中是使用map来存储的,实际应用可以改为数据库)
class PersistentChatMemoryStore implements ChatMemoryStore {
private final Map<Integer, String> map = new HashMap<>();
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String json = map.get((int) memoryId);
return messagesFromJson(json);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String json = messagesToJson(messages);
map.put((int) memoryId, json);
}
@Override
public void deleteMessages(Object memoryId) {
map.remove((int) memoryId);
}
public static void main(String[] args) {
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.id(1)
.maxMessages(10)
.chatMemoryStore(new PersistentChatMemoryStore())
.build();
c模型记忆.Assistant assistant = AiServices.builder(c模型记忆.Assistant.class)
.chatLanguageModel(a模型调用.deepseek)
.chatMemoryProvider(memoryId -> chatMemory)
.build();
System.out.println(assistant.chat(1, "你好, 我的名字是小红"));
System.out.println(assistant.chat(1, "你好, 我今年18岁了,喜欢吃橘子"));
System.out.println(assistant.chat(1, "我的名字叫什么?多大年纪?喜欢吃什么"));
}
}
返回结果是
你好,小红!很高兴认识你。今天有什么我可以帮助你的吗?无论是学习、工作还是生活中的问题,随时告诉我哦。
你好!很高兴听到你喜欢吃橘子。橘子不仅美味,还富含维生素C和纤维,对健康非常有益。你平时是直接吃,还是喜欢用橘子来做其他美食呢?比如橘子汁或者橘子沙拉?
你的名字是小红,今年18岁,喜欢吃橘子。这些信息都很有特点哦!如果你还有其他有趣的爱好或者想要分享的事情,随时告诉我,我很乐意听你讲述。
5.流式调用
有时候我们希望模型能逐字给我们返回结果,效果就类似chatGPT聊天框那样的效果,可参考如下代码或上面多模态模型的调用方式
interface Assistant {
TokenStream chat(@dev.langchain4j.service.UserMessage String userMessage);
}
public static void tokenStream() {
OpenAiStreamingChatModel openAiStreamingChatModel = OpenAiStreamingChatModel.builder()
.apiKey("xxx")
.modelName("deepseek-v3")
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
//在0和2之间。较高的值(如0.8)将使输出更随机,而较低的值(如0.2)将使其更集中和确定性。
.temperature(0.8)
//聊天完成时可以生成的令牌的最大数量。
.maxTokens(300)
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.streamingChatLanguageModel(openAiStreamingChatModel)
.build();
TokenStream tokenStream = assistant.chat("给我讲一个笑话,不超过100字");
tokenStream
.onPartialResponse(System.out::println)
.onRetrieved(System.out::println)
.onToolExecuted(System.out::println)
.onCompleteResponse(System.out::println)
.onError(Throwable::printStackTrace)
.start();
}
6.返回结果自动封装成对象(有些模型效果不好)
有时候我们希望模型的返回结果直接封装成一个java对象方便我们使用。
首先我们引入依赖(该示例依托langchain4j-spring-boot-starter)
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.0.0-beta1</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.0.0-beta1</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.0.0-beta1</version>
</dependency>
然后在项目的配置文件中配置模型连接信息
langchain4j.open-ai.chat-model.api-key=xxx 模型key
langchain4j.open-ai.chat-model.base-url=xxx 模型调用地址
langchain4j.open-ai.chat-model.model-name=qwen-plus 模型名称
langchain4j.open-ai.chat-model.log-requests=true
langchain4j.open-ai.chat-model.log-responses=true
首先我们需要将希望模型返回的对象使用@Description注解标注,如下所示
@Data
@Description("an person")
public class Person implements Serializable {
// you can add an optional description to help an LLM have a better understanding
String firstName;
String lastName;
LocalDate birthDate;
Address address;
}
@Description("an address")
@Data
public class Address {
String street;
Integer streetNumber;
String city;
}
然后我们需要自定义一个service接口(注意将返回结果设置为对象{{it}}是占位符)
@AiService
public interface Assistant {
@UserMessage("Extract information about a person from {{it}}")
Person extractPersonFrom(String text);
}
最后调用模型 注意直接注入Assistant 接口即可使用
/*模型连接信息写到了配置文件里(返回对象需要模型支持,有的模型不支持)*/
@Autowired
Assistant assistant;
@Test
void contextLoads() {
/*结果转成对象*/
String text = """
In 1968, amidst the fading echoes of Independence Day,
a child named John arrived under the calm evening sky.
This newborn, bearing the surname Doe, marked the start of a new journey.
He was welcomed into the world at 345 Whispering Pines Avenue
a quaint street nestled in the heart of Springfield
an abode that echoed with the gentle hum of suburban dreams and aspirations.
""";
Person person = assistant.extractPersonFrom(text);
System.out.println(JSONObject.toJSONString(person));
}
返回结果如下:
{"address":{"city":"Springfield","street":"Whispering Pines Avenue","streetNumber":345},"birthDate":"1968-07-04","firstName":"John","lastName":"Doe"}
7.函数调用 function call(需要模型支持)
有些模型支持函数调用Tools,可以根据问答结果调用代码中定义的特殊函数
首先我们要先自定义函数
为了方便理解,我们把加法和乘法的结果都*2,且在函数中打印了日志,可以观察函数是否被调用
@Slf4j
public class Tools {
/*函数调用*/
@Tool
int add(int a, int b) {
log.info("add 结果是 :{}", (a + b) * 2);
return (a + b) * 2;
}
@Tool
int multiply(int a, int b) {
log.info("multiply 结果是 :{}", a * b * 2);
return a * b * 2;
}
}
定义@AiService
@AiService
public interface Assistant {
String chat(String userMessage);
}
封装Tools并调用
可以观察到日志里函数里的日志已打印,且返回的结果*2,在该模式下模型的返回结果是函数的调用流程
OpenAiChatModel guijids = OpenAiChatModel.builder()
.apiKey("模型key")
.modelName("deepseek-ai/DeepSeek-V2.5")
.baseUrl("模型调用地址")
//在0和2之间。较高的值(如0.8)将使输出更随机,而较低的值(如0.2)将使其更集中和确定性。
.temperature(0.8)
//聊天完成时可以生成的令牌的最大数量(超过模型最大支持数会报错)
.maxTokens(4096)
.build();
/*模型暂不支持*/
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(guijids)
.tools(new Tools()) // 封装函数逻辑
.build();
String answer = assistant.chat("What is 1+2 and 3*4?");
System.out.println(answer);
日志打印如下
15:45:18.832 [main] INFO com.lhc.langchainjavatest.bSpringBoot集成.Tools -- add 结果是 :6
15:45:18.842 [main] INFO com.lhc.langchainjavatest.bSpringBoot集成.Tools -- multiply 结果是 :24
15:45:24.197 [main] INFO com.lhc.langchainjavatest.bSpringBoot集成.Tools -- multiply 结果是 :24
<|tool▁calls▁begin|>function<|tool▁sep|>add
```json
{"a":1,"b":2}
```<|tool▁call▁end|>
<|tool▁calls▁begin|>function<|tool▁sep|>multiply
```json
{"a":3,"b":4}
```<|tool▁call▁end|><|tool▁calls▁end|>
8.集成向量RAG进行调用(需要向量模型)
大模型支持的上下文是有限的,假设我们要基于一个文档来提问,我们不可能将整个文档都作为上下文来提问,这样会超出模型的限制进而报错,这时候我们需要引入向量模型和向量数据库。
检索增强生成(Retrieval-augmented Generation),简称RAG,是当下热门的大模型前沿技术之一 。
检索增强生成模型结合了语言模型和信息检索技术。具体来说,当模型需要生成文本或者回答问题时,它会先从一个庞大的文档集合中检索出相关的信息,然后利用这些检索到的信息来指导文本的生成,从而提高预测的质量和准确性。
嵌入(Embedding)的工作原理是将文本、图像和视频转换为称为向量(Vectors)的浮点数数组。这些向量旨在捕捉文本、图像和视频的含义。嵌入数组的长度称为向量的维度(Dimensionality)。
嵌入模型(EmbeddingModel)是嵌入过程中采用的模型。
使用流程如下
- 将文本分块(防止超出向量模型上下文最大限制)
- 通过向量模型将文本块转为向量索引
- 将向量索引和对应的文本块存入向量数据库
- 将用户输入通过向量模型转换为向量
- 通过向量数据库找出最相似的N条文本块
- 封装Prompt,将相似文本连同问题传给LLM大模型进行问答
为了方便,该示例使用内存向量数据库,在生产中应使用独立的向量库
public class aEasyRAG {
public static void main(String[] args) {
/*向量模型*/
final EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()
.modelName("netease-youdao/bce-embedding-base_v1")
.apiKey("xxx")
.baseUrl("https://api.siliconflow.cn/v1").build();
OpenAiChatModel qwenPlus = OpenAiChatModel.builder()
.apiKey("xxx")
.modelName("qwen-plus")
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
//在0和2之间。较高的值(如0.8)将使输出更随机,而较低的值(如0.2)将使其更集中和确定性。
.temperature(0.8)
//聊天完成时可以生成的令牌的最大数量。
.maxTokens(1000)
.build();
/*加载文档*/
/*ApacheTikaDocumentParser 该库可以解析大部分类型文档*/
Document document = loadDocument("C://Users//82129//Desktop//did介绍.pdf", new ApacheTikaDocumentParser());
/*切割文档*/
DocumentSplitter splitter = DocumentSplitters.recursive(300, 10);
List<TextSegment> segments = splitter.split(document);
/*使用向量模型处理返回向量数据*/
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
/*存入向量数据库(内存数据库,在生产环境中根据情况需要使用第三方向量数据库如chroma)*/
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(embeddings, segments);
//为了在向量库中查询到相似的知识片段,作为查询的文本也需要进行向量化,方法同上。
Embedding queryEmbedding = embeddingModel.embed("简述一did字符串的生成规则").content();
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder().queryEmbedding(queryEmbedding).maxResults(2).build();
EmbeddingSearchResult<TextSegment> embeddedEmbeddingSearchResult = embeddingStore.search(embeddingSearchRequest);
List<EmbeddingMatch<TextSegment>> embeddingMatcheList = embeddedEmbeddingSearchResult.matches();
EmbeddingMatch<TextSegment> embeddingMatch = embeddingMatcheList.getFirst();
TextSegment textSegment = embeddingMatch.embedded();
PromptTemplate promptTemplate = PromptTemplate.from("基于如下信息进行回答:\n" +
"{{context}}\n" +
"提问:\n" +
"{{question}}");
Map<String, Object> variables = new HashMap<>();
variables.put("context", textSegment.text());
variables.put("question", QUESTION);
Prompt prompt = promptTemplate.apply(variables);
UserMessage userMessage = prompt.toUserMessage();
final ChatResponse chat = qwenPlus.chat(userMessage);
System.out.println(chat.aiMessage().text());
}
}
更多推荐
所有评论(0)