ბოლო დროს RAG-ს ზოგჯერ ცუდი რეპუტაცია აქვს: ზოგს ის სრულიად მარტივი ჰგონია (დაწყება მართლაც მარტივია, მასშტაბირება — ნაკლებად), ზოგს კი მიაჩნია, რომ ის „აგენტურმა სისტემებმა“ ჩაანაცვლა (თუმცა ხშირად, ზედაპირს ოდნავ თუ ჩავუღრმავდებით, ისინი ძალიან სწრაფად ემსგავსება RAG-ს...)
ამ ბლოგში ერთი-ორი პრაქტიკული მაგალითით ვაჩვენებთ, როგორ ვუმკლავდებით გავრცელებულ გამოწვევებს, კერძოდ:
ტექსტური და რიცხვითი მონაცემების ერთობლივად დამუშავება და ის, თუ რატომ ვერ უმკლავდება ამას მარტივი RAG: საკვანძო სიტყვები ერთმანეთს ემთხვევა, რიცხვებს კი სემანტიკური მნიშვნელობა არ აქვს.
რატომ არის სასარგებლო ჯერ შეჯამებების შექმნა და შემდეგ მათი ვექტორიზაცია: თითოეული ფრაგმენტისთვის შექმენით მოკლე აღწერითი შეჯამება, შემდეგ კი ვექტორიზაცია და ძიება ამ შეჯამებაზე შეასრულეთ.
როგორ შევქმნათ კონტექსტური შეჯამებები: დაურთეთ მშობელი დოკუმენტის კონტექსტი, რათა მსგავსი ფორმის სტატისტიკური მონაცემები ერთმანეთისგან გაიმიჯნოს.
როდის უნდა დავეყრდნოთ კოდსა და Pydantic-ის მოდელებს: როცა შიგთავსის სიტყვასიტყვით შენარჩუნება მნიშვნელოვანია, საიმედოობისთვის სპეციალური კოდი და/ან Pydantic-ის მოდელი LLM-ის გამოძახებებთან გააერთიანეთ.
საფუძვლები
RAG-სისტემები საფუძვლად უდევს ყველაფერს — მხარდაჭერის ბოტებიდან დაწყებული, ცოდნის შიდა ასისტენტებით დამთავრებული.
როგორც წესი, სისტემის შიგნით შემდეგ ნაბიჯებს ასრულებთ:
წყაროს დოკუმენტებს ფრაგმენტებად ყოფთ
თითოეულ ფრაგმენტს ვექტორულ სივრცეში ასახავთ
მოთხოვნის დროს top-K ფრაგმენტს მოიძიებთ
ამ ფრაგმენტებზე დაყრდნობით პასუხს აგენერირებთ
პოპულარული ხელსაწყოები — LangChain, LlamaIndex და OpenAI-ის Filestore — ამ ნაბიჯებს თითქმის უმარტივეს პროცესად აქცევს. თუმცა რეალურ პროცესებში შეგხვდებათ მონაცემები, რომლებიც მხოლოდ მკვრივი ტექსტი არ არის, და საბაზისო RAG-ს მათთან მუშაობა შეიძლება გაუჭირდეს. მომდევნო სექციებში მონაცემებთან დაკავშირებულ გამოწვევებს კონკრეტული მაგალითებით ვაჩვენებთ და სირთულის მატებასთან ერთად გადაწყვეტას ეტაპობრივად ავაწყობთ.
როცა თქვენი მონაცემები მხოლოდ ტექსტი არ არის (რაც საკმაოდ ხშირია)
განვიხილოთ მონაცემთა შემდეგი ფრაგმენტი თამაშის კონტექსტში:
JSON
ვექტორული წარმოდგენები მუშაობს სიტყვებს შორის სემანტიკური მნიშვნელობისა და გრამატიკის საფუძველზე ნასწავლი კავშირების წყალობით. ზემოთ მოცემული მონაცემები ტექსტისა და რიცხვების ნაზავია, ხოლო ამ კონკრეტული კონტექსტის მიღმა რიცხვებს სიტყვებთან კავშირი არ აქვს. შესაბამისად, შეიძლება ითქვას, რომ მონაცემთა ეს ფრაგმენტი მეტ-ნაკლებად აღწერითი სიტყვებისა და მათ შემდეგ მოცემული შემთხვევითი რიცხვების ერთობლიობაა.
ეს პრობლემა არ იქნებოდა, მხოლოდ ასეთი მონაცემები რომ გვქონდეს, რადგან მოძიებას მაინც შევძლებდით არსებული რამდენიმე აღწერითი სიტყვის ვექტორული წარმოდგენით (ან უბრალოდ text-to-SQL-ს გამოვიყენებდით). მაგრამ რა მოხდება, თუ ეს ფრაგმენტი დამალულია ტექსტით დატვირთულ მრავალ სხვა ფრაგმენტში, რომლებშიც იგივე სიტყვებიც გვხვდება? მაგალითად:
JSON
ახლა წარმოიდგინეთ, რომ გვსურს მოვიძიოთ: “What is the attack range with Draconic Ascension?” დიდი ალბათობით, სასურველ შესაბამის ფრაგმენტს ვერ მოვიძიებთ, რადგან ის იმავე საკვანძო სიტყვების შემცველი სხვა ფრაგმენტების ხმაურშია ჩაკარგული.
პრობლემის საფუძველი ისაა, რომ მონაცემთა ამ ფრაგმენტებს კარგად ვერ ვარჩევთ, მიუხედავად იმისა, რომ ერთი და იმავე თემის შესახებ სხვადასხვა ტიპის ინფორმაციას შეიცავს. შეგვიძლია როგორმე გავამდიდროთ ან გავაუმჯობესოთ ისინი? რა თქმა უნდა, შეგვიძლია:smile:
გაამდიდრეთ მონაცემები მათი შეჯამებით — დიახ, სწორად წაიკითხეთ
უშუალოდ ფრაგმენტის ვექტორიზაციის ნაცვლად, ჯერ შეგვიძლია შევქმნათ შეჯამება, რომელიც მონაცემების თემას აღწერს, შემდეგ კი ვექტორიზაცია და მოძიება ამ შეჯამებაზე შევასრულოთ. გენერირების ეტაპზე კვლავ შეჯამებასთან დაკავშირებულ თავდაპირველ მონაცემებს გამოვიყენებთ.
ამგვარად, ზემოთ ნაჩვენები ორი ფრაგმენტისთვის დაახლოებით ასეთ შეჯამებებს შევქმნიდით:
შეტევის მანძილის, სიჩქარისა და ზიანის სტატისტიკა (ნაგულისხმევად და Draconic Ascension-ის გამოყენებისას).
Draconic Ascension-ის აღწერა და დეტალები, მათ შორის გააქტიურების პირობები, ვიზუალური ეფექტები და ისტორია.
შემდეგ მოთხოვნასაც ვამდიდრებთ, რათა ის შეჯამებას „შევუსაბამოთ“. მაგალითად, მოთხოვნას “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-ზეც გვაქვს.
თუმცა უკეთესი და უფრო ზოგადი მიდგომა ფრაგმენტის კონტექსტური შეჯამების შექმნაა. ანუ მხოლოდ მონაცემთა ფრაგმენტის შეჯამების ნაცვლად, მოდელს შეგვიძლია გადავცეთ როგორც მისი მშობელი დოკუმენტი, ისე თავად ფრაგმენტი და შევქმნათ ზოგადი კონტექსტური შეჯამება. მასში აღვწერთ, როგორ უკავშირდება ეს ფრაგმენტი მშობელ დოკუმენტს, მაგალითად:
ეს ფრაგმენტი შეიცავს პერსონაჟ X-ის ...-ის დეტალურ სტატისტიკას. ფრაგმენტი სრულ დოკუმენტს ავსებს და აჩვენებს X-ის უპირატესობას შეტევის სიჩქარეში...
ეს ფრაგმენტი შეიცავს პერსონაჟ Y-ის ...-ის დეტალურ სტატისტიკას. ფრაგმენტი სრულ დოკუმენტს ავსებს და აჩვენებს, როგორ იზრდება Y-ის მაჩვენებლები მისი სპეციალური უნარის გამოყენებისას...
ეს ფრაგმენტი შეიცავს პერსონაჟ Z-ის ...-ის დეტალურ სტატისტიკას. ფრაგმენტი სრულ დოკუმენტს ავსებს და აჩვენებს Z-ის მაჩვენებლებს, რომელთა წყალობითაც ის გუნდურ მატჩებში ტანკის როლს კარგად ასრულებს...
ეს მეთოდი (რომელიც ნაწილობრივ Anthropic-ის მიდგომითაა შთაგონებული) ზემოთ ნაჩვენები მაგალითისთვის შეიძლება ზედმეტად რთული ჩანდეს, თუმცა ძალიან ეფექტურია იმ ფრაგმენტებისთვის, რომლებიც „კონტექსტიდან ამოგლეჯისას“ შეიძლება არასწორად განიმარტოს. ამასთან, ის ყველა ფრაგმენტისთვის გამოსადეგ ერთიან მიდგომას გვთავაზობს და საინჟინრო პროცესს მოწესრიგებულად ინარჩუნებს.


