Галоўная / Артыкулы / Spring AI протык LangChain4j: адно RAG, разныя супакрытыя выборы

Spring AI протык LangChain4j: адно RAG, разныя супакрытыя выборы

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

736 слоў

Той самы ўзроў RAG быў створаны два разы. Першыя рэзультаты здаваліся важлівымі. Адна з эксперыментаў змяніла выводы.

Выдаленне 85 ліній коду на Java з сервіса RAG не паўлічыла нічога, што маглі б побачыць корыстувальнікі — і завершыла двахтыдзяневыя суперчанкі каманды пра фрэймворкі.

Прыхальнікі Spring AI і LangChain4j прынеслі по сваіх слайд-дэках. Але ніхто не прынёс двойнай версіі рэалізацыі. Обе версіі былі напісаны пад аднаковымі умовамі.

Умовы засталіся незменнымі: адна 240-сторонняя PDF-файл з інформацыяй пра кадры, адны модель для імбеддынгу, 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

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