Создаем ECHO: локальный AI-ассистент с памятью ChromaDB и поиском в сети | AiManual
AiManual Logo Ai / Manual.
06 Фев 2026 Гайд

ECHO: Собираем локального AI-компаньона с долгосрочной памятью и доступом в интернет

Пошаговый гайд по сборке ECHO — локального AI-компаньона с долгосрочной памятью (ChromaDB), поиском в интернете (Trafilatura) и Ollama моделями. Полный офлайн-к

Зачем вообще это нужно?

ChatGPT забывает о вас через 5 минут. Claude хранит историю, но не факты. Все облачные AI-ассистенты работают по принципу «здесь и сейчас» — задал вопрос, получил ответ, контекст очищен. Они не помнят, что вы обсуждали неделю назад, какие статьи читали, какие идеи предлагали.

Локальные модели через Ollama — другое дело. Они работают у вас на машине. Но у них та же проблема: нет долгосрочной памяти. Каждый чат — чистый лист. Вы теряете контекст, факты, всю историю взаимодействия.

ECHO решает эту проблему. Это не просто «еще один локальный ассистент». Это компаньон, который помнит все. Все ваши разговоры, все статьи, которые вы ему показали, все ссылки, которые вы отправили. Он строит персональную базу знаний на вашем компьютере и использует ее для ответов.

На 06.02.2026 актуальный стек выглядит так: Ollama 0.5.3+ с поддержкой новых моделей (Llama 3.2 90B, Qwen2.5 72B), ChromaDB 0.5.7 с улучшенной векторной индексацией, Trafilatura 1.9.0 для чистого скрапинга. Все работает локально, без единого запроса в облако.

Что получается в итоге?

  • Десктопное приложение на Electron (работает на Windows, macOS, Linux)
  • Полная долгосрочная память всех чатов в ChromaDB
  • Автоматический скрапинг ссылок и статей через Trafilatura
  • Поиск в вашей личной базе знаний перед ответом (многоступенчатый RAG)
  • Выбор любой модели из Ollama (включая нецензурированные версии)
  • Полный офлайн-режим или поиск в интернете по запросу

Звучит сложно? На самом деле все собирается за вечер. Главное — понять архитектуру.

Архитектура: как все устроено внутри

ECHO работает по принципу многоступенчатого RAG (Retrieval-Augmented Generation). Это не просто «взял текст из базы и вставил в промпт». Это цепочка решений:

Шаг Что происходит Технология
1. Получение вопроса Пользователь задает вопрос или отправляет ссылку React/Electron интерфейс
2. Поиск в памяти Система ищет релевантные фрагменты в ChromaDB Векторный поиск (all-MiniLM-L6-v2)
3. Скрапинг (если ссылка) Trafilatura извлекает чистый текст, сохраняет в базу Trafilatura + BeautifulSoup
4. Построение контекста Сборка финального промпта из найденных фрагментов Кастомный шаблонизатор
5. Генерация ответа Ollama запускает локальную модель с контекстом Ollama API + выбранная модель
6. Сохранение в память Вопрос и ответ добавляются в ChromaDB для будущих запросов ChromaDB коллекции

Ключевое отличие от простых RAG-систем: ECHO не просто отвечает на вопросы. Он постоянно учится. Каждая ссылка, каждый ваш вопрос, каждый ответ модели — все это попадает в базу знаний. Чем дольше вы им пользуетесь, тем умнее он становится.

Шаг 1: Готовим окружение

1 Устанавливаем Ollama и качаем модель

Ollama — это двигатель. Без него ничего не заработает. На 06.02.2026 актуальная версия — 0.5.3, но проверяйте свежее.

# Для Linux/macOS
curl -fsSL https://ollama.com/install.sh | sh

# Для Windows — качаем с официального сайта
# После установки проверяем
ollama --version

Теперь качаем модель. Я рекомендую начинать с чего-то сбалансированного:

