paint-brush
AI Chatbot Telegram жамааттарын Pro сыяктуу башкарууга жардам береттарабынан@slavasobolev
Жаңы тарых

AI Chatbot Telegram жамааттарын Pro сыяктуу башкарууга жардам берет

тарабынан Iaroslav Sobolev12m2025/01/09
Read on Terminal Reader

өтө узун; Окуу

Telegram чатботу чат билдирүүлөрүнүн тарыхынан маалыматтарды алуу менен суроолорго жооп табат. Ал тарыхтагы эң жакын жоопторду табуу менен тиешелүү жоопторду издейт. Бот LLM жардамы менен издөө натыйжаларын жалпылайт жана колдонуучуга тиешелүү билдирүүлөргө шилтемелер менен акыркы жоопту кайтарып берет.
featured image - AI Chatbot Telegram жамааттарын Pro сыяктуу башкарууга жардам берет
Iaroslav Sobolev HackerNoon profile picture
0-item
1-item


Коомчулуктар, чаттар жана форумдар көптөгөн темалар боюнча чексиз маалымат булагы болуп саналат. Slack көбүнчө техникалык документтерди алмаштырат, ал эми Telegram жана Discord жамааттары оюндар, стартаптар, крипто жана саякат суроолоруна жардам берет. Алгачкы маалыматтын актуалдуулугуна карабастан, ал көп учурда структураланбагандыктан, издөөнү кыйындатат. Бул макалада биз чат билдирүүлөрүнүн тарыхынан маалыматтарды алуу менен суроолорго жооп таба турган Telegram ботун ишке ашыруунун татаалдыктарын изилдейбиз.


Бул жерде бизди күтүп турган кыйынчылыктар:

  • Тиешелүү билдирүүлөрдү табыңыз . Жооп бир нече адамдардын диалогунда же тышкы ресурстарга шилтемеде чачыранды болушу мүмкүн.

  • Оффтопияга көңүл бурбоо . Көптөгөн спам жана тышкаркы темалар бар, биз аларды аныктоону жана чыпкалоону үйрөнүшүбүз керек

  • Приоритетизация . Маалымат эскирип калат. Бүгүнкү күнгө чейин туура жоопту кайдан билесиз?


Биз ишке ашыра турган негизги чатбот колдонуучу агымы

  1. Колдонуучу ботко суроо берет
  2. Бот билдирүүлөрдүн тарыхындагы эң жакын жоопторду табат
  3. Бот LLM жардамы менен издөө натыйжаларын жалпылайт
  4. Колдонуучуга тиешелүү билдирүүлөргө шилтемелер менен акыркы жоопту кайтарат


Келгиле, бул колдонуучу агымынын негизги этаптары аркылуу басып өтүп, биз туш боло турган негизги кыйынчылыктарды белгилейли.

Маалыматтарды даярдоо

Издөө үчүн билдирүү таржымалын даярдоо үчүн, биз бул билдирүүлөрдүн кыстарууларын түзүшүбүз керек - векторланган текст өкүлчүлүктөрү. Вики макаласы же PDF документи менен иштөөдө биз текстти абзацтарга бөлүп, ар бири үчүн сүйлөмдүн кыстарылышын эсептейбиз.


Бирок, биз жакшы структураланган текст үчүн эмес, чаттар үчүн мүнөздүү болгон өзгөчөлүктөрдү эске алышыбыз керек:


  • Бир колдонуучудан бир нече кийинки кыска билдирүүлөр. Мындай учурларда, билдирүүлөрдү чоңураак текст блокторуна бириктирүү керек
  • Кээ бир билдирүүлөр өтө узун жана бир нече түрдүү темаларды камтыйт
  • Мааниси жок билдирүүлөрдү жана спамдарды биз чыпкалашыбыз керек
  • Колдонуучу оригиналдуу билдирүүнү белгилебестен жооп бере алат. Суроо менен жоопту чат тарыхында башка көптөгөн билдирүүлөр менен ажыратса болот
  • Колдонуучу тышкы ресурска шилтеме менен жооп бере алат (мисалы, макала же документ)


Андан кийин, биз орнотуу моделин тандоо керек. Кыймылдарды куруу үчүн көптөгөн ар кандай моделдер бар жана туура моделди тандоодо бир нече факторлорду эске алуу керек.


  • Киргизүү өлчөмү . Ал канчалык жогору болсо, модель маалыматтардан ошончолук көп нюанстарды үйрөнө алат. Издөө такыраак болот, бирок көбүрөөк эс жана эсептөө ресурстарын талап кылат.
  • Кыстаруу модели үйрөтүлгөн берилиштер топтому . Бул, мисалы, сизге керектүү тилди канчалык деңгээлде колдой турганын аныктайт.


