Τον τελευταίο καιρό, το RAG ενίοτε αποκτά κακή φήμη: είτε επειδή θεωρείται πλέον εντελώς τετριμμένο (είναι εύκολο στην αρχή, όχι τόσο όταν επεκτείνεται), είτε επειδή κάποιοι πιστεύουν ότι έχει ξεπεραστεί από τα «agentic systems» (τα οποία, αν εξετάσετε λίγο βαθύτερα την επιφάνεια, σε πολλές περιπτώσεις αρχίζουν πολύ γρήγορα να μοιάζουν με RAG…).
Αυτή η ανάρτηση παρουσιάζει ένα-δύο αναλυτικά παραδείγματα για να δείξει πώς αντιμετωπίζουμε ορισμένες συνηθισμένες προκλήσεις, συγκεκριμένα:
Διαχείριση μεικτών δεδομένων κειμένου και αριθμών και γιατί δυσκολεύουν το απλοϊκό RAG: οι λέξεις-κλειδιά συμπίπτουν και οι αριθμοί δεν έχουν σημασιολογικό νόημα.
Γιατί βοηθά ο σχεδιασμός embeddings με προτεραιότητα στη σύνοψη: δημιουργήστε μια σύντομη περιγραφική σύνοψη ανά τμήμα και κατόπιν δημιουργήστε embedding και εκτελέστε αναζητήσεις στη σύνοψη.
Πώς δημιουργούνται συναφείς συνόψεις: συμπεριλάβετε το ευρύτερο πλαίσιο του γονικού εγγράφου, ώστε να διακρίνονται στατιστικά με παρόμοια μορφή.
Πότε να βασίζεστε σε κώδικα και μοντέλα Pydantic: όταν έχει σημασία το αυτούσιο περιεχόμενο, συνδυάστε προσαρμοσμένο κώδικα ή/και μοντέλο Pydantic με κλήσεις LLM για μεγαλύτερη αξιοπιστία.
Τα βασικά
Τα συστήματα RAG υποστηρίζουν από bot εξυπηρέτησης έως εσωτερικούς βοηθούς γνώσης.
Στο παρασκήνιο, συνήθως:
Χωρίζετε σε τμήματα τα έγγραφα προέλευσης
Ενσωματώνετε κάθε τμήμα σε έναν διανυσματικό χώρο
Ανακτάτε τα κορυφαία K τμήματα κατά την υποβολή του ερωτήματος
Δημιουργείτε μια απάντηση βάσει αυτών των τμημάτων
Δημοφιλή εργαλεία —LangChain, LlamaIndex και Filestore της OpenAI— κάνουν αυτά τα βήματα σχεδόν τετριμμένα. Ωστόσο, σε πραγματικές ροές επεξεργασίας θα συναντήσετε δεδομένα που δεν είναι απλώς πυκνό κείμενο, με τα οποία το βασικό RAG μπορεί να δυσκολευτεί. Στις επόμενες ενότητες παρουσιάζουμε συγκεκριμένα παραδείγματα προκλήσεων σχετικών με δεδομένα και αναπτύσσουμε σταδιακά τη λύση καθώς αυξάνεται η πολυπλοκότητα.
Όταν τα δεδομένα σας δεν είναι μόνο κείμενο (κάτι αρκετά συνηθισμένο)
Εξετάστε το ακόλουθο τμήμα δεδομένων στο πλαίσιο ενός παιχνιδιού:
JSON
Τα embeddings λειτουργούν χάρη στις σχέσεις μεταξύ λέξεων που έχουν μαθευτεί μέσω της σημασιολογίας και της γραμματικής. Στα παραπάνω δεδομένα συνυπάρχουν κείμενο και αριθμοί, αλλά εκτός του συγκεκριμένου πλαισίου οι αριθμοί δεν συνδέονται με τις λέξεις. Θα μπορούσαμε λοιπόν να πούμε ότι αυτό το τμήμα δεδομένων είναι ουσιαστικά ένας συνδυασμός κάπως περιγραφικών λέξεων, ακολουθούμενων από τυχαίους αριθμούς.
Αυτό δεν θα αποτελούσε πρόβλημα αν ήταν ο μοναδικός τύπος δεδομένων μας, επειδή θα μπορούσαμε να κάνουμε ανάκτηση βάσει των embeddings των λίγων διαθέσιμων περιγραφικών λέξεων (ή απλώς να χρησιμοποιήσουμε text-to-SQL). Τι γίνεται όμως αν αυτό το τμήμα είναι θαμμένο ανάμεσα σε πολλά τμήματα πυκνού κειμένου, όπου εμφανίζονται επίσης αυτές οι λέξεις; Για παράδειγμα:
JSON
Φανταστείτε τώρα ότι θέλουμε να ανακτήσουμε το «What is the attack range with Draconic Ascension?» Πιθανότατα δεν θα μπορέσουμε να ανακτήσουμε το σχετικό τμήμα που θέλουμε, επειδή χάνεται μέσα στον θόρυβο άλλων τμημάτων που περιέχουν τις ίδιες λέξεις-κλειδιά.
Το βασικό πρόβλημα είναι ότι δεν μπορούμε να διακρίνουμε καλά αυτά τα τμήματα δεδομένων, παρόλο που περιέχουν διαφορετικά είδη πληροφοριών για το ίδιο θέμα. Θα μπορούσαμε να τα εμπλουτίσουμε ή να τα βελτιώσουμε με κάποιον τρόπο; Φυσικά και μπορούμε:smile:
Εμπλουτίστε τα δεδομένα σας συνοψίζοντάς τα — ναι, καλά διαβάσατε
Αντί να δημιουργήσουμε απευθείας embedding για το ίδιο το τμήμα, μπορούμε πρώτα να δημιουργήσουμε μια σύνοψη που περιγράφει το θέμα των δεδομένων και έπειτα να δημιουργήσουμε embedding και να κάνουμε ανάκτηση βάσει της σύνοψης. Στο στάδιο της δημιουργίας, θα εξακολουθούσαμε να χρησιμοποιούμε τα αρχικά δεδομένα που συνδέονται με τη σύνοψη.
Για τα δύο παραπάνω παραδείγματα τμημάτων, θα δημιουργούσαμε λοιπόν συνόψεις όπως:
Attack statistics of range, speed, and damage (default and with Draconic Ascension).
Description and details of the Draconic Ascension, including activation conditions, visual effects, and lore.
Στη συνέχεια, εμπλουτίζουμε και το ερώτημα ώστε να «ευθυγραμμιστεί» με τη σύνοψη. Για παράδειγμα, θα μετατρέπαμε το «What is the attack range with Draconic Ascension?» σε «What is the statistics of attack range with Draconic Ascension?» Αυτό είναι ιδιαίτερα σημαντικό όταν το ερώτημα ανάκτησης προέρχεται από χρήστες χωρίς τεχνικό υπόβαθρο, οι οποίοι ρωτούν σε ~~«ελεύθερο στιλ»~~ φυσική ανθρώπινη γλώσσα. Άλλωστε, δεν γνωρίζουν ούτε τους απασχολεί πώς λειτουργεί ένα RAG ώστε να μεγιστοποιούνται η ακρίβεια και η ανάκληση.


