Commit 37760883d60ed21aad5c40227fd4f110897227ce
1 parent
86243098
feat(ai1): 对齐模型缓存容量并补充配置变更回归
Showing
2 changed files
with
58 additions
and
6 deletions
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; | @@ -31,8 +31,9 @@ import java.util.function.Predicate; | ||
| 31 | * 1. 对话模型:OpenAiChatModel(OpenAI 兼容协议,覆盖 DeepSeek、GLM、Ollama(/v1) 等) | 31 | * 1. 对话模型:OpenAiChatModel(OpenAI 兼容协议,覆盖 DeepSeek、GLM、Ollama(/v1) 等) |
| 32 | * 2. 嵌入模型:OpenAiEmbeddingModel(向量化) | 32 | * 2. 嵌入模型:OpenAiEmbeddingModel(向量化) |
| 33 | * | 33 | * |
| 34 | - * 模型按 providerId:modelId 缓存复用(编号全局唯一,无需再区分租户),缓存使用 Guava Cache 按容量淘汰最久未访问的模型; | ||
| 35 | - * Provider、模型的配置变更时,由对应 Service 调用 {@link #evictByProviderId}、{@link #evictByModelId} 显式失效 | 34 | + * 模型按 providerId:modelId:配置指纹 缓存复用(编号全局唯一,无需再区分租户),配置变化后自然命中新 key; |
| 35 | + * 缓存使用 Guava Cache 按容量淘汰最久未访问的模型;Provider、模型的配置变更或删除时, | ||
| 36 | + * 由对应 Service 调用 {@link #evictByProviderId}、{@link #evictByModelId} 显式失效本节点缓存 | ||
| 36 | * | 37 | * |
| 37 | * @author 芋道源码 | 38 | * @author 芋道源码 |
| 38 | */ | 39 | */ |
| @@ -47,10 +48,12 @@ public class Ai1LlmModelFactory { | @@ -47,10 +48,12 @@ public class Ai1LlmModelFactory { | ||
| 47 | /** | 48 | /** |
| 48 | * 缓存上限,达到后逐出最久未访问的模型 | 49 | * 缓存上限,达到后逐出最久未访问的模型 |
| 49 | */ | 50 | */ |
| 50 | - private static final int CACHE_MAX = 128; | 51 | + private static final int CACHE_MAX = 500; |
| 51 | 52 | ||
| 52 | /** | 53 | /** |
| 53 | - * 缓存写入后的过期时间:修改 Provider / 模型时只能失效本节点,其他节点最迟在该时间后按新配置重建 | 54 | + * 缓存写入后的过期时间:回收配置变化后不再命中的旧 key |
| 55 | + * | ||
| 56 | + * 每次调用都按最新配置计算 key,其他节点无需等待过期,下次调用即按新配置构建 | ||
| 54 | */ | 57 | */ |
| 55 | private static final Duration CACHE_EXPIRE = Duration.ofMinutes(10); | 58 | private static final Duration CACHE_EXPIRE = Duration.ofMinutes(10); |
| 56 | 59 | ||
| @@ -60,12 +63,12 @@ public class Ai1LlmModelFactory { | @@ -60,12 +63,12 @@ public class Ai1LlmModelFactory { | ||
| 60 | private static final String KEY_SEPARATOR = ":"; | 63 | private static final String KEY_SEPARATOR = ":"; |
| 61 | 64 | ||
| 62 | /** | 65 | /** |
| 63 | - * 对话模型缓存:providerId:modelId:sessionKey → ChatModel | 66 | + * 对话模型缓存:providerId:modelId:配置指纹:sessionKey → ChatModel |
| 64 | */ | 67 | */ |
| 65 | private final Cache<String, OpenAiChatModel> chatModelCache = CacheBuilder.newBuilder() | 68 | private final Cache<String, OpenAiChatModel> chatModelCache = CacheBuilder.newBuilder() |
| 66 | .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build(); | 69 | .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build(); |
| 67 | /** | 70 | /** |
| 68 | - * 嵌入模型缓存:providerId:modelId → EmbeddingModel | 71 | + * 嵌入模型缓存:providerId:modelId:配置指纹 → EmbeddingModel |
| 69 | */ | 72 | */ |
| 70 | private final Cache<String, OpenAiEmbeddingModel> embeddingModelCache = CacheBuilder.newBuilder() | 73 | private final Cache<String, OpenAiEmbeddingModel> embeddingModelCache = CacheBuilder.newBuilder() |
| 71 | .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build(); | 74 | .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build(); |
yudao-module-ai1-springai/src/test/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactoryTest.java
| @@ -187,6 +187,55 @@ public class Ai1LlmModelFactoryTest { | @@ -187,6 +187,55 @@ public class Ai1LlmModelFactoryTest { | ||
| 187 | assertNotSame(chat, chatModel(model, 1L)); | 187 | assertNotSame(chat, chatModel(model, 1L)); |
| 188 | } | 188 | } |
| 189 | 189 | ||
| 190 | + @Test | ||
| 191 | + public void testCache_chatConfigChange() { | ||
| 192 | + // 准备参数 | ||
| 193 | + Ai1ModelRespBO model = model(baseUrl); | ||
| 194 | + Ai1LlmChatTool chatTool = new Ai1LlmChatTool(); | ||
| 195 | + ReflectionTestUtils.setField(chatTool, "llmModelFactory", factory); | ||
| 196 | + Object chat = chatModel(model, 42L); | ||
| 197 | + | ||
| 198 | + // 调用:接口地址变化 | ||
| 199 | + model.setBaseUrl(baseUrl + "/proxy/v1"); | ||
| 200 | + chatTool.chat(model, "系统指令", List.of(), "当前消息", List.of(), List.of(), 42L, null, null); | ||
| 201 | + // 断言 | ||
| 202 | + Object changed = chatModel(model, 42L); | ||
| 203 | + assertNotSame(chat, changed); | ||
| 204 | + assertEquals("/proxy/v1/chat/completions", requests.get(0).path()); | ||
| 205 | + | ||
| 206 | + // 调用:请求 Header 变化 | ||
| 207 | + model.setHeaders(Map.of("X-Session", "{session}", "X-Provider", "changed")); | ||
| 208 | + chatTool.chat(model, "系统指令", List.of(), "当前消息", List.of(), List.of(), 42L, null, null); | ||
| 209 | + // 断言 | ||
| 210 | + assertNotSame(changed, chatModel(model, 42L)); | ||
| 211 | + assertEquals(2, requests.size()); | ||
| 212 | + assertEquals("/proxy/v1/chat/completions", requests.get(1).path()); | ||
| 213 | + assertEquals("changed", requests.get(1).provider()); | ||
| 214 | + assertEquals("42", requests.get(1).session()); | ||
| 215 | + } | ||
| 216 | + | ||
| 217 | + @Test | ||
| 218 | + public void testCache_embeddingConfigChange() { | ||
| 219 | + // 准备参数 | ||
| 220 | + Ai1ModelRespBO model = model(baseUrl); | ||
| 221 | + EmbeddingModel embedding = factory.getOrCreateEmbeddingModel(model); | ||
| 222 | + embedding.embed("旧配置"); | ||
| 223 | + | ||
| 224 | + // 调用:接口地址、请求 Header 变化 | ||
| 225 | + model.setBaseUrl(baseUrl + "/proxy/v1").setHeaders(Map.of("X-Provider", "changed")); | ||
| 226 | + EmbeddingModel changed = factory.getOrCreateEmbeddingModel(model); | ||
| 227 | + changed.embed("新配置"); | ||
| 228 | + // 断言 | ||
| 229 | + assertNotSame(embedding, changed); | ||
| 230 | + assertSame(changed, factory.getOrCreateEmbeddingModel(model)); | ||
| 231 | + assertEquals(2, requests.size()); | ||
| 232 | + assertEquals("/v1/embeddings", requests.get(0).path()); | ||
| 233 | + assertEquals("provider", requests.get(0).provider()); | ||
| 234 | + assertEquals("/proxy/v1/embeddings", requests.get(1).path()); | ||
| 235 | + assertEquals("changed", requests.get(1).provider()); | ||
| 236 | + assertEquals("新配置", requests.get(1).body().get("input").get(0).asText()); | ||
| 237 | + } | ||
| 238 | + | ||
| 190 | // ========== 工具方法 ========== | 239 | // ========== 工具方法 ========== |
| 191 | 240 | ||
| 192 | private Object chatModel(Ai1ModelRespBO model, Long sessionId) { | 241 | private Object chatModel(Ai1ModelRespBO model, Long sessionId) { |