# Llama 3.2 3B — быстро, достаточно умно для начала
ollama pull llama3.2:3b

# Qwen2.5 7B — если есть запас VRAM
ollama pull qwen2.5:7b

# Для серьезной работы — Llama 3.2 90B (нужно 48+ GB VRAM)
# ollama pull llama3.2:90b
💡
Не зацикливайтесь на выборе модели. Все современные LLM (Llama 3.2, Qwen2.5, Mistral 8x22B) на 06.02.2026 дают хорошие результаты. Главное — чтобы модель влезла в вашу видеопамять. Начинайте с малого, потом перейдете к тяжелым моделям.

2 Ставим Python и зависимости

Бэкенд ECHO написан на Python. Нужна версия 3.10+.

# Создаем виртуальное окружение
python -m venv echo_env
source echo_env/bin/activate  # На Windows: echo_env\Scripts\activate

# Устанавливаем зависимости
pip install chromadb==0.5.7
pip install trafilatura==1.9.0
pip install beautifulsoup4
pip install requests
pip install sentence-transformers
pip install flask
pip install flask-cors

Sentence-transformers скачает модель all-MiniLM-L6-v2 при первом запуске (~80 МБ). Убедитесь, что есть интернет на этом этапе. Потом все работает офлайн.

Шаг 2: Пишем бэкенд — мозг системы

Создаем файл echo_backend.py. Это сердце ECHO.

import chromadb
from sentence_transformers import SentenceTransformer
import trafilatura
from trafilatura.settings import use_config
import requests
from bs4 import BeautifulSoup
import json
import os
from datetime import datetime
from flask import Flask, request, jsonify
from flask_cors import CORS

# Инициализируем ChromaDB (персистентная база)
chroma_client = chromadb.PersistentClient(path="./echo_memory")
collection = chroma_client.get_or_create_collection(name="echo_knowledge")

# Модель для эмбеддингов (векторизации текста)
embedder = SentenceTransformer('all-MiniLM-L6-v2')

app = Flask(__name__)
CORS(app)  # Разрешаем запросы из Electron

class EchoBrain:
    def __init__(self):
        self.config = use_config()
        self.config.set("DEFAULT", "EXTRACTION_TIMEOUT", "10")
    
    def scrape_url(self, url):
        """Достаем чистый текст из статьи"""
        try:
            downloaded = trafilatura.fetch_url(url)
            text = trafilatura.extract(downloaded, config=self.config)
            return text if text else "Не удалось извлечь текст"
        except Exception as e:
            return f"Ошибка скрапинга: {str(e)}"
    
    def add_to_memory(self, text, source="user", metadata=None):
        """Добавляем текст в долгосрочную память"""
        if not text or len(text.strip()) < 10:
            return
        
        embedding = embedder.encode(text).tolist()
        doc_id = f"doc_{datetime.now().timestamp()}_{hash(text) % 10000}"
        
        meta = metadata or {}
        meta.update({"source": source, "timestamp": datetime.now().isoformat()})
        
        collection.add(
            documents=[text],
            embeddings=[embedding],
            metadatas=[meta],
            ids=[doc_id]
        )
        return doc_id
    
    def search_memory(self, query, n_results=5):
        """Ищем релевантные фрагменты в памяти"""
        query_embedding = embedder.encode(query).tolist()
        
        results = collection.query(
            query_embeddings=[query_embedding],
            n_results=n_results
        )
        
        if results['documents']:
            return results['documents'][0], results['metadatas'][0]
        return [], []
    
    def build_context(self, query, search_results):
        """Строим контекст для промпта"""
        if not search_results:
            return ""
        
        context_parts = []
        for i, doc in enumerate(search_results):
            context_parts.append(f"[Фрагмент памяти {i+1}]\n{doc}\n")
        
        return "\n".join(context_parts)

brain = EchoBrain()

