Strona główna / Artykuły

Przyjmowanie danych RAG z uwzględnieniem jakości: kierowanie dokumentami według stopnia czytelności

Deklaratywny przepływ docling-pipelines, który ocenia poziom języka, liczba duplikatów oraz wskaźnik czytelności Flescha, a następnie wysyła dokumenty do dwóch indeksów OpenSearch w celu wyszukiwania przez klientów i programistów.

Wydawca: REACTAPP.TOP8 min czytania1893 słów

Źródła: Architecting a Multi-Tier RAG Pipeline: Quality-Aware Document Routing with docling-pipelines

Przenoszenie bazy wiedzy obsługi klienta do wyszukiwarki opartej na AI zazwyczaj kończy się niepowodzeniem w tym samym miejscu: każdy plik jest traktowany tak samo. Realistyczny korpus to mieszanka różnych elementów – zawiera uporządkowane FAQ, ustrukturyzowane przewodniki produktowe, obszerne podręczniki dla programistów, powtarzające się teksty prawnicze oraz kilka uszkodzonych plików, w których w ogóle nie ma czytelnych słów.

Gdy wszystko to umieści się w jednym systemie RAG do rozmów, jakość wyszukiwania spada. Klient, który chce prostej instrukcji, otrzymuje fragmenty podręcznika inżynieryjnego. Wysyłanie bezwartościowych tekstów lub logów systemowych do drogiego modelu w celu stworzenia streszczeń lub tłumaczeń to również marnowanie pieniędzy.

Rozwiązaniem jest kierowanie dokumentów według ich jakości. Projekt dwuwarstwowy polega na przeanalizowaniu każdego pliku, usunięciu pustych stron, ocenie czytelności, a następnie podziale korpusu na odpowiednie grupy.

  1. Poziom 1, wyszukiwanie przez konsumentów. Bardzo łatwe do odczytania dokumenty otrzymują metadane strukturalne, takie jak liczba słów, aby umożliwić wyszukiwanie w formie rozmowy skierowane do klientów.
  2. Poziom 2, wyszukiwanie dla programistów i jako materiał referencyjny. Zawarte w dokumentach materiały techniczne trafiają do osobnego indeksu, aby umożliwić odpowiadanie na zaawansowane pytania techniczne.

docling-pipelines sprawia, że to staje się praktyczne. Jest to framework o otwartym kodzie źródłowym, licencjonowany na zasadach MIT i wspierany przez IBM, który buduje oraz uruchamia grafy przetwarzania dokumentów bezpośrednio na podstawie konfiguracji w formacie JSON. Nie jest potrzebny żaden specjalny kod w Pythonie, ponieważ operatory przekazują sobie dokumenty jako tabele Apache Arrow w pamięci, dzięki czemu pośrednie transformacje i sprawdzania routingu kosztują prawie nic pod względem zasobów CPU i pamięci.

Kształt pipeline’a

