Strona główna / Artykuły

Rozumowanie w oparciu o drzewo dokumentów zamiast wyszukiwania wektorów

PageIndex tworzy hierarchiczną indeksację, umożliwiając modelom językowym nawigację po niej bez konieczności dzielenia tekstu na fragmenty ani używania embeddingów. Jak to działa, wyniki w testach FinanceBench oraz kiedy należy go stosować.

Wydawca: REACTAPP.TOP10 min czytania2394 słów

Źródła: I Threw Out My Vector Database. RAG Got Way Better With PageIndex

Niewiele jest rzeczy tak krępujących jak model, który z pewnością siebie cytuje niewłaściwy fragment 200-stronicowego rocznego raportu w trakcie transmisji na żywo. Następuje typowe dalsze rozwoju wydarzeń: zespół „naprawia” problem poprzez lepsze podziałanie danych, inteligentniejsze pokrywanie się informacji oraz dokładniej dostrojony model embeddingowy – to standardowy cykl ulepszeń, przez który przechodzi każdy inżynier RAG, przekonany, że kolejna modyfikacja w końcu zadziała. Następnie system odpowiada na pytanie dotyczące prognoz przychodów na rok 2023 akapitem z planu wydatków kapitałowych z 2021 roku. Obie sekcje używały podobnego języka finansowego, więc modele embeddingowe uznały je za powiązane. W rzeczywistości tak nie było.

Tego typu błędy rodzą uzasadnione pytanie, czy całe to podejście jest wadliwe od samego początku.

Dlaczego podobieństwo nie wystarcza

Każdy tutorial dotyczący RAG uczy tej samej metody. Dokument dzieli się na fragmenty po 500 słów, które następnie są wstawiane, znajduje się najbliższe wektory do każdego pytania i te fragmenty przekazuje się modelowi językowemu. Brzmi to elegancko – połączenie wyszukiwania semantycznego z generowaniem języka.

Metoda ta pomija fakt, że podobieństwo nie równa się istotności. Baza danych wektorowych znajduje teksty semantycznie bliskie pytaniu, a nie te, które na nie odpowiadają. Akapit o czynnikach ryzyka danego leku oraz akapit o jego skuteczności znajdują się blisko siebie w przestrzeni wektorowej, jednak tylko jeden z nich dotyczy pytania o ryzyka finansowe firmy.

Podział na fragmenty stwarza drugi problem. Gdy dokument prawniczy liczący 200 stron dzieli się na okna po 500 tokenów, wszystko, co autor starannie stworzył, znika: sekcje, odniesienia, logiczny tok. Pozostaje tylko masa fragmentów bez żadnego pojęcia o tym, skąd pochodzą i jak są ze sobą powiązane. Lepszy podział nieco to łagodzi, ale nigdy nie rozwiązuje podstawowego problemu, ponieważ struktura, która została odrzucona, nie może zostać przywrócona.

Człowiek-analizator nigdy by tak nie czytał. Przejrzałby spis treści, przejdzie do odpowiedniej sekcji, przeczyta ją, znając kontekst rozdziału wokół niej, i sprawdzi przypisy. Porusza się po dokumencie jako po dokumencie.

Drzewo zamiast wektorów

Zespół VectifyAI zadał sobie pytanie, jak wyglądałby system RAG, gdyby funkcjonował w ten sposób, i stworzył PageIndex. Gdy tylko idea ta zostanie wypowiedziana na głos, wydaje się niemal niezręcznie oczywista – nie trzeba przekształcać dokumentów w wektory ani wyszukiwać według podobieństwa. Zamiast tego należy stworzyć hierarchiczną drzewiastą strukturę indeksu dokumentu, co w praktyce stanowi maszynowo czytelny spis treści, i pozwolić modelowi LLM poruszać się po tym drzewie w kierunku odpowiednich informacji.

To oznacza brak bazy danych wektorowych, brak dzielenia dokumentów na fragmenty oraz brak wyników porównywania podobieństwa – istnieje tylko ustrukturyzowany indeks oraz inteligentna nawigacja.

Pierwszy krok: tworzenie indeksu

