Langchain4j Chat

无论是工作,生活,大模型今天已经成为我们不可或缺的工具。今天我们要来深入的研究一下与大模型的交互过程。其实,与大模型交互的原理也很简单,如果我们把某个大模型当成是一个后台服务器程序的话,那么与大模型的一次对话就是把我们输入的自然语言当成请求发送给大模型,大模型接收到我们输入的自然语言后再将它的对话内容作为响应返回给我们。

所以,与大模型的一次对话(也称一轮对话),其实就是和大模型的一次请求和响应过程,如果是与大模型展开了多轮对话也就是和模型的多次请求和响应过程。知道了这一交互原理,我们自己也可以通过JAVA代码来实现与大模型的交互,但在真正实现交互之前,我们需要先了解下提示词。

Prompt即提示词,是使用大语言模型的基础,它引导人工智能模型生成特定输出。对于熟悉ChatGPT的用户而言,提示词或许只是输入对话框并发送至API的文本。但是,提示词文本本身并非简单的字符串。

今天我们所使用的大模型的提示词往往可能是多段文本输入,其中每一段文本输入都对应着一个角色。不同的角色从不同的角度出发向大模型阐明目的和上下文。这种结构化方法能够提升与AI交互的效率,目前主要有以下几种角色:

  • System:系统角色设定AI的“人设”、行为准则、背景知识和回答框架。相当于幕后导演,不直接参与对话,但决定了AI的“性格”和“能力边界”。
  • User:用户角色代表普通用户,向大模型提出的问题、指令或需求。
  • Assistant:助手角色代表大模型的回复(主要用来实现大模型的记忆),从而维持多轮对话的连贯性和完整性
  • Tool/Function:工具角色用于告知模型工具执行结果,扩展模型能力

在一段完整的提示词中所包含的不同角色对应的文本,我们称之为Message,即消息。也就是说一个完整的Prompt由一条或多条Message消息组成

为了让大家更好的理解不同的角色,我们以系统角色和用户角色为例。我们希望AI扮演一个鼓励式的数学导师,用简单步骤教小学生解题,那么首先系统角色的消息如下:

[
  {
    "role": "system",
    "content": "你是一个热情耐心的数学导师,专门教小学生。请用简单步骤解答数学题,并在最后加上一句鼓励的话。"
  }
]

当我们想要模型帮我们解题的时候,我们就直接向模型发送请求,请求中包含的完整的提示词如下,其中就包含了系统消息和用户消息(用户消息就是使用者向模型提出的问题),从而让大模型知道:1. 系统设定(我是谁),2. 用户问题(我要回答什么)

[
  {
    "role": "system", 
    "content": "你是一个热情耐心的数学导师,专门教小学生。请用简单步骤解答数学题,并在最后加上一句鼓励的话。"
  },
  {
    "role": "user",
    "content": "小明有5个苹果,妈妈又给了他3个,他现在有几个苹果?"
  }
]

根据系统提示词以及提出的问题,模型的回复如下:

"我们一步步来算哦!\n\n1. 小明一开始有:5个苹果\n2. 妈妈又给了他:3个苹果\n3. 现在总共有:5 + 3 = 8个苹果\n\n小明现在有8个苹果!你真棒,会问这么好的问题!"

如果大家对提示词有兴趣的话,在附录部分给大家做了一点关于提示词的补充!

在理解了提示词,提示词中的消息,以及不同的角色后,接下来我们就准备着手实现与大模型的交互(聊天)了。既然与大模型的一次交互的本质是一次请求和响应过程,那么需要实现与大模型的交互,还有一些基本问题需要解决:

  • 请求大模型就相当于访问大模型所提供的API接口,市面上有很多不同的大模型,其提供的API接口或多或少都有所差别,是否需要针对不同的大模型有针对性的构造不同的请求?
  • 如何构造并发送对大模型的请求,其URL是什么,请求中需要传递哪些参数?
  • 大模型返回的的数据是怎样的,如何接收并解析大模型返回的结果?

仔细想想,以上的问题如果真的要我们自己解决还是很麻烦的。但是以上的这些基本问题都属于访问大模型所要解决的通用问题,这种通用问题当然有通用的框架来解决——Langchain4j。

LangChain4j 的目标是简化将 LLM 集成到 Java 应用程序中的过程,其简化主要体现在以下几个方面:

  • 统一API: 虽然市面上有很多不同的模型,不同的模型的接口之间难免存在差异,LangChain4j 提供统一的 API,避免了学习和实现每个特定 API 的需求,我们可以在无需重写代码的情况下,无缝切换不同的模型。
  • 向使用者屏蔽请求和响应细节: 使用LangChain4j之后,访问大模型的API就变成了对LangChain4j的方法调用,这些方法向用户隐藏了具体的请求细节比如URL,以及响应数据的获取和解析(因为方法返回值就是大模型的响应)
  • 全面的工具箱:与大模型的交互其实比我们想象的要复杂一些,还会涉及到提示词模版,聊天记忆管理,工具调用等功能,而LangChain4j将这些功能已经全部实现,我们只需使用即可。

