💡 Pełny działający przykład dostępny na GitHubie:
document-to-markdown-rag-chunks-java
Problem biura roszczeń: Dlaczego pobieranie RAG nie działa na rzeczywistych plikach
Rurociąg pobierania RAG uruchamiany lokalnie to warstwa parsowania dokumentów, która zamienia pliki już posiadane przez zespół w przeszukiwalny, cytowalny tekst bez wysyłania ich do podmiotu trzeciego. Operacje ubezpieczeniowe pokazują, dlaczego to ważne. Obsługujący roszczenia pyta wewnętrznego asystenta: „co rzeczoznawca powiedział o szkodzie wodnej w tej polisie?”, a odpowiedź znajduje się w e‑mailu z marca ubiegłego roku, w załączonym raporcie PDF, pod nagłówkiem „Findings”. Asystent może ją znaleźć tylko wtedy, gdy pobieranie odczyta załącznik, zachowa nagłówek i zapamięta, z którego e‑maila pochodzi raport.
Większość zadań pobierania popełnia jeden z trzech błędów. Ekstraktor tekstu zwykłego zwraca treść e‑maila i ignoruje załącznik. Stały podział na 512 tokenów przecina raport w środku „Findings”. A hostowany parser rozwiązuje oba problemy, ale dopiero po tym, jak plik roszczenia, zawierający dane osobowe i medyczne, zostanie przesłany na serwery kogoś innego i rozliczany za stronę.
Co zespół naprawdę potrzebuje, to coś wąskiego i konkretnego: odczytywać pliki Word, PDF, EML i ZIP wewnątrz usługi Java, która już posiada dane, zachować ich strukturę jako Markdown, dzielić według nagłówków autora i dołączać źródło oraz rodzica do każdego fragmentu.
Migawka rynkowa: przewodniki z 2026 r. „parsowanie dokumentów dla RAG”, które przeanalizowaliśmy, porównują otwarte parsery Pythona z API w chmurze rozliczanymi za stronę. Biblioteka Java działająca na własnym sprzęcie prawie nie pojawia się w tej dyskusji.
Dlaczego potoki RAG potrzebują Markdowna zamiast zwykłego tekstu?
Markdown zachowuje części dokumentu, które mówią wyszukiwarce, gdzie kończy się jedna idea, a zaczyna kolejna. Nagłówki stają się liniami zaczynającymi się od #, tabele pozostają wierszami i kolumnami, a listy zachowują swoje pozycje. Dzielnik może rozdzielać na podstawie tych nagłówków, a model językowy czytający fragment widzi tę samą strukturę, co człowiek. Zwykły tekst usuwa tę strukturę, pozostawiając rozmiar jako jedyny sygnał podziału.
Dlaczego rozwiązania ogólne nie działają tutaj
Zespoły zazwyczaj próbują jednej z trzech rzeczy, zanim zbudują właściwą warstwę pobierania:
- Ekstrakcja zwykłego tekstu + podział na tokeny: szybka do skonfigurowania, ale fragmenty mieszają sekcje i tracą nagłówek, który można by zacytować.
- API parsowania w chmurze: dobra struktura, ale pliki opuszczają sieć i rachunek rośnie z każdą ponownie indeksowaną stroną.
- Własny parser dla każdego formatu: jedna biblioteka dla DOCX, inna dla PDF, parser MIME dla e‑maili i
java.util.zipdla archiwów, każdy z własnymi przypadkami brzegowymi.
GroupDocs.Parser dla Javy obsługuje wszystkie cztery formaty w jednej klasie Parser i działa w tym samym procesie.
Rozwiązanie: GroupDocs.Parser w przepływie pobierania roszczeń
GroupDocs.Parser dla Javy odczytuje dokument i może zwrócić jego tekst jako Markdown za pomocą getFormattedText. E‑maile i archiwa są udostępniane jako kontenery, których elementy otwierają się jako nowe instancje Parser, więc PDF załączony do e‑maila jest przetwarzany dokładnie tak samo, jak PDF znajdujący się na dysku. Przykładowy projekt opakowuje to w sześć metod statycznych i zapisuje wynik jako JSON Lines, które każdy proces embedowania lub magazyn wektorów może wczytać. Żaden framework RAG nie jest zaangażowany; format wyjściowy jest punktem integracji.
Aby podążać za przykładem, potrzebujesz:
- JDK 11 lub nowszy dla dołączonego
pom.xml(sam kod trzyma się funkcji Java 8) - GroupDocs.Parser dla Javy 26.10 (uzyskaj tymczasową licencję)
- Kilka plików DOCX, PDF, EML lub ZIP
Plik pom.xml projektu deklaruje repozytorium https://releases.groupdocs.com/java/repo/ oraz jedną zależność, więc budowanie to jedno polecenie:
mvn compile exec:java
Implementacja warstwy pobierania krok po kroku
Poniższe kroki przetwarzają jeden plik roszczenia od skrzynki odbiorczej do indeksu. Każdy krok odpowiada jednej lub dwóm metodom w RagChunksDemo.java. Zachowaliśmy je jako zwykłe metody statyczne celowo, abyś mógł podnieść dowolną z nich do istniejącego pracownika bez przyjmowania reszty.
Krok 1 – Otwórz każdy plik i skieruj go
Każdy przesłany plik otrzymuje własny Parser. Nazwa pliku staje się identyfikatorem źródła, a plik najwyższego poziomu nie ma rodzica.
// Jeden wywołanie na każdy przesłany dokument roszczenia
String sourceId = new File(path).getName();
try (Parser parser = new Parser(path)) {
return chunkParser(parser, sourceId, null, 0); // null parent: plik najwyższego poziomu
} catch (UnsupportedDocumentFormatException ex) {
// Nieznany format jest logowany i pomijany, partia kontynuuje działanie
System.out.println("skipped (unsupported format): " + sourceId);
return new ArrayList<>();
}
chunkParser następnie wyciąga treść, dzieli ją i wchodzi w załączniki, dopóki głębokość jest mniejsza niż 2.
Krok 2 – Poproś o Markdown, zaakceptuj zwykły tekst
Krok ekstrakcji sprawdza, co format obsługuje, zamiast sztywno kodować listę rozszerzeń.
if (parser.getFeatures().isFormattedText()) {
// Nagłówki, listy i tabele zachowują się jako Markdown
FormattedTextOptions options =
new FormattedTextOptions(FormattedTextMode.Markdown);
try (TextReader reader = parser.getFormattedText(options)) {
if (reader != null) {
return new ExtractedText("markdown", reader.readToEnd());
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
try (TextReader reader = parser.getText()) {
if (reader != null) { // awaryjny fallback dla formatów bez formatowanego tekstu
return new ExtractedText("plain", reader.readToEnd());
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return new ExtractedText("none", ""); // np. zdjęcie bez OCR
Dlaczego te ustawienia mają znaczenie dla danych roszczeń:
FormattedTextMode.Markdown: nagłówek rzeczoznawcy „Findings” trafia do dzielnika jako## Findings, więc może stać się punktem podziału i cytatem.isFormattedText(): w przykładowym uruchomieniu DOCX i e‑mail zwracają Markdown, a PDF przechodzi w zwykły tekst; poleformatzapisuje, co się stało.- Wynik
none: zdjęcia roszczeń wewnątrz ZIP nie generują fragmentów bez OCR, co jest uczciwe i łatwe do monitorowania.
Krok 3 – Dziel według nagłówków rzeczoznawcy
Dzielnik utrzymuje stos bieżącego nagłówka na każdym z sześciu poziomów Markdown. Nowy nagłówek zamyka bieżącą sekcję i resetuje wszystko poniżej swojego poziomu.
for (String line : markdown.split("\\r?\\n", -1)) {
Matcher heading = HEADING.matcher(line); // "#" do "######" + tytuł
if (heading.matches()) {
// Zamknij poprzednią sekcję pod starą ścieżką
addSection(chunks, section.toString(), sourceId, parentId, path, format);
section.setLength(0);
int level = heading.group(1).length();
stack[level - 1] = heading.group(2).trim();
for (int i = level; i < stack.length; i++) {
stack[i] = null; // zapomnij głębsze nagłówki
}
path = joinPath(stack); // "Heading 1 > Heading 1.2"
}
section.append(line).append('\n');
}
Sekcje powyżej 1500 znaków są przycinane przy podziałach akapitów, więc długa sekcja „Findings” staje się kilkoma fragmentami, które dzielą jedną ścieżkę nagłówka. W dołączonym pliku Word powstają ścieżki takie jak Heading 1 > Heading 1.2 > Heading 1.2.1.
Krok 4 – Przejdź do załączników i zapisz JSON Lines
To krok, na którym najbardziej zależy zespołowi roszczeń. getContainer() wymienia załączniki e‑maili i wpisy ZIP; każdy element otwiera się jako dokument potomny i dziedziczy identyfikator rodzica.
Iterable<ContainerItem> items = parser.getContainer();
if (items == null) {
return chunks; // nie jest e‑mailem ani archiwum
}
for (ContainerItem item : items) {
// Identyfikatory w stylu "claim-0412.eml/report.pdf" zachowują czytelną linię dziedziczenia
String childId = sourceId + "/" + item.getFilePath();
try (Parser child = item.openParser()) {
chunks.addAll(chunkParser(child, childId, sourceId, depth + 1));
} catch (UnsupportedDocumentFormatException ex) {
System.out.println("skipped (unsupported format): " + childId);
}
}
writeJsonl zapisuje potem jedną linię UTF‑8 na fragment z polami id, source, parent, heading_path, format i text. Dla czterech przykładowych wejść uruchomienie generuje 22 linie. Fragment z załącznika przykładowego e‑maila ma identyfikator email-with-attachment.eml/attachment.txt#0, a jego parent to email-with-attachment.eml. Ta para pozwala asystentowi powiedzieć „z raportu załączonego do e‑maila z 14 marca” zamiast „z jakiegoś miejsca w archiwum”.
Rzeczywisty przepływ: cotygodniowa reindeksacja roszczeń
Wyobraź sobie zespół operacji ubezpieczeniowych, który w każdy piątek eksportuje zamknięte skrzynki mailowe jako pliki EML oraz archiwa ZIP z dokumentami pomocniczymi. Pracownik Java już przenosi te eksporty do magazynu dokumentów. Dodanie pobierania oznacza wywołanie chunkDocument dla każdego pliku i dopisanie wyjścia JSON Lines do kolejki embedowania.
Krok 4 zapewnia, że raport rezerwowy pozostaje załączony do e‑maila, który go dostarczył. Krok 3 sprawia, że gdy asystent cytuje „Findings”, cytat zawiera nazwę pliku i nagłówek zamiast samego numeru fragmentu. Moja zasada przy takim indeksie: jeśli fragment nie może być przywołany z powrotem do pliku i nagłówka, nie powinien w ogóle znajdować się w indeksie.
Wpływ na biznes: Co się zmienia
| Aspekt | Typowa konfiguracja przed | Z pipeline GroupDocs.Parser |
|---|---|---|
| Gdzie pliki są parsowane | Endpoint zewnętrznego dostawcy | Wewnątrz własnej JVM |
| Model kosztów parsowania | Za stronę, przy każdym re‑indeksie | Brak opłat za stronę w chmurze |
| Granice fragmentów | Stałe okna tokenów | Nagłówki autora, limit 1500 znaków |
| Załączniki e‑mail | Często pomijane lub spłaszczane | Parsowane jako dokumenty potomne |
| Dane cytowania | Najlepiej nazwa pliku | source, parent i heading_path |
| Zależności w czasie uruchomienia | SDK + dostęp do sieci | Jeden artefakt Maven |
Efekt skumulowany to indeks, który zespół ds. zgodności może zatwierdzić, oraz asystent, którego odpowiedzi wskazują na rzeczywisty dokument i sekcję.
Inne scenariusze ubezpieczeniowe, w których GroupDocs.Parser się sprawdza
Gdy warstwa pobierania istnieje, ten sam kod obsługuje inne zadania intensywnie bazujące na dokumentach:
- Wyszukiwanie treści polisy: dokumenty Word podzielone według nagłówków klauzul, więc odpowiedzi cytują dokładną sekcję.
- Zgłoszenia brokerów: archiwa ZIP z mieszanymi PDF‑ami i arkuszami kalkulacyjnymi indeksowane pozycja po pozycji, każda powiązana ze zgłoszeniem.
- Obsługa skarg: e‑maile klientów i ich załączniki indeksowane razem, przy czym e‑mail jest rodzicem każdego fragmentu załącznika.
Rozpoczęcie pracy w środowisku roszczeń
Sklonuj przykład, uruchom go na kilku własnych wyeksportowanych e‑mailach i otwórz output/chunks.jsonl, aby sprawdzić ścieżki nagłówków i identyfikatory rodziców przed podłączeniem do zadania embedowania. Notatki o architekturze krok po kroku znajdują się w dokumentacji przypadku użycia tego potoku, a referencja wyjścia Markdown wymienia, które formatowanie tryb zachowuje.