paint-brush
AI Chatbot Membantu Mengurus Komuniti Telegram Seperti Prooleh@slavasobolev
sejarah baru

AI Chatbot Membantu Mengurus Komuniti Telegram Seperti Pro

oleh Iaroslav Sobolev12m2025/01/09
Read on Terminal Reader

Terlalu panjang; Untuk membaca

Chatbot Telegram akan mencari jawapan kepada soalan dengan mengekstrak maklumat daripada sejarah mesej sembang. Ia akan mencari jawapan yang berkaitan dengan mencari jawapan yang paling hampir dalam sejarah. Bot meringkaskan hasil carian dengan bantuan LLM dan mengembalikan kepada pengguna jawapan akhir dengan pautan ke mesej yang berkaitan.
featured image - AI Chatbot Membantu Mengurus Komuniti Telegram Seperti Pro
Iaroslav Sobolev HackerNoon profile picture
0-item
1-item


Komuniti, sembang dan forum ialah sumber maklumat yang tidak berkesudahan mengenai pelbagai topik. Slack sering menggantikan dokumentasi teknikal, dan komuniti Telegram dan Discord membantu dengan soalan permainan, permulaan, crypto dan perjalanan. Walaupun terdapat kaitan maklumat secara langsung, ia selalunya sangat tidak tersusun, menjadikannya sukar untuk mencari. Dalam artikel ini, kami akan meneroka kerumitan melaksanakan bot Telegram yang akan mencari jawapan kepada soalan dengan mengekstrak maklumat daripada sejarah mesej sembang.


Berikut adalah cabaran yang menanti kita:

  • Cari mesej yang berkaitan . Jawapannya mungkin tersebar di beberapa dialog orang atau dalam pautan ke sumber luar.

  • Mengabaikan offtopic . Terdapat banyak spam dan luar topik, yang harus kita pelajari untuk mengenal pasti dan menapis

  • Pengutamaan . Maklumat menjadi ketinggalan zaman. Bagaimana anda tahu jawapan yang betul sehingga kini?


Aliran pengguna bot sembang asas yang akan kami laksanakan

  1. Pengguna bertanyakan soalan kepada bot
  2. Bot mencari jawapan terdekat dalam sejarah mesej
  3. Bot meringkaskan hasil carian dengan bantuan LLM
  4. Mengembalikan jawapan akhir kepada pengguna dengan pautan ke mesej yang berkaitan


Mari kita berjalan melalui peringkat utama aliran pengguna ini dan menyerlahkan cabaran utama yang akan kita hadapi.

Penyediaan data

Untuk menyediakan sejarah mesej untuk carian, kita perlu mencipta pembenaman mesej ini - perwakilan teks bervektor. Semasa berurusan dengan artikel wiki atau dokumen PDF, kami akan membahagikan teks kepada perenggan dan mengira Pembenaman Ayat untuk setiap satu.


Walau bagaimanapun, kita harus mengambil kira keistimewaan yang biasa untuk sembang dan bukan untuk teks yang tersusun dengan baik:


  • Berbilang pesanan ringkas berikutnya daripada pengguna tunggal. Dalam kes sedemikian, adalah berbaloi untuk menggabungkan mesej ke dalam blok teks yang lebih besar
  • Beberapa mesej adalah sangat panjang dan merangkumi beberapa topik yang berbeza
  • Mesej dan spam yang tidak bermakna harus kita tapis
  • Pengguna boleh membalas tanpa menandai mesej asal. Soalan dan jawapan boleh dipisahkan dalam sejarah sembang dengan banyak mesej lain
  • Pengguna boleh bertindak balas dengan pautan ke sumber luaran (cth, artikel atau dokumen)


Seterusnya, kita harus memilih model benam. Terdapat banyak model yang berbeza untuk membina benam, dan beberapa faktor mesti dipertimbangkan semasa memilih model yang betul.


  • Dimensi benam . Lebih tinggi ia, lebih banyak nuansa model boleh belajar daripada data. Carian akan menjadi lebih tepat tetapi memerlukan lebih banyak memori dan sumber pengiraan.
  • Set data di mana model pembenaman dilatih. Ini akan menentukan, sebagai contoh, sejauh mana ia menyokong bahasa yang anda perlukan.