როცა ~~ყველაფრის კონტროლი~~ ზედმიწევნითი სიზუსტე გჭირდებათ
ჩვეულებრივ, მონაცემებს მთლიან ერთეულებად ვიღებთ და RAG-სისტემისთვის ფრაგმენტებად ვყოფთ. ამ მაგალითში ოდნავ განსხვავებულ შემთხვევას განვიხილავთ: მონაცემები უკვე დაყოფილია ფრაგმენტებად, მაგრამ ცუდად — ეს ლოგიკური ფრაგმენტიდან შემთხვევით ამოჭრილი ნაწილებია, რომლებიც სინამდვილეში ხელახლა უნდა გაერთიანდეს. ლოგიკური ფრაგმენტი არის შიგთავსის ნაწილი, რომელიც ბუნებრივად ერთიანად უნდა იყოს წარმოდგენილი, მაგალითად, დოკუმენტის ქვესექცია ან აზრობრივად გამართული აბზაცი.


თავდაპირველად ყველა ეს მონაცემი LLM-ს გადავეცით და ვთხოვეთ, თავისი შეხედულებისამებრ დაეჯგუფებინა, შემდეგ კი გაერთიანებული შიგთავსი დაებრუნებინა. LLM ამას კარგად უნდა უმკლავდებოდეს, არა? ერთდროულად კი და არა.
ამ და კიდევ რამდენიმე შემთხვევაში აღმოვაჩინეთ, რომ LLM-ები ხშირად ზარმაცად იქცევა და არასაიმედოა, როცა შიგთავსის სრულად და ზუსტად დაბრუნებას ვითხოვთ, განსაკუთრებით გრძელი კონტექსტის პირობებში. რაც სრულიად გასაგებია. თუმცა ამ კონკრეტულ შემთხვევაში ეს მიუღებელი იყო, რადგან შიგთავსი სიტყვასიტყვით და ზუსტად გვჭირდებოდა — შეჯამების გარეშე და თავდაპირველი შიგთავსის არცერთი ნაწილის გამოტოვების გარეშე. არცერთი დეტალი არ უნდა გამოგვრჩეს.
თუმცა დადებითი ის იყო, რომ მოდელმა დანაწევრებული ფრაგმენტების სემანტიკა და სტრუქტურა შესანიშნავად გაიგო. ოღონდ იმ შემთხვევაში, თუ ზუსტი შიგთავსის ციტირებაზე უარს არ იტყოდა. ოხ:/
მაშ, როგორ გამოვიყენოთ LLM-ის ძლიერი მხარეები და თან თავიდან ავიცილოთ ის, რაშიც არასაიმედოა? ჩვენს ძველ და სანდო მეგობარს — კოდს მივმართეთ (იგულისხმება მორგებული Python-ფუნქცია). და „ამაზე მარტივი რომ ვერ იქნება“ ტიპის Pydantic-ის მოდელს. აი, გადაწყვეტაც:
სექციები თანმიმდევრულად გაიარეთ და მიმდინარე ლოგიკური ფრაგმენტი შეინარჩუნეთ
თითოეულ სექციაზე ჰკითხეთ LLM-ს: ეკუთვნის თუ არა ეს სექცია მიმდინარე ლოგიკურ ფრაგმენტს? პასუხი უნდა იყოს „კი“ ან „არა“ (Pydantic-ის მოდელის შესაბამისად).
თუ პასუხია „კი“, სექცია ფრაგმენტს დაუმატეთ; თუ „არაა“, დასრულებული მიმდინარე ლოგიკური ფრაგმენტი გამოიტანეთ და ამ სექციით ახალი დაიწყეთ.


