Image

Когда RAG на Go свистнет: собираем прототип чата за вечер

Привет, я — Евгений Клецов, Go-разработчик в Cloud.ru. Если вы тоже Go-разработчик, то и вам, наверняка, приходила в голову мысль добавить в свой сервис «немного AI», но казалось, что это требует погружения в незнакомый мир Python и машинного обучения. Каждый день появляются новые AI-стартапы, да и существующие сервисы не отстают с внедрением искусственного интеллекта. Еще недавно это и правда было невозможным без глубоких знаний в области ML/AI, но сейчас всё меняется. Большие текстовые модели обзавелись удобным API для работы и фактически превратились в AI as a Service. Давайте на практике убедимся, что Go тоже прекрасно подходит для разработки подобных приложений на примере RAG.

6b83e05238c96a514cc5a0c8d08d73b1

План

  • Немного теории

  • Архитектура RAG-приложения

  • Выбор инструментов: фреймворки

  • Собираем приложение

    • Шаг 1: Общаемся с моделью

    • Шаг 2: Добавляем «мозг»

    • Шаг 3: Наводим порядок в разговоре

  • Куда двигаться дальше: улучшаем PoC

  • Заключение

Немного теории

Про Retrieval-Augmented Generation (RAG) уже написано много, и подробнее можно почитать в интернете, и даже ChatGPT GigaChat доступно объяснит, поэтому кратко. Эта технология позволяет нам «научить» модель новым знаниям. Модифицировать саму модель мы не можем, но можем добавить в промпт всю новую информацию! Современные модели обладают весьма внушительным контекстным окном в тысячи токенов, что позволяет вместить несколько документов в дополнение к запросу пользователя.

Чтобы передавать не всю библиотеку, а только релевантные блоки текста, нам на помощь приходят специальные базы данных и Embedding-модели. Эти модели умеют преобразовывать текст в векторы таким образом, что похожие по смыслу тексты будут находиться в векторном представлении ближе друг к другу. Берем вектор от запроса пользователя и ищем ближайшие к нему векторы от кусков текстов нашей базы знаний, например штук пять самых близких, и эти куски текста уже добавляем в промпт. А большая модель уже в них будет искать ответ на вопрос.

Давайте же посмотрим, как собрать такое приложение на Go. Нам понадобится:

  • Большая модель для текста

  • Маленькая для эмбеддинга

  • Векторная база данных

  • Синяя изолента Немного кода

Архитектура RAG-приложения

Если посмотреть взглядом бэкендера на концепцию такого приложения, то можно заметить, что она довольно тривиальна и привычна. У нас есть сервер, который принимает запросы пользователя, есть базы данных для хранения истории и знаний, и есть сторонние API, которые нужно вызвать.

Прыгать с парашютом совсем не страшно. Открываешь дверь самолета — а там Google Maps. Вы ведь не боитесь Google Maps?
Прыгать с парашютом совсем не страшно. Открываешь дверь самолета — а там Google Maps. Вы ведь не боитесь Google Maps?

История диалога хранится в привычной базе данных, можем использовать как SQL, так и NoSQL базы, или комбинировать их для достижения целей по скорости и надежности.

База знаний хранится в векторной базе данных, взаимодействие с ней еще проще: даже не нужно писать запросы. Нужно только преобразовать текст от пользователя в вектор и передать его в метод поиска. Для такого преобразования используется API эмбеддера: текст на вход, набор чисел на выходе.

Собираем воедино все блоки текста и отправляем в API большой языковой модели, получаем ответ, сохраняем в историю и отдаем пользователю.

AI-приложение оказывается вовсе не магия, а привычный микросервис, который без особых сложностей можно написать на Go. Способность к потоковой обработке данных и высокая производительность, за которые мы так любим Go, нам в этом помогут.

Выбор инструментов: фреймворки

Для написания приложения с RAG воспользуемся готовым фреймворком для AI-приложений. В принципе можно и с нуля написать, воспользоваться низкоуровневыми библиотеками для работы с базами данных, но это будет долго. Готовые фреймворки заметно упрощают взаимодействие с векторными хранилищами и LLM. Я нашел трех кандидатов для этого:

  • Langchain

  • Eino

  • GenKit

