<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Dskr.dev</title><description>Dmitriy Skrylnikov&apos;s personal blog about web development, TypeScript, self-hosted projects, and practical experiments.</description><link>https://dskr.dev/</link><item><title>How to install the node.js environment properly?</title><link>https://dskr.dev/en/blog/how-to-install-node/</link><guid isPermaLink="true">https://dskr.dev/en/blog/how-to-install-node/</guid><description>I&apos;ll explain why fnm and corepack are necessary</description><pubDate>Mon, 18 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Install Node.JS&lt;/h2&gt;
&lt;p&gt;There are several ways to install Node.JS and it can be difficult to determine which one is best. In this article we will compare the different methods (direct installation, system package managers, nvm, n, nodenv, volta, fnm). TLDR: I prefer to use fnm&lt;/p&gt;
&lt;p&gt;The easiest option for installing Node.JS is to visit &lt;a href=&quot;https://nodejs.org&quot;&gt;nodejs.org &lt;/a&gt; and download the installer. However, this may not be the best approach. A slightly more reliable option is to use a system package manager (&lt;code&gt;brew&lt;/code&gt;, &lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;dnf&lt;/code&gt;). With either method, it is not possible to change the Node.JS version quickly. Often a current project requires a fresh LTS version, a legacy project is running under a previous LTS version. Additionally, if you want to experiment with the latest features, it may be necessary to upgrade to the current version of Node.js.&lt;/p&gt;
&lt;p&gt;The most popular solution is &lt;a href=&quot;https://github.com/nvm-sh/nvm&quot;&gt;Node Version Manager &lt;code&gt;nvm&lt;/code&gt;&lt;/a&gt;.  However, it has some drawbacks. First of all, nvm can be incredibly slow, as each time you open the console with it installed, it can take up to several seconds to load. Secondly, nvm does not work with fish and Windows. Thirdly, if you use globally installed packages, you will need to install them anew every time or register them in a special file. n and nodenv have similar issues.&lt;/p&gt;
&lt;p&gt;The next solution is &lt;a href=&quot;https://volta.sh/&quot;&gt;Volta&lt;/a&gt;. It is written in Rust, which means it should be fast. Supports fish and even windows. Globally installed libraries work even after a version change. But the &lt;code&gt;.nvmrc&lt;/code&gt;/&lt;code&gt;.node-version&lt;/code&gt; files are not supported, although it is suggested to write the node version in the &lt;code&gt;package&apos; as a replacement.json&lt;/code&gt;. Another disadvantage is that &lt;code&gt;pnpm&lt;/code&gt; support is in experimental mode.&lt;/p&gt;
&lt;p&gt;And finally &lt;a href=&quot;https://fnm.vercel.app&quot;&gt;Fast Node Manager &lt;code&gt;fnm&lt;/code&gt;&lt;/a&gt;. It is also written in Rust. It works fast and supports everything you need. fnm is installed with one command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://fnm.vercel.app/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, you can install the Node version.JS to be used by default:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fnm default 18
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can add the &lt;code&gt;.node-version&lt;/code&gt; file to the project, specifying the Node version.The JS that will be used in this project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo 18.17.0 &amp;gt; .node-version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you open the console in the project folder, fnm will automatically enable the desired version Node.JS .&lt;/p&gt;
&lt;p&gt;There is a slight disadvantage if the Node version is specified in the project.JS that is not installed, it will not be installed automatically, but an error will be displayed when opening the console. To install the desired version, you will need to enter as many as two commands&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fnm install
fnm use
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Install pnpm/yarn&lt;/h2&gt;
&lt;p&gt;Node.JS, starting with version 16.13, now supports &lt;a href=&quot;https://github.com/nodejs/corepack&quot;&gt;Corepack&lt;/a&gt;. This utility allows you to use any supported package manager without manual installation. All you need to do is enable corepack:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;corepack enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, when using &lt;code&gt;pnpm&lt;/code&gt;/&lt;code&gt;yarn&lt;/code&gt; for the first time, they will automatically install and work.&lt;/p&gt;
</content:encoded></item><item><title>Six Years of Telegram Bot Development: From a Tokenizer to LLMs, RAG, and Vector Databases</title><link>https://dskr.dev/en/blog/telegram-bot-six-years/</link><guid isPermaLink="true">https://dskr.dev/en/blog/telegram-bot-six-years/</guid><description>How Io evolved from a tokenizer to LLMs, memory, RAG, and vector search</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Io is an LLM bot for Telegram chats. It can reply to user messages, recognize images and voice messages, take the current thread, chat history, and user information into account. The bot is currently active in several dozen chats.&lt;/p&gt;
&lt;p&gt;I’ll tell you how I built it over several years, tried to teach it to remember users, explored RAG and vector databases, and eventually ended up with a system that actually works.&lt;/p&gt;
&lt;h2&gt;How did the idea come about?&lt;/h2&gt;
&lt;p&gt;Many years ago, there was a space-themed community called Alpha Centauri. It had an off-topic chat with a chatbot living in it. LLMs did not exist back then; the bot worked with regular expressions, could check the weather, ban users, remember information, and do other fun things. The chat and the bot eventually disappeared, and people moved to other chats. Some time later, I wanted to build a similar bot.&lt;/p&gt;
&lt;h2&gt;A pre-LLM prototype&lt;/h2&gt;
&lt;p&gt;The first version was written in the summer of 2020. GPT-3 had just been released, it did not speak Russian, and it had not made its way into chats yet, so I had to reinvent the wheel. I did not know regular expressions, so I decided to split text into tokens, identify the meaningful ones, and launch the appropriate function.&lt;/p&gt;
&lt;p&gt;The tokenizer worked like this: it removed links from the text, split it into sentences by punctuation, then split sentences into words, converted them to lowercase, and applied a stemmer.&lt;/p&gt;
&lt;p&gt;For example, the Russian words «расскажи» and «рассказать» become the stem «расска», while «погода» and «погоду» become «погод». This kept the list of commands from growing into dozens of variants of the same word. Russian has declensions, cases, and other complications, so the system was far from perfect.&lt;/p&gt;
&lt;p&gt;Sometimes I still had to write several variants:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;switch (nextWorld) {
  case PorterStemmerRu.stem(&apos;мин&apos;):
  case PorterStemmerRu.stem(&apos;минут&apos;): {
    return number * 60;
  }
  case PorterStemmerRu.stem(&apos;час&apos;): {
    return number * 60 * 60;
  }
  case PorterStemmerRu.stem(&apos;дня&apos;):
  case PorterStemmerRu.stem(&apos;дней&apos;):
  case PorterStemmerRu.stem(&apos;день&apos;): {
    return number * 60 * 60 * 24;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first features included weather forecasts, banning users, voice recognition, and text translation.&lt;/p&gt;
&lt;p&gt;The weather also had its complications. Users could spell city names in different ways, so I used &lt;a href=&quot;https://dadata.ru/&quot;&gt;DaData&lt;/a&gt; to identify the city. I got the actual forecast from &lt;a href=&quot;https://openweathermap.org/&quot;&gt;OpenWeather&lt;/a&gt;, and still do. It is not the best source, but it is free.&lt;/p&gt;
&lt;p&gt;For translation and voice recognition, I used Yandex services. I also wanted to recognize video messages, so I had to bring in &lt;a href=&quot;https://ffmpeg.org/&quot;&gt;FFmpeg&lt;/a&gt; to extract audio from video.&lt;/p&gt;
&lt;p&gt;The bot was abandoned in this state for three long years. I wanted to add more features, of course, but procrastination got in the way, as did the complexity. It was difficult to build something useful and interesting on top of a primitive tokenizer.&lt;/p&gt;
&lt;h2&gt;The first attempts with LLMs&lt;/h2&gt;
&lt;p&gt;I started by trying to write a separate bot. At that point, there was no convenient way to call functions through an LLM. The result was a primitive bot based on GPT-3.5 Turbo. Its context was stored in memory, so nothing survived a restart. It only worked inside a thread.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/02-llm-commands.jpeg&quot; alt=&quot;For extra fun, the bot could be called with different commands, which added different hacks to the system prompt&quot;&gt;&lt;/p&gt;
&lt;p&gt;With that experience behind me, I went back to rewrite Io. The LLM was called when a user addressed the bot and none of the regular commands matched.&lt;/p&gt;
&lt;p&gt;The bot lived in this form until 2024. After that, I added proper context handling. All messages were stored in a database. Each message points to a reply, which creates a conversation tree. This made it possible, for example, to get a different answer to the same request: you only had to reply to the relevant message in the history, and the bot would continue the conversation from that exact point without touching the other branches.&lt;/p&gt;
&lt;p&gt;Around the same time, I switched to GPT-4o mini and added Tool Calling so the model could choose and call the functions it needed. As a result, all the old tokenizer hacks were no longer necessary and were removed. I also removed the user-ban feature because nobody was using it anymore.&lt;/p&gt;
&lt;p&gt;I also added image support. At first, the bot simply passed the image URL in every request to the model, but that turned out to be too expensive. So I moved image processing into a separate stage: when a user sends a photo, the bot immediately sends it through a model, gets a textual description, and saves it to the database. Later, when the image enters the conversation context, the existing description is used instead. This is still how the feature works today.&lt;/p&gt;
&lt;p&gt;Since the bot could be freely added to any chat, image processing initially worked only in trusted chats. Even with that limitation, the bot consumed around $10–20 per month in model costs.&lt;/p&gt;
&lt;h2&gt;Teaching the bot to remember information&lt;/h2&gt;
&lt;h3&gt;The first version of memory&lt;/h3&gt;
&lt;p&gt;In 2025, I read a lot about RAG (Retrieval-Augmented Generation, a method for working with large amounts of data), but I was not ready to deal with vector databases and all the related complexity. So I took the simplest route. I added a &lt;code&gt;metaInfo&lt;/code&gt; field containing JSON with the information collected about a user. I updated it every ten messages with a query like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/03-context-tree.jpeg&quot; alt=&quot;Query for updating a user&apos;s memory&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then I added this information to every request with instructions like these:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/04-image-processing.jpeg&quot; alt=&quot;User information in the request context&quot;&gt;&lt;/p&gt;
&lt;p&gt;Memory worked. Io now knew something about the user.&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Hidden text&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/05-memory-json.jpeg&quot; alt=&quot;Example of saved user facts&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/06-memory-facts.jpeg&quot; alt=&quot;Another example of saved user facts&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/07-funny-dialogue.jpeg&quot; alt=&quot;Sometimes this produced funny conversations&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;But memory was far from perfect. One woman had a gender-neutral name, and Io absolutely refused to remember that she was a woman. I rewrote the metadata and added weights to the facts. The model now estimated the importance of each fact, and I increased the weight when a fact was repeated. That did not help either. The model considered the user’s gender completely unimportant) In the end, the problem was solved by manually editing the database.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/08-gender-fact.jpeg&quot; alt=&quot;Editing a user fact&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/09-unsafe-replies.jpeg&quot; alt=&quot;An unsuccessful attempt to change the bot&apos;s behavior&quot;&gt;&lt;/p&gt;
&lt;p&gt;The same user tried very hard to make Io write something inappropriate. This led to Io responding inadequately to all of her messages.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/10-clear-memory.jpeg&quot; alt=&quot;Example of inadequate replies&quot;&gt;&lt;/p&gt;
&lt;p&gt;I had to add a way to clear the memory.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/11-memory-reset.jpeg&quot; alt=&quot;Clearing a user&apos;s memory&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Vectors, finally&lt;/h2&gt;
&lt;p&gt;The bot survived in this form until 2026. By then, the rise of agents had significantly lowered the barrier to entry for these experiments, and I finally wanted to try vector databases. In my mind, RAG was simple: take all the messages, put them into a vector database, find similar ones for a new request, and add them to the context. That was it — the model knew the user&apos;s history and took it into account. I was very wrong.&lt;/p&gt;
&lt;p&gt;It quickly became clear that searching for similar messages was almost useless. Most of the retrieved messages were just fragments of old conversations with no long-term value. Models benefit much more from knowing stable facts about a user than from rereading their chat history. So I continued using two approaches: vector search over messages and separate fact extraction.&lt;/p&gt;
&lt;p&gt;After that, I rewrote fact collection. The model now extracts a fact and evaluates its importance. Vector search then finds similar facts among the saved ones, and the model decides whether to increase the weight of an existing fact, update it, or save a new one.&lt;/p&gt;
&lt;p&gt;This complexity is necessary because the model can phrase the same fact slightly differently each time. It also made it possible to update information when it contradicts older data. For a new request, the user&apos;s facts are ranked by date and importance, and the top 10 are added to the context.&lt;/p&gt;
&lt;h2&gt;Small but useful features&lt;/h2&gt;
&lt;p&gt;Voice message summarization. If a voice message is long, the bot creates a summary and hides the original text under a spoiler. I also run the transcript through an LLM to add punctuation and remove repetitions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/12-voice-summary.jpeg&quot; alt=&quot;Example of a voice message summary&quot;&gt;&lt;/p&gt;
&lt;p&gt;Guest Mode. A way to call the bot in any chat and talk to it there. It is similar to inline mode, except the conversation is not limited to one request.&lt;/p&gt;
&lt;p&gt;Wikipedia search. It gives the model at least a chance of getting facts right.&lt;/p&gt;
&lt;p&gt;Explicit memory. A way to directly ask the bot to remember something. This solved the problem of having to edit the database manually to correct facts.&lt;/p&gt;
&lt;h2&gt;Monetization&lt;/h2&gt;
&lt;p&gt;I recently added monetization. The free version has daily limits for messages, image recognition, and voice messages. The message limit is soft: after reaching it, the bot switches to a cheaper model. Users can buy a subscription for a week, a month, three months, or a year. The longer the subscription, the higher the daily limit. As a bonus, the limits are slightly increased for everyone in a chat where one of the users has a subscription.&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Hidden text&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/telegram-bot-six-years/13-subscription.jpeg&quot; alt=&quot;Subscription limits and plans&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;Technology stack&lt;/h2&gt;
&lt;p&gt;The original stack was Node.js, TypeScript, Telegraf.js, and OpenAI.&lt;/p&gt;
&lt;p&gt;Later, I switched from Telegraf.js to &lt;a href=&quot;https://grammy.dev/&quot;&gt;grammY&lt;/a&gt;. It has a convenient API, good TypeScript support, fast support for new versions of the Telegram Bot API, and many useful extensions.&lt;/p&gt;
&lt;p&gt;I initially chose &lt;a href=&quot;https://qdrant.tech/&quot;&gt;Qdrant&lt;/a&gt; as the vector database. For my scale, this turned out to be overkill: there was no reason to maintain a separate database when everything fit comfortably into PostgreSQL. So I recently switched to the &lt;a href=&quot;https://github.com/pgvector/pgvector&quot;&gt;pgvector&lt;/a&gt; extension.&lt;/p&gt;
&lt;p&gt;I first used &lt;a href=&quot;https://www.langchain.com/&quot;&gt;LangChain&lt;/a&gt; to work with LLMs, but it turned out to be too complex. In the end, I migrated to &lt;a href=&quot;https://ai-sdk.dev/&quot;&gt;Vercel&apos;s AI SDK&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I started with OpenAI as the provider, then switched to OpenRouter, and later to RouterAI because of payment issues.&lt;/p&gt;
&lt;p&gt;I store prompts and request traces in &lt;a href=&quot;https://langfuse.com/&quot;&gt;Langfuse&lt;/a&gt;. It turned out not to be the best fit for my needs because my prompt changes dynamically, while its template capabilities are limited. Now the static part is stored in Langfuse, and the dynamic part is inserted as one block. I barely use tracing: so far, I have not encountered a task that made me want to inspect it regularly.&lt;/p&gt;
&lt;p&gt;The main model is currently Gemini 3.6 Flash, and the budget model is Gemini 2.5 Flash Lite. I use Nex-N2-mini for fact extraction, summarization, and image recognition.&lt;/p&gt;
&lt;p&gt;I initially generated vectors through OpenRouter, but after switching to RouterAI, embedding generation started taking too long — anywhere from 10 to 60 seconds. The bot vectorizes the current message on every request to find relevant context, which makes it feel slow. So I deployed &lt;a href=&quot;https://github.com/huggingface/text-embeddings-inference&quot;&gt;text-embeddings-inference&lt;/a&gt; with &lt;code&gt;multilingual-e5-small&lt;/code&gt;. I have not compared the quality yet, but embeddings are generated quickly even on a small server.&lt;/p&gt;
&lt;p&gt;Infrastructure is a separate story. At first, everything ran through Docker Compose, but every deployment required logging into the server manually, pulling the changes, and rebuilding the bot. It could have been automated, of course, but I was too lazy. The bot later moved to Coolify, which brought automatic builds, deployments, and backups. However, builds put a heavy load on the server and could take it down for a couple of minutes. Database migrations also caused problems.&lt;/p&gt;
&lt;p&gt;So I decided to move to k3s. Now GitHub Actions builds the bot image, which is then deployed to k3s. Backups are sent to S3 storage on my home NAS. I plan to set up another k3s cluster at home and deploy monitoring there, just to make it look nice. Agents have made this kind of work very quick and easy, although there is always a risk that one of them will eventually delete both my database and my backups.&lt;/p&gt;
&lt;h2&gt;Plans&lt;/h2&gt;
&lt;p&gt;Set up proper monitoring. Fix the bugs — Guest Mode currently does not preserve context, for example. Implement web search, since the bot currently only searches Wikipedia. Improve the infrastructure: the bot is currently unavailable for several minutes during deployment.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Io is still a large playground for my experiments. I try many new ideas from the world of LLMs and infrastructure there first. As long as the project remains enjoyable, it was all worth it. You can try the bot on &lt;a href=&quot;https://t.me/PhoronisBot&quot;&gt;Telegram&lt;/a&gt;. The source code is on &lt;a href=&quot;https://github.com/skrylnikov/Phoronis-tg-bot&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item></channel></rss>