paint-brush
AI Chatbot ndihmon në menaxhimin e komuniteteve të Telegramit si një profesionistnga@slavasobolev
Histori e re

AI Chatbot ndihmon në menaxhimin e komuniteteve të Telegramit si një profesionist

nga Iaroslav Sobolev12m2025/01/09
Read on Terminal Reader

Shume gjate; Te lexosh

Chatbot i Telegram do të gjejë përgjigje për pyetjet duke nxjerrë informacion nga historia e mesazheve të bisedës. Ai do të kërkojë përgjigjet përkatëse duke gjetur përgjigjet më të afërta në histori. Bot përmbledh rezultatet e kërkimit me ndihmën e LLM dhe i kthen përdoruesit përgjigjen përfundimtare me lidhje me mesazhet përkatëse.
featured image - AI Chatbot ndihmon në menaxhimin e komuniteteve të Telegramit si një profesionist
Iaroslav Sobolev HackerNoon profile picture
0-item
1-item


Komunitetet, bisedat dhe forumet janë një burim i pafund informacioni për një mori temash. Slack shpesh zëvendëson dokumentacionin teknik dhe komunitetet Telegram dhe Discord ndihmojnë me pyetjet e lojrave, startup-eve, kriptove dhe udhëtimeve. Pavarësisht nga rëndësia e informacionit të dorës së parë, ai shpesh është shumë i pastrukturuar, duke e bërë të vështirë kërkimin. Në këtë artikull, ne do të eksplorojmë kompleksitetin e zbatimit të një roboti Telegram që do të gjejë përgjigje për pyetjet duke nxjerrë informacion nga historia e mesazheve të bisedës.


Këtu janë sfidat që na presin:

  • Gjeni mesazhe përkatëse . Përgjigja mund të shpërndahet në dialogun e disa njerëzve ose në një lidhje me burimet e jashtme.

  • Duke injoruar offtopic . Ka shumë spam dhe jashtë temave, të cilat duhet të mësojmë t'i identifikojmë dhe filtrojmë

  • Prioritizimi . Informacioni bëhet i vjetëruar. Si e dini përgjigjen e saktë deri më sot?


Rrjedha bazë e përdoruesit të chatbot që do të zbatojmë

  1. Përdoruesi i bën një pyetje robotit
  2. Bot gjen përgjigjet më të afërta në historinë e mesazheve
  3. Bot përmbledh rezultatet e kërkimit me ndihmën e LLM
  4. I kthen përdoruesit përgjigjen përfundimtare me lidhje me mesazhet përkatëse


Le të ecim nëpër fazat kryesore të këtij fluksi të përdoruesve dhe të nxjerrim në pah sfidat kryesore me të cilat do të përballemi.

Përgatitja e të dhënave

Për të përgatitur një histori mesazhesh për kërkim, ne duhet të krijojmë ngulitje të këtyre mesazheve - paraqitje të tekstit të vektorizuar. Ndërkohë që kemi të bëjmë me një artikull wiki ose dokument PDF, ne do ta ndajmë tekstin në paragrafë dhe do të llogarisim ngulitjen e fjalive për secilin.


Sidoqoftë, duhet të kemi parasysh veçoritë që janë tipike për bisedat dhe jo për tekstin e strukturuar mirë:


  • Shumë mesazhe të shkurtra pasuese nga një përdorues i vetëm. Në raste të tilla, ia vlen të kombinohen mesazhet në blloqe teksti më të mëdha
  • Disa nga mesazhet janë shumë të gjata dhe mbulojnë disa tema të ndryshme
  • Mesazhet dhe spamet e pakuptimta duhet t'i filtrojmë
  • Përdoruesi mund të përgjigjet pa etiketuar mesazhin origjinal. Një pyetje dhe një përgjigje mund të ndahen në historikun e bisedës nga shumë mesazhe të tjera
  • Përdoruesi mund të përgjigjet me një lidhje për një burim të jashtëm (p.sh. një artikull ose dokument)


Më pas, duhet të zgjedhim modelin e ngulitjes. Ka shumë modele të ndryshme për ndërtimin e ngulitjeve, dhe disa faktorë duhet të merren parasysh kur zgjidhni modelin e duhur.


  • Dimensioni i ngulitjes . Sa më i lartë të jetë, aq më shumë nuanca modeli mund të mësojë nga të dhënat. Kërkimi do të jetë më i saktë, por kërkon më shumë memorie dhe burime llogaritëse.
  • Grupi i të dhënave mbi të cilin është trajnuar modeli i ngulitjes. Kjo do të përcaktojë, për shembull, sa mirë e mbështet gjuhën që ju nevojitet.