Ознакомился со всеми тремя и сделал для себя следующие выводы.

Genkit

Самый новый из троих, стабильная версия 1.0 вышла в сентябре 2025. Разрабатывается компанией Google.

Плюсы:

  • встроенные инструменты для отладки и разработки;

  • Google;

  • поддержка строгой типизации входных и выходных данных;

  • есть инструменты для мониторинга.

Минусы:

  • плохая поддержка Go, больше нацелен на Node.js, есть интеграции с фреймворками для фронтенда, часть фич отсутствует для Go;

  • выбор моделей ограничен;

  • тесно связан с облачными сервисами самого Google, нет возможности подключить свой Prometheus для сбора метрик, зато свой облачный мониторинг включается одной строкой;

  • векторные хранилища тоже поддерживаются в ограниченном количестве, часть их них также облачные от Google.

Eino

Весьма функциональный фреймворк от ByteDance — создателя TikTok, одного из крупнейших разработчиков ПО в Китае.

Плюсы:

  • серьезный разработчик;

  • есть свои плагины для IDE для облегчения разработки;

  • поддержка строгой типизации;

  • ориентирован на потоковую генерацию;

  • можно расширять и добавлять собственные реализации для индексеров, ретриверов и других элементов;

  • хорошо проработаны хуки жизненного цикла;

  • ориентирован на разработку агентов, оркестрация на основе графов.

Минусы:

  • Не очень большой выбор вариантов для моделей и векторных хранилищ, в основном китайские. Можно реализовать свои адаптеры, но хотелось бы из коробки.

  • Скромная документация при богатом функционале, мало примеров использования, часть документации в коде на китайском без английского перевода.

Langchain

Если не самый популярный, то точно один из таковых, изначально написан на Python, но имеется и его порт на Go.

Плюсы:

  • Очень известен в среде AI-разработчиков, можно проконсультироваться с ними в случае сложностей и вам будет всё понятно, так как в Go версии все те же концепции, что и для Python.

  • Самое большое количество поддерживаемых из коробки провайдеров LLM среди трех кандидатов. Есть локальный запуск в Ollama и из бинарного файла, OpenAI-совместимые API, Google, Mistral, HuggingFace и т. п.

  • Поддержка известных векторных хранилищ.

  • Механизмы кэширования ответов моделей, поддержания памяти чата, чтения и разбиения документов.

  • Богатая библиотека примеров использования в различных сценариях.

Минусы:

  • Отсутствие возможностей для строгой типизации и структуры входных и выходных данных.

  • Возможность реализовать одно и то же несколькими способами разной степени очевидности и понятности.

Genkit очевидно не подходит, по крайней мере на момент написания статьи, но выглядит перспективно. Eino кажется хорош, но сложноват для входа, и некоторых вещей не хватает. В итоге я остановился на Langchain, он оказался в целом понятным, все что нужно в нем уже есть из коробки, и нагуглить решение сложного вопроса будет гораздо проще (пусть даже и на Python). В дальнейших примерах кода буду использовать упомянутую библиотеку langchaingo.

Собираем приложение

Шаг 1: Общаемся с моделью

Для начала нам нужен минимальный чат с моделью, чтобы можно было ей задавать вопросы и читать ее ответы. Модели умеют отдавать результаты генерации в потоковом режиме по мере появления, во всех современных приложениях с AI мы это можем наблюдать. Выглядит красиво, давайте и мы тоже сделаем. Полный пример кода приложения можно посмотреть по ссылке.
В качестве источника знаний я решил взять текст романа А.С. Пушкина «Евгений Онегин».

Я буду использовать llama3, запущенную локально, поскольку она довольно простенькая, а серьезные модели и без нашего RAG хорошо знакомы с содержанием книги. Так можно будет увидеть разницу с RAG и без него.

