【Java手搓OpenManus】-2- 领域模型设计:Message与Memory的建模实践

1. 从一次消息丢失说起:Java 手搓 OpenManus 的领域模型设计

如果你正在用 Java 复刻 OpenManus,或者想搞懂 Agent 底层到底怎么存对话、怎么管上下文,那 Message 和 Memory 这两个类就是你绕不过去的第一道坎。我见过太多人一上来就写 Agent 调度逻辑,结果跑三轮对话就发现历史消息丢了、工具调用结果对不上号、Redis 里存进去的 JSON 反序列化直接报错。问题不在 LLM,而在领域模型没设计好。

OpenManus 原版是 Python 写的,它的 Message 和 Memory 看起来简单,但里面藏着几个关键决策:消息角色用枚举还是字符串、工具调用怎么嵌套、记忆是纯内存还是走 Redis、上下文窗口超了怎么截断。这些决策直接决定了你后面接 LangChain4j、接 OpenAI 兼容接口时顺不顺。这篇就聚焦 Java 领域模型设计,把 Message 与 Memory 的建模实践拆开讲,给你可复制的实体类、接口定义、Redis 实现和单元测试,目标是让你照着敲完就能验证消息流转和记忆存取的正确性。

适合谁看?有 Java 基础、想理解 Agent 底层数据结构的开发者。不需要你懂 Python,但最好写过 Spring Boot。全文围绕 com.openmanus.domain.memory 这个包展开,代码可以直接放进你的项目。我试过把 Message 设计成 Record,结果 Jackson 序列化工具调用链时踩了坑,后面会细说为什么最终选了 Lombok 的 @Data + @Builder。

核心检索词先摆出来:Java 领域模型设计、Message 建模、Memory 记忆存取、OpenManus Java 复刻。你如果是搜“Java Agent 消息模型怎么设计”进来的,这篇就是你要的。

2. 前置准备:TaoToken 接入与项目依赖配置

在动手写实体类之前,得先把 LLM 调用通道准备好,不然你模型设计得再漂亮,也没法验证消息能不能真正发给大模型。这里我用 TaoToken 作为 API 接入层,它兼容 OpenAI 接口格式,Java 侧用 LangChain4j 或者直接 HTTP 调用都行。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点统一用 https://taotoken.net/api ,注意 API 地址后面不加 UTM 参数,保持干净。

你需要先去控制台创建一个 API Key,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。创建完 Key 之后,在项目里配置环境变量,别硬编码到代码里。我一般用 .env 或者 IDE 的运行配置注入:

export TAOTOKEN_API_KEY="sk-你的实际key"
export TAOTOKEN_BASE_URL="https://taotoken.net/api"

模型 ID 这块,你可以先用模型对话页面测试一下哪个模型响应快,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。选好之后把 Model ID 记下来,后面 LangChain4j 配置要用。如果你打算长期跑编码类 Agent,可以看看 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它针对代码场景做了优化。

项目依赖方面,Maven 里至少要有这几样:Lombok、Jackson、Spring Data Redis、JUnit 5。pom.xml 关键片段如下:

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Redis 配置放在 application.yml 里,注意 redis-enabled 默认给 false,这样你本地没装 Redis 也能跑内存版:

spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      timeout: 3000ms

openmanus:
  memory:
    redis-enabled: ${REDIS_MEMORY_ENABLED:false}
    conversation-ttl-days: 7
    max-messages: 100

这里有个细节:max-messages 控制上下文窗口的消息条数,不是 token 数。OpenManus 原版也是按条数截断的,简单粗暴但有效。你后面接 LLM 时,如果发现上下文还是超了,再在 LLMService 层做 token 级裁剪。前置准备就这些,接下来直接进 Message 建模。

3. 可复制配置:Message 实体类与 Memory 接口完整实现

3.1 Message 模型:为什么用 @Data + @Builder 而不是 Record

Java 14+ 的 Record 确实简洁,但 Message 这个类有几个特殊需求:第一,它需要 Builder 模式,因为工具调用消息的构造参数比较多;第二,它需要可变性,某些场景下要修改 content;第三,Jackson 序列化时要忽略 null 字段,Record 对 @JsonInclude 的支持不够灵活;第四,需要工厂方法比如 userMessage()、systemMessage()。所以最终选 Lombok 的 @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor。

角色用枚举而不是字符串,这是类型安全的基本要求。你写 Role.USER 编译期就能检查,写 "user" 拼错了要到运行时才报错。枚举里用 @JsonValue 标注 getValue(),这样 Jackson 序列化出来就是小写字符串,跟 OpenAI API 要求一致。

完整 Message 类放在 com.openmanus.domain.memory 包下:

package com.openmanus.domain.memory;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Map;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Message {

    public enum Role {
        SYSTEM("system"),
        USER("user"),
        ASSISTANT("assistant"),
        TOOL("tool");

        private final String value;

        Role(String value) {
            this.value = value;
        }

        @JsonValue
        public String getValue() {
            return value;
        }
    }

    private Role role;
    private String content;
    private List<ToolCall> toolCalls;
    private String name;
    private String toolCallId;
    private String base64Image;

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class ToolCall {
        private String id;
        private String type;
        private Function function;

        @Data
        @Builder
        @NoArgsConstructor
        @AllArgsConstructor
        public static class Function {
            private String name;
            private String arguments;
        }
    }

    public static Message userMessage(String content, String base64Image) {
        return Message.builder()
                .role(Role.USER)
                .content(content)
                .base64Image(base64Image)
                .build();
    }

    public static Message userMessage(String content) {
        return userMessage(content, null);
    }

    public static Message systemMessage(String content) {
        return Message.builder()
                .role(Role.SYSTEM)
                .content(content)
                .build();
    }

    public static Message assistantMessage(String content, String base64Image) {
        return Message.builder()
                .role(Role.ASSISTANT)
                .content(content)
                .base64Image(base64Image)
                .build();
    }

    public static Message assistantMessage(String content) {
        return assistantMessage(content, null);
    }

    public static Message fromToolCalls(String content, List<ToolCall> toolCalls) {
        return Message.builder()
                .role(Role.ASSISTANT)
                .content(content)
                .toolCalls(toolCalls)
                .build();
    }

    public static Message toolMessage(String content, String name, String toolCallId, String base64Image) {
        return Message.builder()
                .role(Role.TOOL)
                .content(content)
                .name(name)
                .toolCallId(toolCallId)
                .base64Image(base64Image)
                .build();
    }

    public static Message toolMessage(String content, String name, String toolCallId) {
        return toolMessage(content, name, toolCallId, null);
    }

    public Map<String, Object> toMap() {
        return Map.of(
                "role", role.getValue(),
                "content", content != null ? content : ""
        );
    }
}

注意 toMap() 这里只做了基础实现,完整的 LLM API 调用转换会在 LLMService 里处理,因为工具调用消息的格式更复杂。@JsonInclude(NON_NULL) 保证序列化时 null 字段不出现,这样 Redis 里存的 JSON 更干净。

3.2 Memory 接口定义

Memory 接口要提供基本 CRUD、批量添加、获取最近 N 条、最大消息数限制。接口放在 domain 层,实现可以放 infrastructure 层,这样依赖方向是对的:

package com.openmanus.domain.memory;

import java.util.List;

public interface Memory {
    void addMessage(Message message);
    void addMessages(List<Message> messages);
    List<Message> getMessages();
    void clear();
    List<Message> getRecentMessages(int n);
    int getMaxMessages();
    void setMaxMessages(int maxMessages);
}

3.3 MemoryImpl 内存实现

内存版用 ArrayList,不用 CopyOnWriteArrayList,因为 Agent 运行在单线程里,ArrayList 性能更好。如果未来要多线程,再换也不迟。关键点是 getMessages() 返回副本,防止外部修改内部状态:

package com.openmanus.domain.memory;

import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
public class MemoryImpl implements Memory {

    private List<Message> messages;
    private int maxMessages;

    public MemoryImpl() {
        this.messages = new ArrayList<>();
        this.maxMessages = 100;
    }

    public MemoryImpl(int maxMessages) {
        this.messages = new ArrayList<>();
        this.maxMessages = maxMessages;
    }

    @Override
    public void addMessage(Message message) {
        if (message == null) {
            return;
        }
        messages.add(message);
        if (messages.size() > maxMessages) {
            messages = new ArrayList<>(messages.subList(
                    messages.size() - maxMessages, messages.size()));
        }
    }

    @Override
    public void addMessages(List<Message> messages) {
        if (messages == null || messages.isEmpty()) {
            return;
        }
        this.messages.addAll(messages);
        if (this.messages.size() > maxMessages) {
            this.messages = new ArrayList<>(this.messages.subList(
                    this.messages.size() - maxMessages, this.messages.size()));
        }
    }

    @Override
    public List<Message> getMessages() {
        return new ArrayList<>(messages);
    }

    @Override
    public void clear() {
        messages.clear();
    }

    @Override
    public List<Message> getRecentMessages(int n) {
        if (n <= 0 || messages.isEmpty()) {
            return new ArrayList<>();
        }
        int start = Math.max(0, messages.size() - n);
        return new ArrayList<>(messages.subList(start, messages.size()));
    }
}

3.4 Redis 记忆实现:ConversationService 与 ConversationMemory

生产环境需要持久化、多实例共享、自动过期,所以加 Redis 实现。先写 ConversationService,负责对话历史的读写:

package com.openmanus.infrastructure.memory;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.openmanus.domain.memory.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Slf4j
@Service
public class ConversationService {

    private static final String CONVERSATION_PREFIX = "conversation:";
    private static final String CURRENT_CONVERSATION_PREFIX = "user:current_conversation:";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Value("${openmanus.memory.conversation-ttl-days:7}")
    private int conversationTtlDays;

    public String getOrCreateConversationId(String userId) {
        String key = CURRENT_CONVERSATION_PREFIX + userId;
        String conversationId = redisTemplate.opsForValue().get(key);
        if (conversationId == null || conversationId.isEmpty()) {
            conversationId = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(key, conversationId,
                    Duration.ofDays(conversationTtlDays));
        }
        return conversationId;
    }