Μη βγάζετε τα πράγματα από το ευρύτερο πλαίσιο (ισχύει γενικά και στη ζωή)
Ένα επόμενο σενάριο αφορά τη διαχείριση πάρα πολλών τμημάτων δεδομένων που μοιάζουν μεταξύ τους, όπως παρακάτω:
Plain Text
Αν ακολουθήσουμε την ίδια προσέγγιση, φανταστείτε να ρωτάμε «what is character X’s attack range?» Θα παίζαμε ένα παιχνίδι εικασιών βασισμένο στην τύχη με τις συνόψεις που μόλις δημιουργήσαμε, επειδή και αυτές θα έμοιαζαν πολύ μεταξύ τους. Πώς θα μπορούσαμε λοιπόν να τις ξεχωρίσουμε;
Η απλή απάντηση είναι: παρέχοντας το ευρύτερο πλαίσιο. Θα μπορούσαμε απλώς να συμπεριλάβουμε στο τμήμα δεδομένων μια αναφορά στο γονικό του έγγραφο, π.χ. {”character”: “X”} σε αυτήν την περίπτωση. Έτσι, θα μπορούσαμε πλέον να ανακτούμε με ακρίβεια τα σωστά δεδομένα για τον χαρακτήρα X, ακόμη και όταν διαθέτουμε τα ίδια δεδομένα για τους χαρακτήρες Y και Z.
Μια καλύτερη και ευρύτερα εφαρμόσιμη προσέγγιση, ωστόσο, θα ήταν να δημιουργήσουμε μια σύνοψη του τμήματος που λαμβάνει υπόψη το ευρύτερο πλαίσιο. Δηλαδή, αντί να δημιουργήσουμε μια σύνοψη μόνο για το ίδιο το τμήμα δεδομένων, θα μπορούσαμε να περάσουμε τόσο το γονικό έγγραφο όσο και το τμήμα για να δημιουργηθεί μια γενική σύνοψη βάσει του ευρύτερου πλαισίου. Σε αυτήν θα εξηγούμε πώς το τμήμα εντάσσεται στο γονικό του έγγραφο, π.χ.:
This chunk provides detailed statistics of … for character X. The chunk fits into the full document by showing X’s strength in attack speed…
This chunk provides detailed statistics of … for character Y. The chunk fits into the full document by showing Y’s boosted stats with her special ability…
This chunk provides detailed statistics of … for character Z. The chunk fits into the full document by showing Z’s stats that’s well suited as a tank in team matches…
Αυτή η μέθοδος (η οποία είναι εν μέρει εμπνευσμένη από την Anthropic) μπορεί να φαίνεται υπερβολική για το παραπάνω παράδειγμα. Ωστόσο, είναι πολύ αποτελεσματική για τμήματα που θα μπορούσαν να παρερμηνευτούν «εκτός ευρύτερου πλαισίου», ενώ προσφέρει μια ενιαία προσέγγιση για όλα τα τμήματα, διατηρώντας τακτοποιημένη τη ροή μηχανικής.


