typescript and javascript logo
author avatar

Grzegorz Dubiel

27-08-2025

Przekształcanie całych blogów w krótkie podsumowania: Map-Reduce dla LLM-ów

Nasza potrzeba skalowania stale rośnie. Ludzkość przetwarza ogromne ilości danych. Każdego dnia inżynierowie muszą zmagać się z różnymi ograniczeniami. To środowisko często zmusza nas do pójścia na skróty i tworzenia obejść, aby osiągnąć wielkie rzeczy. Jednym z najlepszych przykładów jest wykorzystanie LLM-ów do przetwarzania dużych dokumentów. Z jednej strony mamy wciąż ograniczoną technologię, taką jak LLM-y z zawężonym oknem kontekstowym, z drugiej, ogromne bazy wiedzy reprezentowane przez wykresy, dokumenty, nagrania audio, filmy i zbiory kodu. W krótkim czasie powstało wiele przydatnych aplikacji, takich jak NotebookLM do pracy reaserchu, Claude Code czy Cursor do programowania. Każde z tych narzędzi musi przetwarzać informacje przekraczające okno kontekstowe LLM-a. Aby było to możliwe, musimy wykorzysywać pewne wzorce. Przyjrzyjmy się jednemu z tych wzorców, zwanemu Map-Reduce. Aby łatwiej zrozumieć jego działanie, zbudujemy małą aplikację do podsumowywania blogów. Ten artykuł oraz pomysł na aplikację którą zbudujemy został zainspirowany tutorialem o patternie map-reduce ze strony z dokumentacją LangChain'a.

Map-Reduce Pattern

Pattern ten został zaproponowany w 2004 roku przez dwójkę ludzi z Google’a:Jeffrey Dean'a i Sanjay Ghemawat'a, aby przetwarzać duże ilości rozproszonych danych. Mówiąc prościej, pozwala programowi efektywnie przetwarzać ogromne ilości danych we fragmentach, a następnie redukować je do ostatecznego wyniku, zapewniając szybkość i efektywność całego procesu.

Jak sama nazwa wskazuje, wzorzec jest połączeniem dwóch funkcji:

  • Map, która jednocześnie wywołuje funkcję zwracającą wartość dla każdego fragmentu danych (na przykład licząc oczekiwane słowa kluczowe we fragmencie, zwracając pośrednią parę klucz-wartość reprezentującą każde słowo i liczbę jego wystąpień).

  • Reduce, która przyjmuje wynik funkcjii map i agreguje go do ostatecznego wyniku (na przykład zwracając tabelę z całkowitą liczbą wystąpień każdego słowa w zbiorze danych).

Każde zadanie map może działać niezależnie na różnych węzłach, dlatego proces ten łatwo się skaluje, a stopień równoległości można kontrolować, aby zrównoważyć szybkość i wykorzystanie zasobów.

W kontekście używania LLM-ów ten wzorzec jest szczególnie przydatny, ponieważ zawsze operujemy na ograniczonym oknie kontekstowym. W prawdziwych scenariuszach zawartość lub dane, które chcemy przetworzyć, często przekraczają rozmiar okna kontekstowego. Nie możemy przetworzyć całego bloga za pomocą jednego lub dwóch zapytań do LLM'a. Nawet jeśli byłoby to możliwe, proces ten był by bardzo wolny i potencjalnie niedokładny, ponieważ LLM-y zazwyczaj lepiej radzą sobie z mniejszymi fragmentami danych.

Założenia projektu

Pierwszą rzeczą, którą chciałbym zaznaczyć, jest to, że w tym artykule spróbujemy zaimplementować ten wzorzec przy minimalnym wsparciu frameworków do budowania rozwiązań AI, takich jak langgraph. Główną logikę zaimplementujemy samodzielnie; wykorzystamy jedynie funkcje pomocnicze z langchain do dzielenia tekstu i formatowania danych.

Nasza logika zostanie podzielona na następujące kroki:

  1. Faza wstępna (Pre-processing phase) – Zbieranie wybranych artykułów bloga przy użyciu vibe-coded web scrapera.

  2. Faza mapowania (Mapping phase) – Przechodzenie przez każdy artykuł, dzielenie go na części i podsumowywanie każdej z nich za pomocą LLM równolegle, wykorzystując map prompt.

  3. Faza redukcji (Reducing phase) – Jeśli podsumowania wygenerowane w fazie mapowania przekraczają limit, zostają skondensowane poprzez podział na podlisty, następnie każda podlista zostaje streszczona ponownie w jedno podsumowanie zbiorcze przy użyciu promptu reduce. Faza redukcji działa rekurencyjnie, dopóki zmapowane lub skondensowane podsumowania nie zmieszczą się w limicie i nie zostaną sprowadzone do ostatecznego podsumowania, albo dopóki nie zostanie osiągnięty limit rekurencji. W przeciwnym razie, jeśli podsumowania mieszczą się w limicie od razu, są bezpośrednio redukowane do finalnego podsumowania przy użyciu wspomnianego promptu reduce.