接下来我们需要通过LangChain4j访问阿里的通义千问大模型实现一轮对话,为了能够正确访问通义千问,我们必须知道以下三个信息:

  • 通义千问的的base_url: 这个值决定了我们向大模型发起请求的地址
  • 访问通义千问所需的api_key:这个值象征着访问通义千问的合法身份。开发者需要有一个合法的身份标识来表示自己是合法的用户,只有合法的用户才能成功访通义千问大模型
  • 所访问的具体模型名称model: 通义千问还有细分的不同模型,它们各有自己的名字,在访问大模型的时候,我们必须指定具体的模型名称。

首先,如果是新用户,先打开阿里云官方网站注册一个账号

image-20260126162511531
image-20260126155045768

新用户注册成功之后,打开阿里云百联平台,首先会有一个开通阿里云百炼平台的协议,签署之后,会赠送100万免费tokens额度,弹出如下通知窗口:

image-20260126161827119

如果不是首次使用阿里云百炼平台的用户,则不会有上述弹窗。进入云百炼平台后,现申请api-key

image-20260126164704468
image-20260126164825172
image-20260126165126716
image-20260126165237020

关于base_url对于通义千问模型而言,固定为https://api.deepseek.com,至于模型名称,我们通常使用deepseek-v4-pro或者qwen-plus

到此为止,api_key, base_url以及模型名称就全都有了,接下来我们就是基于Langchain4j来帮我们实现与大模型的一轮对话多轮对话

Langchain4j提供了两种抽象层次供我们使用:

  • 低层次的抽象: 在低层次的抽象中,Langchain4j提供了很多的组件,但在实现功能的时候,我们需要自己“组合”多个组件来完成所需功能。其优点是我们自己可以完全控制我们所使用的组件及其组合过程,但是缺点是需要写大量的机械代码(代码几乎是固定的)。
  • 高层次的抽象: 在高层次的抽象中,提供了AiServices,它封装了底层的组件组合及其使用,开发者只需要做简单的声明即可完成所需功能,因此可以少写很多代码

低抽象层次实现

快速入门

在使用Langchain4j之前需要引入其依赖

<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-open-ai</artifactId>
  <version>1.15.0</version>
  <scope>compile</scope>
</dependency>

创建ChatModel接口的对象,指定base_url, api_key以及模型名称

OpenAiChatModel model = OpenAiChatModel.builder()
  // base_url
  .baseUrl("https://api.deepseek.com")
  // api-Key
  .apiKey(System.getenv("deepseek-api-key")) // 提供API-KEY
  // 调用的模型名称
  .modelName("deepseek-v4-pro")
  //.logRequests(true) // 记录请求日志,可以先注释掉
  .build();  

紧接着,调用ChatModel接口的chat方法传递提示词Prompt开始对话,ChatModel接口中主要有两个常用的chat方法:

public interface ChatModel {

    
    /*
       对话时传递一个仅包含用户消息的提示词
    */
    String chat(String userMessage);
        
    /*
      对话时可以传递多个角色的消息,ChatMessage接口主要有以下实现类:
      1. SystemMessage:代表系统角色消息
      2. UserMessage: 代表用户角色消息
      3. AiMessage: 代表助手角色消息
    */
    ChatResponse chat(List<ChatMessage> messages)
}

我们先使用简单的chat方法实现一轮对话:

// 传入用户消息最为提示词,开始对话
String chatResult = model.chat("你会编程么?");
System.out.println(chatResult);

接着,在使用第二个chat方法,同时传入系统提示词和用户提示词实现一轮对话:

// 构造系统消息
SystemMessage systemMessage = SystemMessage
    .from("假设你是一名精通JAVA语言的开发者,回答任何编程相关的问题你都会基于JAVA语言来回答");
// 构造用户消息
UserMessage userMessage = UserMessage.from("你会编程吗?");

/*
   传入系统消息,用户消息,将其构造为一个完整的提示词并发起请求
   ChatResponse对象封装了大模型的整个响应
*/
ChatResponse chatResponse = model.chat(Arrays.asList(systemMessage, userMessage));
// 从响应对象中获取真正代表大模型回复内容的AiMessage对象
AiMessage aiMessage = chatResponse.aiMessage();
// 从AiMessage对象中获取大模型回复的字符串内容
String chat = aiMessage.text();
System.out.println(chat);

如果把日志打开

.logRequests(true) // 记录请求日志,可以先注释掉
.logResponses(true) // 记录响应日志,可以先注释掉

需要引入日志依赖

<!-- 引入日志依赖-->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.5.18</version>
</dependency>

控制台中会将过程也输出,【前面这段就是日志】

11:49:53.327 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP request:
- method: POST
- url: https://api.deepseek.com/chat/completions
- headers: [Authorization: Beare...dd], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "deepseek-reasoner",
  "messages" : [ {
    "role" : "user",
    "content" : "你会编程么?"
  } ],
  "stream" : false
}

