Шматагентная рэкамендацыя K-драм: BERTopic, RAG і Reciprocal Rank Fusion
Рэкамендацыі K-драм з улікам густу: тэгі тропаў праз BERTopic, RAG на ChromaDB, агент-інтэрв’юер, пашырэнне запыту і Reciprocal Rank Fusion перад пераранжаваннем праз LLM.
Крыніцы: **K-DramaBoo: Building a Multi-Agent, Taste-Aware K-Drama Recommender with RAG and AgentsSDK**
Чаму ствараць рэкамендэр карэйскіх драм
Чым больш стае спіс перакананых драм, тым сложней знайсці наступную карэйскую драму, якая варта пераканання. Тэмы на форумах дапамагаюць час ад часу, але пазней стаюць трыдзевной задачай. KDramaBoo — гэта канвейн з калькольнікамі агентаў, які прагне автаматызаваць такі пошук. Ён выконвае генераванне з падкрэпленнем дадзенням на адной 1 500 адгэнераваных анкетах драм і рэкамендуе ўпрадкі на адной падставе профілю падабанняў пераканальніка і яго тэкушчага эмачнага настрою.
Карэйскі хвал, або Hallyu, адказвае на пытанне чаму гэта адбываецца, і гэта стосуецца не толькі K-pop.
У графіку паказана колькасць драм на годзіну, які былі взяты толькі з TMDB і AsianWiki — двух істочнікаў, які насцярожка прызначаны для захаднай аўдытарыі, і гэта колькасць стабільна расце. Развіцце ў гады пандэміі, верагацейна, мае два прычыны. Глобальная аўдытарыя, якая застрала в домах, знайшла K-драмы на Netflix і іншых стрымінгавых платформах, а межынародны успех Crash Landing on You (2019) і Squid Game (2021) прывабіў захадную увагу і прынёс больш размов на англійскай мове ў Інтэрнэце.
Стварэнне корпуса
Значныя часткі работы складалася з работы з базайнама дадзеных. API TMDB сапраўдзіў падачу 1 500 к-драма з 2010 года і аднойчы, кожны элемент быў адзначаны сярэднім рэйтынгам галасавання, а таксама коментарамі фанатоў, паспэўкамі і дыскусіямі з AsianWiki і r/KDRAMA. Оба сайты былі аналізаваны з выкарыстоўваннем экспансійнага рэгулювання частоты запытоў, што зайняло апошнія адзін дзень. Ёжы старыя і непопулярныя серыя не патраплялі ў рэзультаты, таму усё, што было выдана раней чым у 2017 году або мела рэйтынг ніжэй 6,5, было адключана, чым колькасць элементаў зменілася з 1 500 на 874. Гэты фільтр ёсць свядомым выборам, адтолькі што рэкамендатар, які постаўляе забутыя тытулы, быстра вяртае доверлівасць.
Анотацыі, коментары і тэмы дыскусій — гэта купа неструктураванага тэксту, і на ўсём гэтым сістэма стварае прафіль кожнай драмы, які выходзіць за межы простых метададзэных. Прафіль включае тое, што людзі насправды шукаюць падбіраючы тытул для перагляду:
- Тэгі тропаў ўзмежаваныя больш, чым жанры. Яны называюць паўтараючыяся сюжэтныя элементы, дынаміку персанажаў або складовыя історыі, якія фанаты выкарыстоўваюць як крэтынгі, напрыклад, фальшывыя зносіны або шлюб па контракту.
- Тэмы опісваюць, пра шта эмцыянальна і філасофскі ўжоўся драматычны серыал. When Life Gives You Tangerines — гэта не проста серыал пра повсякдзеннае жыццё, адколькі ён развівае тэмы стойкасці, вячнае адданасці і раскрэпачэння між паколеннямі.
- Тэндэнцыя опісвае рытм і структуру нарады, ад павольнага розвітку да швырага руху.
- Эмцыянальны тон опісвае загальную атмасферу, укладаючы чы серыал занадта серйозны, каб яго можна было атрымліваць як лёгкі для перагляду.
Модэль Pydantic задае гэтую структуру, у якой ёсць поля для ID TMDB, назвы, тэгаў тропаў і іншых атрыбутаў.
class DramaProfile(BaseModel):
tmdb_id: int = Field(description="TMDB id associated with the drama")
name: str = Field(description="Name of the Korean drama")
trope_tags: list[str] = Field(description="List of trope tags extracted from topic modeling on the drama synopsis. E.g. 'Contract Marriage', 'Enemies to Lovers', 'Revenge Plot'")
theme_labels: list[str] = Field(description="3-5 deep themes extracted from fan discussion. What viewers understood the drama to really be about emotionally and philosophically. E.g. 'grief processed through quiet solidarity', 'dignity in invisible ordinary lives', 'love as a force that exposes class anxiety'")
pacing: str = Field(description="A prose description of the drama's narrative pacing and structural arc.")
emotional_tone: str = Field(description="A prose description of the drama's overall emotional atmosphere and feeling.")
community_sentiment: str = Field(description="A prose description of fan community reception — what fans broadly agree on, what divides opinion, and any recurring specific reactions such as second lead syndrome, time skip backlash, or satisfying/unsatisfying ending")
Тэгі тропаў з модэлюванням тэм
Тэгі тропаў былі атрыманы з анатазаў, а чатыры іншыя атрыбуты — з каментараў, пастаў і дыскусый фанатоў. Для тэгаў у працэсе выкарыстоўваецца BERTopic з слойам прэдставлення GPT-4o-mini, а порган 0.75 скарочывае колькасць адхыленых значэнняў. Канфігурацыя задае методы зменшэння выразнасці і мадэлі кластэрування.
# Initializing models for BERT Topic Modeling
# 1. Dimesionality Reduction
N_NEIGHBORS = 10
MIN_DIST = 0
N_COMPONENTS = 3
umap_model = umap.UMAP(
n_neighbors=N_NEIGHBORS,
min_dist=MIN_DIST,
n_components=N_COMPONENTS,
metric='cosine',
random_state=42)
# 2. Clustering Model
MIN_CLUSTER_SIZE = 7
MIN_SAMPLES = 2
clustering_model = hdbscan.HDBSCAN(
min_cluster_size=MIN_CLUSTER_SIZE,
min_samples=MIN_SAMPLES,
cluster_selection_method='eom',
metric='euclidean'
)
# 3. Vectorizer - to train including stop words
vectorizer = CountVectorizer(stop_words="english")
# 4. C-TF-IDF Model
ctfidf_model = ClassTfidfTransformer(bm25_weighting=True)
# 5. Representation Model
representation_model = OpenAI_BertRep(client=openai_client,
model="gpt-4o-mini",
delay_in_seconds=2,
prompt=custom_prompt,
nr_docs=5,
generator_kwargs = {"temperature":0}
)
Чаму бы не дасць кожны ананазі ШМІ і не папросіць атрыбуты? Вікна кантэксту дастаткова вялікія, таму калькі сотня токенав не ствараюць проблем. Але прыгожа ў тым, што ШМІ бачыць адну драму за раз і не мае уявлення пра весь корпус. У однай серыі ён можа называць „контрактныя шлюбы“ саме так, у іншай — „фальшывыя шлюбы“, а ў трэцій — „шлюбы за выгодай“, чыяму слоўнік застаецца нэўнацоўным. BERTopic аб’еднвае всі драмы, таму падобныя ананазі отрымаюць адныя і тыя ж атрыбуты. Важнае значэнне мае і косць: адпаведна 900 вызоў ШМІ, по аднаму на кожную драму, плюс плата за кожную новую серыю. Метод на базе тэматычнага мадэлявання быў безкоштовным, масштабаваным і відтворуемым.
ШМІ былі застосаваны для абстрактных характэрыстыкаў, а самэ ўсё тону эмоцый і тэм, калі патрэбна пераклацці фанатскія дыскусіі ў прозу, а таксама для выправлення неправільна прызначаных атрыбутаў.
Короткая агульная інфармацыя пра RAG
RAG — гэта просты спосаб прынесць LLM адпрацоўваны кантэкст, каб яго адказы сталі лепшымі; у гэтым прыкладзе — рэкамендаціі каляндарных драм. Разбіўка на часткі, імбеддынг і перыяркаванне — гэта інжынерныя методы, створаныя на адной гэтай ідэі. Архітэктура мае два рэжымы: прыем дакументаў і адпаведзанне.
У рэжыме прыема дакументы раздзеляюцца на часткі, якія імбеддуюцца, а вектары зберагаюцца ў базе дадзенаў. Пад час адпаведзання запыт корыстніка таксама імбеддуецца за дапамогою таго ж кодэра, а часткі, якія маюць наібольша сэмабільнасць, выбіраюцца за дапамогою косай сэмабільнасці і дадаюцца ў запыт для LLM. Мадэль отрымае толькі адпрацоўваныя дакументы, а не всю базу дадзенаў, і адпаведзае на адвяртаныя ёй у кантэксце запыты, ідэальна залікаючыся верным яму.
Першым крокам было ператворыць кожны DramaProfile у дакумент, з’едынаўшы ўсія яго значэнні ў адну строчку. Шырокія строчкі былі апрацаваны за дапамогою модэлю OpenAI text-embedding-3-small і зберагліся ў экземплярэ ChromaDB, чым завершылася процэдура прыемкі.
def create_embeddings(chunks: list[Document], ids: list):
if os.path.exists(DB_NAME):
Chroma(persist_directory=DB_NAME, embedding_function=embeddings).delete_collection()
vectorstore = Chroma(persist_directory=DB_NAME, embedding_function=embeddings)
vectorstore.add_documents(documents=chunks, ids=ids)
collection = vectorstore._collection
count = collection.count()
sample_embedding = collection.get(limit=1, include=["embeddings"])["embeddings"][0]
dimensions = len(sample_embedding)
print(f"There are {count:,} vectors with {dimensions:,} dimensions in the vector store")
return vectorstore
Чаму разбіўка на часткі паслабляе працэс адшукування
Няўтаўрожнікі дадзеных запытаюць пра відсутній крок — разбіўку на часткі. У дакументах існуе прыродная структура, якая прапануе пяць частак, по аднай на кожную польную прафіля. У практыцы разбіўка на часткі паслабіла якасць адшукування пасляэтапна.
Возьмімо Mr. Queen — камедыю пра сучаснага кухара, які опінуецца у тэле царыні эпахі Чосон і мусі рашыць палатныя палітычныя прыблуды і борбу за владу, адночасна дапамагаючы каралю. Яна займае высокія месцы сярод кумічных к-драм пра членаў каралевскай сям’і, пры тым яе не адшукалі за гэтым самэм запытам. Алгорытм адшукування счыл частку з анотацыяй найболей рэлевантной для запыту пра сюжэт, але сумарная анотацыя з TMDB і AsianWiki ніколі не згадвае камедыю. У опісе тэмпу розвітку сюжэту ж гэта згадваецца, як паказана ў прыкладзе ніжэй.
Санасіз охопляе шэф-пакухара, тэлы коралевы, карала і дварцовыя інтрыгі, без жаднага натхнення гуморам. У опісе рытму паказана камедый нарастанне ў першыя эпізоды, якое пазней пераходзіць у швыдкія жарты, пры чым дзеякія фанаты зазначаюць павтарэнне жартоў у апошніх 10–12 эпізодах. Раздзелэнне профіляў значыць, што кожны частак быў занадта малы, каб падтрымваць цэлы кантэкст, таму падбор работаў толькі ў межах аднаго аспекта, а не ўсіх аспектаў шоу.
Агент для адбору профіляў падачкі
Короткі запіты на кшталт „кумірныя к-драмы пра коралевскую сям’ю“ не відображаюць ні пасання глядача, ні яго настрою, і ўсё ж ў яных занадта мало інфармацыі, каб ўзяць з іх ўсё. Таму замест таго, каб прасіць пра дэталізаваны запіт, агент для інтэрв’ю ведае розмову з корыстнікам праз спецыяльны інструмент, збіраючы інформацыю пра упадабанняя ў жанры, традыцыі, эмоцыяныя тэмы, тэнпас і факторы, якія не дазволяюць выбраць певны варыянт, на адной фіксаванай сэтцы запытанняў. Выбір інструмента ёсць обавязковым, таму агент кожны раз вызывае інструмент і не намагаецца адпавядаць сам. Тып выходных даных — TasteProfile, які гарантуе правераны об’ект Pydantic з усіма семью полямі, заполненымі і з правым тыпам.
class TasteProfile(BaseModel):
genre_preference: str = Field(description="User's preference on drama genres")
trope_preference: str = Field(description="User's preference on storylines and tropes like fake dating etc.")
theme_preference: str = Field(description="Broader themes the user would like dramas to explore")
ending_preference: str = Field(description="User's preference on how the drama ends")
emotional_tolerance: str = Field(description="User's preference on how emotionally intense they'd like the drama to be")
pace_preference: str = Field(description="User's preference on drama's pacing")
dealbreakers: str = Field(description="Reasons/dealbreakers the user would not enjoy a drama")
@function_tool
def get_user_answers(question: str) -> str:
"""
Ask the user the given question and return their answer.
Args:
question: The question to ask the user
"""
answer = input(question)
return answer
interview_agent = Agent(name="Interview_Agent",
instructions=interview_system_prompt,
model="gpt-4o-mini",
tools=[get_user_answers],
model_settings=ModelSettings(tool_choice="required"),
output_type=TasteProfile)
Адаптаванне параду працоў па характэру смаку
Кожна ячэйка ў TasteProfile ўтвараее вольны тэкст, і вікорыстоўванне гэтага об’екта як адной запыткі не ўзгодзіцца. Семь ячэек апісваюць разныя сэмантычныя віміры, і ўз’еднанне іх дае слабкае захаванне інформацыі, якое не адпавядае жаднаму конкрэтнаму віміру сільна.
Агент-канструэнт запытак берае TasteProfile і перапісвае вольны тэкст на мову, якая падходзіць для пошуку. Ён дзеліць семь ячэек на два чы тры фокусаваныя запыткі: адну для тэмаў, адну для жанру і трапаў, і адну для тэндэнцыі і тону, пры чым кожны запыткі ведае пошук у сваім сае вімірэ. Гэта супамінае тэхніку расшырэння запытак, якая часта вікорыстоўваецца ў RAG для падвышэння кількасці аднаходзячыхся рэзультатаў. Значэнне параметра temperature задаецца як 0, таму модэль на кожным кроцы выбірае токен з найвышэйшай верыятнасцю, а фіксаваны seed робіць працэс выбору якомога болей дэтэрміністычным. Той самы парад працоў па характэру смаку павінен даўаць тыя ж запыткі па кожным запуску.
Фюзыя процэсаў аднаходжэння і ранкіравання
Кожны запіт адпрацоўваецца за дапамою таго ж модэлю text-embedding-3-small, які і для дакументаў з драмамі, таму яны аба ўтвараюць адну вектарную прастору. ChromaDB адрабатвае пошук на адпоўненасці косай дзейнасці і вяртае 20 самых схожых дакументаў на кожны запіт. Калі ў профіле ёсць два чы тры запіты, то гэта дае два чы тры спісы з 20 драмамі, якія можаць перакрывацца.
Reciprocal Rank Fusion з’едынае іх. Цэў популярны алгорытм для з’едынэння кальколька незалежных спісаў рэзультатаў у адны значымы, і ён надае прыорытэт драмам, якія займаюць высокія месцы ў кальколька семантычных вімэраў. Рэйтынг кожной драмы вылічаецца на адвесе ўсіх яе месцаў у спісах, па формуле 1 / (k + ранг). Паколькі спісы маюць фіксаваную дужыну, k задаецца равным 20, што дазволяе розніцу 2 разы межы самым высокім і самым нізкім рангамі.
Складаныя спісы яшчэ ігнаруюць популярнасць і свежасць. Таму сумарны бал складаецца з RRF, нормалізаванага сярэга галасоў і показніка свежасці, пры чым вагі для кожнага з эўлементаў вылучаюцца праз эксперыменты. Dla RRF назначаецца значэнне 0,7, таму рэлевантнасць да заявленых праганаў мае перавагу, а для сярэга галасоў і показніка свежасці назначаецца 0,25 і 0,05 адпаведна, каб дадаць інфармацыю пра якасць і свежасць.
Фінальная перацэнка LLM
Рангаваны спіс стае кантэкстам, які надаецца LLM, які выбірае чатыры драмы, якія наіболей адпавядаюць праганам карыстальніка, і прабывае персоналізаваную ацэнку кожнай з іх. Гэтае адпаведзенне і ёсць рэкамендацыяй, яю бачыць карыстальніка.
Дваэтапны дзеянні ўзначальна. Процэс выкарыстоўвання інфармацыі налаштаваны на ўсвядомленне і охопляе шырокі спектар, тады кожны правдападобна рэлевантны фільм потрапляе ў спіс кандыдаў. LLM налаштаваны на тачнасць і выконвае такія адгукі, якія арыметыка не можа даць. Жадная функцыя ацэнкі не зазначыць, што драма пра шафара, які знаходзіць мэту праз гатованне, падходзіць глядачу, які хацеў історыю пра сэнс у повсякдзеннай працы. Мадэль, якая чытае профіль падабанняў пад цэлым дакументам драмы, можа стварыць такую зв’язку, а раздзяленне дапамагае кожнаму этапу выконваць тое, у чым ён найкрашчы.
def refine_recommendations(taste_profile, ranked_list):
print('Refining Recommendations ---')
user_prompt = create_drama_user_prompt(taste_profile=taste_profile, ranked_drama_list=ranked_list[:20])
messages = [{"role": "system", "content": assessment_prompt},
{"role": "user", "content": user_prompt}]
client = OpenAI()
response = client.beta.chat.completions.parse(model='gpt-4o-mini',
messages=messages,
response_format=AssessmentOutput,
temperature=0,
seed=42)
result = response.choices[0].message.parsed
return result
Рэзультат
У дэмаверыянтным працаванні персонаж, які спяржаны з пакалункамі, атрымляе The Glory як свой главны варыянт. Revenge of Others, High School Return of a Gangster і Weak Hero займаюць тры засталія месца. Кожны выбор супакоўваецца адгукамі «Мне спадабаецца», «Мне гэта пасабляе» або «Сумешана адгука», а таксама конкрэтнымі прычынамі, чаму ён падходзіць па вкусу пераглядача. Увага, выказаная ў дыскусіях фанатоў, паказвае тое, што пераглядач можа не спадабацца, напрыклад, суперэкстрэмальны фінал або павтаральны эпізод. Паколькі адказы базуюць як на персоналіях, так і на рэальных адгуках спяльноты, яны здаюцца персональнымі, а не універсальнымі. Інтэрфейс — гэта прыклад прыемплы Gradio, а дапамога AI-асістэнта была важна для яго HTML-фарматавання.
Далей для такой сістэмы выконуецца ацэнка. Рэйтынгі аддакласнасці RAGAS і рэлевантнасці адказоў паказваюць, чы рэспансы застаюцца прыкладзенымі да атрыманага кантэксту, а аналіз стабільнасці паказвае, насколькі аднаковыя ўсунення застаюцца праз паўтарныя запускі з тым самым профілем падабенства. Фіксаваныя значэнні „seeds“ і тэмпература 0 на этапы запіту ўчыняюць гэты другі пераконтроль значымым, таму што будзь-яя варыяцыя тады выходзіць з процэса атрымання і фінальнага апрацоўкі LLM, а не з формулювання запіту.
Спаднія матэрыялы
- Layer by Layer: Designing a Real RAG Retrieval Pipeline for AI Agents — Планаванне каналу RAG з выкарыстоўванням контрактов SourceDocument і DocumentChunk, чанкавання, імбеддынгу і ChromaDB, усё гэта пад кераваннем RetrievalService, які падтрымлівае стабільнасць агента.