封装一个通用的 LLM Client 并实现完整的对话记忆管理
这一篇,我们亲手封装通用的 LLM Client,实现对话记忆管理,让 SimpleAgent 真正拥有理解你、记住你的能力。
写在前面
在上一篇文章接入大模型:从第一个 Hello World 到多模态对话中,我们一起用 curl 和 Java 代码成功调用了大模型 API并完成了对话。
那一刻,你是不是也感受到了那种小小的成就感?😊
但我知道,那只是一个开始。你可能已经发现了几个让人心痒的问题:
- 代码写死了 API 地址和模型名称,换一个模型就要改代码
- 对话历史是手动维护的 List,有点笨拙
- SimpleAgent 还是一个"傻瓜式"的机器人,不会真正思考
别担心,今天我们一起解决这些问题。
想象一下,如果我们能给这个机器人换一颗真正的大脑,让它能够理解你、记住你、帮助你,那该多好啊!
这正是我们要一起做的三件事:
- 封装一个通用的 LLM Client,支持切换不同模型
- 实现自动的对话记忆管理,让智能体记住你们的对话
- 让 SimpleAgent 真正接入大模型,成为一个能思考的智能伙伴
但你要基础,这篇文档给出的 Demo 非常简单,没有复杂的设计模式,没有承上启下的架构,这是为了让你能够快速理解,并不代表市面上的智能替代吗都是这么简单。
呃,其实也真有这么简单的,我曾记得某年某日, 我在自媒体上看到过一篇文章,某某大牛一天复刻某某程序,源码仅 xx 大 这也侧面印证了,智能体的能力核心其实就是在底层驱动的模型上。
所以,无需紧张,沏杯茶,点根烟儿,做和放宽。还是那句话,它其实很简单。
作为 Java 开发者,你一定对"面向接口编程"这个老朋友很熟悉了。今天,我们就用这个熟悉的朋友,来做一件温暖的事情:定义一个 LLM 调用的接口。
为什么要定义接口呢? 让我慢慢告诉你。
因为不同的大模型(OpenAI、智谱、DeepSeek 等)虽然 API 格式相似,但地址、参数可能略有不同。通过接口,我们可以:
- 屏蔽底层差异,上层代码不用关心具体是哪个模型
- 方便切换模型,只需要换一个实现类
- 便于测试,可以用 Mock 实现替代真实的 API 调用
/**
* LLM 调用接口
*
* 就像你定义 Service 接口一样,先定义规范,再实现细节
*/
public interface LlmClient {
/**
* 发送消息给大模型,获取回复
*
* @param messages 消息列表
* @return 大模型的回复内容
*/
String chat(List<Map<String, String>> messages);
}
就这么简单,对吧? 一个接口,一个方法,输入是消息列表,输出是大模型的回复。是不是很清晰?
你可能会问:List<Map<String, String>> 这种写法有点丑,能不能用更优雅的方式?当然可以!让我们定义一个 Message 类:
/**
* 消息类
* 封装角色和内容,比 Map 更清晰
*/
public class Message {
private String role; // system、user、assistant
private String content; // 消息内容
public Message(String role, String content) {
this.role = role;
this.content = content;
}
// 静态工厂方法,让代码更简洁
public static Message system(String content) {
return new Message("system", content);
}
public static Message user(String content) {
return new Message("user", content);
}
public static Message assistant(String content) {
return new Message("assistant", content);
}
// getter 方法
public String getRole() { return role; }
public String getContent() { return content; }
}
有了 Message 类,接口可以改成这样:
public interface LlmClient {
String chat(List<Message> messages);
}
是不是感觉舒服多了? 现在调用的时候可以这样写,代码清晰又优雅:
List<Message> messages = List.of(
Message.system("你是一个Java技术专家"),
Message.user("如何在Spring Boot中配置数据源?")
);
String reply = llmClient.chat(messages);
是不是比 Map.of("role", "system", "content", "...") 清晰多了? 这就是我们用心设计的结果。
第二步:实现 OpenAI 兼容的 Client
现在我们来实现一个支持 OpenAI 协议的 Client。这个 Client 可以对接所有兼容 OpenAI 协议的大模型。
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
/**
* OpenAI 兼容的 LLM 客户端
*
* 支持所有兼容 OpenAI Chat 协议的大模型:
* - OpenAI(GPT-3.5、GPT-4)
* - 智谱(GLM-4)
* - DeepSeek
* - 月之暗面(Kimi)
* - 百川
* - 等等...
*/
public class OpenAiLlmClient implements LlmClient {
private final String apiKey;
private final String apiUrl;
private final String model;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
/**
* 构造函数
*
* @param apiKey API 密钥
* @param apiUrl API 地址(不同服务商地址不同)
* @param model 模型名称
*/
public OpenAiLlmClient(String apiKey, String apiUrl, String model) {
this.apiKey = apiKey;
this.apiUrl = apiUrl;
this.model = model;
this.httpClient = HttpClient.newHttpClient();
this.objectMapper = new ObjectMapper();
}
@Override
public String chat(List<Message> messages) {
try {
// 构建请求体
String requestBody = buildRequestBody(messages);
// 发送 HTTP 请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString()
);
// 解析响应
return parseResponse(response.body());
} catch (Exception e) {
throw new RuntimeException("调用大模型失败:" + e.getMessage(), e);
}
}
/**
* 构建请求体
*/
private String buildRequestBody(List<Message> messages) throws Exception {
Map<String, Object> body = Map.of(
"model", model,
"messages", messages.stream()
.map(m -> Map.of("role", m.getRole(), "content", m.getContent()))
.toList()
);
return objectMapper.writeValueAsString(body);
}
/**
* 解析响应,提取大模型的回复
*/
private String parseResponse(String responseBody) throws Exception {
return responseBody;
}
}
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
return (String) message.get("content");
}
}
**代码逻辑清晰:** 构造函数接收配置,`chat` 方法发送请求,`parseResponse` 解析响应。
**`apiUrl` 参数:** 不同服务商地址不同:
LlmClient llmClient = new OpenAiLlmClient(
"your-api-key",
"https://api.deepseek.com/v1/chat/completions",
"deepseek-chat"
);
// 发送消息
List<Message> messages = List.of(
Message.system("你是一个友好的助手"),
Message.user("你好,请介绍一下你自己")
);
其他代码完全不用动, 这就是封装带来的温暖拥抱。想象一下,你想试试别的模型,只需要轻轻改两个参数,就像换一件衣服那么简单。
第三步:对话记忆管理
还记得快速补全你的认知差中讲的 Memory 吗?
LLM 本身是无状态的。每次调用它,它都是一个全新的、什么都不记得的"大脑"。
所以我们需要自己管理对话历史。但每次都手动维护 List 太麻烦了,让我们封装一个 ChatMemory 类:
import java.util.ArrayList;
import java.util.List;
/**
* 对话记忆管理器
*
* 自动维护对话历史,支持:
* - 添加消息
* - 获取完整历史
* - 清空历史
* - 限制历史长度(避免 Token 超限)
*/
public class ChatMemory {
private final List<Message> messages;
private final int maxMessages; // 最大消息数量
public ChatMemory() {
this(100); // 默认最多保留 100 条消息
}
public ChatMemory(int maxMessages) {
this.messages = new ArrayList<>();
this.maxMessages = maxMessages;
}
/**
* 添加系统提示
*/
public void setSystemPrompt(String prompt) {
// 移除已有的 system 消息
messages.removeIf(m -> "system".equals(m.getRole()));
// 添加新的 system 消息到最前面
messages.add(0, Message.system(prompt));
}
/**
* 添加用户消息
*/
public void addUserMessage(String content) {
messages.add(Message.user(content));
trimIfNeeded();
}
/**
* 添加助手回复
*/
public void addAssistantMessage(String content) {
messages.add(Message.assistant(content));
trimIfNeeded();
}
/**
* 获取所有消息(用于发送给大模型)
*/
public List<Message> getMessages() {
return new ArrayList<>(messages);
}
/**
* 清空对话历史(保留系统提示)
*/
public void clear() {
Message systemMessage = messages.stream()
.filter(m -> "system".equals(m.getRole()))
.findFirst()
.orElse(null);
messages.clear();
if (systemMessage != null) {
messages.add(systemMessage);
}
}
/**
* 裁剪历史,避免超过最大长度
* 保留最近的消息,移除最早的消息(但保留 system 提示)
*/
private void trimIfNeeded() {
while (messages.size() > maxMessages) {
// 找到第一条非 system 消息并移除
int index = -1;
for (int i = 0; i < messages.size(); i++) {
if (!"system".equals(messages.get(i).getRole())) {
index = i;
break;
}
}
if (index >= 0) {
messages.remove(index);
} else {
break;
}
}
}
这个 ChatMemory 做了什么?
- 自动维护消息列表:你只管添加消息,它帮你管理顺序
- 系统提示保护:清空历史时,会保留 system 提示
- 自动裁剪:消息太多时,自动移除最早的消息,避免 Token 超限
- 线程安全:可以用在多线程环境(如果需要的话)
使用示例
// 创建记忆管理器
ChatMemory memory = new ChatMemory();
memory.setSystemPrompt("你是一个Java技术专家,擅长Spring Boot开发");
// 第一轮对话
memory.addUserMessage("你好,我是小明,正在学习Spring Boot");
String reply1 = llmClient.chat(memory.getMessages());
memory.addAssistantMessage(reply1);
// 第二轮对话
memory.addUserMessage("你能给我一个REST API的例子吗?");
String reply2 = llmClient.chat(memory.getMessages());
memory.addAssistantMessage(reply2);
// 查看历史
System.out.println("对话轮次:" + memory.size());
这个示例展示了ChatMemory的核心功能:
- 创建记忆管理器:
new ChatMemory()创建实例,setSystemPrompt()设置系统提示 - 记录对话:
addUserMessage()和addAssistantMessage()分别记录用户和助手的消息 - 获取历史:
getMessages()返回完整的对话历史,用于发送给大模型 - 查看状态:
size()获取当前消息数量,了解对话轮次
是不是比手动维护 List 方便多了?
现在,我们有了:
- ✅ 通用的 LLM Client 接口
- ✅ 支持多模型切换的实现
- ✅ 自动的对话记忆管理
最后一步,让我们把这些组装起来,升级我们的 SimpleAgent。
第四步:升级 SimpleAgent
还记得Agent开发入门中的 SimpleAgent 吗?
它的 decide 方法只是简单地拼接字符串,不会真正思考。现在,让我们给它换上一个真正的大脑:
import java.util.List;
/**
* 智能体(Agent)
*
* 通过大模型实现真正的思考能力
* 感知 → 思考(大模型)→ 行动 → 记忆
*/
public class SmartAgent {
private final String name;
private final LlmClient llmClient;
private final ChatMemory memory;
private final ToolRegistry toolRegistry; // 工具注册表,后面会用到
public SmartAgent(String name, String systemPrompt, LlmClient llmClient) {
this.name = name;
this.llmClient = llmClient;
this.memory = new ChatMemory();
this.memory.setSystemPrompt(systemPrompt);
this.toolRegistry = new ToolRegistry();
}
/**
* 处理用户输入,返回回复
*
* @param userInput 用户输入
* @return 智能体的回复
*/
public String chat(String userInput) {
// 1. 感知:记录用户输入
memory.addUserMessage(userInput);
// 2. 思考:调用大模型
String response = llmClient.chat(memory.getMessages());
// 3. 记忆:保存助手回复
memory.addAssistantMessage(response);
return response;
}
/**
* 清空对话历史
*/
public void clearMemory() {
memory.clear();
}
/**
* 获取对话历史
*/
public List<Message> getHistory() {
return memory.getMessages();
return name;
}
}
看到区别了吗?
原来的 SimpleAgent:
public String decide(String perception) {
// 假装在思考
return "处理: " + perception;
}
它只是简单地拼接字符串,不会真正思考。
现在的 SmartAgent:
String response = llmClient.chat(memory.getMessages());
// 真正调用大模型进行思考
这就是"换大脑"的过程。 就像给一个玩具换上了真正的引擎,让它能够真正地思考和感受。
使用示例
// 对话
String reply1 = agent.chat("你好,我是小明,正在学习Spring Boot");
System.out.println(reply1);
String reply2 = agent.chat("你能给我一个简单的REST API例子吗?");
System.out.println(reply3); // 会回答"你叫小明"
完整示例:让智能体真正"思考"
让我们把所有代码组合起来,运行一个完整的示例:
import java.util.List;
public class AgentDemo {
public static void main(String[] args) {
// 1. 创建大模型客户端(以 DeepSeek 为例)
LlmClient llmClient = new OpenAiLlmClient(
"your-api-key", // 替换成你的真实 API Key
"https://api.deepseek.com/v1/chat/completions",
"deepseek-chat"
);
// 2. 创建智能体
SmartAgent agent = new SmartAgent(
"Java助手",
"""
你是一个专业的Java技术顾问,擅长Spring Boot和微服务架构。
你的特点是:
- 用通俗易懂的语言解释技术概念
- 总是给出可运行的代码示例
- 像朋友一样耐心解答问题
""",
llmClient
);
// 3. 开始对话
System.out.println("=== Java 技术助手已启动 ===");
System.out.println("输入 'quit' 退出");
System.out.println();
// 模拟对话
String[] questions = {
"你好,我是小明,正在学习Spring Boot",
"什么是RESTful API?能给我一个例子吗?",
"你还记得我叫什么吗?"
};
for (String question : questions) {
System.out.println("小明:" + question);
String reply = agent.chat(question);
System.out.println("助手:" + reply);
System.out.println();
}
}
}
运行结果示例:
=== Java 技术助手已启动 ===
输入 'quit' 退出
小明:你好,我是小明,正在学习Spring Boot
助手:你好小明!很高兴认识你。Spring Boot 是一个非常棒的框架,
它让 Java Web 开发变得简单很多。如果你有任何问题,随时问我哦~
小明:什么是RESTful API?能给我一个例子吗?
**看到了吗?** 这就是我们用心打造的效果:
- 智能体真正理解了你的问题,不再是简单的字符串匹配
- 它记住了你的名字(小明),就像一个老朋友一样
- 它的回答自然、友好、有帮助,充满了温暖的关怀
这就是我们今天一起实现的温暖效果。 从一个简单的机器人,到一个有温度的智能伙伴。
- 智能体真正理解了你的问题
总结:一段温暖的旅程
让我们一起回顾一下今天这段温暖的旅程:
-
定义了 LLM 接口
LlmClient接口:统一的调用方式,就像一个共同的语言Message类:清晰的数据结构,让沟通更顺畅
-
实现了 OpenAI 兼容的 Client
- 支持所有兼容 OpenAI 协议的大模型,就像一个万能的翻译官
- 切换模型只需要改地址和模型名,简单又温暖
-
实现了对话记忆管理
ChatMemory类:自动维护对话历史,让你的智能体记住你们的每一个瞬间- 支持系统提示、自动裁剪,贴心又周到
-
升级了 SimpleAgent
- 从"假装思考"变成"真正思考",就像从玩具变成了伙伴
- 支持多轮对话、记忆上下文,让交流更自然
你现在拥有的温暖工具:
LlmClient- 调用大模型的桥梁Message- 沟通的语言ChatMemory- 记忆的守护者SmartAgent- 你的智能伙伴
这些就是构建智能体的核心组件。 掌握了它们,你就可以开始构建更复杂、更有温度的应用了。
下一步:继续这段温暖的旅程
在下一篇文章中,我们将继续这段温暖的旅程,一起探索:
- 给智能体添加工具能力(Function Calling),让它能够做更多的事情
- 让智能体能够调用外部 API,连接更广阔的世界
- 手撸一个桌面文件整理 Agent:实现一个能真正操作文件的智能体,包含文件扫描、分类、移动、重命名等实用工具
这些就是构建智能体的核心组件。 掌握了它们,你就可以开始构建更复杂、更有温度的应用了。
记住:你已经从一个传统 Java 开发者,变成了一个 AI 应用开发者。 这只是开始,更多的可能性等着你去探索,更多的温暖等着你去创造。
动手试试上面的代码吧! 当你看到智能体真正"思考"的时候,当你感受到它理解你的时候,你会体验到一种奇妙的成就感。那不仅仅是技术的成功,更是你用心创造的温暖。