Коомчулуктар, чаттар жана форумдар көптөгөн темалар боюнча чексиз маалымат булагы болуп саналат. Slack көбүнчө техникалык документтерди алмаштырат, ал эми Telegram жана Discord жамааттары оюндар, стартаптар, крипто жана саякат суроолоруна жардам берет. Алгачкы маалыматтын актуалдуулугуна карабастан, ал көп учурда структураланбагандыктан, издөөнү кыйындатат. Бул макалада биз чат билдирүүлөрүнүн тарыхынан маалыматтарды алуу менен суроолорго жооп таба турган Telegram ботун ишке ашыруунун татаалдыктарын изилдейбиз. Бул жерде бизди күтүп турган кыйынчылыктар: . Жооп бир нече адамдардын диалогунда же тышкы ресурстарга шилтемеде чачыранды болушу мүмкүн. Тиешелүү билдирүүлөрдү табыңыз . Көптөгөн спам жана тышкаркы темалар бар, биз аларды аныктоону жана чыпкалоону үйрөнүшүбүз керек Оффтопияга көңүл бурбоо . Маалымат эскирип калат. Бүгүнкү күнгө чейин туура жоопту кайдан билесиз? Приоритетизация Биз ишке ашыра турган негизги чатбот колдонуучу агымы Колдонуучу ботко суроо берет Бот билдирүүлөрдүн тарыхындагы эң жакын жоопторду табат Бот LLM жардамы менен издөө натыйжаларын жалпылайт Колдонуучуга тиешелүү билдирүүлөргө шилтемелер менен акыркы жоопту кайтарат Келгиле, бул колдонуучу агымынын негизги этаптары аркылуу басып өтүп, биз туш боло турган негизги кыйынчылыктарды белгилейли. Маалыматтарды даярдоо Издөө үчүн билдирүү таржымалын даярдоо үчүн, биз бул билдирүүлөрдүн кыстарууларын түзүшүбүз керек - векторланган текст өкүлчүлүктөрү. Вики макаласы же PDF документи менен иштөөдө биз текстти абзацтарга бөлүп, ар бири үчүн сүйлөмдүн кыстарылышын эсептейбиз. Бирок, биз жакшы структураланган текст үчүн эмес, чаттар үчүн мүнөздүү болгон өзгөчөлүктөрдү эске алышыбыз керек: Бир колдонуучудан бир нече кийинки кыска билдирүүлөр. Мындай учурларда, билдирүүлөрдү чоңураак текст блокторуна бириктирүү керек Кээ бир билдирүүлөр өтө узун жана бир нече түрдүү темаларды камтыйт Мааниси жок билдирүүлөрдү жана спамдарды биз чыпкалашыбыз керек Колдонуучу оригиналдуу билдирүүнү белгилебестен жооп бере алат. Суроо менен жоопту чат тарыхында башка көптөгөн билдирүүлөр менен ажыратса болот Колдонуучу тышкы ресурска шилтеме менен жооп бере алат (мисалы, макала же документ) Андан кийин, биз орнотуу моделин тандоо керек. Кыймылдарды куруу үчүн көптөгөн ар кандай моделдер бар жана туура моделди тандоодо бир нече факторлорду эске алуу керек. . Ал канчалык жогору болсо, модель маалыматтардан ошончолук көп нюанстарды үйрөнө алат. Издөө такыраак болот, бирок көбүрөөк эс жана эсептөө ресурстарын талап кылат. Киргизүү өлчөмү Кыстаруу модели үйрөтүлгөн . Бул, мисалы, сизге керектүү тилди канчалык деңгээлде колдой турганын аныктайт. берилиштер топтому Издөө натыйжаларынын сапатын жакшыртуу үчүн биз билдирүүлөрдү тема боюнча категорияларга бөлсөк болот. Мисалы, фронтендди иштеп чыгууга арналган чатта колдонуучулар төмөнкү темаларды талкуулай алышат: CSS, инструмент, React, ж.б. темалар. Vue Бизге ошондой эле кыстарууларды жана мета-маалыматтарды (оригиналдуу постторго, категорияларга, даталарга шилтемелер) сактоо үчүн вектордук маалымат базасы керек болот. Бул максат үчүн , же сыяктуу көптөгөн вектордук сактагычтар бар. кеңейтүүсү менен кадимки PostgreSQL да иштейт. FAISS Milvus Pinecone pgvector Колдонуучулардын суроосу иштетилүүдө Колдонуучунун суроосуна жооп берүү үчүн биз суроону издөө формасына айландырышыбыз керек, ошону менен суроонун кыстарылышын эсептеп, ошондой эле анын ниетин аныкташыбыз керек. Суроо боюнча семантикалык издөөнүн натыйжасы чат тарыхындагы окшош суроолор болушу мүмкүн, бирок аларга жооптор эмес. Муну жакшыртуу үчүн, биз популярдуу (гипотетикалык документ кыстаруу) оптималдаштыруу ыкмаларынын бирин колдоно алабыз. Идея LLM аркылуу суроого гипотетикалык жоопту жаратып, андан кийин жооптун кыстарылышын эсептөө. Бул ыкма кээ бир учурларда суроолордун ордуна жооптор арасында тиешелүү билдирүүлөрдү так жана натыйжалуу издөөгө мүмкүндүк берет. HyDE Эң керектүү билдирүүлөрдү табуу Бизде суроо киргизүү болгондон кийин, биз маалымат базасынан эң жакын билдирүүлөрдү издей алабыз. LLM чектелген контексттик терезеге ээ, андыктан издөө натыйжалары өтө көп болсо, биз бардык издөө натыйжаларын кошо албай калышыбыз мүмкүн. Жоопторду кантип биринчи орунга коюу керек деген суроо туулат. Бул үчүн бир нече ыкмалар бар: . Убакыттын өтүшү менен маалымат эскирип, жаңы билдирүүлөргө артыкчылык берүү үчүн формуласынын жардамы менен жаңылык упайын эсептей аласыз. Акыркы упай 1 / (today - date_of_message + 1) (суроонун жана билдирүүлөрдүн темасын аныктоо керек). Бул сиз издеп жаткан темага тиешелүү билдирүүлөрдү гана калтырып, издөөңүздү кыскартууга жардам берет Метадайындарды чыпкалоо. . Бардык популярдуу маалымат базалары тарабынан колдоого алынган классикалык толук тексттик издөө кээде пайдалуу болушу мүмкүн. Толук текст издөө . Жоопторду тапкандан кийин, аларды суроого "жакындык" даражасы боюнча иргеп, эң керектүүсүн гана калтырсак болот. Кайра рейтингдөө үчүн модели талап кылынат, же биз, мисалы, кайра рейтингдөө API колдоно алабыз. Reranking CrossEncoder Cohere'ден Акыркы жоопту түзүү Мурунку кадамда издөө жана сорттоодон кийин, биз LLM контекстине туура келе турган 50-100 эң актуалдуу постторду сактай алабыз. Кийинки кадам колдонуучунун баштапкы суроо-талаптарын жана издөө натыйжаларын колдонуу менен LLM үчүн так жана кыска сунушту түзүү болуп саналат. Ал LLMге суроого, колдонуучунун суроосуна жана контекстке кандай жооп берүү керек экенин - биз тапкан тиешелүү билдирүүлөрдү көрсөтүүсү керек. Бул үчүн, бул аспектилерди эске алуу зарыл: бул маалыматты кантип иштетүү керектигин түшүндүрүүчү моделдин көрсөтмөлөрү. Мисалы, сиз LLMге берилген маалыматтардан гана жооп издеңиз деп айта аласыз. Системалык кеңештер - биз киргизүү катары колдоно ала турган билдирүүлөрдүн максималдуу узундугу. Биз колдонгон моделге туура келген токенизатордун жардамы менен токендердин санын эсептей алабыз. Мисалы, OpenAI Тиктокенди колдонот. Контексттин узундугу - мисалы, температура моделдин жоопторуна канчалык креативдүү боло турганына жооп берет. Моделдин гиперпараметрлери . Бул абдан чоң жана күчтүү модели үчүн ашыкча төлөө дайыма эле татыктуу эмес. Бул ар кандай моделдер менен бир нече сыноолорду жүргүзүү жана алардын натыйжаларын салыштыруу үчүн мааниси бар. Кээ бир учурларда, ресурстарды аз талап кылган моделдер, эгерде алар жогорку тактыкты талап кылбаса, ишти аткарышат. Модель тандоо Ишке ашыруу Эми бул кадамдарды NodeJS менен ишке ашырууга аракет кылалы. Бул жерде мен колдоно турган технологиялык стек: жана NodeJS TypeScript - Telegram бот алкагы Грэмми - биздин бардык маалыматтар үчүн негизги сактагыч катары PostgreSQL - текст кыстарууларды жана билдирүүлөрдү сактоо үчүн PostgreSQL кеңейтүүсү pgvector - LLM и жана кыстаруу моделдери OpenAI API - ДБ өз ара аракеттенүүсүн жөнөкөйлөтүү үчүн 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 китепканасында бар сыяктуу демейки ыкмалар баарлашуунун бардык өзгөчөлүктөрүн эске албайт. Бирок, Telegram учурда, биз тиешелүү билдирүүлөрдү жана колдонуучулар жөнөткөн жоопторду камтыган Telegram пайдалана алабыз. RecursiveCharacterTextSplitter 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-баалоо