Введение
Когда вы внедряете функции ИИ в сервис, вы быстро сталкиваетесь с такой ситуацией. Пользователь исправление. Да, я исправил.
Редактировать администратора, подтвердить текст администратора
Я хочу использовать разные 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 непосредственно в бизнес-логику, а обернуть в Proxy класс для каждой модели.
В текущем проекте используется этот подход.
@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 за пределами реестра.
Таким образом, когда способ вызова модели прост и одинаков, реестр подходит, но если способ использования различен для каждой модели, то способ Proxy более естественен.
Выбранный способ в текущем проекте
В текущем проекте Способ @Qualifier + Proxy классавыбрал.
|
элемент |
Оценка |
|
Диапазон видимости строки @Qualifier |
Изоляция внутри класса Proxy |
|
Отделение логики предварительной обработки по модели |
Инкапсуляция логики изменения размера изображений и кодирования в ImageChatModelProxy |
|
Дополнительные функции текстовой модели |
Объединение JSON парсинга, парсинга списка и других в ChatModelProxy |
|
Связность бизнес-кода |
Сервис зависит только от типа Proxy |
Роли двух моделей явно различаются.
Текстовая модель используется для выполнения таких задач, как общий разговор, парсинг JSON-ответов и т.д. В то время как изображенческая модель требует отдельного предварительного процесса обработки для обработки входных изображений.
Поэтому имеет больше смысла разделить две модели на отдельные прокси с собственными обязанностями, а не пытаться объединить их в один интерфейс.
Конечно, если количество моделей увеличится до трех или более, или появится необходимость выбирать модели во время выполнения, можно рассмотреть паттерн реестра.
Завершая
-
С помощью автоматической настройки LangChain4J трудно тщательно управлять несколькими ChatModel.
-
Если требуется использовать несколько моделей, их можно определить самостоятельно с помощью @Bean и различать с помощью @Qualifier.
-
Используя прокси, можно инкапсулировать логику предобработки и постобработки для каждой модели, и бизнес-логика может не знать имена бинов моделей.
-
Если моделей становится много или важно заменять их на основе настроек, стоит рассмотреть паттерн реестра.
Лично в этой ситуации Наиболее подходящий выбор - это прокси-методСчитается, что.
Текстовая модель и модель изображения - это не просто два «ChatModel», но также отличаются по способу использования и ответственности. Следовательно, лучше иметь Proxy, который четко демонстрирует ответственность каждой модели, чем сначала абстрагировать с помощью общего интерфейса или реестра с точки зрения удобства обслуживания.
Тед