Головна / Статті

Spring AI проти LangChain4j: один і той самий RAG, різні приховані варіанти вибору

Та сама додаток Java RAG, створена двічі: Spring AI проти LangChain4j — різниця у кількості рядків зменшується завдяки налаштуванням Spring, а неправильна відповідь виявила стандартні налаштування чанкінгу.

Видавець: REACTAPP.TOP4 хв читання811 слів

Джерела: Spring AI Vs LangChain4j: Same RAG Pipeline, One Was 3× Less Code

Ту саму схему RAG було створено двічі. Перші результати здавалися вирішальними. Одне експериментування змінило висновок.

Видалення 85 рядків коду на Java з сервісу RAG не змінило нічого того, що могли б побачити користувачі — і поклало край двотижневим суперечкам команди щодо фреймворків.

Шанувальники Spring AI та LangChain4j принесли по своїх слайд-презентаціях. Але ніхто не приніс дубльованої реалізації. Обидві версії були написані за ідентичних умов.

Умови залишилися незмінними: один PDF з 240 сторінками про HR, одна модель ембеддингу, 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, коли важливі чіткі елементи конвеєра та налаштовування розділення/отримання даних.
  • Не обирайте лише за кількістю рядків. Після створення обох варіантів корисним критерієм стає: скільки варіантів конвеєра ви готові залишити у стандартних налаштуваннях фреймворку? Почніть з цього — перш ніж хтось видалить вісімдесят п’ять рядків та проголосить перемогу.