რა თქმა უნდა, აქ ოდნავ მეტ ტოკენს ვიყენებთ, ვიდრე მთელი შიგთავსის ერთჯერადი დამუშავებისას, მაგრამ ამ კონკრეტულ შემთხვევაში ზუსტი შიგთავსის შენარჩუნება უმთავრესი იყო, ამიტომ მცირე დამატებითი ხარჯი ნამდვილად ღირდა.
ეს ძალიან მარტივი გადაწყვეტაა, თუმცა მნიშვნელოვან პრინციპს ეფუძნება: როცა ზედმიწევნითი სიზუსტე გვჭირდება, მხოლოდ LLM-ებს არ უნდა დავეყრდნოთ, რადგან ისინი მაინც ალბათური სისტემებია.
მორგებული კოდითა და ფუნქციებით, ასევე Pydantic-ის მოდელებით, შესაძლებელია პროგნოზირებადი და საიმედო შედეგის მიღება ისე, რომ LLM-ების შესაძლებლობებიც სრულად გამოვიყენოთ.
გენერაციული AI-ის გადაწყვეტის შექმნა ერთდროულად საინჟინრო და ხელოვნური ინტელექტის გამოწვევაა. ვიმედოვნებთ, ამ მაგალითებმა თქვენი უნიკალური გამოწვევების გადაჭრისკენ გიბიძგათ. საინჟინრო მიდგომაზე დაფუძნებული გენერაციული AI-გადაწყვეტების შესახებ მეტი ინფორმაციისთვის გაეცანით ჩვენს ბლოგპოსტს როუტერზე დაფუძნებული აგენტური სისტემის დიზაინის შესახებ.