PageIndex nie dzieli danego mu dokumentu na fragmenty. Analizuje jego naturalną strukturę, czyli sekcje, podsekcje, nagłówki oraz logiczne grupy, tworząc hierarchię przypominającą sposób, w jaki ekspert zorganizowałby treść. Wyobraź sobie automatycznie wygenerowaną spis treści, tylko bogatszą. Każdy węzeł zawiera coś więcej niż tylko tytuł: streszczenie tematu, który obejmuje, pytania, na które może wiarygodnie odpowiedzieć, oraz informacje o jego relacji z węzłami rodzeństwa i nadrzędnymi.

100-stronicowa specyfikacja techniczna może dać początek drzewu o głębokości trzech lub czterech poziomów, gdzie na szczycie znajdują się główne obszary funkcjonalne, a w węzłach podrzędnych – bardziej szczegółowe tematy. Logiczna hierarchia dokumentu pozostaje taka sama, jaką ustalił jego autor. Proces ten jest wykonywany raz na dokument, na początku, a indeks jest przechowywany, przy czym oryginalna treść pozostaje nienaruszona i dostępna.

Drugi krok: analiza drzewa

Gdy przychodzi zapytanie, PageIndex nie włącza go do swojej struktury ani nie przeprowadza wyszukiwania najbliższego sąsiada. Zamiast tego LLM przemieszcza się po drzewie indeksu. Czyta węzły najwyższego poziomu i ocenia, który gałąź najprawdopodobniej zawiera odpowiedź, następnie idzie tą gałęzią, czyta kolejny poziom i ponownie analizuje sytuację. Kontynuuje takie schodzenie w dół, aż znajdzie odpowiednie sekcje.

To właśnie ta część zmienia zasady funkcjonowania. Następnie pobiera te kompletne sekcje z oryginalnego dokumentu, bez żadnych fragmentacji, i wykorzystuje je jako kontekst dla odpowiedzi.

Proces wydobywania informacji jest zaprojektowany tak, by był wyjaśnialny. Można dokładnie zobaczyć, które węzły przeszło LLM oraz na jakich stronach oryginału się zatrzymało. Nie ma żadnego tajemniczego wskaźnika kosinusowego, a łańcuch rozumowania jest widoczny.

Próba działania

Konfiguracja jest prosta. Zacznij od sklonowania repozytorium i zainstalowania jego zależności:

git clone https://github.com/VectifyAI/PageIndex.git
cd PageIndex
pip3 install --upgrade -r requirements.txt

Następnie utwórz plik .env w katalogu głównym, który będzie zawierał klucz API:

CHATGPT_API_KEY=your_openai_api_key_here

Aby skatalogować plik PDF, skieruj na niego skrypt. Dobrym przykładem jest raport finansowy, ponieważ to właśnie w takich przypadkach narzędzie sprawdza się najlepiej:

python3 run_pageindex.py --pdf_path /path/to/annual_report.pdf

Dokumenty Markdown używają innej flagi:

python3 run_pageindex.py --md_path /path/to/technical_spec.md

To zakończa konfigurację. Nie ma bazy danych wektorowych do ustawienia, żadnego modelu embeddingu do wyboru, ani parametrów dzielenia na fragmenty, nad którymi trzeba się zastanawiać.

Przetwarzanie dokumentu tworzy strukturę indeksu w przybliżeniu w taki sposób, tutaj uproszczoną:

{
  "document": "Apple Inc. Annual Report 2023",
  "index": {
    "title": "Apple Inc. Annual Report 2023",
    "summary": "Comprehensive financial and operational report covering revenue, product segments, risks, and strategic outlook",
    "children": [
      {
        "title": "Business Overview",
        "summary": "Company description, product lines, and market position",
        "pages": [1, 8],
        "children": [...]
      },
      {
        "title": "Financial Results",
        "summary": "Revenue, operating income, EPS, and segment performance for fiscal 2023",
        "pages": [45, 72],
        "children": [
          {
            "title": "Revenue by Product Category",
            "summary": "iPhone, Mac, iPad, Wearables, and Services revenue breakdown",
            "pages": [46, 52]
          },
          {
            "title": "Geographic Revenue Distribution",
            "summary": "Americas, Europe, Greater China, Japan, Rest of Asia Pacific",
            "pages": [53, 58]
          }
        ]
      },
      {
        "title": "Risk Factors",
        "summary": "Operational, market, regulatory, and competitive risks",
        "pages": [89, 110]
      }
    ]
  }
}

