Skip to main content

概述

ChatModelFactory是LangChat Pro的核心组件之一,负责动态创建和管理各种大语言模型(LLM)的实例。它采用工厂模式,支持多种模型供应商(OpenAI、Ollama、Qwen、Qianfan、Zhipu等),实现了模型实例的缓存和Listener绑定机制。

设计模式

工厂模式(Factory Pattern)

ChatModelFactory使用工厂模式来:
  1. 封装复杂的模型创建逻辑
  2. 统一不同供应商的模型实例化方式
  3. 支持模型实例的缓存和复用
  4. 为每个请求绑定独立的Listener

核心组件

ChatModelFactory

路径: langchat-core/src/main/java/cn/langchat/core/factory/ChatModelFactory.java 职责: 动态创建和管理ChatModel实例
@Slf4j
@Component
public class ChatModelFactory {

    /**
     * 本地实例缓存 - ChatModel (不包含listener)
     */
    private Cache<String, ChatModel> chatModelCache;

    /**
     * 本地实例缓存 - StreamingChatModel (不包含listener)
     */
    private Cache<String, StreamingChatModel> streamingChatModelCache;

    @Resource
    private ChatListener chatListener;

    @Resource
    private AigcModelService aigcModelService;

    @Resource
    private CacheUtils cacheUtils;
}

核心方法

StreamingChatModel 相关方法

1. getStreamingModel(带chatId)

/**
 * 获取Streaming聊天模型实例(带chatId绑定listener)
 */
public StreamingChatModel getStreamingModel(
    BaseChatContext context, 
    String chatId
) {
    if (StrUtil.isBlank(chatId)) {
        throw new IllegalArgumentException("chatId不能为空");
    }

    AigcModel model = getModelById(context.getModelId());
    validateModelType(model);

    // 每次请求都创建新的Model实例(包含该请求的listener)
    return createStreamingModelWithListener(model, context, chatId);
}
用途: 用于聊天场景,需要绑定listener来追踪请求和记录日志

2. getStreamingModel(不带chatId)

/**
 * 获取Streaming聊天模型实例(不带listener,用于非聊天场景)
 */
public StreamingChatModel getStreamingModel(BaseChatContext context) {
    AigcModel model = getModelById(context.getModelId());
    validateModelType(model);
    return getStreamingModelInstance(model, context);
}
用途: 用于Embedding、向量检索等非聊天场景

ChatModel 相关方法

1. getChatModel(带chatId)

/**
 * 获取聊天模型实例(带chatId绑定listener)
 */
public ChatModel getChatModel(BaseChatContext context, String chatId) {
    if (StrUtil.isBlank(chatId)) {
        throw new IllegalArgumentException("chatId不能为空");
    }

    AigcModel model = getModelById(context.getModelId());
    validateModelType(model);

    // 每次请求都创建新的Model实例(包含该请求的listener)
    return createChatModelWithListener(model, context, chatId);
}

2. getChatModel(不带chatId)

/**
 * 获取聊天模型实例(不带listener,用于非聊天场景)
 */
public ChatModel getChatModel(BaseChatContext context) {
    AigcModel model = getModelById(context.getModelId());
    validateModelType(model);
    return getChatModelInstance(model, context);
}

模型创建流程

1. 获取模型配置

private AigcModel getModelById(String modelId) {
    if (modelId == null || modelId.trim().isEmpty()) {
        throw new IllegalArgumentException("模型ID不能为空");
    }

    AigcModel model = aigcModelService.getById(modelId);
    if (model == null) {
        throw new IllegalArgumentException("模型配置不存在: " + modelId);
    }

    return model;
}

2. 创建StreamingChatModel(带Listener)

private StreamingChatModel createStreamingModelWithListener(
    AigcModel model, 
    BaseChatContext context, 
    String chatId
) {
    log.debug("创建StreamingChatModel实例: modelId={}, chatId={}, provider={}",
            model.getId(), chatId, model.getProvider());

    // 创建该请求专属的listener
    ChatModelListener listener = chatListener.createListener(chatId);

    BaseChatContext mergedContext = ModelCacheSupport.mergeContext(model, context);

    return switch (model.getProvider().toLowerCase()) {
        case ProviderConst.ollama -> createOllamaStreamingModel(model, mergedContext, listener);
        case ProviderConst.dashscope -> createDashscopeStreamingModel(model, mergedContext, listener);
        case ProviderConst.baiducloud -> createBaiducloudStreamingModel(model, mergedContext, listener);
        case ProviderConst.zhipu -> createZhipuStreamingModel(model, mergedContext, listener);
        default -> createOpenAiStreamingModel(model, mergedContext, listener);
    };
}