Për të përmirësuar cilësinë e rezultateve të kërkimit, ne mund t'i kategorizojmë mesazhet sipas temës. Për shembull, në një bisedë kushtuar zhvillimit të frontendit, përdoruesit mund të diskutojnë tema të tilla si: CSS, vegla pune, React, Vue, etj. Ju mund të përdorni LLM (më të shtrenjta) ose metoda klasike të modelimit të temave nga bibliotekat si BERTopic për të klasifikuar mesazhet sipas temave.


Do të na duhet gjithashtu një bazë të dhënash vektoriale për të ruajtur ngulitje dhe meta-informacione (lidhje me postimet origjinale, kategoritë, datat). Shumë depo vektoriale, të tilla si FAISS , Milvus ose Pinecone , ekzistojnë për këtë qëllim. Një PostgreSQL e rregullt me shtrirjen pgvector do të funksionojë gjithashtu.

Përpunimi i një pyetjeje të përdoruesit

Për t'iu përgjigjur pyetjes së një përdoruesi, ne duhet ta konvertojmë pyetjen në një formë të kërkueshme, dhe kështu të llogarisim futjen e pyetjes, si dhe të përcaktojmë qëllimin e saj.


Rezultati i një kërkimi semantik për një pyetje mund të jetë pyetje të ngjashme nga historia e bisedës, por jo përgjigjet e tyre.


Për ta përmirësuar këtë, ne mund të përdorim një nga teknikat më të njohura të optimizimit HyDE (ngulitje hipotetike të dokumenteve). Ideja është të gjenerohet një përgjigje hipotetike për një pyetje duke përdorur LLM dhe më pas të llogaritet përfshirja e përgjigjes. Kjo qasje në disa raste lejon kërkim më të saktë dhe efikas të mesazheve përkatëse midis përgjigjeve sesa pyetjeve.


Gjetja e mesazheve më të rëndësishme

Pasi të kemi futjen e pyetjes, mund të kërkojmë mesazhet më të afërta në bazën e të dhënave. LLM ka një dritare konteksti të kufizuar, kështu që mund të mos jemi në gjendje të shtojmë të gjitha rezultatet e kërkimit nëse ka shumë. Shtrohet pyetja se si t'u jepet përparësi përgjigjeve. Ka disa qasje për këtë:


  • Rezultati i fundit . Me kalimin e kohës, informacioni bëhet i vjetëruar dhe për t'i dhënë përparësi mesazheve të reja, mund të llogaritni rezultatin e fundit duke përdorur formulën e thjeshtë 1 / (today - date_of_message + 1)


  • Filtrimi i meta të dhënave. (duhet të identifikoni temën e pyetjes dhe postimet). Kjo ndihmon për të ngushtuar kërkimin tuaj, duke lënë vetëm ato postime që janë të rëndësishme për temën që kërkoni


  • Kërkimi me tekst të plotë . Kërkimi klasik me tekst të plotë, i cili mbështetet mirë nga të gjitha bazat e të dhënave të njohura, ndonjëherë mund të jetë i dobishëm.


  • Rirenditje . Pasi të kemi gjetur përgjigjet, mund t'i renditim ato sipas shkallës së 'afërsisë' me pyetjen, duke lënë vetëm ato më të rëndësishmet. Rirenditja do të kërkojë një model CrossEncoder , ose mund të përdorim API-në e rirenditjes, për shembull, nga Cohere .


Gjenerimi i përgjigjes përfundimtare

Pas kërkimit dhe renditjes në hapin e mëparshëm, ne mund të mbajmë 50-100 postimet më të rëndësishme që do të përshtaten në kontekstin LLM.


Hapi tjetër është të krijoni një kërkesë të qartë dhe koncize për LLM duke përdorur pyetjen origjinale të përdoruesit dhe rezultatet e kërkimit. Ai duhet t'i specifikojë LLM-së se si t'i përgjigjet pyetjes, pyetjes së përdoruesit dhe kontekstit - mesazhet përkatëse që gjetëm. Për këtë qëllim, është thelbësore të merren parasysh këto aspekte:


  • System Prompt janë udhëzime për modelin që shpjegojnë se si duhet të përpunojë informacionin. Për shembull, mund t'i thoni LLM të kërkojë një përgjigje vetëm në të dhënat e dhëna.


  • Gjatësia e kontekstit - gjatësia maksimale e mesazheve që mund të përdorim si hyrje. Ne mund të llogarisim numrin e shenjave duke përdorur tokenizuesin që korrespondon me modelin që përdorim. Për shembull, OpenAI përdor Tiktoken.


  • Hiperparametrat e modelit - për shembull, temperatura është përgjegjëse për sa krijues do të jetë modeli në përgjigjet e tij.


  • Zgjedhja e modelit . Nuk ia vlen gjithmonë të paguani shumë për modelin më të madh dhe më të fuqishëm. Ka kuptim të kryhen disa teste me modele të ndryshme dhe të krahasohen rezultatet e tyre. Në disa raste, modelet me më pak burime intensive do ta bëjnë punën nëse nuk kërkojnë saktësi të lartë.