Wszystko znajduje się w jednym pliku JSON opisującym graf skierowany acykliczny:

  1. ingest odczytuje pliki PDF, TXT, DOCX i HTML z dysku.
  2. extract wydobywa tekst ustrukturyzowany za pomocą biblioteki Docling.
  3. detect_language identyfikuje język każdego dokumentu.
  4. dedup wykorzystuje hashe treści, aby usunąć duplikatowe wiersze.
  5. doc_quality oblicza metryki strukturalne, takie jak całkowita liczba słów.
  6. readability wyznacza wyniki mierzące złożoność tekstu.
  7. quality_branching kieruje proces na podstawie wyniku Flesch Reading Ease: wartości powyżej 60 trafiają na ścieżkę wysokiej jakości, a 60 lub mniej – na ścieżkę niskiej jakości.
  • Szlak wysokiej jakości uruchamia ml_enrichment_high_quality, embeddings_high_quality oraz vectordb_high_quality. Dodaje cechy językowe oparte na sztucznej inteligencji, tworzy wektory embeddingów i zapisuje je do indeksu high_quality_docs.
  • Szlak niskiej jakości uruchamia sql_filter_low_quality, embeddings_low_quality oraz vectordb_low_quality. Usuwa puste wiersze, embeduje to, co pozostało, i zapisuje wyniki do low_quality_docs.
  • Profilowanie przed podziałem

    Każdy dokument wymaga profilu, zanim zostanie skierowany dalej. Po tym, jak ingest odczyta dane, a extract przekształci strukturę w tekst, każdy kolejny operator działa jako transformator: sprawdza tabelę i dodaje nowe kolumny do tej samej tabeli Arrow w pamięci. Najpierw przeprowadzane są tanie operacje wykonywane wyłącznie na procesorze: identyfikacja języka, wykrywanie duplikatów oraz ocena złożoności tekstu.

    Identyfikacja języka

    Późniejsze modele ML oraz formuły mierzące czytelność wymagają informacji o języku, dlatego najpierw przeprowadzana jest jego identyfikacja.

    {
          "name": "detect_language",
          "type": "lang_detect",
          "config": {
            "doc_column": "content"
          },
          "depends_on": [
            "extract"
          ]
        }
    

    Operator czyta kolumnę content i dodaje lang_name (kod ISO 639-1) oraz lang_score. Umieszczenie ich tuż po ekstrakcji oznacza, że każdy krok wymagający uwzględnienia języka może korzystać z ich wyników.

    Deduplikacja

    Dokumenty duplikowane marnują zasoby obliczeniowe potrzebne do embedowania i zakłócają wyniki wyszukiwania.

    {
          "name": "dedup",
          "type": "ededup",
          "config": {
            "doc_column": "content",
            "doc_id_column": "doc_id_hash"
          },
          "depends_on": [
            "detect_language"
          ]
        }
    

    Odnosi się do hasza zawartości przechowywanego w doc_id_hash oraz do surowego tekstu w content, a następnie usuwa powtarzające się wiersze z tabeli. Robienie tego wcześnie pozwala uniknąć niepotrzebnej pracy na późniejszych etapach.

    Jakość dokumentu

    Bazowe sprawdzenia strukturalne wykrywają puste strony oraz nietypową kompozycję.

    {
          "name": "doc_quality",
          "type": "doc_quality",
          "config": {
            "doc_column": "content",
            "doc_content_column": "content",
            "text_lang": "en"
          },
          "depends_on": [
            "dedup"
          ]
        }
    

    To dodaje 11 kolumn strukturalnych, wszystkie z prefiksem docq_. Najważniejsza z nich to docq_total_words, która przedstawia łączną liczbę słów w każdym dokumencie.

    Czytelność

    Wzory obliczające czytelność oceniają trudność na podstawie długości słów, sylab oraz struktury zdań.

    {
          "name": "readability",
          "type": "readability",
          "config": {
            "doc_column": "content",
            "readability_score_list": [
              "flesch_reading_ease",
              "flesch_kincaid_grade",
              "gunning_fog",
              "difficult_words",
              "dale_chall_readability_score"
            ]
          },
          "depends_on": [
            "doc_quality"
          ]
        }
    

    Tablica readability_score_list wskazuje operatorowi, jakie metryki należy obliczyć i dodać jako kolumny. Ustalanie tras polega na ocenie Flesch Reading Ease, która wynosi od 0 do 100:

    • Ponad 60 oznacza prosty, dostępny język odpowiedni dla ogólnej publiczności centrum pomocy.
    • 60 lub mniej oznacza złożoną, techniczną prozę wymagającą wiedzy specjalistycznej.

    Krok rozgałęziania

    Operator rozgałęziania dzieli zbiór danych Arrow na oddzielne ścieżki przetwarzania.

    {
          "name": "quality_branching",
          "type": "branching",
          "config": {
            "branches": {
              "high_quality_branch": {
                "link_name": "High Quality Documents",
                "criteria_json": {
                  "criteria_list": [
                    {
                      "variable": "flesch_reading_ease",
                      "operator": ">",
                      "value": 60
                    }
                  ],
                  "logical_operator": "AND"
                }
              },
              "low_quality_branch": {
                "link_name": "Low Quality Documents",
                "criteria_json": {
                  "criteria_list": [
                    {
                      "variable": "flesch_reading_ease",
                      "operator": "<=",
                      "value": 60
                    }
                  ],
                  "logical_operator": "AND"
                }
              }
            }
          },
          "depends_on": [
            "readability"
          ]
        }
    

    Ukierunkowanie jest kontrolowane przez deklaratywny blok criteria_json zawierający cztery pola:

    • variable określa kolumnę do sprawdzenia, na przykład flesch_reading_ease.
    • operator wskazuje operację porównania, taką jak > lub <=.
    • value to próg wartości, w tym przypadku 60.
    • logical_operator łączy kilka warunków za pomocą AND lub OR.

    Gdy regułą jest flesch_reading_ease > 60, silnik ocenia warunki bezpośrednio, dzieli tabelę w pamięci i uruchamia każdą z następnych ścieżek.

    Szeroka jakość: wzbogacanie, włączanie, indeksowanie

    Dokumenty przekazywane do ścieżki high_quality_branch najpierw przechodzą proces wzbogacania za pomocą algorytmów ML językowych.

    {
          "name": "ml_enrichment_high_quality",
          "type": "ml_enrichment",
          "config": {
            "doc_column": "content",
            "lang_column": "lang_name",
            "output_column_prefix": "ml_",
            "error_column_name": "enrichment_error"
          },
          "depends_on": [
            "quality_branching.high_quality_branch"
          ]
        }
    

    ml_enrichment analizuje tekst za pomocą modeli rozumiejących język i tworzy metadane takie jak liczba zdań, właściwości sylab oraz listy rzeczowników, wszystko to w celu lepszego wyszukiwania. Zależy od kolumny lang_name z etapu wykrywania. Ponieważ jest to część procesu wymagająca dużych zasobów CPU, działa tylko na tekstach czytelnych. Ustawienie output_column_prefix dodaje przedrostek ml_ do nowych kolumn, w tym do ml_num_words.

    Następnie tekst przekształca się w wektory o wymiarze 2560. Modelem jest openai/granite4:latest, dostarczany przez lokalny serwer Ollama za pośrednictwem LiteLLM:

    {
          "name": "embeddings_high_quality",
          "type": "embeddings",
          "config": {
            "embeddings_column": "embeddings",
            "overlap_ratio": 0.2,
            "doc_column": "content",
            "provider_config": {
              "api_base": "http://localhost:11434/v1",
              "api_key": "${OLLAMA_API_KEY}",
              "model_id": "openai/granite4:latest"
            },
            "provider": "litellm"
          },
          "depends_on": [
            "ml_enrichment_high_quality"
          ]
        }
    

    Ostatnią krokiem jest indeksowanie dokumentów w OpenSearch, przy czym feature_mappings łączą kolumny Arrow z polami OpenSearch:

    {
          "name": "vectordb_high_quality",
          "type": "vectordb",
          "config": {
            "provider": "opensearch",
            "doc_id_column": "doc_id_hash",
            "embeddings_column": "embeddings",
            "vector_dimension": 2560,
            "create_index": "true",
            "provider_config": {
              "index_name": "high_quality_docs",
              "host": "localhost",
              "port": 9200,
              "username": "${OPENSEARCH_USERNAME}",
              "password": "${OPENSEARCH_PASSWORD}",
              "use_ssl": false,
              "verify_certs": false,
              "batch_size": 100,
              "engine": "faiss",
              "algorithm": "hnsw",
              "space_type": "l2"
            },
            "feature_mappings": [
              {"feature_name": "doc_id_hash", "mapped_column_name": "pk"},
              {"feature_name": "name", "mapped_column_name": "doc_name"},
              {"feature_name": "content", "mapped_column_name": "text"},
              {"feature_name": "embeddings", "mapped_column_name": "vector_embeddings"},
              {"feature_name": "lang_name", "mapped_column_name": "language"},
              {"feature_name": "flesch_reading_ease", "mapped_column_name": "readability_score"},
              {"feature_name": "ml_num_words", "mapped_column_name": "word_count"}
            ]
          },
          "depends_on": [
            "embeddings_high_quality"
          ]
        }
    

    Szlak niskiej jakości: filtr, wgranie, indeksowanie

    Dokumenty znajdujące się na low_quality_branch omijają kosztowny krok ml_enrichment. Niektóre z nich po ekstrakcji mogą nie zawierać żadnych czytelnych słów, na przykład uszkodzone pliki lub puste skany, a wgranie pustej strony tylko dodaje szum do przestrzeni wektorowej. Filtr eliminuje je:

    {
          "name": "sql_filter_low_quality",
          "type": "sql_filter",
          "config": {
            "criteria_json": {
              "criteria_list": [
                {
                  "variable": "docq_total_words",
                  "operator": ">",
                  "value": 0
                }
              ],
              "logical_operator": "AND"
            }
          },
          "depends_on": [
            "quality_branching.low_quality_branch"
          ]
        }
    

    Filtr sprawdza kolumnę docq_total_words z tabeli doc_quality i odrzuca wszystkie dokumenty zawierające zero słów.

    Dokumenty, które przetrwają, głównie materiały techniczne, są włączane do tego samego modelu granite4 i zapisywane w oddzielnym indeksie OpenSearch:

    {
          "name": "vectordb_low_quality",
          "type": "vectordb",
          "config": {
            "provider": "opensearch",
            "doc_id_column": "doc_id_hash",
            "embeddings_column": "embeddings",
            "vector_dimension": 2560,
            "create_index": "true",
            "provider_config": {
              "index_name": "low_quality_docs",
              "host": "localhost",
              "port": 9200,
              "username": "${OPENSEARCH_USERNAME}",
              "password": "${OPENSEARCH_PASSWORD}",
              "use_ssl": false,
              "verify_certs": false,
              "batch_size": 100,
              "engine": "faiss",
              "algorithm": "hnsw",
              "space_type": "l2"
            },
            "feature_mappings": [
              {"feature_name": "doc_id_hash", "mapped_column_name": "pk"},
              {"feature_name": "name", "mapped_column_name": "doc_name"},
              {"feature_name": "content", "mapped_column_name": "text"},
              {"feature_name": "embeddings", "mapped_column_name": "vector_embeddings"},
              {"feature_name": "lang_name", "mapped_column_name": "language"},
              {"feature_name": "flesch_reading_ease", "mapped_column_name": "readability_score"},
              {"feature_name": "difficult_words", "mapped_column_name": "difficult_words"}
            ]
          },
          "depends_on": [
            "embeddings_low_quality"
          ]
        }
    

    Ponieważ ta gałąź pominęła proces wzbogacania, dla tych dokumentów nie istnieje word_count. Zamiast tego wykorzystywana jest obliczona liczba difficult_words, co pomaga przy zapytaniach technicznych.

    Dwa indeksy, dwa style zapytań

    Odrębne indeksy dla poszczególnych poziomów czytelności umożliwiają różne sposoby wyszukiwania przez odbiorców.

    Wyszukiwanie w centrum pomocy

    Dla użytkowników końcowych zapytanie trafia do high_quality_docs. Zawiera ono jedynie łatwe do przeczytania poradniki (readability_score > 60) o rzeczywistej treści (word_count > 100):

    curl -XGET "http://localhost:9200/high_quality_docs/_search" \
      -H 'Content-Type: application/json' -d'
    {
      "query": {
        "bool": {
          "must": [
            {
              "knn": {
                "vector_embeddings": {
                  "vector": [0.015, -0.023, 0.004],
                  "k": 10
                }
              }
            }
          ],
          "filter": [
            {
              "range": {
                "readability_score": {
                  "gt": 60
                }
              }
            },
            {
              "range": {
                "word_count": {
                  "gt": 100
                }
              }
            }
          ]
        }
      }
    }'
    

    Wyszukiwanie referencji dla programistów

    Dla zaawansowanych użytkowników szukających specyfikacji zapytanie skierowane jest na low_quality_docs. Analizuje ono złożone materiały i zachowuje fragmenty zawierające wiele trudnych terminów technicznych (difficult_words > 5):

    curl -XGET "http://localhost:9200/low_quality_docs/_search" \
      -H 'Content-Type: application/json' -d'
    {
      "query": {
        "bool": {
          "must": [
            {
              "knn": {
                "vector_embeddings": {
                  "vector": [0.015, -0.023, 0.004],
                  "k": 10
                }
              }
            }
          ],
          "filter": [
            {
              "range": {
                "difficult_words": {
                  "gt": 5
                }
              }
            }
          ]
        }
      }
    }'
    

    Rozruch

    W repozytorium znajduje się skrypt konfiguracyjny, który tworzy środowisko wirtualne, instaluje zależności, uruchamia lokalne kontenery OpenSearch oraz rejestruje modele Ollamy.

    Krok 1: konfiguracja środowiska

    Klonuj repozytorium i uruchom skrypt:

    git clone https://github.com/IBM/docling-pipelines.git
    cd docling-pipelines
    ./scripts/setup_docling_pipelines_environment.sh
    

    Lub zainstaluj bezpośrednio z PyPI:

    pip install docling-pipelines
    

    Krok 2: ustawienie zmiennych środowiskowych

    export OLLAMA_API_KEY=ollama
    export OPENSEARCH_USERNAME=admin
    export OPENSEARCH_PASSWORD=<your-password>
    

    Krok 3: uruchomienie procesu

    Zainicjuj pipeline z linii poleceń:

    docling-pipelines --flow-file sample_flows/advanced/branching_quality_routing.json
    

    Co wyświetla wykonywanie

    Wynik odzwierciedla przepływ, przy czym każda transformacja oraz decyzja o kierowaniu są rejestrowane:

    $ docling-pipelines --flow-file sample_flows/advanced/branching_quality_routing.json
    [INFO] [ingest] Loading files from tests/fixtures/customer_support_docs/
    [INFO] [ingest] Ingested 12 files.
    [INFO] [extract] Running extract_operator using docling_library...
    [INFO] [extract] Extracted text from 12 documents.
    [INFO] [detect_language] Running lang_detect...
    [INFO] [detect_language] Complete. lang_name assigned.
    [INFO] [dedup] Running ededup...
    [INFO] [dedup] Removed 2 duplicate documents. 10 documents remaining.
    [INFO] [doc_quality] Running doc_quality...
    [INFO] [doc_quality] Computed docq_* features for 10 documents.
    [INFO] [readability] Running readability...
    [INFO] [readability] Computed readability features for 10 documents.
    [INFO] [quality_branching] Branching 10 documents...
    [INFO] [quality_branching] Routed 6 documents to link 'High Quality Documents' (flesch_reading_ease > 60).
    [INFO] [quality_branching] Routed 4 documents to link 'Low Quality Documents' (flesch_reading_ease <= 60).
    [INFO] [ml_enrichment_high_quality] Running ml_enrichment on 6 documents...
    [INFO] [embeddings_high_quality] Generating embeddings with model 'openai/granite4:latest'...
    [INFO] [vectordb_high_quality] Uploaded 6 documents to OpenSearch index 'high_quality_docs'.
    [INFO] [sql_filter_low_quality] Running sql_filter on 4 documents...
    [INFO] [sql_filter_low_quality] Filtered out 0 empty documents. 4 documents remaining.
    [INFO] [embeddings_low_quality] Generating embeddings with model 'openai/granite4:latest'...
    [INFO] [vectordb_low_quality] Uploaded 4 documents to OpenSearch index 'low_quality_docs'.
    [INFO] Flow execution finished successfully in 4.23s.
    

    Przeniesienie metryk jakości i reguł routingu z niestandardowego kodu Python na deklaratywną konfigurację sprawia, że proces integracji jest szybki i łatwy w utrzymaniu. Koszt przetwarzania zależy od wartości dokumentu, a aplikacja RAG otrzymuje czyste, dostosowane indeksy dla każdej grupy odbiorców.

    Koncepcja rozgałęziania działa z dowolną metryką obliczoną wcześniej w procesie. Można zmienić kryteria routingu na język, wynik oceny ryzyka PII lub niestandardową etykietę klasyfikatora. Ponieważ logika pipeline jest oddzielona od kodu orkiestracji, zespoły mogą rozwijać pipeline bez zwiększania obciążenia związanego z jego utrzymaniem.

    Literatura pokrewna