    public List<Message> getConversationHistory(String conversationId) {
        try {
            String key = CONVERSATION_PREFIX + conversationId;
            String historyJson = redisTemplate.opsForValue().get(key);
            if (historyJson == null || historyJson.isEmpty()) {
                return new ArrayList<>();
            }
            return objectMapper.readValue(historyJson,
                    new TypeReference<List<Message>>() {});
        } catch (Exception e) {
            log.error("Failed to get conversation history: " + conversationId, e);
            return new ArrayList<>();
        }
    }

    public void updateConversationHistory(String conversationId,
                                          List<Message> messages, int maxMessages) {
        try {
            List<Message> history = getConversationHistory(conversationId);
            history.addAll(messages);
            if (history.size() > maxMessages) {
                history = new ArrayList<>(history.subList(
                        history.size() - maxMessages, history.size()));
            }
            String key = CONVERSATION_PREFIX + conversationId;
            String historyJson = objectMapper.writeValueAsString(history);
            redisTemplate.opsForValue().set(key, historyJson,
                    Duration.ofDays(conversationTtlDays));
        } catch (Exception e) {
            log.error("Failed to update conversation history: " + conversationId, e);
        }
    }

    public void clearConversationHistory(String conversationId) {
        redisTemplate.delete(CONVERSATION_PREFIX + conversationId);
    }

    public void clearCurrentConversation(String userId) {
        redisTemplate.delete(CURRENT_CONVERSATION_PREFIX + userId);
    }
}

ConversationMemory 实现 Memory 接口,conversationId 作为参数传入而不是成员变量固定,这样同一个实例可以服务不同对话。它带本地缓存减少 Redis 访问:

package com.openmanus.infrastructure.memory;

import com.openmanus.domain.memory.Memory;
import com.openmanus.domain.memory.Message;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
public class ConversationMemory implements Memory {

    private final ConversationService conversationService;
    private int maxMessages;
    private String conversationId;
    private List<Message> localCache;
    private boolean cacheDirty = false;
    private boolean cacheInitialized = false;

    public ConversationMemory(ConversationService conversationService,
                              String conversationId, int maxMessages) {
        this.conversationService = conversationService;
        this.conversationId = conversationId;
        this.maxMessages = maxMessages;
        this.localCache = new ArrayList<>();
    }

    public void setConversationId(String conversationId) {
        if (this.conversationId != null && !this.conversationId.equals(conversationId)) {
            saveToRedis();
        }
        this.conversationId = conversationId;
        this.cacheInitialized = false;
        this.localCache.clear();
        this.cacheDirty = false;
    }

    private void loadFromRedis() {
        if (conversationService != null && conversationId != null && !cacheInitialized) {
            List<Message> messages = conversationService.getConversationHistory(conversationId);
            this.localCache = new ArrayList<>(messages);
            this.cacheDirty = false;
            this.cacheInitialized = true;
        }
    }

    private void saveToRedis() {
        if (conversationService != null && conversationId != null && cacheDirty) {
            conversationService.updateConversationHistory(conversationId, localCache, maxMessages);
            this.cacheDirty = false;
        }
    }

    @Override
    public void addMessage(Message message) {
        if (message == null) {
            return;
        }
        if (conversationId != null && !cacheInitialized) {
            loadFromRedis();
        }
        localCache.add(message);
        if (localCache.size() > maxMessages) {
            localCache = new ArrayList<>(localCache.subList(
                    localCache.size() - maxMessages, localCache.size()));
        }
        cacheDirty = true;
        if (conversationId != null) {
            saveToRedis();
        }
    }

    @Override
    public void addMessages(List<Message> messages) {
        if (messages == null || messages.isEmpty()) {
            return;
        }
        if (conversationId != null && !cacheInitialized) {
            loadFromRedis();
        }
        localCache.addAll(messages);
        if (localCache.size() > maxMessages) {
            localCache = new ArrayList<>(localCache.subList(
                    localCache.size() - maxMessages, localCache.size()));
        }
        cacheDirty = true;
        if (conversationId != null) {
            saveToRedis();
        }
    }

    @Override
    public List<Message> getMessages() {
        if (conversationId != null && !cacheInitialized) {
            loadFromRedis();
        }
        if (cacheDirty && conversationId != null) {
            saveToRedis();
        }
        return new ArrayList<>(localCache);
    }

    @Override
    public void clear() {
        localCache.clear();
        cacheDirty = true;
        if (conversationService != null && conversationId != null) {
            conversationService.clearConversationHistory(conversationId);
            cacheDirty = false;
            cacheInitialized = false;
        }
    }

    @Override
    public List<Message> getRecentMessages(int n) {
        if (n <= 0) {
            return new ArrayList<>();
        }
        if (conversationId != null && !cacheInitialized) {
            loadFromRedis();
        }
        if (localCache.isEmpty()) {
            return new ArrayList<>();
        }
        if (cacheDirty && conversationId != null) {
            saveToRedis();
        }
        int start = Math.max(0, localCache.size() - n);
        return new ArrayList<>(localCache.subList(start, localCache.size()));
    }

    @Override
    public int getMaxMessages() {
        return maxMessages;
    }

    @Override
    public void setMaxMessages(int maxMessages) {
        this.maxMessages = maxMessages;
        if (localCache.size() > maxMessages) {
            localCache = new ArrayList<>(localCache.subList(
                    localCache.size() - maxMessages, localCache.size()));
            cacheDirty = true;
            if (conversationId != null) {
                saveToRedis();
            }
        }
    }
}