Zbatimi

Tani le të përpiqemi t'i zbatojmë këto hapa me NodeJS. Këtu është grupi i teknologjisë që do të përdor:


  • NodeJS dhe TypeScript
  • Grammy - Korniza e robotëve të Telegramit
  • PostgreSQL - si një ruajtje kryesore për të gjitha të dhënat tona
  • pgvector - Shtesa PostgreSQL për ruajtjen e ngulitjes së tekstit dhe mesazheve
  • OpenAI API - LLM и modelet e embeddings
  • Mikro-ORM - për të thjeshtuar ndërveprimet db


Le të kapërcejmë hapat bazë të instalimit të varësive dhe konfigurimit të botit të telegramit dhe të kalojmë drejtpërdrejt te veçoritë më të rëndësishme. Skema e bazës së të dhënave, e cila do të nevojitet më vonë:


 import { Entity, Enum, Property, Unique } from '@mikro-orm/core'; @Entity({ tableName: 'groups' }) export class Group extends BaseEntity { @PrimaryKey() id!: number; @Property({ type: 'bigint' }) channelId!: number; @Property({ type: 'text', nullable: true }) title?: string; @Property({ type: 'json' }) attributes!: Record<string, unknown>; } @Entity({ tableName: 'messages' }) export class Message extends BaseEntity { @PrimaryKey() id!: number; @Property({ type: 'bigint' }) messageId!: number; @Property({ type: TextType }) text!: string; @Property({ type: DateTimeType }) date!: Date; @ManyToOne(() => Group, { onDelete: 'cascade' }) group!: Group; @Property({ type: 'string', nullable: true }) fromUserName?: string; @Property({ type: 'bigint', nullable: true }) replyToMessageId?: number; @Property({ type: 'bigint', nullable: true }) threadId?: number; @Property({ type: 'json' }) attributes!: { raw: Record<any, any>; }; } @Entity({ tableName: 'content_chunks' }) export class ContentChunk extends BaseEntity { @PrimaryKey() id!: number; @ManyToOne(() => Group, { onDelete: 'cascade' }) group!: Group; @Property({ type: TextType }) text!: string; @Property({ type: VectorType, length: 1536, nullable: true }) embeddings?: number[]; @Property({ type: 'int' }) tokens!: number; @Property({ type: new ArrayType<number>((i: string) => +i), nullable: true }) messageIds?: number[]; @Property({ persist: false, nullable: true }) distance?: number; }


Ndani dialogët e përdoruesve në copa

Ndarja e dialogëve të gjatë midis përdoruesve të shumtë në copa nuk është detyra më e parëndësishme.


Fatkeqësisht, qasjet e paracaktuar si RecursiveCharacterTextSplitter , të disponueshme në bibliotekën Langchain, nuk marrin parasysh të gjitha veçoritë specifike të bisedës. Megjithatë, në rastin e Telegramit, ne mund të përfitojmë nga threads e Telegramit që përmbajnë mesazhe të lidhura dhe përgjigjet e dërguara nga përdoruesit.


Sa herë që vjen një grup i ri mesazhesh nga dhoma e bisedës, roboti ynë duhet të kryejë disa hapa:


  • Filtro mesazhet e shkurtra sipas një liste fjalësh ndaluese (p.sh. 'përshëndetje', 'mirupafshim', etj.)
  • Bashkoni mesazhet nga një përdorues nëse ato janë dërguar në mënyrë të njëpasnjëshme brenda një periudhe të shkurtër kohe
  • Gruponi të gjitha mesazhet që i përkasin të njëjtit thread
  • Bashkoni grupet e mesazheve të marra në blloqe teksti më të mëdha dhe ndani më tej këto blloqe teksti në copa duke përdorur RecursiveCharacterTextSplitter
  • Llogaritni futjet për secilën pjesë
  • Vazhdoni copat e tekstit në bazën e të dhënave së bashku me ngulitje të tyre dhe lidhjet me mesazhet origjinale


 class ChatContentSplitter { constructor( private readonly splitter RecursiveCharacterTextSplitter, private readonly longMessageLength = 200 ) {} public async split(messages: EntityDTO<Message>[]): Promise<ContentChunk[]> { const filtered = this.filterMessage(messages); const merged = this.mergeUserMessageSeries(filtered); const threads = this.toThreads(merged); const chunks = await this.threadsToChunks(threads); return chunks; } toThreads(messages: EntityDTO<Message>[]): EntityDTO<Message>[][] { const threads = new Map<number, EntityDTO<Message>[]>(); const orphans: EntityDTO<Message>[][] = []; for (const message of messages) { if (message.threadId) { let thread = threads.get(message.threadId); if (!thread) { thread = []; threads.set(message.threadId, thread); } thread.push(message); } else { orphans.push([message]); } } return [Array.from(threads.values()), ...orphans]; } private async threadsToChunks( threads: EntityDTO<Message>[][], ): Promise<ContentChunk[]> { const result: ContentChunk[] = []; for await (const thread of threads) { const content = thread.map((m) => this.dtoToString(m)) .join('\n') const texts = await this.splitter.splitText(content); const messageIds = thread.map((m) => m.id); const chunks = texts.map((text) => new ContentChunk(text, messageIds) ); result.push(...chunks); } return result; } mergeMessageSeries(messages: EntityDTO<Message>[]): EntityDTO<Message>[] { const result: EntityDTO<Message>[] = []; let next = messages[0]; for (const message of messages.slice(1)) { const short = message.text.length < this.longMessageLength; const sameUser = current.fromId === message.fromId; const subsequent = differenceInMinutes(current.date, message.date) < 10; if (sameUser && subsequent && short) { next.text += `\n${message.text}`; } else { result.push(current); next = message; } } return result; } // .... }


Embeddings

Tjetra, ne duhet të llogarisim ngulitje për secilën prej pjesëve. Për këtë ne mund të përdorim modelin OpenAI text-embedding-3-large


 public async getEmbeddings(chunks: ContentChunks[]) { const chunked = groupArray(chunks, 100); for await (const chunk of chunks) { const res = await this.openai.embeddings.create({ input: c.text, model: 'text-embedding-3-large', encoding_format: "float" }); chunk.embeddings = res.data[0].embedding } await this.orm.em.flush(); }



Duke iu përgjigjur pyetjeve të përdoruesve

Për t'iu përgjigjur pyetjes së një përdoruesi, ne fillimisht numërojmë futjen e pyetjes dhe më pas gjejmë mesazhet më të rëndësishme në historikun e bisedës


 public async similaritySearch(embeddings: number[], groupId; number): Promise<ContentChunk[]> { return this.orm.em.qb(ContentChunk) .where({ embeddings: { $ne: null }, group: this.orm.em.getReference(Group, groupId) }) .orderBy({[l2Distance('embedding', embedding)]: 'ASC'}) .limit(100); }



Pastaj ne i rirenditim rezultatet e kërkimit me ndihmën e modelit të rirenditjes së Cohere


 public async rerank(query: string, chunks: ContentChunk[]): Promise<ContentChunk> { const { results } = await cohere.v2.rerank({ documents: chunks.map(c => c.text), query, model: 'rerank-v3.5', }); const reranked = Array(results.length).fill(null); for (const { index } of results) { reranked[index] = chunks[index]; } return reranked; }



Më pas, kërkoni nga LLM t'i përgjigjet pyetjes së përdoruesit duke përmbledhur rezultatet e kërkimit. Versioni i thjeshtuar i përpunimit të një pyetje kërkimi do të duket si ky:


 public async search(query: string, group: Group) { const queryEmbeddings = await this.getEmbeddings(query); const chunks = this.chunkService.similaritySearch(queryEmbeddings, group.id); const reranked = this.cohereService.rerank(query, chunks); const completion = await this.openai.chat.completions.create({ model: 'gpt-4-turbo', temperature: 0, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: this.userPromptTemplate(query, reranked) }, ] ] return completion.choices[0].message; } // naive prompt public userPromptTemplate(query: string, chunks: ContentChunk[]) { const history = chunks .map((c) => `${c.text}`) .join('\n----------------------------\n') return ` Answer the user's question: ${query} By summarizing the following content: ${history} Keep your answer direct and concise. Provide refernces to the corresponding messages.. `; }



Përmirësime të mëtejshme

Edhe pas të gjitha optimizimeve, ne mund të ndiejmë se përgjigjet e robotëve me fuqi LLM janë jo ideale dhe jo të plota. Çfarë tjetër mund të përmirësohet?


  • Për postimet e përdoruesve që përfshijnë lidhje, ne gjithashtu mund të analizojmë faqet e internetit dhe përmbajtjen e dokumenteve pdf.

  • Query-Routing - drejtimi i pyetjeve të përdoruesve drejt burimit, modelit ose indeksit më të përshtatshëm të të dhënave bazuar në qëllimin dhe kontekstin e pyetjes për të optimizuar saktësinë, efikasitetin dhe koston.

  • Ne mund të përfshijmë burime të rëndësishme për temën e dhomës së bisedës në indeksin e kërkimit - në punë, mund të jetë dokumentacion nga Confluence, për bisedat për viza, faqet e internetit të konsullatës me rregulla, etj.

  • RAG-Evaluation - Ne duhet të krijojmë një tubacion për të vlerësuar cilësinë e përgjigjeve të robotit tonë