package main func main() { ctx := context.Background() slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))) llm := initOllamaModel() srv := server.NewServer(chat.NewService(llm)) if err := srv.Run(); err != nil { slog.Error(«failed to run server», «error», err) os.Exit(1) } } func initOllamaModel() *ollama.LLM { modelName := «llama3.2:3b» llm, err := ollama.New(ollama.WithModel(modelName)) if err != nil { slog.Error(«failed to create ollama llm», «error», err) os.Exit(1) } llm.CallbacksHandler = metrics.NewCallbackHandler(modelName) return llm }

Сервер будет обрабатывать сообщения чата отдельной ручкой и отдавать результат с помощью Server-Sent Events. Это проще, чем вебсокеты, особенно в части масштабирования и балансировки нагрузки. Так мы реализуем появление текста по мере генерации.

Также добавим главную страницу с простеньким фронтендом для отображения чата и отправки сообщений.

package server type ChatService interface { Chat(ctx context.Context, in chat.Message) <-chan string } type Server struct { chat ChatService } func (s *Server) Run() error { r := chi.NewRouter() r.Use(middleware.Recoverer) r.Get(«/», s.handleRoot) r.Post(«/chat/{chatID}», s.handleChat) return http.ListenAndServe(«:8080», r) } func (s *Server) handleChat(w http.ResponseWriter, req *http.Request) { chatID, err := uuid.Parse(chi.URLParam(req, «chatID»)) if err != nil || chatID == uuid.Nil { slog.Error(«invalid chat ChatID», «error», err, «chatID», chi.URLParam(req, «chatID»)) http.Error(w, «Invalid chat ChatID», http.StatusBadRequest) return } w.Header().Set(«Content-Type», «text/event-stream») w.Header().Set(«Cache-Control», «no-cache») w.Header().Set(«Connection», «keep-alive») flusher, ok := w.(http.Flusher) if !ok { http.Error(w, «Streaming unsupported», http.StatusInternalServerError) return } input, err := io.ReadAll(req.Body) if err != nil { http.Error(w, «Error reading request body», http.StatusBadRequest) } defer req.Body.Close() ch := s.chat.Chat(req.Context(), chat.Message{ ChatID: chatID, Text: string(input), }) for msg := range ch { _, _ = fmt.Fprintf(w, «data: %snn», msg) flusher.Flush() } } func (s *Server) handleRoot(w http.ResponseWriter, _ *http.Request) { w.Header().Set(«Content-Type», «text/html; charset=utf-8») data, err := embedFiles.ReadFile(«html/index.html») if err != nil { slog.Error(«error reading file», «error», err) http.Error(w, «internal server error», http.StatusInternalServerError) } else { _, _ = w.Write(data) } }

И непосредственно реализация чата с моделью и потоковым ответом.

type Service struct { llm llms.Model } func NewService(llm llms.Model) *Service { return &Service{llm: llm} } func (s *Service) Chat(ctx context.Context, msg Message) <-chan string { res := make(chan string) content := []llms.MessageContent{ llms.TextParts(llms.ChatMessageTypeSystem, «Ты специалист по творчеству Александра Сергеевича Пушкина. Тебе нужно ответить на вопрос.»), } content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, msg.Text)) go s.generateContent(ctx, msg.ChatID, content, res) return res } func (s *Service) generateContent(ctx context.Context, chatID uuid.UUID, content []llms.MessageContent, out chan<- string) { defer close(out) completion, err := s.llm.GenerateContent(ctx, content, llms.WithTemperature(0.5), llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error { if ctx.Err() != nil { return fmt.Errorf(«error: %w», ctx.Err()) } out <- fmt.Sprintf(«%s», chunk) return nil }), ) if err != nil { slog.Error(«failed to generate content», «error», err) out <- «Failed to generate content» return } slog.Debug(«completion result», «result», *completion) }

Для реализации потокового ответа сервисный метод возвращает канал, из которого мы будем читать блоки текста и возвращать в ответе сервера. Если потоковый ответ не требуется, то можно просто дождаться результата вызова GenerateContent.

llama3 не очень хорошо знакома с творчеством Александра Сергеевича, но она старалась
llama3 не очень хорошо знакома с творчеством Александра Сергеевича, но она старалась

