Стратегия развертывания мульти ChatModel на LangChain4J

Стратегия развертывания мульти ChatModel на LangChain4J

Введение

Когда вы начинаете интегрировать AI функции в сервис, вы быстро сталкиваетесь с такой ситуацией.

Я хочу использовать разные ChatModel в зависимости от функции, как мне это управлять?

При разработке AI-сервиса по рекомендации туристических мест я столкнулся с этой проблемой.

Сначала я думал, что текстовая основанная модель для общения будет достаточно. Но когда используется изображение туристической достопримечательности, необходимо проанализировать это изображение и извлечь его характеристики и описание. В этом случае Текстовые модели сами по себе не могут понимать изображения었습니да.

В конце концов, возникла необходимость в двух моделях.

Модель текста: специализированная разговорная модель, подходящая для быстрого ответа и обработки длинного контекста.

Мультимодальная модель: модель с поддержкой зрения, способная понимать содержание на основе изображений

Здесь начались раздумья.

Как управлять несколькими ChatModel в среде Spring Boot + LangChain4J?

Ограничения автоконфигурации LangChain4J

LangChain4J поддерживает автоматическую настройку Spring Boot. Например, если вы настроите следующим образом в application.yml, то бин ChatModel будет автоматически зарегистрирован.

langchain4j:
  ollama:
    chat-model:
      base-url: https://ollama.example.com
      model-name: model-name

Однако у этого подхода есть ограничения.

Автоматическая настройка происходит по умолчанию Способ регистрации одной пустой ChatModelЭто близко. Таким образом, если необходимо использовать несколько моделей для различных целей, полагаться только на автоматическую настройку трудно.

В конечном итоге, чтобы явно управлять несколькими моделями, необходимо определять их напрямую с помощью @Bean.

Сравнение методов

Метод 1. Разделение с помощью @Qualifier

Первый способ, который приходит на ум, - это использование @Qualifier для наименования каждого бина модели.

@Configuration
public class ModelConfig {
    @Primary
    @Bean
    @Qualifier("textChatModel")
    public ChatModel textChatModel() {
        return OllamaChatModel.builder()
                .baseUrl("https://ollama.example.com")
                .modelName("chat-model-name")
                .build();
    }
    @Bean
    @Qualifier("imageChatModel")
    public ChatModel imageChatModel() {
        return OllamaChatModel.builder()
                .baseUrl("https://ollama.example.com")
                .modelName("multi-modal-model-name")
                .build();
    }
}

Часть, которая использует, получает его следующим образом.

@Service
public class SomeService {
    private final ChatModel textModel;
    private final ChatModel imageModel;
    public SomeService(
            @Qualifier("textChatModel") ChatModel textModel,
            @Qualifier("imageChatModel") ChatModel imageModel
    ) {
        this.textModel = textModel;
        this.imageModel = imageModel;
    }
}

Этот метод прост и интуитивно понятен. Однако с увеличением числа моделей строка @Qualifier будет распространилась на несколько классов.

Поскольку он основан на строках, трудно поймать опечатки на этапе компиляции, и замена модели или изменение имени также повлияет на более широкий контекст.

Способ 2. Использование @Qualifier + прокси-класса

Второй способ — это обернуть @Qualifier моделями прокси-классов, не exposing его напрямую в бизнес-логике.

В текущем проекте используется этот метод.