Untuk meningkatkan kualiti hasil carian, kami boleh mengkategorikan mesej mengikut topik. Contohnya, dalam sembang yang didedikasikan untuk pembangunan bahagian hadapan, pengguna boleh membincangkan topik seperti: CSS, perkakas, React, Vue, dll. Anda boleh menggunakan kaedah pemodelan topik LLM (lebih mahal) atau klasik daripada perpustakaan seperti BERTopic untuk mengklasifikasikan mesej mengikut topik.


Kami juga memerlukan pangkalan data vektor untuk menyimpan embeddings dan meta-maklumat (pautan ke siaran asal, kategori, tarikh). Banyak storan vektor, seperti FAISS , Milvus atau Pinecone , wujud untuk tujuan ini. PostgreSQL biasa dengan sambungan pgvector juga akan berfungsi.

Memproses soalan pengguna

Untuk menjawab soalan pengguna, kami perlu menukar soalan kepada bentuk yang boleh dicari, dan dengan itu mengira pembenaman soalan itu, serta menentukan niatnya.


Hasil carian semantik pada soalan boleh menjadi soalan yang serupa daripada sejarah sembang tetapi bukan jawapan kepada soalan tersebut.


Untuk imporve ini, kita boleh menggunakan salah satu teknik pengoptimuman HyDE (hypothetical document embeddings) yang popular. Ideanya adalah untuk menjana jawapan hipotesis kepada soalan menggunakan LLM dan kemudian mengira pembenaman jawapan. Pendekatan ini dalam beberapa kes membolehkan carian yang lebih tepat dan cekap untuk mesej yang berkaitan antara jawapan dan bukannya soalan.


Mencari mesej yang paling relevan

Sebaik sahaja kami mempunyai soalan membenamkan, kami boleh mencari mesej terdekat dalam pangkalan data. LLM mempunyai tetingkap konteks yang terhad, jadi kami mungkin tidak dapat menambah semua hasil carian jika terdapat terlalu banyak. Timbul persoalan bagaimana untuk mengutamakan jawapan. Terdapat beberapa pendekatan untuk ini:


  • Skor terkini . Lama kelamaan, maklumat menjadi lapuk, dan untuk mengutamakan mesej baharu, anda boleh mengira skor kekinian menggunakan formula mudah 1 / (today - date_of_message + 1)


  • Penapisan metadata. (anda perlu mengenal pasti topik soalan dan siaran). Ini membantu untuk mengecilkan carian anda, meninggalkan hanya siaran yang berkaitan dengan topik yang anda cari


  • Carian teks penuh . Carian teks penuh klasik, yang disokong dengan baik oleh semua pangkalan data popular, kadangkala boleh membantu.


  • Kedudukan semula . Sebaik sahaja kami telah menemui jawapan, kami boleh menyusunnya mengikut tahap 'kedekatan' dengan soalan, meninggalkan hanya yang paling relevan. Penarafan semula memerlukan model CrossEncoder , atau kami boleh menggunakan API penarafan semula, contohnya, daripada Cohere .


Menjana tindak balas akhir

Selepas mencari dan mengisih dalam langkah sebelumnya, kami boleh menyimpan 50-100 siaran paling berkaitan yang sesuai dengan konteks LLM.


Langkah seterusnya ialah membuat gesaan yang jelas dan ringkas untuk LLM menggunakan pertanyaan asal pengguna dan hasil carian. Ia harus menyatakan kepada LLM cara menjawab soalan, pertanyaan pengguna dan konteks - mesej berkaitan yang kami temui. Untuk tujuan ini, adalah penting untuk mempertimbangkan aspek berikut:


  • System Prompt ialah arahan kepada model yang menerangkan cara ia harus memproses maklumat. Sebagai contoh, anda boleh memberitahu LLM untuk mencari jawapan hanya dalam data yang disediakan.


  • Panjang konteks - panjang maksimum mesej yang boleh kami gunakan sebagai input. Kita boleh mengira bilangan token menggunakan tokenizer yang sepadan dengan model yang kita gunakan. Sebagai contoh, OpenAI menggunakan Tiktoken.


  • Hiperparameter model - sebagai contoh, suhu bertanggungjawab untuk sejauh mana kreatif model itu dalam tindak balasnya.


  • Pilihan model . Ia tidak selalu bernilai membayar lebih untuk model yang paling besar dan berkuasa. Masuk akal untuk menjalankan beberapa ujian dengan model yang berbeza dan membandingkan keputusannya. Dalam sesetengah kes, model kurang intensif sumber akan melakukan kerja jika mereka tidak memerlukan ketepatan yang tinggi.