Изначально я использовал другую модель, и она знала правильный ответ ещё до внедрения цитат из книги. Перебрал несколько разных, пока не нашёл ту, которая не знает, как же звали Ленского.

Шаг 2: Добавляем «мозг»

Настала очередь базы знаний. В langchaingo есть несколько адаптеров для популярных векторных баз данных, я буду использовать Qdrant. Почему именно его? Он довольно популярный, часто попадался мне на глаза, я захотел попробовать его. А еще его можно запустить в Docker без параметров, и в комплекте есть вебадминка, для тестовой среды очень удобно. Для эмбеддинга буду использовать модель EmbeddingGemma также локально.

func initOllamaEmbedder() embeddings.Embedder { llm, err := ollama.New(ollama.WithModel(«embeddinggemma:300m»)) if err != nil { slog.Error(«failed to create llm», «error», err) os.Exit(1) } embedder, err := embeddings.NewEmbedder(llm) if err != nil { slog.Error(«failed to create embedder», «error», err) os.Exit(1) } return embedder } func initQdrantStore(embedder embeddings.Embedder) vectorstores.VectorStore { url, err := url.Parse(«http://localhost:6333/») if err != nil { slog.Error(«failed to parse url», «error», err) os.Exit(1) } store, err := qdrant.New( qdrant.WithURL(*url), qdrant.WithCollectionName(«Onegin»), qdrant.WithEmbedder(embedder), ) if err != nil { slog.Error(«failed to create qdrant store», «error», err) os.Exit(1) } return store }

В коде можно заметить параметр CollectionName, здесь необходимо указать имя коллекции, которую мы создадим в Qdrant. Запустим его в Docker и настроим.

docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant

Открываем http://localhost:6333/dashboard и сразу попадаем на страницу Коллекций. Нажимаем Create Collection, вводим имя коллекции, которое прописали в коде. Далее отвечаем на пару вопросов, выбирая подходящие опции. Я выбираю:

  • Global search (у нас один набор данных для всех пользователей).

  • Simple single embedding (сложные варианты поиска нам пока не нужны).

Последняя опция очень важная, здесь у нас задается размерность вектора. Он должен совпадать с аналогичным параметром модели, иначе мы не сможем ничего сохранить. Для EmbeddingGemma он равен 768, для вашего варианта модели должен быть указан в документации к ней. Устанавливаем его, метрика по умолчанию косинус, оставляем её. В последнем шаге нам предлагают добавить индексы по полям, но мы не будем добавлять к нашим векторам дополнительные метаданные, так что пропускаем этот шаг и сохраняем коллекцию.

Текст Онегина находим в интернете без регистрации и смс, общественное достояние как-никак. Имеющийся текст необходимо разбить на куски, которые мы векторизируем. Они должны быть не слишком большими, чтобы не перегружать модель, и не слишком маленькими, чтобы в них была полезная информация и потенциальный ответ на вопрос пользователя. Воспользуемся модулем textsplitter нашей библиотеки langchaingo, разбивать будем по 400 токенов с перекрытием в 100 токенов. У нас нет цели точно затюнить поиск, а этого хватит для теста.

func main() { ctx := context.Background() slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))) embedder := initOllamaEmbedder() store := initQdrantStore(embedder) splitter := textsplitter.NewTokenSplitter( textsplitter.WithChunkSize(400), textsplitter.WithChunkOverlap(100), ) slog.Info(«splitter created») file, err := os.Open(«evgenii-onegin.txt») if err != nil { slog.Error(«failed to open file», «error», err) os.Exit(1) } defer file.Close() loader := documentloaders.NewText(file) docs, err := loader.LoadAndSplit(ctx, splitter) if err != nil { slog.Error(«failed to load and split documents», «error», err) os.Exit(1) } slog.Info(«documents loaded and split», «count», len(docs)) ids, err := store.AddDocuments(ctx, docs) if err != nil { slog.Error(«failed to add documents to store», «error», err) os.Exit(1) } slog.Info(«documents added to store», «count», len(ids)) }