@Service
public class ChatModelProxy {
    private final ChatModel chatModel;
    private final ObjectMapper objectMapper;
    public ChatModelProxy(
            @Qualifier("textChatModel") ChatModel chatModel,
            ObjectMapper objectMapper
    ) {
        this.chatModel = chatModel;
        this.objectMapper = objectMapper;
    }
    public String chat(Prompt prompt) {
        // 텍스트 모델 호출
    }
    public  T chat(Prompt prompt, Class clazz) {
        // 응답 JSON 파싱
    }
    public  List chatAnswerInList(Prompt prompt, Class
       clazz) { // 리스트 형태 응답 파싱 } }

Модель, предназначенная для анализа изображений, отделяется отдельным Proxy.

@Service
public class ImageChatModelProxy {
    private final ChatModel chatModel;
    public ImageChatModelProxy(
            @Qualifier("imageChatModel") ChatModel chatModel
    ) {
        this.chatModel = chatModel;
    }
    public String chatWithImage(String imageUrl, String textPrompt) {
        String base64 = fetchAsResizedBase64(imageUrl);
        String mimeType = detectMimeType(imageUrl);
        UserMessage message = UserMessage.from(
                ImageContent.from(base64, mimeType, ImageContent.DetailLevel.AUTO),
                TextContent.from(textPrompt)
        );
        return chatModel.chat(message).aiMessage().text();
    }
}

Таким образом, бизнес-логике больше не нужно знать о @Qualifier.

@Service
public class ChatTask {
    private final ChatModelProxy chatModelProxy;
    private final ImageChatModelProxy imageChatModelProxy;
    public ChatTask(
            ChatModelProxy chatModelProxy,
            ImageChatModelProxy imageChatModelProxy
    ) {
        this.chatModelProxy = chatModelProxy;
        this.imageChatModelProxy = imageChatModelProxy;
    }
}

Преимущества этого подхода очевидны.

Строка @Qualifier существует только внутри Proxy, и слой сервиса не должен знать конкретные имена бинов каждого модели. Кроме того, такие логики предварительной и последующей обработки, как загрузка изображений, кодирование в Base64 и разбор JSON, также могут быть инкапсулированы внутри Proxy.

Способ 3. Использование модели реестра, основанного на настройках

Если количество моделей увеличивается до трех и более, или если необходимо добавлять или заменять модели только с помощью настроек, можно рассмотреть вариант паттерна регистрации.

@Configuration
public class ModelRegistryConfig {
    @Bean
    public Map chatModelRegistry(ModelProperties props) {
        Map registry = new HashMap<>();
        props.getModels().forEach((name, config) -> {
            ChatModel model = OllamaChatModel.builder()
                    .baseUrl(config.getBaseUrl())
                    .modelName(config.getModelName())
                    .build();
            registry.put(name, model);
        });
        return registry;
    }
}

Настройки можно управлять следующим образом.

my-service:
  models:
    text:
      base-url: https://ollama.example.com
      model-name: text-model-name
      timeout: 10m
    image:
      base-url: https://ollama.example.com
      model-name: multi-modal-model-name
      timeout: 30m

Этот способ полезен, когда количество моделей велико или необходимо заменить модели на основе настройки.

Однако есть и недостатки. Если требуется разная предобработка для каждой модели, просто Map Недостаточно просто управлять этим.

Например, модели изображений нуждаются в загрузке изображений, изменении их размера, определении типа MIME и кодировании Base64. Эта логика в конечном итоге будет размещена в отдельном сервисе или прокси-слое за пределами реестра.

Таким образом, когда способ вызова модели прост и одинаков, реестр подходит, но если способ использования отличается для каждой модели, то метод Proxy более естественен.

Способ, выбранный в текущем проекте

В текущем проекте @Qualifier + Proxy класс способВыбран.

Элемент

Оценка

Диапазон отображения строки @Qualifier

Скрытие внутри класса Proxy

Отделение логики предобработки по моделям

Капсуляция логики изменения размера изображений и кодирования в ImageChatModelProxy

Дополнительные функции текстовой модели

Объединение парсинга JSON, парсинга списков и других функций в ChatModelProxy

Уровень связанности бизнес-кода

Сервис зависит только от типа Proxy

Роли двух моделей четко различаются.

Текстовая модель используется для общих разговоров, парсинга JSON-ответов и других задач. В то время как для модели изображений требуется отдельный предварительный процессинг для обработки входящих изображений.

Поэтому, вместо того чтобы насильно объединять две модели в один интерфейс, лучше разделить их на Proxy с отдельными обязанностями.

Конечно, если количество моделей увеличится до трех или более, или если возникнет необходимость выбирать модель во время выполнения, мы можем рассмотреть использование паттерна реестра.

В завершение

Управлять несколькими моделями ChatModel только с помощью автоматической настройки LangChain4J сложно.

Если необходимо использовать несколько моделей, вы можете определить @Bean вручную и различать их с помощью @Qualifier.

Использование прокси позволяет инкапсулировать логику пред- и постобработки для каждой модели, и бизнес-логике не нужно знать имена бинов моделей.

Если количество моделей увеличивается или важна замена на основе настроек, стоит рассмотреть паттерн реестра.

Лично я считаю, что в этой ситуации самым подходящим выбором является прокси-методЯ так думаю.

Модель текста и модель изображения — это не просто «две ChatModel», а они имеют разные способы использования и ответственность. Поэтому лучше иметь Proxy, который показывает ответственность каждой модели, а не сначала абстрагировать с помощью общего интерфейса или реестра, что будет проще с точки зрения обслуживания.

Тед

Site footer