MemoryFactory 根据配置决定创建哪种实现:

package com.openmanus.infrastructure.memory;

import com.openmanus.domain.memory.Memory;
import com.openmanus.domain.memory.MemoryImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MemoryFactory {

    @Autowired(required = false)
    private ConversationService conversationService;

    @Value("${openmanus.memory.max-messages:100}")
    private int maxMessages;

    @Value("${openmanus.memory.redis-enabled:false}")
    private boolean redisEnabled;

    public Memory createMemory(String conversationId) {
        if (redisEnabled && conversationService != null) {
            return new ConversationMemory(conversationService, conversationId, maxMessages);
        }
        return new MemoryImpl(maxMessages);
    }

    public Memory createMemoryForUser(String userId) {
        if (redisEnabled && conversationService != null) {
            String conversationId = conversationService.getOrCreateConversationId(userId);
            return new ConversationMemory(conversationService, conversationId, maxMessages);
        }
        return new MemoryImpl(maxMessages);
    }
}

两种实现的对比很清晰:MemoryImpl 存内存、不持久化、不支持多实例、性能快,适合开发和单实例;ConversationMemory 存 Redis、持久化、支持多实例、自动过期、性能稍慢,适合生产。你按场景选就行。

4. 验证请求:单元测试与 Redis 存取实测

写完代码不验证等于没写。先写单元测试覆盖 Message 工厂方法和 Memory 截断逻辑。测试类放在 src/test/java/com/openmanus/domain/memory 下:

package com.openmanus.domain.memory;

import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class MessageMemoryTest {

    @Test
    void testUserMessageFactory() {
        Message msg = Message.userMessage("你好");
        assertEquals(Message.Role.USER, msg.getRole());
        assertEquals("你好", msg.getContent());
        assertNull(msg.getBase64Image());
    }

    @Test
    void testToolMessageFactory() {
        Message msg = Message.toolMessage("结果", "search", "call_123");
        assertEquals(Message.Role.TOOL, msg.getRole());
        assertEquals("search", msg.getName());
        assertEquals("call_123", msg.getToolCallId());
    }

    @Test
    void testMemoryTruncation() {
        MemoryImpl memory = new MemoryImpl(3);
        memory.addMessage(Message.userMessage("1"));
        memory.addMessage(Message.userMessage("2"));
        memory.addMessage(Message.userMessage("3"));
        memory.addMessage(Message.userMessage("4"));
        List<Message> messages = memory.getMessages();
        assertEquals(3, messages.size());
        assertEquals("2", messages.get(0).getContent());
        assertEquals("4", messages.get(2).getContent());
    }

    @Test
    void testGetRecentMessages() {
        MemoryImpl memory = new MemoryImpl(10);
        for (int i = 1; i <= 5; i++) {
            memory.addMessage(Message.userMessage(String.valueOf(i)));
        }
        List<Message> recent = memory.getRecentMessages(2);
        assertEquals(2, recent.size());
        assertEquals("4", recent.get(0).getContent());
        assertEquals("5", recent.get(1).getContent());
    }

    @Test
    void testGetMessagesReturnsCopy() {
        MemoryImpl memory = new MemoryImpl(10);
        memory.addMessage(Message.userMessage("原始"));
        List<Message> copy = memory.getMessages();
        copy.clear();
        assertEquals(1, memory.getMessages().size());
    }
}

跑 mvn test,五个测试全绿说明 Message 工厂和 Memory 截断逻辑没问题。接下来验证 Redis 存取。把 application.yml 里 redis-enabled 改成 true,启动类里加一行日志:

@Slf4j
@SpringBootApplication
@EnableConfigurationProperties(OpenManusConfig.class)
public class OpenManusApplication implements CommandLineRunner {

    @Autowired
    private MemoryFactory memoryFactory;

    public static void main(String[] args) {
        SpringApplication.run(OpenManusApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        Memory memory = memoryFactory.createMemoryForUser("testuser123");
        memory.addMessage(Message.userMessage("第一条消息"));
        memory.addMessage(Message.assistantMessage("收到"));
        log.info("当前消息数: {}", memory.getMessages().size());
    }
}

启动后看日志,应该输出 当前消息数: 2。然后去 Redis 里查:

redis-cli
> keys user:current_conversation:*
1) "user:current_conversation:testuser123"
> get user:current_conversation:testuser123
"a1b2c3d4-..."
> keys conversation:*
1) "conversation:a1b2c3d4-..."
> get conversation:a1b2c3d4-...
"[{\"role\":\"user\",\"content\":\"第一条消息\"},{\"role\":\"assistant\",\"content\":\"收到\"}]"

看到 JSON 里 role 是小写、content 正常,说明 Jackson 序列化和 @JsonValue 都生效了。再重启应用,调用 getMessages(),应该能从 Redis 加载回两条消息,证明持久化没问题。这一步验证通过,你的领域模型就算立住了。

5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth 报错

接入过程中最容易撞的几个报错,我按真实日志给你对照排查。