Perlaksanaan

Sekarang mari cuba laksanakan langkah-langkah ini dengan NodeJS. Berikut ialah timbunan teknologi yang akan saya gunakan:



Mari langkau langkah asas memasang kebergantungan dan persediaan bot telegram dan teruskan ke ciri yang paling penting. Skema pangkalan data, yang akan diperlukan kemudian:


 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; }


Pisahkan dialog pengguna kepada beberapa bahagian

Membahagikan dialog panjang antara berbilang pengguna kepada beberapa bahagian bukanlah tugas yang paling remeh.


Malangnya, pendekatan lalai seperti RecursiveCharacterTextSplitter , tersedia dalam pustaka Langchain, tidak mengambil kira semua keanehan khusus untuk berbual. Walau bagaimanapun, dalam kes Telegram, kami boleh memanfaatkan threads Telegram yang mengandungi mesej berkaitan dan balasan yang dihantar oleh pengguna.


Setiap kali kumpulan mesej baharu tiba dari bilik sembang, bot kami perlu melakukan beberapa langkah:


  • Tapis mesej ringkas mengikut senarai perkataan henti (cth 'hello', 'bye', dsb.)
  • Gabungkan mesej daripada seorang pengguna jika ia dihantar secara berturut-turut dalam tempoh masa yang singkat
  • Himpunkan semua mesej yang dimiliki oleh urutan yang sama
  • Gabungkan kumpulan mesej yang diterima ke dalam blok teks yang lebih besar dan bahagikan lagi blok teks ini kepada beberapa bahagian menggunakan RecursiveCharacterTextSplitter
  • Kirakan benam bagi setiap ketul
  • Kekalkan potongan teks dalam pangkalan data bersama-sama dengan pembenaman dan pautannya ke mesej asal


 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

Seterusnya, kita perlu mengira benam untuk setiap ketulan. Untuk ini kita boleh menggunakan model 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(); }



Menjawab soalan pengguna

Untuk menjawab soalan pengguna, kami mula-mula mengira pembenaman soalan dan kemudian mencari mesej yang paling berkaitan dalam sejarah sembang


 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); }



Kemudian kami menyusun semula hasil carian dengan bantuan model penyusunan semula 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; }



Seterusnya, minta LLM menjawab soalan pengguna dengan meringkaskan hasil carian. Versi ringkas pemprosesan pertanyaan carian akan kelihatan seperti ini:


 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.. `; }



Penambahbaikan selanjutnya

Walaupun selepas semua pengoptimuman, kami mungkin merasakan jawapan bot yang dikuasakan LLM adalah tidak ideal dan tidak lengkap. Apa lagi yang boleh diperbaiki?


  • Untuk siaran pengguna yang menyertakan pautan, kami juga boleh menghuraikan kandungan halaman web dan dokumen pdf.

  • Penghalaan Pertanyaan — mengarahkan pertanyaan pengguna ke sumber data, model atau indeks yang paling sesuai berdasarkan niat dan konteks pertanyaan untuk mengoptimumkan ketepatan, kecekapan dan kos.

  • Kami boleh memasukkan sumber yang berkaitan dengan topik bilik sembang kepada indeks carian — di tempat kerja, ia boleh berupa dokumentasi daripada Confluence, untuk sembang visa, tapak web konsulat dengan peraturan, dsb.

  • RAG-Evaluation - Kami perlu menyediakan saluran paip untuk menilai kualiti respons bot kami