Издөө натыйжаларынын сапатын жакшыртуу үчүн биз билдирүүлөрдү тема боюнча категорияларга бөлсөк болот. Мисалы, фронтендди иштеп чыгууга арналган чатта колдонуучулар төмөнкү темаларды талкуулай алышат: CSS, инструмент, React, Vue ж.б. темалар.


Бизге ошондой эле кыстарууларды жана мета-маалыматтарды (оригиналдуу постторго, категорияларга, даталарга шилтемелер) сактоо үчүн вектордук маалымат базасы керек болот. Бул максат үчүн FAISS , Milvus же Pinecone сыяктуу көптөгөн вектордук сактагычтар бар. pgvector кеңейтүүсү менен кадимки PostgreSQL да иштейт.

Колдонуучулардын суроосу иштетилүүдө

Колдонуучунун суроосуна жооп берүү үчүн биз суроону издөө формасына айландырышыбыз керек, ошону менен суроонун кыстарылышын эсептеп, ошондой эле анын ниетин аныкташыбыз керек.


Суроо боюнча семантикалык издөөнүн натыйжасы чат тарыхындагы окшош суроолор болушу мүмкүн, бирок аларга жооптор эмес.


Муну жакшыртуу үчүн, биз популярдуу HyDE (гипотетикалык документ кыстаруу) оптималдаштыруу ыкмаларынын бирин колдоно алабыз. Идея LLM аркылуу суроого гипотетикалык жоопту жаратып, андан кийин жооптун кыстарылышын эсептөө. Бул ыкма кээ бир учурларда суроолордун ордуна жооптор арасында тиешелүү билдирүүлөрдү так жана натыйжалуу издөөгө мүмкүндүк берет.


Эң керектүү билдирүүлөрдү табуу

Бизде суроо киргизүү болгондон кийин, биз маалымат базасынан эң жакын билдирүүлөрдү издей алабыз. LLM чектелген контексттик терезеге ээ, андыктан издөө натыйжалары өтө көп болсо, биз бардык издөө натыйжаларын кошо албай калышыбыз мүмкүн. Жоопторду кантип биринчи орунга коюу керек деген суроо туулат. Бул үчүн бир нече ыкмалар бар:


  • Акыркы упай . Убакыттын өтүшү менен маалымат эскирип, жаңы билдирүүлөргө артыкчылык берүү үчүн 1 / (today - date_of_message + 1) формуласынын жардамы менен жаңылык упайын эсептей аласыз.


  • Метадайындарды чыпкалоо. (суроонун жана билдирүүлөрдүн темасын аныктоо керек). Бул сиз издеп жаткан темага тиешелүү билдирүүлөрдү гана калтырып, издөөңүздү кыскартууга жардам берет


  • Толук текст издөө . Бардык популярдуу маалымат базалары тарабынан колдоого алынган классикалык толук тексттик издөө кээде пайдалуу болушу мүмкүн.


  • Reranking . Жоопторду тапкандан кийин, аларды суроого "жакындык" даражасы боюнча иргеп, эң керектүүсүн гана калтырсак болот. Кайра рейтингдөө үчүн CrossEncoder модели талап кылынат, же биз, мисалы, Cohere'ден кайра рейтингдөө API колдоно алабыз.


Акыркы жоопту түзүү

Мурунку кадамда издөө жана сорттоодон кийин, биз LLM контекстине туура келе турган 50-100 эң актуалдуу постторду сактай алабыз.


Кийинки кадам колдонуучунун баштапкы суроо-талаптарын жана издөө натыйжаларын колдонуу менен LLM үчүн так жана кыска сунушту түзүү болуп саналат. Ал LLMге суроого, колдонуучунун суроосуна жана контекстке кандай жооп берүү керек экенин - биз тапкан тиешелүү билдирүүлөрдү көрсөтүүсү керек. Бул үчүн, бул аспектилерди эске алуу зарыл:


  • Системалык кеңештер бул маалыматты кантип иштетүү керектигин түшүндүрүүчү моделдин көрсөтмөлөрү. Мисалы, сиз LLMге берилген маалыматтардан гана жооп издеңиз деп айта аласыз.


  • Контексттин узундугу - биз киргизүү катары колдоно ала турган билдирүүлөрдүн максималдуу узундугу. Биз колдонгон моделге туура келген токенизатордун жардамы менен токендердин санын эсептей алабыз. Мисалы, OpenAI Тиктокенди колдонот.


  • Моделдин гиперпараметрлери - мисалы, температура моделдин жоопторуна канчалык креативдүү боло турганына жооп берет.


  • Модель тандоо . Бул абдан чоң жана күчтүү модели үчүн ашыкча төлөө дайыма эле татыктуу эмес. Бул ар кандай моделдер менен бир нече сыноолорду жүргүзүү жана алардын натыйжаларын салыштыруу үчүн мааниси бар. Кээ бир учурларда, ресурстарды аз талап кылган моделдер, эгерде алар жогорку тактыкты талап кылбаса, ишти аткарышат.