Faza wstępna (Pre-processing phase)

Jak już wspomniałem, do pozyskania artykułów do podsumowania użyjemy scrapera. Nie będziemy zagłębiać się w jego kod. Wywołamy scraper w funkcji main, aby pobrać wszystkie posty z mojego bloga:

typescript

Następnie musimy zadeklarować główną funkcję dla summarizer'a — miejsce, w którym będzie działać główna logika:

typescript

Funkcja ma dwa parametry: documents, które stanowią dane wejściowe (w naszym przypadku artykuły), oraz opcjonalny parametr maxIterations, określający, ile operacji rekurencyjnych można wykonać w sytuacji, gdy treść nie mieści się w limicie.

Faza mapowania (Mapping phase)

Pierwszą rzeczą, którą robimy, jest przejście po wszystkich postach na blogu i przygotowanie ich dla text splittera, formatując je do typu Document:

typescript

Teraz przygotowujemy funkcję, która podzieli nasze dokumenty na fragmenty i podsumuje każdy z fragmentów za pomocą LLM-a.

Aby móc ją stworzyć, musimy najpierw przygotować kilka rzeczy:

  1. LLM model runnable

    Użyjemy gpt-5-mini. Dla naszego summarizer'a ten model jest lekki i szybki — w pełni wystarczający do tego zadania. Do obsługi instancji modelu wykorzystamy integrację openai z langchain.

    typescript

  2. prompt

    W naszym wypadku kluczowym zadaniem fazy mapowania jest podsumowanie każdego posta na blogu. Potrzebujemy promptu, który zostanie wysłany razem z tekstem posta lub jego fragmentem do LLM-a w celu podsumowania:

    typescript

    Prompt jest zwracany przez funkcję template, która przyjmuje treść posta na blogu lub jego fragment jako argument. Dzięki temu możemy dynamicznie tworzyć prompt z osadzoną treścią do podsumowania.

  3. text splitter

    Musimy przygotować tokenTextSplitter, który potrafi podzielić dokument na mniejsze poddokumenty:

    typescript

    Używamy bardzo małego limitu dla fragmentu, aby zilustrować rekurencyjny proces analizowania tekstów.

Teraz możemy stworzyć funkcję odpowiedzialną za uruchamianie mapperów:

typescript

Artykuły zostaną podzielone na mniejsze fragmenty (poddokumenty), każdy fragment zostanie następnie wysłany wraz z map prompt do LLM-a w celu podsumowania. Metoda batch pozwala nam efektywnie wysyłać wiele zapytań do LLM równolegle. Możemy kontrolować maksymalną liczbę jednoczesnych zapytań wysyłanych w jednej partii w konfiguracji instancji modelu LLM. Wyniki z wszystkich mapperów zostaną zwrócone do przetworzenia w następnej fazie.

Faza redukcji (Reducing phase)

W następnym kroku weźmiemy wszystkie podsumowania zwrócone w fazie mapowania i scalimy je w jedno ostateczne podsumowanie. W tej fazie musimy również obsłużyć sytuację, w której podsumowania z mapperów przekraczają limit. Zkompresujemy listę podsumowań, dzieląc ją na podlisty, a następnie wykonamy podsumowanie każdej podlisty. Jako rezultat otrzymamy jedno podsumowanie na podlistę.

Pierwsza funkcja, którą tworzymy dla fazy redukcji, określi, czy w ogóle musimy wykonać łączenie podsumowań:

typescript

Funkcja lengthFunction sumuje liczbę tokenów we wszystkich podsumowaniach. Funkcja sprawdzająca porównuje tę łączną liczbę tokenów z ustalonym limitem, aby upewnić się, że podsumowania go nie przekraczają.

Łączenie Podsumowań(Collapsing)

Następnie stworzymy funkcję do rekurencyjnego łączenia podsumowań. Łączenie będzie odbywać się poprzez wywołanie LLM i rządanie skonsolidowania listy podsumowań.