Załóżmy, że analityk prosi o dane dotyczące przychodów z iPhone’ów w roku fiskalnym 2023. Zamiast szukać podobnych fragmentów tekstu, model językowy czyta indeks, dochodzi do wniosku, że sekcja „Wyniki finansowe > Przychody według kategorii produktów” jest właściwym kierunkiem, udaje się tam i pobiera w całości strony od 46 do 52. Odpowiedź zawiera dokładną odniesienie do strony, a ponieważ model nigdy nie widział sekcji „Czynniki ryzyka”, nic nie zakłóca jej klarowności.

Społeczna wersja tego procesu wyszukiwania opartego na rozumowaniu wygląda w kodzie tak:

import json
from openai import OpenAI

client = OpenAI()
def navigate_index(query: str, index_node: dict, depth: int = 0) -> list[dict]:
    """
    Recursively navigate the document index using LLM reasoning.
    Returns list of relevant leaf nodes with page references.
    """
    children = index_node.get("children", [])
    if not children:
        # Leaf node: return this section as relevant
        return [index_node]
    # Ask the LLM which branches are relevant to the query
    children_summary = "\n".join([
        f"[{i}] {child['title']}: {child['summary']}"
        for i, child in enumerate(children)
    ])
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    "You are navigating a document index to find sections relevant "
                    "to a query. Select the index numbers of sections that are likely "
                    "to contain the answer. Return a JSON array of selected indices."
                )
            },
            {
                "role": "user",
                "content": (
                    f"Query: {query}\n\n"
                    f"Available sections:\n{children_summary}\n\n"
                    f"Which sections should I look into? Return JSON array of indices only."
                )
            }
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    selected = json.loads(response.choices[0].message.content).get("indices", [])
    relevant_nodes = []
    for idx in selected:
        if idx             # Recurse into selected branches
            relevant_nodes.extend(
                navigate_index(query, children[idx], depth + 1)
            )
    return relevant_nodes

def answer_with_pageindex(query: str, index: dict, document_pages: dict) -> str:
    """
    Full PageIndex retrieval and answer generation.
    """
    # Navigate the index to find relevant sections
    relevant_nodes = navigate_index(query, index)
    # Retrieve full text from identified pages
    context_parts = []
    citations = []
    for node in relevant_nodes:
        pages = node.get("pages", [])
        if pages:
            page_start, page_end = pages[0], pages[1]
            for page_num in range(page_start, page_end + 1):
                if page_num in document_pages:
                    context_parts.append(document_pages[page_num])
                    citations.append(f"p.{page_num}")
    context = "\n\n".join(context_parts)
    # Generate answer with full, unchunked context
    answer_response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    "Answer the question based on the provided document sections. "
                    "Be precise. If the answer involves numbers or dates, quote them exactly."
                )
            },
            {
                "role": "user",
                "content": f"Document sections:\n{context}\n\nQuestion: {query}"
            }
        ],
        temperature=0
    )
    answer = answer_response.choices[0].message.content
    citation_str = ", ".join(set(citations))
    return f"{answer}\n\n**Source:** {citation_str}"

Zobacz, czego brakuje: embeddingi, podobieństwo kosinowe, bazy danych wektorowych oraz logika dzielenia na fragmenty. Ustrukturyzowane rozumowanie na podstawie indeksu dokumentów zastępuje cały proces wyszukiwania.

Dowody z benchmarku finansowego

FinanceBench gromadzi pytania dotyczące rzeczywistych dokumentów finansowych, takich jak zeznania SEC, raporty roczne i konferencje dotyczące wyników finansowych. Odpowiedzi muszą być precyzyjne pod względem liczbowym, sekcje muszą być dokładnie odwoływane wzajemnie, a przypisy muszą być uważnie przeczytane. RAG oparty na wektorach ma tu trudności, ponieważ dokumenty finansowe zawierają wiele podobnie brzmiących sformułowań na różne tematy.

Mafin 2.5, oparty na PageIndex, osiągnął dokładność 98,7% na FinanceBench. Typowe systemy RAG wektorowe w porównywalnych testach finansowych osiągają wyniki od 65 do 80%, w zależności od konfiguracji. Ta różnica nie jest znikoma – oddziela narzędzie, któremu analityk inwestycyjny może naprawdę zaufać, od takiego, które wytwarza wiarygodny bezsens, który trzeba weryfikować linijka po linijce.

