Spring AI протык LangChain4j: адно RAG, разныя супакрытыя выборы
Два варыянты Java RAG, створаны на адной інструкцыі, паказалі, што разлік у колькасці ліній зменшыўся праз інтеграцыю з Spring, а некоректны адказ пра відпачынак выявіў стандартныя настройкі фрагментавання.
Той самы ўзроў 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.
Не выбірайце толькі на адной падставе колькасці ліній. Пасля стварэння обох варыянтаў корыстны фільтр стае такім: сколькі варыянтав пайплайну вы гатовы застаўіць у стандартных налаштаваннях фрэймворку? Пачніце з гэтага — прычаму ніхто не мусі выдаліць вясемдзесят пяць ліній і прызнаць сабе перамогу.