知识目录 / Agent开发

封装一个通用的 LLM Client 并实现完整的对话记忆管理

这一篇,我们亲手封装通用的 LLM Client,实现对话记忆管理,让 SimpleAgent 真正拥有理解你、记住你的能力。

写在前面

在上一篇文章接入大模型:从第一个 Hello World 到多模态对话中,我们一起用 curl 和 Java 代码成功调用了大模型 API并完成了对话。

那一刻,你是不是也感受到了那种小小的成就感?😊

但我知道,那只是一个开始。你可能已经发现了几个让人心痒的问题:

  • 代码写死了 API 地址和模型名称,换一个模型就要改代码
  • 对话历史是手动维护的 List,有点笨拙
  • SimpleAgent 还是一个"傻瓜式"的机器人,不会真正思考

别担心,今天我们一起解决这些问题。

想象一下,如果我们能给这个机器人换一颗真正的大脑,让它能够理解你、记住你、帮助你,那该多好啊!

这正是我们要一起做的三件事:

  1. 封装一个通用的 LLM Client,支持切换不同模型
  2. 实现自动的对话记忆管理,让智能体记住你们的对话
  3. 让 SimpleAgent 真正接入大模型,成为一个能思考的智能伙伴

但你要基础,这篇文档给出的 Demo 非常简单,没有复杂的设计模式,没有承上启下的架构,这是为了让你能够快速理解,并不代表市面上的智能替代吗都是这么简单。

呃,其实也真有这么简单的,我曾记得某年某日, 我在自媒体上看到过一篇文章,某某大牛一天复刻某某程序,源码仅 xx 大 这也侧面印证了,智能体的能力核心其实就是在底层驱动的模型上。

所以,无需紧张,沏杯茶,点根烟儿,做和放宽。还是那句话,它其实很简单。

作为 Java 开发者,你一定对"面向接口编程"这个老朋友很熟悉了。今天,我们就用这个熟悉的朋友,来做一件温暖的事情:定义一个 LLM 调用的接口。

为什么要定义接口呢? 让我慢慢告诉你。

因为不同的大模型(OpenAI、智谱、DeepSeek 等)虽然 API 格式相似,但地址、参数可能略有不同。通过接口,我们可以:

  • 屏蔽底层差异,上层代码不用关心具体是哪个模型
  • 方便切换模型,只需要换一个实现类
  • 便于测试,可以用 Mock 实现替代真实的 API 调用
java
/**
 * LLM 调用接口
 * 
 * 就像你定义 Service 接口一样,先定义规范,再实现细节
 */
public interface LlmClient {
    
    /**
     * 发送消息给大模型,获取回复
     * 
     * @param messages 消息列表
     * @return 大模型的回复内容
     */
    String chat(List<Map<String, String>> messages);
}

就这么简单,对吧? 一个接口,一个方法,输入是消息列表,输出是大模型的回复。是不是很清晰?

你可能会问:List<Map<String, String>> 这种写法有点丑,能不能用更优雅的方式?当然可以!让我们定义一个 Message 类:

java
/**
 * 消息类
 * 封装角色和内容,比 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 类,接口可以改成这样:

java
public interface LlmClient {
    String chat(List<Message> messages);
}

是不是感觉舒服多了? 现在调用的时候可以这样写,代码清晰又优雅:

java
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 协议的大模型。

java
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 类:

java

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 做了什么?

  1. 自动维护消息列表:你只管添加消息,它帮你管理顺序
  2. 系统提示保护:清空历史时,会保留 system 提示
  3. 自动裁剪:消息太多时,自动移除最早的消息,避免 Token 超限
  4. 线程安全:可以用在多线程环境(如果需要的话)

使用示例

java
// 创建记忆管理器
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的核心功能:

  1. 创建记忆管理器new ChatMemory() 创建实例,setSystemPrompt() 设置系统提示
  2. 记录对话addUserMessage()addAssistantMessage() 分别记录用户和助手的消息
  3. 获取历史getMessages() 返回完整的对话历史,用于发送给大模型
  4. 查看状态size() 获取当前消息数量,了解对话轮次

是不是比手动维护 List 方便多了?

现在,我们有了:

  • ✅ 通用的 LLM Client 接口
  • ✅ 支持多模型切换的实现
  • ✅ 自动的对话记忆管理

最后一步,让我们把这些组装起来,升级我们的 SimpleAgent。

第四步:升级 SimpleAgent

还记得Agent开发入门中的 SimpleAgent 吗?

它的 decide 方法只是简单地拼接字符串,不会真正思考。现在,让我们给它换上一个真正的大脑:

java
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:

java
public String decide(String perception) {
    // 假装在思考
    return "处理: " + perception;
}

它只是简单地拼接字符串,不会真正思考。

现在的 SmartAgent:

java
String response = llmClient.chat(memory.getMessages());
// 真正调用大模型进行思考

这就是"换大脑"的过程。 就像给一个玩具换上了真正的引擎,让它能够真正地思考和感受。

使用示例

java

// 对话
String reply1 = agent.chat("你好,我是小明,正在学习Spring Boot");
System.out.println(reply1);

String reply2 = agent.chat("你能给我一个简单的REST API例子吗?");

System.out.println(reply3); // 会回答"你叫小明"

完整示例:让智能体真正"思考"

让我们把所有代码组合起来,运行一个完整的示例:

java
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();
        }
    }
}

运行结果示例:

text
=== Java 技术助手已启动 ===
输入 'quit' 退出

小明:你好,我是小明,正在学习Spring Boot
助手:你好小明!很高兴认识你。Spring Boot 是一个非常棒的框架,
它让 Java Web 开发变得简单很多。如果你有任何问题,随时问我哦~

小明:什么是RESTful API?能给我一个例子吗?
**看到了吗?** 这就是我们用心打造的效果:
- 智能体真正理解了你的问题,不再是简单的字符串匹配
- 它记住了你的名字(小明),就像一个老朋友一样
- 它的回答自然、友好、有帮助,充满了温暖的关怀

这就是我们今天一起实现的温暖效果。 从一个简单的机器人,到一个有温度的智能伙伴。

  • 智能体真正理解了你的问题

总结:一段温暖的旅程

让我们一起回顾一下今天这段温暖的旅程:

  1. 定义了 LLM 接口

    • LlmClient 接口:统一的调用方式,就像一个共同的语言
    • Message 类:清晰的数据结构,让沟通更顺畅
  2. 实现了 OpenAI 兼容的 Client

    • 支持所有兼容 OpenAI 协议的大模型,就像一个万能的翻译官
    • 切换模型只需要改地址和模型名,简单又温暖
  3. 实现了对话记忆管理

    • ChatMemory 类:自动维护对话历史,让你的智能体记住你们的每一个瞬间
    • 支持系统提示、自动裁剪,贴心又周到
  4. 升级了 SimpleAgent

    • 从"假装思考"变成"真正思考",就像从玩具变成了伙伴
    • 支持多轮对话、记忆上下文,让交流更自然

你现在拥有的温暖工具:

  • LlmClient - 调用大模型的桥梁
  • Message - 沟通的语言
  • ChatMemory - 记忆的守护者
  • SmartAgent - 你的智能伙伴

这些就是构建智能体的核心组件。 掌握了它们,你就可以开始构建更复杂、更有温度的应用了。

下一步:继续这段温暖的旅程

在下一篇文章中,我们将继续这段温暖的旅程,一起探索:

  • 给智能体添加工具能力(Function Calling),让它能够做更多的事情
  • 让智能体能够调用外部 API,连接更广阔的世界
  • 手撸一个桌面文件整理 Agent:实现一个能真正操作文件的智能体,包含文件扫描、分类、移动、重命名等实用工具

这些就是构建智能体的核心组件。 掌握了它们,你就可以开始构建更复杂、更有温度的应用了。


记住:你已经从一个传统 Java 开发者,变成了一个 AI 应用开发者。 这只是开始,更多的可能性等着你去探索,更多的温暖等着你去创造。

动手试试上面的代码吧! 当你看到智能体真正"思考"的时候,当你感受到它理解你的时候,你会体验到一种奇妙的成就感。那不仅仅是技术的成功,更是你用心创造的温暖。