Struktura wyjaśnia to. Dokumenty finansowe to dokładnie te przypadki, gdy hierarchia ma największe znaczenie. Zbiorcza bilans, przypisy do sprawozdań oraz MD&A wszystkie mają jasną, przemyślaną strukturę, na której analitycy polegają cały czas. PageIndex zachowuje i wykorzystuje tę strukturę, natomiast dzielenie na wektorowe fragmenty ją niszczy.

Kiedy wybrać które

PageIndex nie zastępuje powszechnie systemów RAG wektorowych. Odpowiedni wybór zależy od projektu.

Dobrze sprawdza się w następujących sytuacjach:

  • Dokumenty mają wyraźną hierarchię, podobnie jak raporty, podręczniki, umowy i specyfikacje.
  • Dokładność oraz poprawne cytowanie są niepodważalne.
  • Dziedzina jest specjalistyczna, np. finanse, prawo, medycyna lub praca regulacyjna.
  • Pytania wymagają odniesień między kilkoma sekcjami jednego dokumentu.
  • Ważna jest wyjaśnialność, a system musi pokazać, dlaczego podał takie wyniki.

Tradycyjny RAG wektorowy nadal ma sens wtedy, gdy:

  • Korpus składa się z tysięcy krótkich, niezależnych dokumentów, takich jak zgłoszenia obsługi klienta lub recenzje produktów.
  • Dokumenty nie mają spójnej struktury.
  • Celem jest przybliżone wyszukiwanie semantyczne w dużym korpusie.
  • Głównym problemem jest opóźnienie, a proces dzielenia na fragmenty jest już zoptymalizowany.

Większość przypadków użycia dokumentów w przedsiębiorstwach, w których system musi być naprawdę przydatny, a nie tylko imponujący podczas demonstracji, bardziej przypomina pierwszą listę niż drugą.

Dla użycia w produkcji bez obciążenia infrastrukturą, PageIndex oferuje również interfejs REST API oraz integrację MCP. Konfiguracja programisty wygląda w ten sposób:

import requests

PAGEINDEX_API_KEY = "your_api_key"
BASE_URL = "https://api.pageindex.ai/v1"
def upload_document(file_path: str) -> str:
    """Upload a document and get back a document_id."""
    with open(file_path, "rb") as f:
        response = requests.post(
            f"{BASE_URL}/documents",
            headers={"Authorization": f"Bearer {PAGEINDEX_API_KEY}"},
            files={"file": f}
        )
    return response.json()["document_id"]

def query_document(document_id: str, question: str) -> dict:
    """Query an indexed document and get a cited answer."""
    response = requests.post(
        f"{BASE_URL}/query",
        headers={
            "Authorization": f"Bearer {PAGEINDEX_API_KEY}",
            "Content-Type": "application/json"
        },
        json={
            "document_id": document_id,
            "question": question
        }
    )
    return response.json()

# Example usage
doc_id = upload_document("q3_earnings_report.pdf")
result = query_document(doc_id, "What was total revenue in Q3?")
print(result["answer"])
print(f"Sources: {result['citations']}")

Gdy plik jest przesyłany, API automatycznie tworzy indeks, a po złożeniu zapytania zwraca ustrukturyzowane odpowiedzi z odniesieniami do stron, które można prześledzić.

Główna idea

Poza wynikami benchmarków dzieje się coś głębszego. Standardowe rozwiązania typu RAG traktują dokument jako zbiór powiązanych fragmentów tekstu i ignorują strukturę stworzoną przez jego autora. Każdy nagłówek, podział na sekcje oraz odnośnik stanowią sygnały – sposób, w jaki autor wskazuje czytelnikom, dokąd iść – natomiast metoda dzielenia na fragmenty odrzuca to wszystko, zanim spróbuje odtworzyć istotę treści z niczego. PageIndex traktuje dokument takim, jakim jest: ustrukturyzowanym dziełem przeznaczonym do nawigacji przez kompetentnego czytelnika.

Doświadczony analityk przeglądający roczny raport nie czyta od pierwszej strony. Sprawdza spis treści, przechodzi do części, która go interesuje, weryfikuje dane w porównaniu z notatkami i wraca do sekcji MD&A. LLM w PageIndex robi to samo.

