Commit 37760883d60ed21aad5c40227fd4f110897227ce

Authored by YunaiV
1 parent 86243098

feat(ai1): 对齐模型缓存容量并补充配置变更回归

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 31 * 1. 对话模型:OpenAiChatModel(OpenAI 兼容协议,覆盖 DeepSeek、GLM、Ollama(/v1) 等)
32 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 38 * @author 芋道源码
38 39 */
... ... @@ -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 58 private static final Duration CACHE_EXPIRE = Duration.ofMinutes(10);
56 59  
... ... @@ -60,12 +63,12 @@ public class Ai1LlmModelFactory {
60 63 private static final String KEY_SEPARATOR = ":";
61 64  
62 65 /**
63   - * 对话模型缓存:providerId:modelId:sessionKey → ChatModel
  66 + * 对话模型缓存:providerId:modelId:配置指纹:sessionKey → ChatModel
64 67 */
65 68 private final Cache<String, OpenAiChatModel> chatModelCache = CacheBuilder.newBuilder()
66 69 .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build();
67 70 /**
68   - * 嵌入模型缓存:providerId:modelId → EmbeddingModel
  71 + * 嵌入模型缓存:providerId:modelId:配置指纹 → EmbeddingModel
69 72 */
70 73 private final Cache<String, OpenAiEmbeddingModel> embeddingModelCache = CacheBuilder.newBuilder()
71 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 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 241 private Object chatModel(Ai1ModelRespBO model, Long sessionId) {
... ...