支持的模型供应商

1. OpenAI(及兼容OpenAI API的模型)

OpenAiStreamingChatModel

private OpenAiStreamingChatModel createOpenAiStreamingModel(
    AigcModel model, 
    BaseChatContext context, 
    ChatModelListener listener
) {
    JdkHttpClientBuilder jdkHttpClientBuilder = JdkHttpClient.builder()
        .httpClientBuilder(HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1));

    OpenAiStreamingChatModel.OpenAiStreamingChatModelBuilder builder = 
        OpenAiStreamingChatModel.builder()
            .apiKey(model.getApiKey())
            .returnThinking(true)
            .baseUrl(model.getBaseUrl())
            .modelName(model.getModel())
            .maxTokens(model.getMaxToken())
            .temperature(model.getTemperature())
            .timeout(Duration.ofMinutes(model.getTimeout()))
            .logRequests(true)
            .logResponses(true)
            .topP(model.getTopP())
            .httpClientBuilder(jdkHttpClientBuilder);

    // 只有 text2text 类型才添加 responseFormat、metadata 和 listeners
    if (ModelTypeEnum.text2text.name().equals(model.getType())) {
        builder.responseFormat(getJsonObjectIfContainsJson(model.getFormat()))
            .metadata(Map.of(ModelTypeEnum.CHAT_MODEL.name(), JSON.toJSONString(model)));
        if (listener != null) {
            builder.listeners(List.of(listener));
        }
    }

    return builder.build();
}
支持的模型: GPT-3.5、GPT-4、Claude、DeepSeek等兼容OpenAI API的模型

2. Ollama(本地模型)

OllamaStreamingChatModel

private OllamaStreamingChatModel createOllamaStreamingModel(
    AigcModel model, 
    BaseChatContext context, 
    ChatModelListener listener
) {
    OllamaStreamingChatModel.OllamaStreamingChatModelBuilder builder = 
        OllamaStreamingChatModel.builder()
            .think(true)
            .returnThinking(true)
            .baseUrl(model.getBaseUrl())
            .modelName(model.getModel())
            .temperature(model.getTemperature())
            .topP(model.getTopP())
            .timeout(Duration.ofMinutes(model.getTimeout()));

    // 只有 text2text 类型才添加 responseFormat 和 listeners
    if (ModelTypeEnum.text2text.name().equals(model.getType())) {
        builder.responseFormat(parseResponseFormat(model.getFormat()));
        if (listener != null) {
            builder.listeners(List.of(listener));
        }
    }

    return builder.build();
}
支持的模型: Llama 2、Llama 3、Mistral、Gemma等

3. Qwen(通义千问)

QwenStreamingChatModel

private QwenStreamingChatModel createDashscopeStreamingModel(
    AigcModel model, 
    BaseChatContext context, 
    ChatModelListener listener
) {
    QwenStreamingChatModel.QwenStreamingChatModelBuilder builder = 
        QwenStreamingChatModel.builder()
            .apiKey(model.getApiKey())
            .modelName(model.getModel())
            .baseUrl(model.getBaseUrl())
            .maxTokens(model.getMaxToken())
            .temperature(Float.parseFloat(model.getTemperature().toString()))
            .topP(model.getTopP());

    // 只有 text2text 类型才添加 defaultRequestParameters 和 listeners
    if (ModelTypeEnum.text2text.name().equals(model.getType())) {
        builder.defaultRequestParameters(QwenChatRequestParameters.builder()
                .responseFormat(parseResponseFormat(model.getFormat()))
                .custom(Map.of(ModelTypeEnum.CHAT_MODEL.name(), JSON.toJSONString(model)))
                .enableThinking(true)
                .build());
        if (listener != null) {
            builder.listeners(List.of(listener));
        }
    }

    return builder.build();
}
支持的模型: qwen-turbo、qwen-plus、qwen-max等

4. Qianfan(百度千帆)

QianfanStreamingChatModel