第一个是 401 Unauthorized。如果你在 LLMService 里调 TaoToken API 返回 401,先检查 API Key 有没有正确注入。常见原因是环境变量名写错,比如代码里读 TAOTOKEN_API_KEY,但你 export 的是 TAOTOKEN_KEY。另一个原因是 Key 前后有空格,复制的时候带上了换行。排查命令:

echo $TAOTOKEN_API_KEY | head -c 10

确认前缀是 sk- 且没有多余字符。如果 Key 没问题还是 401,去控制台确认这个 Key 有没有被禁用或额度耗尽。

第二个是 local proxy failed 或连接超时。这个报错通常出现在你本地网络环境有额外代理设置时。检查你的 JVM 启动参数有没有 -Dhttp.proxyHost 之类的配置,有的话去掉。另外确认 TAOTOKEN_BASE_URL 写的是 https://taotoken.net/api,不要多加路径或者少写 /api。如果你用 LangChain4j,Base URL 配置项名字可能是 baseUrl 而不是 apiBase,看具体版本。

第三个是 reading choices 相关报错,完整信息类似 Cannot deserialize value of type ... from Array value (token JsonToken.START_ARRAY) 或者 reading choices 字段解析失败。这是因为 LLM 返回的 JSON 结构和你的响应类对不上。OpenAI 兼容接口的响应里 choices 是数组,每个元素有 message 字段。如果你的响应类把 choices 定义成了对象而不是 List,就会报这个。检查你的 ChatCompletionResponse 类:

public class ChatCompletionResponse {
    private List<Choice> choices;

    public static class Choice {
        private Message message;
        private String finishReason;
    }
}

第四个是 OAuth 相关报错,比如 OAuth token exchange failed 或 invalid_grant。如果你用的是 Claude Code 或者某些需要 OAuth 的客户端,注意 TaoToken 的 API Key 模式不需要走 OAuth 流程。在 Claude Code 里配置时,Base URL 填 https://taotoken.net/api,API Key 填你的 sk- 开头的 Key,Model ID 填你在模型对话页面选好的那个。三件套缺一不可,少一个就会报认证失败。如果你用 CC Switch 或 Cline MCP,同样确保 Base URL、Key、Model ID 三项都填对。

还有一个隐蔽的坑:Redis 里存的消息反序列化失败。报错类似 Cannot construct instance of Message。这是因为 Message 类缺少无参构造函数,Jackson 反序列化时需要它。检查你有没有加 @NoArgsConstructor。另外 Role 枚举的反序列化,Jackson 默认按枚举名匹配,但 Redis 里存的是小写 "user",所以要么加 @JsonCreator 静态方法,要么确保 @JsonValue 和反序列化对称。最稳妥的方式是给 Role 加一个 @JsonCreator:

@JsonCreator
public static Role fromValue(String value) {
    for (Role role : values()) {
        if (role.value.equals(value)) {
            return role;
        }
    }
    throw new IllegalArgumentException("Unknown role: " + value);
}

加上这段,Redis 存取就不会再报枚举解析错了。这几个错排查完,你的消息流转和记忆存取基本就稳了。

6. 下一步:接入 LLM 服务与 Coding Plan 长期验证

领域模型立住之后,下一步就是接 LLM 服务封装,用 LangChain4j 把 Message 列表转成 API 请求,把响应转回 Message 存进 Memory。这一步的验证建议用真实模型跑几轮多轮对话,观察上下文截断是否符合预期。如果你要长期跑编码类 Agent,可以用 Coding Plan 做持续验证,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它针对代码场景的模型做了优化,适合反复调试工具调用链。

API Key 管理和接入文档在这里:API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。模型对话测试页面还是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,换模型对比响应速度很方便。

最后留一个实用技巧:Memory 的 maxMessages 不要设太大,100 条消息如果每条平均 200 token,上下文就 2 万 token 了,很多模型扛不住。我一般设 50 条,然后在 LLMService 层再做一次 token 级裁剪。另外 ConversationMemory 的本地缓存和 Redis 之间有一致性窗口,多实例部署时如果对一致性要求高,可以把 saveToRedis() 改成每次读写都同步,代价是性能下降。单实例场景下当前实现够用。

相关推荐

IEC 61851-23:2023 电动汽车导电充电系统 - 第23部分:直流电动汽车供电设备 - 完整英文电子版(388页)

IEC 61851-23:2023适用于用于在供电网络与电动汽车(EV)之间实现能量传输的电动车供电设备,A侧额定最大电压可达1000伏交流电或最高1500伏直流电,B侧额定最大电压最高可达1500伏直流电。 本文件规定了系统A、系统B和系统C的电动车供应设备,定义见附件AA、附件BB和附件CC。

【Java手搓OpenManus】-2- 领域模型设计

本文介绍了OpenManus系统的核心领域模型设计,重点实现了Message(消息)和Memory(记忆)两个核心组件。Message模型采用Lombok的@Data和@Builder注解实现,支持多种消息角色(系统、用户、助手、工具)和内容类型,并提供了便捷的工厂方法。Memory接口定义了基本的CRUD操作,当前实现使用ArrayList存储消息,支持最大消息数限制。设计决策考虑了类型安全、性能优化和未来扩展性,为后续开发奠定了良好的基础。

bblb的博客 856

phone-harness/mcp 把手机接进AI(源码附安装部署教程)