Dlatego też metoda ta sprawdza się w specjalistycznych dziedzinach. Raport z badań klinicznych ma inną strukturę niż umowa prawnicza czy specyfikacja techniczna, ale w każdym z tych dokumentów autor stosuje wewnętrzną logikę, której korzystają eksperci. PageIndex odzwierciedla tę logikę i umożliwia efektywne wyszukiwanie informacji na jej podstawie.

Inna diagnoza

Smutnym wnioskiem jest to, że lata wysiłków mogły być skierowane na niewłaściwy problem. Uwaga skupiała się na strategiach dzielenia na fragmenty, podczas gdy rzeczywistą wadą było niszczenie struktury od samego początku. Modele embeddingowe były dostosowywane, mimo że podobieństwo semantyczne nigdy nie stanowiło właściwego sygnału do wyszukiwania. Aby poprawić jakość wyników, dodawano narzędzia do ponownego sortowania, podczas gdy sam sposób wyszukiwania był wąskim gardłem.

Ekosystem RAG stworzył złożone rozwiązania, aby nadrobić pierwotny błąd związany z dzieleniem tekstu na fragmenty. PageIndex proponuje alternatywę: bez fragmentów, bez modeli embeddingowych, bez wyszukiwania na podstawie podobieństwa. Należy stworzyć indeks odzwierciedlający rzeczywistą strukturę dokumentu i przeglądać go poprzez rozumowanie.

To nie odpowiada na wszystko. Wyszukiwanie wektorowe wciąż przewyższa inne metody w zakresie wyszukiwania semantycznego wśród milionów krótkich, nieustrukturyzowanych dokumentów. Jednak w przypadku szerokiej grupy pytań dotyczących długich, ustrukturyzowanych dokumentów, które obejmują większość potrzeb przedsiębiorstw, drzewo hierarchiczne zasługuje na poważną uwagę. Uruchom ponownie demo, które dwukrotnie zawiodło, zadając pytanie o prognozy przychodów na rok 2023 – system teraz przechodzi do odpowiedniej sekcji, pobiera właściwe strony i odpowiada z dokładnymi cytatami. Żadnego błędnego roku, żadnej błędnej sekcji, żadnego nieuzasadnionego przekonania.

PageIndex jest oprogramowaniem otwartym i można je uruchomić samodzielnie na GitHubie; dostępna jest również interfejs czatu w chmurze oraz dokumentacja API z odniesieniami do REST i MCP, aby można było je wypróbować na własnych dokumentach.

Literatura pokrewna

  • Spring AI: Lepsze wyniki RAG dzięki hybrydowemu wyszukiwaniu z użyciem Lucene i RRF — Połącz wyszukiwanie wektorowe z algorytmem Lucene BM25 w mechanizmie odzyskiwania danych Spring AI oraz połącz rankingi za pomocą Reciprocal Rank Fusion, aby RAG odpowiadał zarówno znaczeniu, jak i dokładnym terminom.
  • Jak API agentów OpenAI zmienia ekonomikę pracy z wykorzystaniem AI — Agent zarządza uruchamianiem modeli, wywołaniami narzędzi, środowiskami testowymi, próbami ponownymi oraz przeglądami przez ludzi, dzięki czemu liczba tokenów nie jest już wskaźnikiem kosztu. Zamiast tego mierz liczbę zatwierdzonych zadań.
  • Hybrydowe wyszukiwanie w RAG: BM25, Rank Fusion i Reranking przewyższają wektory — Dlaczego czyste wyszukiwanie wektorowe pomija dokładne terminy w RAG oraz jak BM25 w połączeniu z wyszukiwaniem semantycznym, Reciprocal Rank Fusion i narzędziem do ponownego rankowania zwiększyło dokładność wyszukiwania do 91%.
  • Pinecone, Weaviate, Milvus, Qdrant czy Chroma: Jak wybrać bazę danych wektorową — Proste wyjaśnienie zalet każdej z baz danych wektorowych, od rozwiązań bez serwerów i hybrydowego wyszukiwania po indeksowanie w języku Rust i na GPU, wraz z wadami i sytuacjami, w których każda z nich sprawdza się najlepiej.