11:50:08.370 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP response:
- status code: 200
- headers: [access-control-allow-credentials: true], [connection: keep-alive], [content-type: application/json], [date: Sat, 11 Oct 2025 03:50:01 GMT], [server: CW], [set-cookie: [HWWAFSESTIME=1760154597499; path=/, HWWAFSESID=670f9127856081c4297; path=/]], [strict-transport-security: max-age=31536000; includeSubDomains; preload], [transfer-encoding: chunked], [vary: origin, access-control-request-method, access-control-request-headers], [x-content-type-options: nosniff], [x-ds-trace-id: 4496f0839c4637e6f7ff18bba64799f9]
- body: {"id":"78febfee-b411-49e4-8f1d-1d9fc43421af","object":"chat.completion","created":1760154601,"model":"deepseek-reasoner","choices":[{"index":0,"message":{"role":"assistant","content":"是的,我能够帮助解决编程相关的问题!我可以提供以下支持:\n\n1. **语言支持**  \n   包括 Python、JavaScript、Java、C++、HTML/CSS、SQL 等常见语言的代码示例、调试和优化建议。\n\n2. **场景覆盖**  \n   - 基础语法/算法讲解  \n   - 项目结构设计  \n   - 自动化脚本编写  \n   - 数据分析/可视化  \n   - Web 开发前后端问题\n\n3. **实用功能**  \n   - 代码解释与注释  \n   - 错误排查与修复  \n   - 代码重构建议  \n   - 学习资源推荐\n\n**举个例**:  \n如果你需要一段 Python 爬虫代码,我可以立即生成并说明每步逻辑;如果遇到报错,把错误信息发我即可分析原因。\n\n请告诉我你的具体需求(语言/场景/问题),我会尽力提供清晰可用的解决方案 👨💻","reasoning_content":"哦,用户问我会不会编程。这个问题比较基础,但需要明确回应能力范围。考虑直接肯定回答,同时列举具体能提供的帮助类型,让用户快速了解我的编程支持维度。\n\n想到可以按常见需求分类说明,比如代码编写、调试、教学等,这样用户能直观找到需要的帮助。结尾加个具体邀请,引导用户提出实际编程问题。\n\n用简洁的条目式结构,但避免显得机械。考虑在结尾使用表情符号增加亲和力。"},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":8,"completion_tokens":296,"total_tokens":304,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":101},"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":8},"system_fingerprint":"fp_ffc7281d48_prod0820_fp8_kvcache"}

提示词模版

通过快速入门案例,我们已经可以基于不同角色的消息构造提示词(系统消息和用户消息)实现一轮对话了,接下来我们实现一个需求,让大模型帮我们写出生成斐波那契数列的代码。实现如下:

OpenAiChatModel model = OpenAiChatModel.builder()
  // base_url
  .baseUrl("https://api.deepseek.com")
  // api-Key
  .apiKey(System.getenv("deepseek-api-key")) // 提供API-KEY
  // 调用的模型名称
  .modelName("deepseek-v4-pro")
  //.logRequests(true)  // 打印请求日志
  .build(); 

// 构造系统消息
SystemMessage systemMessage = SystemMessage.from("你是一个专业的JAVA开发,你只能使用JAVA语言来写代码,不能使用其他语言。");
// 构造用户消息
UserMessage userMessage = UserMessage.from("写一个斐波那契数列的代码");

/*
   传入系统消息,用户消息,将其构造为一个完整的提示词并发起请求
   ChatResponse对象封装了大模型的整个响应
*/
ChatResponse chatResponse = model.chat(Arrays.asList(systemMessage, userMessage));
// 从响应对象中获取真正代表大模型回复内容的AiMessage对象
AiMessage aiMessage = chatResponse.aiMessage();
// 从AiMessage对象中获取大模型回复的字符串内容
String chat = aiMessage.text();
System.out.println(chat);

上面是基于JAVA语言生成的代码,如果我们要基于C语言,或者其他比如C++,Python,Go之类的语言来生成代码,只需要对系统消息稍作修改,将其中的JAVA语言改成其他目标语言的名称即可。

于是,我们发现有时提示词所包含的消息可能并不是一段内容完整的文本,这段文本已经定义好了确定的框架,但是个别关键的地方尚未确定,需要在实际使用时动态替换为所需的具体内容。其实,这段预设好的文本框架就是提示词模版,我们可以将刚才的提示词中的系统消息改造如下:

你是一个专业的{{language}}开发,你只能使用{{language}}语言来写代码,不能使用其他语言。

在这段针对系统消息的提示词模版中,系统消息的大框架是确定的, 唯一不确定的是具体的语言,但到底是哪种语言呢?{{}} 表示一个占位符,{{language}}中的language表示占位符的名称,在真正使用的时候,我们只需要将该提示词模版中的占位符填充为具体的语言名称即可。

在Langchain4j中有专门的对象来表示提示词模版,以及实现模版中占位符的填充,我们将上面的代码改造为使用提示词模版的版本如下:

OpenAiChatModel model = OpenAiChatModel.builder()
  // base_url
  .baseUrl("https://api.deepseek.com")
  // api-Key
  .apiKey(System.getenv("deepseek-api-key")) // 提供API-KEY
  // 调用的模型名称
  .modelName("deepseek-v4-pro")
  //.logRequests(true)  // 打印请求日志
  .build(); 


// 创建系统消息的提示词模版
PromptTemplate systemTemplate = PromptTemplate
    .from("你是一个专业的{{language}}开发,你只能使用{{language}}语言来写代码,不能使用其他语言。");

/*
   创建Map对象,用来存储真正填充占位符的值
   Map中的key表示占位符的名字,value表示用来替换该占位符的值
   如果模版中有多个不同的占位符要填充,则可以向Map中放入多个key-value键值对
*/
HashMap<String, Object> templateParamMap = new HashMap<>();
templateParamMap.put("language", "JAVA");

// 利用Map对象中的各个键值对填充模版中对应的占位符的值, 得到Prompt对象
Prompt systemPrompt = systemTemplate.apply(templateParamMap);

// 将Prompt对象转化为系统消息
SystemMessage systemMessage = systemPrompt.toSystemMessage();
// 构造用户消息
UserMessage userMessage = UserMessage.from("写一个斐波那契数列的代码");

/*
   传入系统消息,用户消息,将其构造为一个完整的提示词并发起请求
   ChatResponse对象封装了大模型的整个响应
*/
ChatResponse chatResponse = model.chat(systemMessage, userMessage);
// 从响应对象中获取真正代表大模型回复内容的AiMessage对象
AiMessage aiMessage = chatResponse.aiMessage();
// 从AiMessage对象中获取大模型回复的字符串内容
String chat = aiMessage.text();
System.out.println(chat);

大家会发现,使用低层次的抽象来实现提示词模版,代码确实会麻烦一些,因为模版创建,占位符的填充等都需要自己写代码完成。但是提示词模版是可以复用的,有了提示词模版我们就可以更好实现提示词的维护!

高抽象层次实现

在基于低层次的抽象实现了一轮对话,尤其实现了基于提示词模版的一轮对话之后,我们可以明显的感觉到使用低层次的抽象,我们可能会用到不同的组件,比如ChatModel,PromptTemplate,SystemMessage,UserMessage等并将其组合在一起,代码较为繁杂。

相比较而言,使用高层次的抽象AiServices就会简单很多,只需要做简单声明,剩下的事情都由框架自动完成,尤其在实现聊天记忆,Tools工具调用,以及RAG知识库等复杂功能的时候,它可以减少大量代码,提升开发效率。

AiServices基本使用

在使用前同样需要引入Langchain4j依赖, 但是为了方便大家理解理解,还需引入如下依赖,引入日志依赖的目的是以便将我们发送的请求内容在控制台中打印出来。

<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j</artifactId>
  <version>1.15.0</version>
</dependency>
<!-- 引入日志依赖-->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.5.18</version>
</dependency>

首先,我们定义一个具有单个方法chat的接口,该方法将 String作为输入并返回String

public interface FirstAssistant {

   /*
      chat方法的方法参数默认表示用户消息字符串
      chat方法的返回值表示大模型返回的字符串内容
   */
   String chat(String userMessage);
}

接着就可以实现一轮对话了

OpenAiChatModel model = OpenAiChatModel.builder()
  // base_url
  .baseUrl("https://api.deepseek.com")
  // api-Key
  .apiKey(System.getenv("deepseek-api-key")) // 提供API-KEY
  // 调用的模型名称
  .modelName("deepseek-v4-pro")
  .logRequests(true) // 记录请求日志
  .build(); 

// 使用AiServices类的create方法,我们所定义的接口称之为AI服务接口,我们所创建我们接口(子类)对象,我们称之为AI服务实例
FirstAssistant assistant = AiServices.create(FirstAssistant.class, model);

// 调用AI服务实例定义的方法, 传递用户消息即可(也就是说提示词中只包含用户消息)
String chat = assistant.chat("你是谁");

AI服务实例的生成是基于JDK的动态代理技术实现的(感兴趣的话可以去了解一下,这里不做统一讲解了)

我们可以看一下发起的请求日志如下:

- method: POST
- url: https://api.deepseek.com
- headers: [Authorization: Beare...dd], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "deepseek-v4-pro",
  "messages" : [ {
    "role" : "user",
    "content" : "你是谁?"
  } ],
  "stream" : false
}

重点观察body部分,我们会发现调用AI服务实例的方法所发起的请求中,确实将方法参数的值当成了用户角色的提示词。

接下来,有同学可能会想如果调用AI服务实例的方法时,我还想在提示词中使用系统消息呢?很简单我们只需要使用@SystemMessage来声明即可。

public interface FirstAssistant {

   /*
      通过@SystemMessage注解来声明我们所要使用的系统消息即可
   */
   @SystemMessage("假设你现在是一名JAVA讲师,专门帮助别人解决JAVA开发的问题")
   String chat(String userMessage);
}

其他代码不变

OpenAiChatModel model = OpenAiChatModel.builder()
  // base_url
  .baseUrl("https://api.deepseek.com")
  // api-Key
  .apiKey(System.getenv("deepseek-api-key")) // 提供API-KEY
  // 调用的模型名称
  .modelName("deepseek-v4-pro")
  .logRequests(true) // 记录请求日志
  .build(); 

// 使用AiServices类的create方法,我们所定义的接口称之为AI服务接口,我们所创建我们接口(子类)对象,我们称之为AI服务实例
FirstAssistant assistant = AiServices.create(FirstAssistant.class, model);

// 调用AI服务实例定义的方法, 传递用户消息即可(也就是说提示词中只包含用户消息)
String chat = assistant.chat("你是谁");

我们再来看一下请求日志:

- method: POST
- url: https://api.deepseek.com
- headers: [Authorization: Beare...dd], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "deepseek-v4-pro",
  "messages" : [ {
    "role" : "system",
    "content" : "假设你现在是一名JAVA讲师,专门帮助别人解决JAVA开发的问题"
  }, {
    "role" : "user",
    "content" : "你是谁?"
  } ],
  "stream" : false
}

可以看到完整的提示词中包含了系统提消息和用户消息!

更简单的提示词模版

使用AiServices之后,不论是系统消息还是用户消息,都可以使用提示词模版,而且简单到令人发指,只需使用@SystemMessage,以及@UserMessage注解声明模版,使用@V注解声明占位符的值即可。

public interface TopCoder {
  /*
     1. @SystemMessage和@UserMessage分别用来声明系统消息的提示词模版和用户消息的提示词模版
     2. 方法的每个参数就用来填充占位符,@V注解的属性值指定对应的占位符的名称
     
  */
  @SystemMessage("你是一个专业的{{language}}开发,你只能使用{{language}}语言来写代码,不能使用其他语言,且代码必须简洁且具有较好的可读性")
  @UserMessage("请完成{{task}}的代码")
  String program(@V("language")String type, @V("task")String function);
}

其他代码仍然不变

OpenAiChatModel model = OpenAiChatModel.builder()
  // base_url
  .baseUrl("https://api.deepseek.com")
  // api-Key
  .apiKey(System.getenv("deepseek-api-key")) // 提供API-KEY
  // 调用的模型名称
  .modelName("deepseek-v4-pro")
  .logRequests(true) // 记录请求日志
  .build(); 

// 使用AiServices类的create方法,我们所定义的接口称之为AI服务接口,我们所创建我们接口(子类)对象,我们称之为AI服务实例
TopCoder topCoder = AiServices.create(TopCoder.class, model);

// 调用AI服务实例定义的方法, 传递用来填充提示词模版的值即可
String code = topCoder.program("JAVA", "斐波那契数列");

请求日志如下:

- method: POST
- url: https://api.deepseek.com
- headers: [Authorization: Beare...dd], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "deepseek-v4-pro",
  "messages" : [ {
    "role" : "system",
    "content" : "你是一个专业的JAVA开发,你只能使用JAVA语言来写代码,不能使用其他语言,且代码必须简洁且具有较好的可读性"
  }, {
    "role" : "user",
    "content" : "请完成斐波那契数列的代码"
  } ],
  "stream" : false
}

模版文件

在实际开发中,我们所使用的提示词模版通常会比较长,为了更好的维护提示词模版,我们通常会将写在一个单独的文件——模版文件中。但是这又会带来一个问题就是,如何读取模版文件中的提示词模版内容呢?我们只需要在自定义AI服务接口方法上指定模版文件名称,Langchain4j就能自动帮我们读取模版文件中的模版内容了!

public interface ExamPreparationAssistant {
	/*
       1. 读取系统消息的提示词模版文件中的内容,通过SystemMessage注解的fromResource属性指定要读取的提示词模版文件名称
       2. 如果要读取用户消息的提示词模版文件中的内容也是同理,通过@UserMessage的fromResource指定模版文件名称即可
    */
  @SystemMessage(fromResource = "system-message.txt")
  String chat(String message);
}

需要注意的是我们必须把模版文件放在Maven工程的resource目录下才可以!!

image-20251118162302097

无状态的大模型

实现了一轮对话,很多同学可能会觉得多轮对话就很简单了,只要把多轮对话当成是多次一轮对话即可,真的是这样吗?接下来我们来看一个简单的多论对话的例子,首先定义AI服务接口

public interface ChatAssistant {
    
    String chat(String userMessage);
    
}

实现两轮对话

OpenAiChatModel model = OpenAiChatModel.builder()
  // base_url
  .baseUrl("https://api.deepseek.com")
  // api-Key
  .apiKey(System.getenv("deepseek-api-key")) // 提供API-KEY
  // 调用的模型名称
  .modelName("deepseek-v4-pro")
  .logRequests(true) // 记录请求日志
  .build(); 

// 使用AiServices类的create方法,我们所定义的接口称之为AI服务接口,我们所创建我们接口(子类)对象,我们称之为AI服务实例
ChatAssistant chatAssistant = AiServices.create(ChatAssistant.class, model);

// 第一轮对话
String firstChatContent = chatAssistant.chat("我人在武汉,今天天气不错,你觉得呢?");
System.out.println(firstChatContent);

// 第二轮对话
String secondChatContent = chatAssistant.chat("我在哪?");
System.out.println(secondChatContent);

两轮对话的回复分别如下:

第一轮的回复:
虽然我无法感知实时天气,但根据天气预报和武汉的气候特点,今天(如果是春秋季)确实很可能阳光和煦、温度宜人。如果天气晴好,不妨去东湖边散步、登黄鹤楼远眺,或感受江滩的微风——武汉的春日和秋日总是充满生机。当然,户外活动时也建议关注温差和空气质量哦!🌤️
第二轮的回复:
我无法直接获取你的物理位置信息哦!😊

如果你想知道自己现在的地理位置,可以:
- 查看手机或电脑上的地图应用
- 在设备设置中查看定位信息
- 使用地图网站或应用(如Google Maps、百度地图等)

很明显这两轮对话的上下文是割裂的,对于大模型而言第二轮对话和第一轮对话好像是无关的,因此及时我们在第一轮对话中已经告诉了大模型我们的位置,在第二轮对话中它还是不知道我们的位置。

为什么会这样呢?原因很简单,因为大模型是无状态的,也就是说大模型是没有记忆的,因此实际上我们每次与大模型的对话都相当于是开启了一次新的会话(对话)。

因为大模型是无状态的,所以,实现多论对话的核心问题就变成了如何让大模型基于我们在同一次会话中的多轮对话的历史消息即会话上下文与我们交互。(需要注意的是,这里的消息指的是前面所说的提示词中包含的各个角色的消息)

会话记忆

想要大模型在同一次会话的多轮对话中基于会话上下文与我们交互?其实,解决思路并不困难,既然大模型没有状态,但是实现多论对话需要大模型知道会话的上下文,那么我们可以自己来维护会话的历史内容即会话上下文,每次对话都将其发给大模型,具体思路如下:

  • 首先,我们把一次会话中的系统消息,以及用户消息,以及助手消息(大模型给我们返回的响应)等,按照时间顺序全部保存起来,这就相当于我们自己保存了会话的历史消息
  • 在同一次会话中,每次和大模型交互时,我们不仅给大模型发送本轮的提示词,也把我们保存下来的本次会话的历史消息发送给大模型
  • 大模型虽然没有记忆,但它可以理解,因此它就知道在这个会话中用户前面说了什么以及它自己回复了什么,就可以基于这个会话上下文来回复接下来的内容
  • 这样一来,我们只要保证在一次会话中,把我们所保存的会话的历史消息作为会话上下文发送给大模型,大模型就相当于有了会话的记忆,从而实现多轮对话

所以实现多论对话的核心,就在于保存会话的上下文也就是实现会话记忆

会话记忆的实现,还有一个非常关键的问题是,我们是否会将会话的全部历史消息作为会话上下文,也就是会话记忆发送给大模型?从上面的分析来看,好像是需要的,但实际却不会这样做,主要原因如下:

  • 为了适应 LLM 的上下文窗口。 LLM 一次可以处理的令牌(token令牌参见附录)数量是有限制的。在某些时候,对话可能会超过这个限制。在这种情况下,需要淘汰一些消息。通常,最旧的消息会被淘汰,但如果需要,也可以实现更复杂的算法。
  • 为了控制成本。 每个令牌都有成本,这使得每次调用 LLM 的成本逐渐增加。淘汰不必要的消息可以降低成本。
  • 为了控制延迟。 发送给 LLM 的令牌越多,处理所需的时间就越长。

因此,实际我们只会保存会话历史消息的一部分作为会话记忆,这就意味着,随着时间的推移,会话历史消息越来越多,那么就必然要从会话记忆中淘汰掉一些会话记忆,以便将更新的消息添加到会话记忆中。随之而来的问题是,淘汰哪些会话记忆呢?当然淘汰的策略有很多,Langchain4j默认使用的有两种:

  • 考虑提示词的条数,保留最近的 N 条消息,淘汰不再适用的旧提示词。
  • 考虑提示词包含的Token数量。保留最近的 N 个令牌,并根据需要淘汰旧的消息。消息是不可分割的,而一条消息中包含多个Token令牌,如果最近的N个令牌中只能包含一条消息的部分Token令牌,那么这条消息就将被淘汰。

多轮对话的实现

为了方便我们实现会话记忆,Langchain4j提供了ChatMemory接口表示会话记忆,并基于以上两种不同的淘汰策略提供了两个不同的实现类:

  • MessageWindowChatMemory:采用的淘汰算法是保留最近的N条
  • TokenWindowChatMemory:采用的淘汰算法是保留最近N个令牌

实现了ChatMemory接口的子类,即会话记忆实现类,它会自动将会话中的历史消息内容保存到内存,并根据自己所实现的淘汰算法更新维护会话记忆中的历史消息内,因此,基于ChatMemory,一次会话中的多轮对话的实现就很简单了,代码如下:

public interface ChatAssistant {
    
    String chat(String userMessage);
    
}

基于会话记忆实现两轮对话:

OpenAiChatModel model = OpenAiChatModel.builder()
  // base_url
  .baseUrl("https://api.deepseek.com")
  // api-Key
  .apiKey(System.getenv("deepseek-api-key")) // 提供API-KEY
  // 调用的模型名称
  .modelName("deepseek-v4-pro")
  .logRequests(true) // 记录请求日志
  .build(); 

// 创建维护最近10条消息的ChatMemory对象
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);


// 使用AiServices类的create方法,我们所定义的接口称之为AI服务接口,我们所创建我们接口(子类)对象,我们称之为AI服务实例
ChatAssistant chatAssistant = AiServices.builder(ChatAssistant.class)
    // 设置chatModel对象
    .chatModel(model)
    // 设置chatMemory对象
    .chatMemory(chatMemory)
    // 构造AI服务实例
    .build();

// 第一轮对话
String firstChatContent = chatAssistant.chat("我人在武汉,今天天气不错,你觉得呢?");
System.out.println(firstChatContent);

// 第二轮对话
String secondChatContent = chatAssistant.chat("我在哪?");
System.out.println(secondChatContent);

两轮对话的回复如下:

第一轮对话的回复:
根据最新天气数据,武汉今天天气确实很好!  
- **天气**:多云转晴,当前气温约26℃,体感舒适。  
- **建议**:空气质量为良,适合户外活动,比如去东湖绿道散步、江滩骑行,或者打卡樱花季尾声的景点。  

不过,武汉春季天气变化较快,傍晚温差可能略大,建议带件薄外套备用。如果是问未来几天的安排,明天(25日)可能有小雨,若计划出游可以优先今天哦!  

(如果你有具体出行需求,比如想去某个区域,可以告诉我,帮你看看更细致的天气建议~)
第二轮对话的回复:
根据之前的对话记录,您曾提到自己**人在武汉**。  

如果您需要了解武汉此刻的详细天气、交通或周边推荐,可以随时告诉我哦! 😊

并且我们可以在仔细看一看两轮对话所发起的请求:

- method: POST
- url: https://api.deepseek.com
- headers: [Authorization: Beare...dd], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "deepseek-v4-pro",
  "messages" : [ {
    "role" : "user",
    "content" : "我人在武汉,今天天气不错,你觉得呢?"
  } ],
  "stream" : false
}
 method: POST
- url: https://api.deepseek.com
- headers: [Authorization: Beare...dd], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
  "model" : "deepseek-v4-pro",
  "messages" : [ {
    "role" : "user",
    "content" : "我人在武汉,今天天气不错,你觉得呢?"
  }, {
    "role" : "assistant",
    "content" : "根据最新天气数据,武汉今天天气确实很好!  \n- **天气**:多云转晴,当前气温约26℃,体感舒适。  \n- **建议**:空气质量为良,适合户外活动,比如去东湖绿道散步、江滩骑行,或者打卡樱花季尾声的景点。  \n\n不过,武汉春季天气变化较快,傍晚温差可能略大,建议带件薄外套备用。如果是问未来几天的安排,明天(25日)可能有小雨,若计划出游可以优先今天哦!  \n\n(如果你有具体出行需求,比如想去某个区域,可以告诉我,帮你看看更细致的天气建议~)"
  }, {
    "role" : "user",
    "content" : "我在哪?"
  } ],
  "stream" : false
}

可以看到,第二轮对话所发起的完整提示词中确实包含了前面的历史消息!

需要注意的是,以上的示例中,请求中的提示词只包含了用户消息和助手消息,大家可以自行测试包含系统消息,以及使用提示词模版的情况。

Prompt提示词

关于提示词,给大家补充一下其他分类,以及一些优化技巧

其他常见分类

除了基于角⁠色的分类外,我们还‌可以从功能角度对提示词进行分类,仅作了‎解即可。

  • 指令型提⁠示词(Instruct‌ional Prompts):明确告诉 AI‎ 模型需要执行的任务,‌通常以命令式语句开头。
翻译以下文本为英文:春天来了,花儿开了。
  • 对话型⁠提示词(Conver‌sational Prompts):模拟‎自然对话,以问答形式‌与 AI 模型交互。
你认为人工智能会在未来取代人类工作吗?
  • 创意型⁠提示词(Creati‌ve Prompts):引导 AI 模型‎进行创意内容生成,如‌故事、诗歌、广告文案等。
写一个发生在未来太空殖民地的短篇科幻故事,主角是一位机器人工程师。
  • 角色扮⁠演提示词(Role‌-Playing Prompts):‎让 AI 扮演特定‌角色或人物进行回答
假设你是爱因斯坦,如何用简单的语言解释相对论?
  • 少样本⁠学习提示词(Few-‌Shot Prompts):提供一些示例‎,引导 AI 理解所‌需的输出格式和风格。
将以下句子改写为正式商务语言:
示例1:
原句:这个想法不错。
改写:该提案展现了相当的潜力和创新性。