phone-harness/mcp 做的不是模拟器玩具,而是把真实手机操作能力接成一个标准 MCP 服务:给 Claude、Cursor、ChatGPT、Codex、VS Code 等客户端填一个 URL,完成 OAuth 登录后,AI 就能直接看屏幕、点按、输入、滑动、启动 App,甚至连真机 iPhone 和云端 Android 模拟器都能统一调度。 它的价值在于把过去依赖 Mac、adb、配对线缆和本地环境的移动端自动化,压缩成“一个地址+一次登录”的接入成本,特别适合需要让智能体真正落到手机场景里的测试、演示和流程执行。 项目把能力拆成一组很实用的工具:列设备、启停设备、截图、识别屏幕文字、坐标点击、长按、滑动、输入、按键、查前台应用、启动应用,基本覆盖了手机远程操控的主路径。 收费方式也写得很直白:云端 Android 按分钟计费,iPhone 是包月订阅,临时会话和保存状态的设备也区分清楚,减少了“能不能跑、怎么计费、数据会不会丢”这类实际使用时最常见的疑问。 更重要的是,它没有试图重新发明一套生态,而是紧贴 MCP,把“让 AI 真正碰到手机”这件事做成可插拔基础设施。

Spring AI Alibaba开发实战:从入门到高级应用_spring ai alibaba 从入门到实战-优快云博客

Spring AI Alibaba是阿里巴巴开源的一个基于Spring生态的AI应用开发框架,它构建在Spring AI之上,为Java开发者提供了便捷接入阿里云通义系列大模型的能力。该框架将阿里巴巴强大的AI能力与Spring Cloud微服务生态完美结合,致力于帮助开发者快速构建具有AI功能的现代化应用。抽象了AI应用开发中的常见模式,如提示词管理、对话记忆、函数调用等,大大降低了AI集成的复杂度。

求知若渴、虚心若愚 1599

基于 Paxos 协议以及分布式架构,实现了高可用和线性扩展 OceanBase 数据库运行在常见的服务器集群上,不依赖特殊的硬件架构 一个分布式关系型数据库

基于 Paxos 协议以及分布式架构,实现了高可用和线性扩展。OceanBase 数据库运行在常见的服务器集群上,不依赖特殊的硬件架构。一个分布式关系型数据库

关于AI开发方法论-以文档为索引的高质量协作开发:让一次现象描述精准命中几个代码块

对AI的编码规范进行约束

Spring AI 实战:从 0 到 1 构建 AI 恋爱大师与自主规划智能体 YuManus

本篇技术指南以开源仓库中的《Spring AI - AI 超级智能体项目实战》教程为骨架,围绕"AI 恋爱大师应用 + 拥有自主规划能力的超级智能体(YuManus)"两条开发主线,系统拆解企业级 AI 应用开发所需的核心概念、技术选型与实现原理。读完本文,你将理解 ReAct 智能体、工具调用、RAG 知识库、MCP 模型上下文协议等前沿技术在 Spring AI 框架下的落地方式,掌握一条从需

gitblog_00745的博客 528

数字雕刻基于CatmulI-Clark细分与笔刷位移场的ZBrush流程建模:月哨兵角色精细度上限分析

内容概要:本文以“月哨兵”科幻角色数字雕刻工程为实例,系统构建了从程序化粗模到高精度雕刻的完整数学模型。研究涵盖重拓扑、Catmul-Clark细分、笔刷位移场建模、多部件组装及精细度上限分析等核心环节。通过ZRemesher将304,050顶点粗模重拓扑为28,600顶点的全四边形网格,并逐级细分至732万面,实测面数递推F(k)=F₀·4ᵏ完全符合理论。利用ZBrush Python接口注入Standard笔刷操作,首次通过同拓扑OBJ逐顶点差分获得真实位移场数据,实测最大位移0.454873,8.756%顶点发生显著位移。提出“理论-内存-拓扑-视觉”四层精细度上限模型,指出在24GB内存条件下工程合理上限为10⁷~10⁸面,视觉收益受渲染分辨率制约趋于饱和。最终完成755万面的组装网格并经工程文件回读验证一致性。; 适合人群:具备三维建模与数字雕刻基础,熟悉ZBrush工作流,从事角色资产开发或程序化内容生成的技术美术、图形学研究人员及高阶数字艺术家。; 使用场景及目标:①理解Catmul-Clark细分曲面在工业级雕刻流程中的数学机制;②掌握基于脚本驱动的可复核雕刻工程构建方法;③评估高面数模型的内存代价与视觉收益平衡;④学习重拓扑、位移场量化、多部件合并等关键技术的工程实现。; 阅读建议:本文强调数学建模与实证闭环,建议结合附录中的脚本与数据文件进行复现实践,重点关注逐顶点差分、细分递推验证与四层上限模型的构建逻辑,以深入掌握高精度数字雕刻的系统性工程方法。

企业级 AI 超级智能体实战:用 Spring AI 打造 AI 恋爱大师应用与自主规划智能体 YuManus

