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

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

Дослідження Twin Java RAG, засноване на одному посібнику, показало, що розбіжності у кількості рядків зменшуються при інтеграції з Spring, а неправильна відповідь щодо відпусток виявила стандартні налаштування часткового оброблення даних.

736 слів

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