private QianfanStreamingChatModel createBaiducloudStreamingModel(
    AigcModel model, 
    BaseChatContext context, 
    ChatModelListener listener
) {
    QianfanStreamingChatModel.QianfanStreamingChatModelBuilder builder = 
        QianfanStreamingChatModel
            .builder()
            .apiKey(model.getApiKey())
            .modelName(model.getModel())
            .baseUrl(model.getBaseUrl())
            .temperature(model.getTemperature())
            .topP(model.getTopP());

    // 只有 text2text 类型才添加 responseFormat
    if (ModelTypeEnum.text2text.name().equals(model.getType())) {
        builder.responseFormat(getJsonObjectIfContainsJson(model.getFormat()));
    }

    return builder.build();
}
支持的模型: ERNIE-Bot、ERNIE-Bot-turbo、ERNIE-Speed等

5. Zhipu(智谱AI)

ZhipuAiStreamingChatModel

private ZhipuAiStreamingChatModel createZhipuStreamingModel(
    AigcModel model, 
    BaseChatContext context, 
    ChatModelListener listener
) {
    ZhipuAiStreamingChatModel.ZhipuAiStreamingChatModelBuilder builder = 
        ZhipuAiStreamingChatModel
            .builder()
            .apiKey(model.getApiKey())
            .baseUrl(model.getBaseUrl())
            .model(model.getModel())
            .maxToken(model.getMaxToken())
            .temperature(model.getTemperature())
            .topP(model.getTopP())
            .readTimeout(Duration.ofMinutes(model.getTimeout()))
            .connectTimeout(Duration.ofMinutes(model.getTimeout()));

    // 只有 text2text 类型才添加 listeners
    if (ModelTypeEnum.text2text.name().equals(model.getType())) {
        if (listener != null) {
            builder.listeners(List.of(listener));
        }
    }

    return builder.build();
}
支持的模型: GLM-4、GLM-3-Turbo等

缓存机制

缓存策略

ChatModelFactory使用Caffeine缓存模型实例:
private Cache<String, ChatModel> chatModelCache;
private Cache<String, StreamingChatModel> streamingChatModelCache;

缓存获取

private StreamingChatModel getStreamingModelInstance(
    AigcModel model, 
    BaseChatContext context
) {
    String cacheKey = ModelCacheSupport.generateCacheKey("chat:streaming", model);

    return getStreamingChatModelCache().get(cacheKey, k -> {
        log.info("缓存未命中,创建新的StreamingChatModel实例: modelId={}, provider={}",
                model.getId(), model.getProvider());
        return createStreamingChatModel(model, context, null);
    });
}

缓存键生成

public class ModelCacheSupport {
    
    public static String generateCacheKey(String prefix, AigcModel model) {
        // 基于模型配置生成唯一缓存键
        return String.format("%s:%s:%s:%s:%s", 
            prefix,
            model.getProvider(),
            model.getModel(),
            model.getBaseUrl(),
            model.getTemperature());
    }
    
    public static BaseChatContext mergeContext(
        AigcModel model, 
        BaseChatContext context
    ) {
        // 合并模型配置和上下文
        var merged = SimpleChatContext.of(model);
        if (context != null) {
            merged.setFormat(context.getFormat());
            // 合并其他配置
        }
        return merged;
    }
}

缓存设计要点

  1. 不缓存Listener: Listener绑定到具体请求,不能缓存
  2. 基于配置缓存: 相同配置的模型实例可以复用
  3. 懒加载: 首次使用时创建并缓存
  4. 内存缓存: 使用Caffeine,高性能

Listener绑定机制

ChatListener

路径: langchat-core/src/main/java/cn/langchat/core/observability/ChatListener.java 职责: 监听模型调用事件
public interface ChatListener {
    
    /**
     * 为特定请求创建Listener
     */
    ChatModelListener createListener(String chatId);
}

Listener创建流程

// 1. 在LcChatService中
String chatId = LcIdUtil.getUUID();
StreamingChatModel model = modelFactory.getStreamingModel(context, chatId);

// 2. 在ChatModelFactory中
ChatModelListener listener = chatListener.createListener(chatId);

// 3. Listener绑定到模型
builder.listeners(List.of(listener));

Listener功能

  1. 请求追踪: 追踪每个请求的开始和结束
  2. Token统计: 统计输入和输出Token数量
  3. 日志记录: 记录请求和响应详情
  4. 计费: 计算API调用成本
  5. 监控: 性能监控和告警

响应格式处理

ResponseFormat

private ResponseFormat parseResponseFormat(String format) {
    if (format == null) {
        return ResponseFormat.TEXT; // 默认
    }
    return switch (format.trim().toLowerCase()) {
        case "json" -> ResponseFormat.JSON;
        case "text" -> ResponseFormat.TEXT;
        default -> ResponseFormat.TEXT;
    };
}