Stwórzmy reduce prompt dla tego zadania:

typescript

Następnie musimy przygotować dwie dodatkowe funkcje do podziału listy podsumowań na podlisty.

Pierwszą będzie kolejna funkcja dzieląca tokeny, odpowiedzialna za obsługę edge case'a, w którym pojedyńcze podsumowanie z listy przekracza maksymalny rozmiar podlisty.

typescript

RecursiveCharacterTextSplitter jest wystarczający, ponieważ w tym przypadku skupiamy się na pojedynczym, dużym podsumowaniu które wykracza poza limit. Splitter dzieli tekst rekurencyjnie — od zdań do słów — starając się zachować możliwie jak najdłuższe fragmenty.

Teraz możemy zdefiniować funkcję do dzielienia listy:

typescript

Funkcja wygląda dość skomplikowanie, ponieważ zadanie nie jest trywialne. Funkcja Iteruje po każdym streszczeniu, automatycznie dzieląc go na mniejsze fragmenty, jeśli jego długość przekracza limit podlisty. Każdy potencjalny fragment jest następnie dodawany do listy kadnydującej. Liczba tokenów tej listy kadnydującej jest obliczana za pomocą funkcji lengthFunction, zdefiniowanej wcześniej. Jeśli długość przekroczy limit, bieżąca podlista zostaje dodana do tablicy wynikowej i rozpoczynana jest nowa podlista. W przeciwnym razie fragment jest dodawany do bieżącej podlisty.

Potrzebujemy jeszcze jednej funkcji, aby skompresować podsumowania:

typescript

Funkcja wykonuje równoległe wywołania do LLM w celu podsumowania każdej podlisty. W efekcie lista podsumowań staje się krótsza.

Teraz możemy zdefiniować główną funkcję do rekurencyjnego kompresowania podsumowań:

typescript

W tej funkcji możemy łatwo i przejrzyście zorganizować każdy krok kompresowania listy, ponieważ nasza złożona logika jest schludnie zapakowana w opisowe funkcje. Najpierw dzielimy listę podsumowań na podlisty. Następnie redukujemy każdą podlistę w partii przy użyciu LLM. Kolejno sprawdzamy, czy zredukowana lista mieści się w oknie kontekstowym. Jeśli nie, wykonujemy kolejną kompresję; w przeciwnym razie, lub gdy osiągnięto limit rekurencji, po prostu zwracamy zredukowaną listę.

typescript

Ostateczna Redukcja

Po wykonaniu łączenia i upewnieniu się, że lista zmapowanych podsumowań nie przekracza limitu okna kontekstowego, możemy zredukować podsumowania do jednej skonsolidowanej, ostatecznej wersji listy.

W tym celu używamy promptu reduceTemplate, którego używaliśmy wcześniej podczas łączenia podlist podsumowań.

Zdefiniujmy funkcję do redukcji pojedynczej listy podsumowań:

typescript

Wcześniej robiliśmy coś podobnego, kompresując każdą podlistę podsumowań w partiach. Teraz redukujemy tylko jedną listę naraz.

Ostatnią rzeczą, którą powinniśmy zrobić, jest wywołanie tej funkcji w głównej funkcji podsumowującej:

typescript

Podsumowanie

Po zaimplementowaniu naszej aplikacji możemy wyraźnie zobaczyć, jak wzorzec Map-Reduce sprawdza się w świecie generatywnej sztucznej inteligencji. Dzięki temu wzorcowi możemy pokonać ograniczenia okna kontekstowego LLM'a. W tworzeniu oprogramowania proste, sprawdzone w boju rozwiązania, wynalezione wiele lat temu, mogą działać doskonale w połączeniu z nowymi, przełomowymi technologiami, takimi jak AI. Pokazuje to, że zanim zdecydujemy się na nową technologię, powinniśmy zapytać, jakie wzorce możemy zastosować, aby przezwyciężyć jej ograniczenia. Nie bójmy się korzystać ze sprawdzonych wzorców i algorytmów i łączyć je z kodem wykorzystującym najnowocześniejsze technologie.

Jeśli chcesz dowiedzieć się więcej o oknie kontekstowym, sprawdź mój artykuł o zarządzaniu oknem kontekstu w GPT-4o-mini.

Również sprawdź repozytorium naszego blog summarizera na GitHubie.

typescript and javascript logogreg@aboutjs.dev

©Grzegorz Dubiel | 2026