Όταν πρέπει να είστε ~~μανιακοί με τον έλεγχο~~ σχολαστικοί
Συνήθως λαμβάνουμε τα δεδομένα ως ολοκληρωμένες ενότητες και τα χωρίζουμε σε τμήματα για ένα σύστημα RAG. Σε αυτό το παράδειγμα παρουσιάζουμε κάτι λίγο διαφορετικό: δεδομένα χωρισμένα σε τμήματα, αλλά σε ακατάλληλα τμήματα. Πρόκειται για τυχαίες ενότητες ενός λογικού τμήματος, οι οποίες πρέπει στην πραγματικότητα να ομαδοποιηθούν ξανά. Λογικό τμήμα σημαίνει ένα μέρος περιεχομένου που εκ φύσεως πρέπει να παραμένει ενιαίο, όπως μια υποενότητα εγγράφου ή μια συνεκτική παράγραφος.


Η πρώτη μας απόπειρα με αυτά τα δεδομένα ήταν να τα εισαγάγουμε όλα σε μια κλήση LLM, ζητώντας του να τα ομαδοποιήσει όπως θεωρούσε κατάλληλο και κατόπιν να επιστρέψει το ομαδοποιημένο περιεχόμενο. Ένα LLM θα έπρεπε να τα καταφέρει αρκετά καλά, σωστά; Και ναι και όχι.
Έχουμε διαπιστώσει, και σε αρκετές άλλες περιπτώσεις, ότι τα LLM τείνουν να λειτουργούν κάπως «τεμπέλικα» και δεν είναι αξιόπιστα όταν απαιτείται ολόκληρο και ακριβές το περιεχόμενο, ιδίως όταν το συγκείμενο είναι μεγάλο. Και αυτό είναι απολύτως λογικό. Για τη συγκεκριμένη περίπτωση χρήσης, όμως, αυτό ήταν απαγορευτικό, επειδή χρειαζόμαστε το ακριβές περιεχόμενο λέξη προς λέξη — χωρίς συνόψεις και χωρίς να παραλείπεται κανένα μέρος του αρχικού περιεχομένου. Δεν μπορούμε να χάσουμε καμία λεπτομέρεια.
Και φυσικά, η θετική πλευρά ήταν ότι κατανοούσε εξαιρετικά τη σημασιολογία και τις δομές των κατακερματισμένων τμημάτων. Αρκεί να μη δυστροπεί όταν του ζητάμε να παραθέσει αυτούσιο το περιεχόμενο. Ανάθεμα:/
Πώς θα μπορούσαμε λοιπόν να αξιοποιήσουμε όσα κάνει καλά ένα LLM, αποφεύγοντας παράλληλα όσα δεν εκτελεί αξιόπιστα; Στραφήκαμε στον παλιό καλό μας φίλο, τον κώδικα (βλέπε προσαρμοσμένη συνάρτηση Python). Και σε ένα «απλούστερο δεν γίνεται» μοντέλο Pydantic. Ιδού η λύση:
Διατρέξτε τις ενότητες, διατηρώντας παράλληλα ένα τρέχον λογικό τμήμα
Σε κάθε ενότητα, ρωτήστε το LLM: ανήκει αυτή η ενότητα στο τρέχον λογικό τμήμα; Απάντησε ναι ή όχι (σύμφωνα με το μοντέλο Pydantic).
Αν ναι, προσαρτήστε την ενότητα στο τμήμα. Αν όχι, εξαγάγετε το τρέχον λογικό τμήμα, καθώς έχει ολοκληρωθεί, και ξεκινήστε ένα νέο με αυτήν την ενότητα.