private String getJsonObjectIfContainsJson(String format) {
    if (format != null && format.toLowerCase().contains("json")) {
        return "json_object";
    }
    return "text";
}
支持格式:
  • text - 文本格式(默认)
  • json - JSON格式

扩展新模型供应商

步骤1: 添加供应商常量

ProviderConst中添加新供应商:
public class ProviderConst {
    public static final String openai = "openai";
    public static final String ollama = "ollama";
    public static final String dashscope = "dashscope";
    public static final String baiducloud = "baiducloud";
    public static final String zhipu = "zhipu";
    // 添加新供应商
    public static final String newprovider = "newprovider";
}

步骤2: 实现模型创建方法

ChatModelFactory中添加:
private StreamingChatModel createNewProviderStreamingModel(
    AigcModel model, 
    BaseChatContext context, 
    ChatModelListener listener
) {
    // 根据新供应商的SDK创建模型
    NewProviderStreamingChatModel builder = NewProviderStreamingChatModel.builder()
        .apiKey(model.getApiKey())
        .modelName(model.getModel())
        .baseUrl(model.getBaseUrl())
        .temperature(model.getTemperature())
        .maxTokens(model.getMaxToken())
        .build();

    // 如果需要listener
    if (ModelTypeEnum.text2text.name().equals(model.getType())) {
        if (listener != null) {
            builder.listeners(List.of(listener));
        }
    }

    return builder.build();
}

private ChatModel createNewProviderChatModel(
    AigcModel model, 
    BaseChatContext context, 
    ChatModelListener listener
) {
    // 实现ChatModel版本
    // ...
}

步骤3: 更新创建逻辑

createStreamingModelWithListenercreateChatModelWithListener中添加case:
return switch (model.getProvider().toLowerCase()) {
    case ProviderConst.ollama -> createOllamaStreamingModel(model, mergedContext, listener);
    case ProviderConst.dashscope -> createDashscopeStreamingModel(model, mergedContext, listener);
    case ProviderConst.baiducloud -> createBaiducloudStreamingModel(model, mergedContext, listener);
    case ProviderConst.zhipu -> createZhipuStreamingModel(model, mergedContext, listener);
    case ProviderConst.newprovider -> createNewProviderStreamingModel(model, mergedContext, listener);
    default -> createOpenAiStreamingModel(model, mergedContext, listener);
};

步骤4: 更新缓存键生成(如果需要)

如果新供应商有特殊配置参数,更新ModelCacheSupport.generateCacheKey

配置说明

AigcModel 配置字段

字段类型说明示例
idString模型ID”model-001”
providerString供应商”openai”
modelString模型名称”gpt-4”
baseUrlStringAPI地址https://api.openai.com/v1
apiKeyStringAPI密钥”sk-…“
secretKeyString密钥(部分供应商)“secret-…“
typeString模型类型”text2text”, “text2image”
temperatureDouble温度参数0.7
topPDoubleTop-P参数0.9
maxTokenInteger最大Token数2000
timeoutInteger超时时间(分钟)5
formatString响应格式”json”, “text”

最佳实践

1. 使用正确的获取方法

  • 带chatId: 聊天场景,需要Listener追踪
  • 不带chatId: Embedding、向量化等场景

2. 合理设置缓存

  • 缓存避免重复创建模型实例
  • 但不缓存Listener,每次请求独立创建
  • 缓存键包含所有配置参数

3. 模型类型判断

// 只有 text2text 类型才添加 listener
if (ModelTypeEnum.text2text.name().equals(model.getType())) {
    builder.listeners(List.of(listener));
}

4. 异常处理

  • 模型不存在时抛出异常
  • 模型ID为空时抛出异常
  • Supplier不支持时使用默认OpenAI实现

性能优化

1. 模型实例缓存

  • 使用Caffeine本地缓存
  • 避免重复创建相同配置的模型
  • 懒加载机制

2. 懒加载Cache

private Cache<String, ChatModel> getChatModelCache() {
    if (chatModelCache == null) {
        synchronized (this) {
            if (chatModelCache == null) {
                chatModelCache = cacheUtils.getCache(ModelCacheConst.ChatModelCache);
            }
        }
    }
    return chatModelCache;
}

3. Listener分离

  • 不缓存Listener
  • 每次请求创建独立的Listener
  • 避免线程安全问题

参考文档