@app.route('/api/chat', methods=['POST'])
def chat():
    data = request.json
    query = data.get('query', '')
    
    # Шаг 1: Ищем в памяти
    relevant_docs, _ = brain.search_memory(query)
    
    # Шаг 2: Строим контекст
    context = brain.build_context(query, relevant_docs)
    
    # Шаг 3: Формируем промпт для Ollama
    prompt = f"""Ты — ECHO, AI-компаньон с долгосрочной памятью.
    
Контекст из памяти:
{context}

Вопрос пользователя: {query}

Ответь, используя контекст если он релевантен. Если информации недостаточно, скажи об этом честно.
Ответ:"""
    
    # Шаг 4: Отправляем в Ollama
    ollama_response = requests.post(
        'http://localhost:11434/api/generate',
        json={
            "model": "llama3.2:3b",  # Модель по умолчанию
            "prompt": prompt,
            "stream": False
        }
    )
    
    if ollama_response.status_code == 200:
        response_text = ollama_response.json().get('response', 'Нет ответа')
        
        # Шаг 5: Сохраняем вопрос и ответ в память
        brain.add_to_memory(query, source="user", metadata={"type": "question"})
        brain.add_to_memory(response_text, source="echo", metadata={"type": "answer"})
        
        return jsonify({"response": response_text})
    else:
        return jsonify({"error": "Ollama не отвечает"}), 500

@app.route('/api/add_url', methods=['POST'])
def add_url():
    """Добавляем статью по ссылке в память"""
    data = request.json
    url = data.get('url', '')
    
    text = brain.scrape_url(url)
    if text and "Ошибка" not in text:
        doc_id = brain.add_to_memory(text, source="web", metadata={"url": url})
        return jsonify({"success": True, "doc_id": doc_id, "preview": text[:200]})
    
    return jsonify({"error": text}), 400

if __name__ == '__main__':
    app.run(port=5000, debug=True)

Это упрощенная версия, но она уже работает. Запускаем:

python echo_backend.py

Сервер запустится на порту 5000. Теперь нужен фронтенд.

Шаг 3: Electron-интерфейс — лицо системы

Создаем директорию echo-electron и инициализируем проект:

mkdir echo-electron
cd echo-electron
npm init -y
npm install electron react react-dom @mui/material @mui/icons-material axios

Основной файл main.js:

const { app, BrowserWindow } = require('electron')
const path = require('path')

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  })
  
  win.loadFile('index.html')
  // win.webContents.openDevTools() // Раскомментируйте для отладки
}

app.whenReady().then(createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit()
})

И простой index.html с React-компонентом:





    
    ECHO - Локальный AI-компаньон
    


    

React-компонент в renderer.js (используем современный синтаксис):

import React, { useState } from 'react'
import ReactDOM from 'react-dom/client'
import { 
  Box, TextField, Button, Paper, Typography, 
  Container, List, ListItem, ListItemText, 
  Chip, Alert, CircularProgress 
} from '@mui/material'
import { Send as SendIcon, Link as LinkIcon } from '@mui/icons-material'
import axios from 'axios'

const API_URL = 'http://localhost:5000/api'

