Главная / Статьи / Spring AI против LangChain4j: один и тот же RAG, разные скрытые параметры выбора

Spring AI против LangChain4j: один и тот же RAG, разные скрытые параметры выбора

Два варианта реализации Java RAG, описанных в одном руководстве, показали, что различия в количестве строк сокращаются при интеграции с Spring, а неверный ответ относительно отпусков выявил стандартные настройки фрагментации.

736 слов

Тот же самый пайплайн RAG был создан дважды. Первые результаты казались однозначными. Одно экспериментальное изменение изменило выводы.

Удаление 85 строк кода на Java из сервиса RAG не изменило того, что могли увидеть пользователи, — и положило конец двухнедельным спорам команды о выборе фреймворков.

Поклонники Spring AI и LangChain4j принесли по своим слайд-декам. Никто не принес дублирующей реализации. Обе версии были написаны при идентичных условиях.

Условия оставались неизменными: один PDF-документ объемом 240 страниц, одна модель эмбеддингов, база данных Postgres с расширением pgvector, одна модель чата и набор из 200 вопросов, заданных еще до создания любой из версий кода.

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

Одинаковая структура пайплайна

Ничего необычного в процессе обработки данных:

Question
   |
   v
Embed Query
   |
   v
Vector Search
   |
   v
Top 4 Chunks
   |
   v
Build Context
   |
   v
Chat Model
   |
   v
Answer

Процесс ввода данных также был специально сделан скучным:

PDF
 ↓
Extract Text
 ↓
Split Into Chunks
 ↓
Generate Embeddings
 ↓
Store In pgvector

Речь шла о сравнении фреймворков, а не о новой архитектуре, которая могла бы исказить результаты.

Версия Spring AI

Конфигурация оставалась минимальной:

spring:
  ai:
    openai.api-key: ${OPENAI_KEY}
    vectorstore.pgvector:
      initialize-schema: true

Обработка данных как компонент Spring:

@Component
class Ingest {
  Ingest(VectorStore store) {
    var docs =
        new TikaDocumentReader(
            "classpath:/docs/handbook.pdf").get();

store.add(new TokenTextSplitter().apply(docs));
  }
}

Интерфейс HTTP для вопросов:

@RestController
class AskApi {
  private final ChatClient ai;

AskApi(ChatClient.Builder b, VectorStore store) {
    this.ai = b.defaultAdvisors(
        new QuestionAnswerAdvisor(store)).build();
  }
  @GetMapping("/ask")
  String ask(@RequestParam String q) {
    return ai.prompt().user(q).call().content();
  }
}

QuestionAnswerAdvisor скрывал процессы поиска информации, сбора контекста и вставки промптов. Для реализации функции поиска практически не требовался специальный код — что казалось преимуществом, пока всё не изменилось позже.

Версия LangChain4j

При прямом построении архитектуры сразу становилось видно больше компонентов:

var embed =
    OpenAiEmbeddingModel.builder()
        .apiKey(key)
        .build();

var store =
    PgVectorEmbeddingStore.builder()
        .host("localhost")
        .port(5432)
        .database("rag")
        .user("app")
        .password(pw)
        .table("chunks")
        .dimension(1536)
        .build();
EmbeddingStoreIngestor.builder()
    .documentSplitter(
        DocumentSplitters.recursive(500, 60))
    .embeddingModel(embed)
    .embeddingStore(store)
    .build()
    .ingest(
        FileSystemDocumentLoader.loadDocument(path));

Создание механизма поиска также было полностью открытым:

var retriever =
    EmbeddingStoreContentRetriever.builder()
        .embeddingStore(store)
        .embeddingModel(embed)
        .maxResults(4)
        .minScore(0.6)
        .build();

Читаемо — да. Но компактнее ли, чем у Spring AI? Сначала казалось, что нет. Объем кода на Java в приложении составил примерно 41 против 126 — примерно в три раза больше у LangChain4j. Похоже, вопрос о компактности был решен в пользу последнего варианта.

Затем система ответила на вопрос о отпуске по уходу за ребенком.

Ответ, которого не было в руководстве

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

Employees are entitled to 12 days of paternity leave,
subject to the conditions listed in the policy.

В руководстве указано 15 дней. Число 12 нигде там не упоминается. Анализ контекста показал истинную причину: таблицы из руководства были раздроблены стандартным инструментом обработки. Связанные строки распределились по отдельным фрагментам; система восстановила отдельные правдоподобные части, а модель соединила их в некорректный, но связный ответ.

Эта ошибка существовала до появления модели для чатов. Количество строк в фреймворке не позволяло оценить качество ответа.

Разница в 3 раза на самом деле была не из-за фреймворка

Самая важная строка:

new TokenTextSplitter().apply(docs)

Одно только обращение к системе приводило к принятию решений, которые не проходили проверку: обработка таблиц, границы блоков данных, перекрытия, сохраняющаяся метаданные и то, что фактически видит система при поиске информации. Spring AI делал игнорирование этих решений легким. LangChain4j делал большее количество из них видимыми в коде приложения.

При первом сравнении Spring AI, интегрированный в Spring Boot, отличался от более ручной настройки LangChain4j. Пересоздание LangChain4j с его интеграцией в Spring сократило количество строк кода приложения до 58. Разница в сложности сократилась с примерно 3× до примерно 1,4×. Сложность переместилась в саму фреймворковую структуру, а не исчезла из всей системы.

Что выбрать на практике

  • Существующие сервисы Spring Boot → Spring AI для соблюдения стандартов и упрощения работы с обычными решениями типа RAG.
  • Отдельное приложение на Java или сложные пользовательские решения по получению данных → LangChain4j, когда важны четко определенные компоненты пайплайна и возможность настройки размера фрагментов/процесса поиска.
  • Не выбирайте решение исключительно на основе количества строк. После создания обоих вариантов полезным критерием становится: сколько вариантов пайплайна вы готовы оставить в стандартных настройках фреймворка? Начните с этого — прежде чем кто-то удаст восемьдесят пять строк и объявит о победе.