Φυσικά, εδώ χρησιμοποιούμε λίγο περισσότερα token από ό,τι σε μία διέλευση ολόκληρου του περιεχομένου. Για τη συγκεκριμένη περίπτωση χρήσης, όμως, όπου η διατήρηση του ακριβούς περιεχομένου αποτελεί ύψιστη προτεραιότητα, άξιζε απόλυτα το (μικρό) πρόσθετο κόστος.
Πρόκειται για μια πολύ απλή λύση, η οποία όμως ακολουθεί μια σημαντική αρχή: όταν απαιτείται σχολαστικότητα, δεν θέλουμε να βασιζόμαστε αποκλειστικά στα LLM, καθώς είναι εκ φύσεως πιθανοτικά.
Μπορούμε να χρησιμοποιήσουμε προσαρμοσμένο κώδικα ή συναρτήσεις και μοντέλα Pydantic, ώστε να πετύχουμε προβλέψιμο και αξιόπιστο αποτέλεσμα, αξιοποιώντας παράλληλα πλήρως τις δυνατότητες των LLM.
Η δημιουργία μιας λύσης παραγωγικής τεχνητής νοημοσύνης αποτελεί εξίσου πρόκληση μηχανικής και τεχνητής νοημοσύνης. Ελπίζουμε αυτά τα παραδείγματα να σας ενέπνευσαν να αντιμετωπίσετε τις δικές σας ιδιαίτερες προκλήσεις. Για περισσότερα σχετικά με λύσεις παραγωγικής τεχνητής νοημοσύνης που δίνουν προτεραιότητα στη μηχανική, διαβάστε την ανάρτησή μας για τον σχεδιασμό agentic συστημάτων βάσει router.