diff --git a/yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactory.java b/yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactory.java index 6e0645c..8613e7e 100644 --- a/yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactory.java +++ b/yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactory.java @@ -31,8 +31,9 @@ import java.util.function.Predicate; * 1. 对话模型:OpenAiChatModel(OpenAI 兼容协议,覆盖 DeepSeek、GLM、Ollama(/v1) 等) * 2. 嵌入模型:OpenAiEmbeddingModel(向量化) * - * 模型按 providerId:modelId 缓存复用(编号全局唯一,无需再区分租户),缓存使用 Guava Cache 按容量淘汰最久未访问的模型; - * Provider、模型的配置变更时,由对应 Service 调用 {@link #evictByProviderId}、{@link #evictByModelId} 显式失效 + * 模型按 providerId:modelId:配置指纹 缓存复用(编号全局唯一,无需再区分租户),配置变化后自然命中新 key; + * 缓存使用 Guava Cache 按容量淘汰最久未访问的模型;Provider、模型的配置变更或删除时, + * 由对应 Service 调用 {@link #evictByProviderId}、{@link #evictByModelId} 显式失效本节点缓存 * * @author 芋道源码 */ @@ -47,10 +48,12 @@ public class Ai1LlmModelFactory { /** * 缓存上限,达到后逐出最久未访问的模型 */ - private static final int CACHE_MAX = 128; + private static final int CACHE_MAX = 500; /** - * 缓存写入后的过期时间:修改 Provider / 模型时只能失效本节点,其他节点最迟在该时间后按新配置重建 + * 缓存写入后的过期时间:回收配置变化后不再命中的旧 key + * + * 每次调用都按最新配置计算 key,其他节点无需等待过期,下次调用即按新配置构建 */ private static final Duration CACHE_EXPIRE = Duration.ofMinutes(10); @@ -60,12 +63,12 @@ public class Ai1LlmModelFactory { private static final String KEY_SEPARATOR = ":"; /** - * 对话模型缓存:providerId:modelId:sessionKey → ChatModel + * 对话模型缓存:providerId:modelId:配置指纹:sessionKey → ChatModel */ private final Cache chatModelCache = CacheBuilder.newBuilder() .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build(); /** - * 嵌入模型缓存:providerId:modelId → EmbeddingModel + * 嵌入模型缓存:providerId:modelId:配置指纹 → EmbeddingModel */ private final Cache embeddingModelCache = CacheBuilder.newBuilder() .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build(); diff --git a/yudao-module-ai1-springai/src/test/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactoryTest.java b/yudao-module-ai1-springai/src/test/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactoryTest.java index bed9e23..5d84146 100644 --- a/yudao-module-ai1-springai/src/test/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactoryTest.java +++ b/yudao-module-ai1-springai/src/test/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactoryTest.java @@ -187,6 +187,55 @@ public class Ai1LlmModelFactoryTest { assertNotSame(chat, chatModel(model, 1L)); } + @Test + public void testCache_chatConfigChange() { + // 准备参数 + Ai1ModelRespBO model = model(baseUrl); + Ai1LlmChatTool chatTool = new Ai1LlmChatTool(); + ReflectionTestUtils.setField(chatTool, "llmModelFactory", factory); + Object chat = chatModel(model, 42L); + + // 调用:接口地址变化 + model.setBaseUrl(baseUrl + "/proxy/v1"); + chatTool.chat(model, "系统指令", List.of(), "当前消息", List.of(), List.of(), 42L, null, null); + // 断言 + Object changed = chatModel(model, 42L); + assertNotSame(chat, changed); + assertEquals("/proxy/v1/chat/completions", requests.get(0).path()); + + // 调用:请求 Header 变化 + model.setHeaders(Map.of("X-Session", "{session}", "X-Provider", "changed")); + chatTool.chat(model, "系统指令", List.of(), "当前消息", List.of(), List.of(), 42L, null, null); + // 断言 + assertNotSame(changed, chatModel(model, 42L)); + assertEquals(2, requests.size()); + assertEquals("/proxy/v1/chat/completions", requests.get(1).path()); + assertEquals("changed", requests.get(1).provider()); + assertEquals("42", requests.get(1).session()); + } + + @Test + public void testCache_embeddingConfigChange() { + // 准备参数 + Ai1ModelRespBO model = model(baseUrl); + EmbeddingModel embedding = factory.getOrCreateEmbeddingModel(model); + embedding.embed("旧配置"); + + // 调用:接口地址、请求 Header 变化 + model.setBaseUrl(baseUrl + "/proxy/v1").setHeaders(Map.of("X-Provider", "changed")); + EmbeddingModel changed = factory.getOrCreateEmbeddingModel(model); + changed.embed("新配置"); + // 断言 + assertNotSame(embedding, changed); + assertSame(changed, factory.getOrCreateEmbeddingModel(model)); + assertEquals(2, requests.size()); + assertEquals("/v1/embeddings", requests.get(0).path()); + assertEquals("provider", requests.get(0).provider()); + assertEquals("/proxy/v1/embeddings", requests.get(1).path()); + assertEquals("changed", requests.get(1).provider()); + assertEquals("新配置", requests.get(1).body().get("input").get(0).asText()); + } + // ========== 工具方法 ========== private Object chatModel(Ai1ModelRespBO model, Long sessionId) {