function App() {
  const [query, setQuery] = useState('')
  const [messages, setMessages] = useState([])
  const [loading, setLoading] = useState(false)
  const [urlInput, setUrlInput] = useState('')
  
  const handleSend = async () => {
    if (!query.trim()) return
    
    setMessages(prev => [...prev, { text: query, sender: 'user' }])
    setLoading(true)
    
    try {
      const response = await axios.post(`${API_URL}/chat`, { query })
      setMessages(prev => [...prev, { 
        text: response.data.response, 
        sender: 'echo' 
      }])
    } catch (error) {
      setMessages(prev => [...prev, { 
        text: 'Ошибка соединения с сервером', 
        sender: 'error' 
      }])
    }
    
    setLoading(false)
    setQuery('')
  }
  
  const handleAddUrl = async () => {
    if (!urlInput.trim()) return
    
    try {
      const response = await axios.post(`${API_URL}/add_url`, { url: urlInput })
      if (response.data.success) {
        setMessages(prev => [...prev, { 
          text: `Статья добавлена в память: ${response.data.preview}...`, 
          sender: 'system' 
        }])
        setUrlInput('')
      }
    } catch (error) {
      alert('Ошибка при добавлении статьи')
    }
  }
  
  return (
    
      
        
          ECHO — Локальный AI-компаньон
        
        
          Долгосрочная память: ChromaDB • Модель: Ollama • Поиск: Trafilatura
        
        
        
           setUrlInput(e.target.value)}
          />
          }
            onClick={handleAddUrl}
          >
            Добавить
          
        
      
      
      
        
          {messages.map((msg, idx) => (
            
              
                
                
              
            
          ))}
          {loading && (
            
              
              ECHO думает...
            
          )}
        
      
      
      
         setQuery(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && handleSend()}
          disabled={loading}
        />
        }
          onClick={handleSend}
          disabled={loading || !query.trim()}
        >
          Отправить
        
      
    
  )
}

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render()

Запускаем Electron:

npx electron .

И вот он — ваш личный ECHO. Работает полностью локально.

Где это взять готовое и как развивать

Я выложил полную версию на GitHub (ссылка в профиле). Там есть:

  • Настройка разных моделей Ollama через интерфейс
  • Экспорт/импорт базы знаний ChromaDB
  • Пакеты для Windows/macOS/Linux
  • Расширенный скрапинг с обработкой PDF
  • Голосовой интерфейс (интеграция с Whisper)

Но главное — вы теперь понимаете архитектуру. Можете добавлять функции:

# Например, автономный поиск в интернете
def web_search(self, query):
    """Ищем в интернете через Searxng или локальный поисковик"""
    # Реализация поиска
    pass

# Или интеграция с локальными документами
def index_documents(self, folder_path):
    """Индексируем все текстовые файлы в папке"""
    for file in os.listdir(folder_path):
        if file.endswith('.txt') or file.endswith('.md'):
            with open(os.path.join(folder_path, file), 'r') as f:
                text = f.read()
                self.add_to_memory(text, source="local", metadata={"file": file})

Что делать, когда все сломалось

Типичные проблемы и решения:

Проблема Причина Решение
Ollama не отвечает Сервис не запущен или порт занят ollama serve в отдельном терминале
ChromaDB ошибка доступа Нет прав на запись в папку Проверьте права или смените путь
Трафилатура не скрапит Сайт блокирует ботов или требует JS Добавьте headers или используйте Selenium
Модель не грузится Не хватает VRAM или места на диске Возьмите меньшую модель или освободите память

Зачем все это, если есть ChatGPT?

Три причины:

  1. Приватность. Все ваши разговоры остаются на вашем компьютере. Никто не читает, не анализирует, не использует для тренировки моделей.
  2. Долгосрочная память. ChatGPT забывает через 4096 токенов. ECHO помнит годы.
  3. Нецензурированность. Локальные модели можно настроить как угодно. Без фильтров, без ограничений, без «извините, я не могу ответить на этот вопрос».

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

На 06.02.2026 тренд очевиден: массовый уход от облачных AI к локальным решениям. Причины — цена (токены дорожают), приватность (скандалы с утечками), контроль (компании меняют правила игры). ECHO — ваш личный островок в этом море.

Что дальше?

Собранный ECHO — база. Дальше можно:

  • Добавить голосовой интерфейс (вот гайд по голосовым ассистентам)
  • Подключить управление умным домом
  • Настроить автономный поиск в интернете
  • Сделать мобильную версию
  • Интегрировать с календарем и почтой

Но самое главное — вы теперь не просто пользователь AI. Вы его создатель. И это меняет все.

P.S. Если не хотите собирать с нуля — готовые сборки в моем Telegram-канале. Но честно — собрать самому в разы интереснее.