Ишке ашыруу

Эми бул кадамдарды NodeJS менен ишке ашырууга аракет кылалы. Бул жерде мен колдоно турган технологиялык стек:


  • NodeJS жана TypeScript
  • Грэмми - Telegram бот алкагы
  • PostgreSQL - биздин бардык маалыматтар үчүн негизги сактагыч катары
  • pgvector - текст кыстарууларды жана билдирүүлөрдү сактоо үчүн PostgreSQL кеңейтүүсү
  • OpenAI API - LLM и жана кыстаруу моделдери
  • Mikro-ORM - ДБ өз ара аракеттенүүсүн жөнөкөйлөтүү үчүн


Көз карандылыкты орнотуунун жана телеграмма ботун жөндөөнүн негизги кадамдарын өткөрүп жиберип, түз эле эң маанилүү функцияларга өтөлү. Кийинчерээк керектелүүчү маалымат базасынын схемасы:


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


Колдонуучу диалогдорун бөлүктөргө бөлүңүз

Бир нече колдонуучулардын ортосундагы узун диалогдорду бөлүктөргө бөлүү эң маанилүү иш эмес.


Тилекке каршы, Langchain китепканасында бар RecursiveCharacterTextSplitter сыяктуу демейки ыкмалар баарлашуунун бардык өзгөчөлүктөрүн эске албайт. Бирок, Telegram учурда, биз тиешелүү билдирүүлөрдү жана колдонуучулар жөнөткөн жоопторду камтыган Telegram threads пайдалана алабыз.


Чат бөлмөсүнөн билдирүүлөрдүн жаңы партиясы келген сайын, биздин бот бир нече кадамдарды аткарышы керек:


  • Кыска билдирүүлөрдү токтотуу сөздөрдүн тизмеси боюнча чыпкалаңыз (мисалы, "салам", "кош бол" ж.б.)
  • Бир колдонуучудан келген билдирүүлөрдү бириктириңиз, эгерде алар кыска убакыттын ичинде катары менен жөнөтүлсө
  • Бир темага тиешелүү бардык билдирүүлөрдү топтоо
  • Кабыл алынган билдирүү топторун чоңураак текст блокторуна бириктириңиз жана андан ары бул текст блокторун RecursiveCharacterTextSplitter аркылуу бөлүктөргө бөлүңүз
  • Ар бир бөлүкчө үчүн кыстарууларды эсептеңиз
  • Базадагы текст бөлүктөрүн, алардын кыстаруулары жана түпнуска билдирүүлөргө шилтемелери менен бирге сактаңыз


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


Кыстармалар

Андан кийин, биз бөлүкчөлөрдүн ар бири үчүн кыстарууларды эсептөө керек. Бул үчүн биз 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(); }



Колдонуучунун суроолоруна жооп берүү

Колдонуучунун суроосуна жооп берүү үчүн, биз адегенде суроонун кыстарылышын эсептейбиз, анан чат тарыхынан эң керектүү билдирүүлөрдү табабыз.


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



Андан кийин биз Cohere's reranking моделинин жардамы менен издөө натыйжаларын кайра ранжирлейбиз


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



Андан кийин, LLMден издөө натыйжаларын жалпылоо менен колдонуучунун суроосуна жооп берүүсүн сураныңыз. Издөө суроосун иштетүүнүн жөнөкөйлөштүрүлгөн версиясы төмөнкүдөй болот:


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



Андан ары жакшыртуу

Бардык оптималдаштыруудан кийин да, LLM тарабынан иштетилген боттун жооптору идеалдуу эмес жана толук эмес деп ойлошубуз мүмкүн. Дагы эмнени жакшыртууга болот?


  • Шилтемелерди камтыган колдонуучунун билдирүүлөрү үчүн биз веб-баракчаларды жана pdf-документтердин мазмунун талдай алабыз.

  • Суроо-багыттоо — колдонуучунун суроо-талаптарын тактыкты, эффективдүүлүктү жана бааны оптималдаштыруу үчүн суроонун ниетине жана контекстине негизделген эң ылайыктуу маалымат булагына, моделине же индексине багыттоо.

  • Биз издөө индексине чат бөлмөсүнүн темасына тиешелүү ресурстарды киргизсек болот — жумушта бул Confluence документациясы, виза чаттары, консулдук веб-сайттар эрежелери ж.б. болушу мүмкүн.

  • RAG-баалоо - Биздин боттун жоопторунун сапатын баалоо үчүн куурду орнотуу керек





L O A D I N G
. . . comments & more!

About Author

Iaroslav Sobolev HackerNoon profile picture
Iaroslav Sobolev@slavasobolev
i'm writing about software engineering and AI

ТАГИП АЛУУ

БУЛ МАКАЛА БЕРИЛГЕН...