示例2:
原句:我们明天见。
改写:期待明日与您会面,继续我们的商务讨论。

现在请改写:这个价格太高了。

Prompt 优化技巧

我们先来看一些基本技巧:

  • 明确指定任务和角色: 为 AI ⁠提供清晰的任务描述‌和角色定位,帮助模型理解背景和期望。
系统:你是一位经验丰富的Python教师,擅长向初学者解释编程概念。
用户:请解释 Python 中的列表推导式,包括基本语法和 2-3 个实用示例。
  • 提供详细说明和具体示例: 提供足够的⁠上下文信息和期望的‌输出格式示例,减少模型的不确定性。
请提供一个社交媒体营销计划,针对一款新上市的智能手表。计划应包含:
1. 目标受众描述
2. 三个内容主题
3. 每个平台的内容类型建议
4. 发布频率建议

示例格式:
目标受众: [描述]
内容主题: [主题1], [主题2], [主题3]
平台策略: [平台] - [内容类型] - [频率]
  • 使用结构化格式引导思维: 通过列表、表格等结构化格式,使指令更易理解,输出更有条理。
分析以下公司的优势和劣势:
公司: Tesla

请使用表格格式回答,包含以下列:
- 优势(最少3项)
- 每项优势的简要分析
- 劣势(最少3项)
- 每项劣势的简要分析
- 应对建议

接下来,再稍微看一点进阶的技巧:

  • 思维链提示法(‌Chain-of-Thought): 引导模型展示推理过程,逐步思考问题,提高复杂问题的准确性。
问题:一个商店售卖T恤,每件15元。如果购买5件以上可以享受8折优惠。小明买了7件T恤,他需要支付多少钱?

请一步步思考解决这个问题:
1. 首先计算7件T恤的原价
2. 确定是否符合折扣条件
3. 如果符合,计算折扣后的价格
4. 得出最终支付金额
  • 少样本学习(F‌ew-Shot Learning): 通过提供几⁠个输入 – 输出对的示‌例,帮助模型理解任务模式和期望输出。
我将给你一些情感分析的例子,然后请你按照同样的方式分析新句子的情感倾向。

输入: "这家餐厅的服务太差了,等了一个小时才上菜"
输出: 负面,因为描述了长时间等待和差评服务

输入: "新买的手机屏幕清晰,电池也很耐用"
输出: 正面,因为赞扬了产品的多个方面

现在分析这个句子:
"这本书内容还行,但是价格有点贵"
  • 分步骤指导(Step-by-Step): 将复杂任务分解为可管理的步骤,确保模型完成每个关键环节。
请帮我创建一个简单的网站落地页设计方案,按照以下步骤:

步骤1: 分析目标受众(考虑年龄、职业、需求等因素)
步骤2: 确定页面核心信息(主标题、副标题、价值主张)
步骤3: 设计页面结构(至少包含哪些区块)
步骤4: 制定视觉引导策略(颜色、图像建议)
步骤5: 设计行动召唤(CTA)按钮和文案
  • 自我评估和修正:让模型评估自己的输出并进行改进,提高准确性和质量。
解决以下概率问题:
从一副标准扑克牌中随机抽取两张牌,求抽到至少一张红桃的概率。

首先给出你的解答,然后:
1. 检查你的推理过程是否存在逻辑错误
2. 验证你使用的概率公式是否正确
3. 检查计算步骤是否有误
4. 如果发现任何问题,提供修正后的解答
  • 知识检索和引用: 引导模型检索相关信息并明确引用信息来源,提高可靠性
请解释光合作用的过程及其在植物生长中的作用。在回答中:
1. 提供光合作用的科学定义
2. 解释主要的化学反应
3. 描述影响光合作用效率的关键因素
4. 说明其对生态系统的重要性

对于任何可能需要具体数据或研究支持的陈述,请明确指出这些信息的来源,并说明这些信息的可靠性。
  • 多视角分析:引导模型从不同角度、立场或专业视角分析问题,提供全面见解。
分析"城市应该禁止私家车进入市中心"这一提议:

请从以下4个不同角度分析:
1. 环保专家视角
2. 经济学家视角
3. 市中心商户视角
4. 通勤居民视角

对每个视角:
- 提供支持该提议的2个论点
- 提供反对该提议的2个论点
- 分析可能的折中方案

Token

Token是AI模型工作的基本构建块。在输入时,模型将单词转换为Token;在输出时,将转换回单词。在英语中,一个Token大约相当于一个单词的75%。作为参考,莎士比亚全集约90万字,大约相当于120万个Token。也许更重要的是,Token=美元。在使用AI模型时,费用由使用的Token数量决定。输入和输出都会计入总Token数。

此外,模型有Token限制,限制单次API调用中处理的文本量。这个阈值通常被称为”上下文窗口”。模型不会处理超过此限制的任何文本。例如,ChatGPT3的Token限制为4K,而GPT4提供8K、16K和32K等不同选项。Anthropic的Claude AI模型有100K的Token限制,而Meta最近的研究推出了1MToken限制的模型。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