Запускаем нашу простенькую программу, через несколько минут наблюдаем в админке заполненную коллекцию. Добавляем к нашему приложению поиск в базе.

func (s *Service) Chat(ctx context.Context, msg Message) <-chan string { res := make(chan string) docs, err := s.store.SimilaritySearch(ctx, msg.Text, maxResults) if err != nil { slog.Error(«failed to search docs from vector store», «error», err) } slog.Debug(«found docs», «docs», docs) content := []llms.MessageContent{ llms.TextParts(llms.ChatMessageTypeSystem, «Ты специалист по творчеству Александра Сергеевича Пушкина. Тебе нужно ответить на вопрос.»), } if len(docs) > 0 { content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, «Ты знаешь следующие документы:»)) } for _, doc := range docs { content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, doc.PageContent)) } content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, msg.Text)) go s.generateContent(ctx, msg.ChatID, content, res) return res }

Здесь я добавил также дополнительные инструкции для модели: системный промпт для обозначения задачи и пояснение, что вообще за текст тут добавлен. Перезапускаем приложение и повторяем наш вопрос модели.

Модель поняла, что в её распоряжении несколько отрывков книги, и нашла в них ответ.
Модель поняла, что в её распоряжении несколько отрывков книги, и нашла в них ответ.

Шаг 3: Наводим порядок в разговоре

Теперь давайте добавим сохранение истории сообщений и опробуем на более серьезной модели. Здесь также нет ничего сложного, нам нужно сохранить в базу запрос пользователя и ответ модели, а в следующем обращении загрузить предыдущие вопросы и ответы. Чтобы не перегружать модель и сэкономить платные токены, мы можем ограничить историю по количеству сообщений или токенов, или даже делать суммаризацию переписки с помощью модели. Хранить историю можем в любой базе данных, в своём примере я использую PostgreSQL.

func (h *historyStorage) Save(ctx context.Context, id uuid.UUID, role llms.ChatMessageType, content string) error { ctx, cancel := context.WithTimeout(ctx, pgQueryTimeout) defer cancel() query := `INSERT INTO history (chat_id, role, message) VALUES ($1, $2, $3)` if _, err := h.db.Exec(ctx, query, id, role, content); err != nil { return fmt.Errorf(«insert into history: %w», err) } return nil } func (h *historyStorage) Load(ctx context.Context, id uuid.UUID) ([]llms.MessageContent, error) { ctx, cancel := context.WithTimeout(ctx, pgQueryTimeout) defer cancel() query := `SELECT role, message FROM history WHERE chat_id = $1 ORDER BY created_at DESC LIMIT 4` rows, err := h.db.Query(ctx, query, id) if err != nil { return nil, fmt.Errorf(«query history: %w», err) } var res []llms.MessageContent for rows.Next() { if err = rows.Err(); err != nil { return nil, fmt.Errorf(«history row error: %w», err) } var ( role string content string ) if err = rows.Scan(&role, &content); err != nil { return nil, fmt.Errorf(«scan history row: %w», err) } res = append(res, llms.TextParts(llms.ChatMessageType(role), content)) } slices.Reverse(res) return res, nil }func (s *Service) Chat(ctx context.Context, msg Message) <-chan string { res := make(chan string) history, err := s.history.Load(ctx, msg.ChatID) if err != nil { slog.Error(«failed to load history», «error», err) } err = s.history.Save(ctx, msg.ChatID, llms.ChatMessageTypeHuman, msg.Text) if err != nil { slog.Error(«failed to save user message to history», «error», err) } docs, err := s.store.Search(ctx, msg.Text) if err != nil { slog.Error(«failed to search docs from vector store», «error», err) } slog.Debug(«found docs», «docs», docs) content := []llms.MessageContent{ llms.TextParts(llms.ChatMessageTypeSystem, «Ты специалист по творчеству Александра Сергеевича Пушкина. Тебе нужно ответить на вопрос.»), } if len(history) > 0 { for _, historyMsg := range history { content = append(content, historyMsg) } } if len(docs) > 0 { content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, «Ты знаешь следующие документы:»)) } for _, doc := range docs { content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, doc.PageContent)) } content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, msg.Text)) go s.generateContent(ctx, msg.ChatID, content, res) return res }