> 本文基于 AI/AI项目教程/🔥 企业级项目:开发 AI 恋爱大师应用 + 拥有自主规划能力的超级智能体.md 展开,结合 Vibe Coding 零基础教程/20 项目实战/鱼皮的原创项目/Spring AI - AI 超级智能体项目实战.md、MCP 服务开发 等仓库资料深度解读。 ## 一、项目背景与核心目标 这是一套以 **AI 开发实战** 为核心的企业级项目教程,最终产出两个

gitblog_00867的博客 225

公司内网软件下载站,公司内网软件下载站

公司内网软件下载站,公司内网软件下载站

通信行业基于Tuned与Systemd的CentOS 7性能调优:自动化资源治理与软中断优化实践

内容概要:本文围绕CentOS 7在通信行业大规模服务器集群中的系统性能优化,提出基于Tuned与Systemd的自动化、可复制、可回滚的性能治理方案。重点涵盖Tuned自定义Profile实现CPU、内存、网络和磁盘的综合调优,结合RPS/RFS与中断亲和优化软中断处理,利用Systemd资源控制对关键服务进行CPU、内存、IO的硬性限额,并通过sar、ethtool等工具构建可观测闭环验证调优效果。案例展示了短信网关在高并发场景下通过资源隔离显著降低延迟、提升稳定性的实践路径。最后展望了向eBPF自适应调优、Kubernetes声明式编排与全面可观测性演进的趋势。; 适合人群:具备Linux系统管理与运维基础,从事通信、云计算或高性能服务端开发的工程师,尤其是负责大规模服务器集群性能调优的技术人员。; 使用场景及目标:①解决通信类业务中软中断瓶颈、单核打满、延迟波动等问题;②实现系统级性能调优策略的标准化、自动化部署与快速回滚;③为时延敏感型服务(如短信网关、VoIP)提供资源隔离与QoS保障; 阅读建议:本文强调“按业务特征定制”与“可复制治理”的工程方法论,建议结合实际生产环境,先在测试集群验证参数有效性,重点关注Tuned的继承机制、RPS掩码设置与Systemd资源配额的协同作用,并持续通过监控数据反馈调优成效。

PUMA:DOA估计模式的改进实现(Matlab代码实现)

内容概要:本文围绕PUMA(Propagator Method-based DOA Estimation)算法在波达方向(DOA)估计中的改进实现展开,重点在于通过Matlab编程对算法进行仿真与性能验证。作为阵列信号处理领域的高分辨率DOA估计算法,PUMA在传统方法基础上提升了估计精度与鲁棒性,尤其在低信噪比、小样本等恶劣条件下仍能保持优异性能。文中系统阐述了PUMA算法的理论基础、数学模型及其相较于MUSIC、ESPRIT等经典算法的优势,并通过仿真实验进行对比分析,展示其在分辨力、稳定性和计算效率方面的综合优势,适用于实际雷达、无线通信与电子侦察等应用场景。; 适合人群:具备信号处理、通信工程、电子信息或雷达系统等相关专业背景的研究生、科研人员及工程技术人员,熟悉阵列信号处理理论与Matlab编程环境者尤佳; 使用场景及目标:①用于学术研究中DOA估计算法的性能优化与新方法对比验证;②应用于雷达系统、5G/6G通信、智能天线、电子对抗等领域的高精度空间谱估计任务;③作为Matlab仿真实践资源,支持课程设计、学位论文复现与科研项目开发; 阅读建议:建议结合阵列信号处理基础知识体系学习本资源,重点关注PUMA算法的改进机制与核心代码实现逻辑,建议动手运行并调整仿真参数以深入理解不同因素(如阵元数、信噪比、快拍数)对估计性能的影响。

syslog_test.py

syslog_test.py

优快云首页 发布文章 优快云同步助手 阻尼器磁流变阻尼器MR Damper非线性(阻尼、滞回)特性研究(Matlab代码实现) 46 100 摘要:会在推荐、列表等场景外露,帮助读者

内容概要:本文系统整理了一套面向科研仿真的Matlab/Simulink资源合集,涵盖电力电子、控制理论、信号处理、电力系统、智能优化算法、机器学习、图像处理、路径规划、通信技术、雷达追踪、车间调度等多个前沿科研领域。重点资源包括三相PWM整流器双闭环控制、磁流变阻尼器非线性特性建模、双有源桥(DAB)DC-DC变换器优化控制、电网不平衡下的功率平衡策略、卡尔曼滤波状态估计、多类型路径规划与调度算法等,多数为顶级EI期刊成果复现,配套仿真模型、原理说明、参考文献及代码,具备较强的学术参考价值与工程实践意义。; 适合人群:适用于从事电气工程、自动化、控制科学与工程、信号与信息处理、计算机、机械电子等方向的硕士/博士研究生及科研人员,要求具备Matlab/Simulink基础操作能力及相关专业背景;也适合高校教师指导课程设计、毕业设计或科研项目开发。; 使用场景及目标:①辅助高水平论文研究与期刊成果复现;②支撑课程设计、毕业设计与科研项目开发;③深入理解先进控制策略(如VOC、SVPWM、模糊PID、滑模控制)与系统建模方法;④结合YALMIP等优化工具开展算法设计与验证;⑤拓展智能优化、机器学习、路径规划等跨学科研究能力。; 阅读建议:建议按照资源目录顺序系统浏览,优先关注标注“顶级EI复现”的高价值模型,结合公众号“荔枝科研社”提供的网盘资料与技术文档进行仿真实践,通过动手调试加深对算法逻辑与系统动态特性的理解,提升科研建模与创新能力。

Simulink “双碳”背景下汽车减少碳排放建模与仿真

内容概要:本文围绕“双碳”战略背景,系统阐述了基于Simulink的汽车碳排放建模与仿真技术,重点引入模糊PID控制优化动力系统性能,以实现节能减排目标。文档整合了多个Matlab/Simulink科研仿真案例,涵盖无刷直流电机(BLDC)控制、三相PWM整流器、电池管理系统、路径规划、信号处理、智能优化算法及图像处理等领域,全面展示Matlab在智能控制与绿色能源系统中的应用价值。同时强调科研应兼具严谨逻辑、创新思维与工具赋能,并提供完整的仿真资源链接与学习路径支持实践落地。; 适合人群:具备Matlab/Simulink基础,从事自动化、电气工程、车辆工程、控制科学、电力电子及智能制造等相关领域的科研人员或高年级本科生、研究生;适用于开展节能减排、新能源系统优化、智能控制算法研究等课题的研究者。; 使用场景及目标:①学习在Simulink中构建汽车能耗与排放仿真模型并应用模糊PID进行动态优化;②掌握BLDC、PWM整流器等关键部件的建模与控制策略设计;③借鉴丰富案例提升科研效率,拓展在智能控制、路径规划、信号处理等方向的应用能力;④通过提供的网盘资源获取完整代码与资料,支撑论文写作与项目开发。; 阅读建议:建议按照资源目录顺序系统学习,结合百度网盘提供的Matlab/Simulink代码动手实践,重点关注控制策略的设计、仿真结果的分析与优化过程,同时注重培养科研思维与工程实际问题的结合能力。

西部牛仔_1794033330851.apk

西部牛仔_1794033330851.apk

计算轴向磁铁和环状磁铁的磁场(Matlab代码实现)

内容概要:本文提供了用于计算轴向磁化圆柱形磁铁与环状磁铁磁场分布的Matlab代码实现方案,基于经典电磁学理论,特别是毕奥-萨伐尔定律或等效磁荷模型,采用数值积分方法对磁体在三维空间中产生的磁场强度进行建模与仿真。程序能够精确计算任意空间点的磁场矢量分量,支持不同几何参数(如内外径、高度、磁化方向)和材料属性的设定,适用于永磁体在电机、传感器、磁悬浮系统等工程电磁装置中的前期分析与优化设计。文档虽简洁,但聚焦核心算法实现,突出物理建模与编程计算的紧密结合。; 适合人群:具备电磁场与电磁波基础知识及Matlab编程能力的高校本科生、研究生、科研人员和电气工程、自动化、应用物理等领域的工程技术人员; 使用场景及目标:①用于教学实践中帮助学生理解轴向磁化永磁体的磁场分布规律;②支撑科研项目中对磁路结构的设计与验证;③为工业应用中磁性元件的布局优化与性能预测提供快速仿真工具; 阅读建议:建议结合电磁场理论教材同步学习,运行代码时应关注变量单位的一致性与网格划分的精度,鼓励通过修改磁体尺寸、磁化强度等参数开展拓展性仿真实验,以深化对磁场空间分布特性的理解。

状态估计变分贝叶斯近似的递归噪声自适应卡尔曼滤波(Matlab代码实现)

内容概要:本文详细介绍了一种基于变分贝叶斯近似的递归噪声自适应卡尔曼滤波方法,并配套提供了完整的Matlab代码实现。该方法旨在解决传统卡尔曼滤波在面对过程噪声和观测噪声统计特性未知或时变情况下性能退化的问题,通过引入变分贝叶斯推断技术,实现对噪声协方差的在线联合估计与动态更新,从而提升滤波器在非平稳环境中的状态估计精度与鲁棒性。文中系统阐述了算法的理论基础、数学推导流程、递归计算结构及关键实现步骤,涵盖了概率图模型构建、似然函数近似、变分迭代优化等核心技术环节,适用于复杂动态系统中的自适应信号处理与状态估计任务。; 适合人群:具备良好线性系统理论、概率统计与贝叶斯估计基础,并熟悉Matlab编程的研究生、科研人员及从事导航、控制、传感融合等相关领域的工程技术人员; 使用场景及目标:①应对实际工程中因噪声先验信息缺失或环境变化导致的标准卡尔曼滤波失效问题;②应用于无人机导航、智能驾驶、工业状态监测、传感器融合等高精度自适应估计场景,提升系统在不确定环境下的稳定性和准确性; 阅读建议:建议读者结合所提供的Matlab代码逐模块研读,重点理解变分贝叶斯迭代过程中噪声协方差矩阵的后验更新机制,并通过仿真实验对比所提方法与标准KF/EKF/UKF的估计性能差异,深入掌握噪声自适应滤波的设计思想与实现技巧。

上一篇: Figma MCP 使用指南:让 AI 直接“看懂“你的设计稿,TaoToken 统一 Key 接入实战
下一篇: Java 开发者实战:用 Spring AI 接入 MCP 服务并改到 TaoToken
IndigoNight21
博客等级 码龄2年 552粉丝 4094原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IndigoNight21

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值