Здесь я сделал простейший вариант ограничения истории — добавил лимит в запрос. Обратите внимание, мы не сохраняем в историю результаты поиска по базе знаний — они нам там ни к чему. Модель даст ответ с их учетом, и его мы запишем в историю.

func initOpenAIModel() *openai.LLM { modelName := «GigaChat/GigaChat-2-Max» client := &http.Client{ Transport: initModelAPITransport(modelName), Timeout: 300 * time.Second, } cbHandler := metrics.NewCallbackHandler(modelName) // Token set in OPENAI_API_KEY env llm, err := openai.New( openai.WithModel(modelName), openai.WithBaseURL(«https://foundation-models.api.cloud.ru/v1»), openai.WithHTTPClient(client), openai.WithCallback(cbHandler), ) if err != nil { slog.Error(«failed to create openai llm», «error», err) os.Exit(1) } return llm }

Спросим что-нибудь посложнее и посмотрим на нашу историю.

779dde8c555ef5f6780182aba38cf8df
61724750fe01529f0a2459da44497b68

Куда двигаться дальше: улучшаем PoC

Приведенный пример приложения является лишь Proof-of-Concept, что подобные приложения можно писать на Go. Для полноценного внедрения в продакшн его ещё потребуется доработать.

Потребуются инструменты наблюдения: логи, метрики, трейсинг. Здесь в целом все как в обычном бэкенде: отслеживаем обращения к базам данных, сторонним сервисам (моделям), но добавляется один нюанс. Будет не лишним добавить метрики расхода токенов, поскольку они тарифицируются в платных моделях. Для этих целей в ответах моделей в langchaingo есть поле GenerationInfo, из которого можно получить информацию о количестве входных и выходных токенов, подсчитанных самой моделью. Также библиотека предоставляет возможность задать обработчики для событий жизненного цикла.

func (c CallbackHandler) HandleLLMGenerateContentEnd(_ context.Context, res *llms.ContentResponse) { if len(res.Choices) == 0 { return } if input, ok := res.Choices[0].GenerationInfo[«PromptTokens»].(int); ok { IncInputTokens(c.model, input) } if output, ok := res.Choices[0].GenerationInfo[«CompletionTokens»].(int); ok { IncOutputTokens(c.model, output) } }

Для улучшения поиска по базе данных можно задействовать дополнительные поля с метаданными, совместить векторный и полнотекстовый поиск, а также добавить ранжирование результатов с помощью Reranker-моделей (cross-encoder).

Стоит уделить внимание настройке таймаутов для обращений к моделям. Модель может генерировать ответ очень долго, особенно если используется размышление (thinking) или объяснение (reasoning), да и вопрос может быть нетривиальным, и тогда время до окончания генерации может исчисляться минутами. Можно ограничить количество токенов в генерации, но это не всегда приемлемо. Необходимо настроить их таким образом, чтобы генерация не прерывалась на середине, но также и не блокировать сервис в случае сетевых проблем.

Заключение

Я сам думал, что внедрить AI в приложение на Go будет непросто. На практике же оказалось, что RAG, да и не только он — это всего лишь архитектурные паттерны, а не эксклюзивная технология, и он может быть реализован на любом языке программирования. В свою очередь, Go это отличный выбор для реализации AI-приложений:

  • Библиотеки и фреймворки активно развиваются.

  • Высокая производительность и поддержка многопоточности открывают возможности для создания высоконагруженных приложений.

  • Легкая интеграция в существующие системы на Go.

  • Простота развертывания и эксплуатации (DevOps-friendly).

Ну а если хочется научить свою LLM-ку чему-то новому, но возиться с языками программирования вообще нет желания, у моих коллег из Evolution есть сервис Managed RAG. С документацией и примерами.

А вы уже пробовали писать AI приложения на Go? Расскажите, с какими проблемами столкнулись в процессе и какие инструменты показали себя лучше всего?

Источник: habr.com

✅ Найденные теги: Когда, новости

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых