Локальный ИИ-ассистент Jarvis: пошаговая инструкция для начинающих | AiManual
AiManual Logo Ai / Manual.
31 Янв 2026 Гайд

Собираем локального Jarvis за вечер: ваш первый персональный ИИ-ассистент

Создайте персонального ИИ-ассистента на своем компьютере без отправки данных в облако. Полный гайд по установке, настройке и интеграциям.

Зачем собирать своего Jarvis, когда есть Siri и Alexa?

Ответ прост: потому что это ваш личный цифровой дворецкий. Он не продает ваши данные рекламодателям. Не отключается, когда у Google случаются проблемы. И не говорит "извините, я этого пока не умею", когда вы просите что-то сложнее погоды.

Я собрал десяток таких систем за последний год. От монстров на восьмиядерных серверах до скромных помощников на Raspberry Pi. И знаете что? Самые полезные - те, что делают ровно то, что вам нужно. Не больше.

Это не очередной туториал "установи Docker и запусти контейнер". Я покажу, почему каждая часть системы работает именно так, и что делать, когда что-то ломается (а это обязательно случится).

1 Что нам понадобится (и почему именно это)

Забудьте про требования "минимум 32GB RAM". Мы будем собирать рабочую систему, а не исследовательский стенд. Вот что действительно нужно:

Компонент Минимум Рекомендация Зачем
Процессор 4 ядра 6+ ядер LLM жрут CPU как голодные студенты макароны
Оперативная память 8GB 16GB Модель + система + ваш браузер с 50 вкладками
Видеокарта Не нужна NVIDIA с 6GB+ VRAM Ускорит работу в 3-5 раз, но можно и на CPU
Диск 10GB свободно SSD 20GB+ Модели весят 4-8GB каждая

Операционная система? Любая, где работает Python 3.10+. Я буду показывать на Ubuntu, но для Windows и macOS различия минимальны.

2 Выбираем модель: меньше - лучше

Здесь большинство новичек совершают первую ошибку: пытаются запустить Llama 3.1 70B на ноутбуке 2018 года. Результат предсказуем - система впадает в кому.

На 31 января 2026 года у нас есть отличные маленькие модели, которые справляются с задачами ассистента:

  • Qwen2.5-Coder-1.5B - если ваш ассистент будет работать с кодом
  • Gemma-2-2B - сбалансированный вариант для общего использования
  • Phi-3.5-mini - удивительно умная для своего размера
  • DeepSeek-V2-Lite - китайская модель с хорошим английским
💡
Начинайте с самой маленькой модели, которая решает ваши задачи. Вы всегда сможете переключиться на более крупную, если не хватит интеллекта. Обратный путь (с 7B на 2B) психологически сложнее.

Я выбрал Gemma-2-2B. Почему? Она отлично справляется с function calling (вызовом функций), что критично для ассистента. И весит всего 1.5GB в формате GGUF.

3 Ставим Ollama - наш локальный движок

Ollama - это как Docker для моделей. Скачиваете, запускаете, модель работает. Никаких танцев с бубном вокруг CUDA и трансформеров.

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

# Для Windows - скачайте установщик с сайта
# Или через Winget, если любите терминал
winget install Ollama.Ollama

Проверяем установку:

ollama --version
# Должно показать что-то вроде: ollama version 0.8.0

Теперь самое интересное - загружаем модель:

ollama pull gemma2:2b
# Ждем. Загрузка 1.5GB займет время
# Можно ускорить, если есть NVIDIA карта
OLLAMA_GPU="0" ollama pull gemma2:2b

Если у вас медленный интернет или ограниченный трафик, скачайте модель вручную с Hugging Face и положите в ~/.ollama/models/. Ollama подхватит ее автоматически.

4 Пишем сердце ассистента на Python

Вот где начинается магия. Мы создадим простой, но расширяемый каркас. Не копируйте код слепо - понимайте, что каждая строчка делает.

Сначала устанавливаем зависимости:

pip install requests python-dotenv openai

Создаем файл .env для конфигурации:

# .env
OLLAMA_BASE_URL=http://localhost:11434
MODEL_NAME=gemma2:2b
WEATHER_API_KEY=your_key_here  # получите на openweathermap.org

Теперь основной файл assistant.py:

import os
import json
import requests
from datetime import datetime
from typing import Dict, Any
from dotenv import load_dotenv

load_dotenv()

class JarvisAssistant:
    def __init__(self):
        self.base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
        self.model = os.getenv("MODEL_NAME", "gemma2:2b")
        self.weather_api_key = os.getenv("WEATHER_API_KEY")
        
        # Наши инструменты (функции), которые умеет выполнять ассистент
        self.tools = [
            {
                "type": "function",
                "function": {
                    "name": "get_weather",
                    "description": "Get current weather for a city",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "city": {
                                "type": "string",
                                "description": "City name, e.g. Moscow"
                            }
                        },
                        "required": ["city"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "set_reminder",
                    "description": "Set a reminder for specific time",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "message": {
                                "type": "string",
                                "description": "Reminder text"
                            },
                            "time": {
                                "type": "string",
                                "description": "Time in HH:MM format"
                            }
                        },
                        "required": ["message", "time"]
                    }
                }
            }
        ]
    
    def call_ollama(self, prompt: str, use_tools: bool = True) -> Dict[str, Any]:
        """Отправляем запрос к Ollama API"""
        url = f"{self.base_url}/api/chat"
        
        messages = [
            {
                "role": "system",
                "content": "You are Jarvis, a helpful AI assistant. Use tools when needed."
            },
            {"role": "user", "content": prompt}
        ]
        
        payload = {
            "model": self.model,
            "messages": messages,
            "stream": False
        }
        
        if use_tools:
            payload["tools"] = self.tools
        
        try:
            response = requests.post(url, json=payload, timeout=30)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            print(f"Error calling Ollama: {e}")
            return {"message": {"content": "Sorry, I'm having trouble thinking right now."}}
    
    def get_weather(self, city: str) -> str:
        """Получаем погоду через OpenWeatherMap API"""
        if not self.weather_api_key:
            return "Weather API key not configured"
            
        url = f"https://api.openweathermap.org/data/2.5/weather"
        params = {
            "q": city,
            "appid": self.weather_api_key,
            "units": "metric",
            "lang": "ru"
        }
        
        try:
            response = requests.get(url, params=params, timeout=5)
            data = response.json()
            
            if response.status_code == 200:
                temp = data["main"]["temp"]
                desc = data["weather"][0]["description"]
                return f"In {city}: {temp}°C, {desc}"
            else:
                return f"Could not get weather for {city}: {data.get('message', 'Unknown error')}"
        except Exception as e:
            return f"Weather service error: {e}"
    
    def set_reminder(self, message: str, time: str) -> str:
        """Устанавливаем напоминание (пока просто сохраняем в файл)"""
        try:
            # Проверяем формат времени
            datetime.strptime(time, "%H:%M")
            
            reminders_file = "reminders.json"
            reminders = []
            
            # Читаем существующие напоминания
            if os.path.exists(reminders_file):
                with open(reminders_file, "r") as f:
                    reminders = json.load(f)
            
            # Добавляем новое
            reminders.append({
                "message": message,
                "time": time,
                "created": datetime.now().isoformat()
            })
            
            # Сохраняем
            with open(reminders_file, "w") as f:
                json.dump(reminders, f, indent=2)
            
            return f"Reminder set for {time}: {message}"
        except ValueError:
            return "Invalid time format. Use HH:MM (24-hour format)"
        except Exception as e:
            return f"Failed to set reminder: {e}"
    
    def process_response(self, response: Dict[str, Any]) -> str:
        """Обрабатываем ответ от модели, выполняем функции если нужно"""
        message = response.get("message", {})
        
        # Проверяем, хочет ли модель вызвать функцию
        tool_calls = message.get("tool_calls", [])
        
        if tool_calls:
            results = []
            for tool_call in tool_calls:
                function_name = tool_call["function"]["name"]
                arguments = json.loads(tool_call["function"]["arguments"])
                
                if function_name == "get_weather":
                    result = self.get_weather(arguments["city"])
                elif function_name == "set_reminder":
                    result = self.set_reminder(
                        arguments["message"], 
                        arguments["time"]
                    )
                else:
                    result = f"Unknown function: {function_name}"
                
                results.append(result)
            
            # Формируем финальный ответ
            return "\n".join(results)
        else:
            # Простой текстовый ответ
            return message.get("content", "No response")
    
    def chat(self, user_input: str) -> str:
        """Основной метод для общения"""
        response = self.call_ollama(user_input)
        return self.process_response(response)

if __name__ == "__main__":
    assistant = JarvisAssistant()
    
    print("Jarvis initialized. Type 'quit' to exit.")
    print("Try: 'What's the weather in Moscow?' or 'Remind me to call mom at 18:00'")
    
    while True:
        try:
            user_input = input("\nYou: ").strip()
            
            if user_input.lower() in ["quit", "exit", "bye"]:
                print("Jarvis: Goodbye!")
                break
            
            if user_input:
                response = assistant.chat(user_input)
                print(f"Jarvis: {response}")
                
        except KeyboardInterrupt:
            print("\n\nInterrupted. Exiting...")
            break
        except Exception as e:
            print(f"Error: {e}")

Запускаем:

python assistant.py

Если все настроено правильно, вы увидите приглашение для ввода. Попробуйте спросить про погоду или установить напоминание.

5 Добавляем голос: говорим и слушаем

Текстовый интерфейс - это скучно. Настоящий Jarvis разговаривает. Добавим голосовой ввод и вывод.

Устанавливаем дополнительные библиотеки:

pip install speechrecognition pyaudio pyttsx3

PyAudio может быть сложной для установки на некоторых системах. Для Linux часто нужны dev-пакеты: `sudo apt install portaudio19-dev python3-pyaudio`. На macOS: `brew install portaudio`. На Windows обычно работает из коробки.

Создаем файл voice_interface.py:

import speech_recognition as sr
import pyttsx3
import time

class VoiceInterface:
    def __init__(self):
        # Инициализация распознавания речи
        self.recognizer = sr.Recognizer()
        self.microphone = sr.Microphone()
        
        # Инициализация синтеза речи
        self.tts_engine = pyttsx3.init()
        
        # Настройки голоса (можно менять)
        voices = self.tts_engine.getProperty('voices')
        self.tts_engine.setProperty('voice', voices[0].id)  # Первый доступный голос
        self.tts_engine.setProperty('rate', 180)  # Скорость речи
        
        print("Voice interface initialized. Say 'Jarvis' to activate.")
    
    def listen_for_wake_word(self, wake_word="jarvis") -> bool:
        """Слушаем wake word"""
        with self.microphone as source:
            print("Listening for wake word...")
            self.recognizer.adjust_for_ambient_noise(source, duration=0.5)
            
            try:
                audio = self.recognizer.listen(source, timeout=3, phrase_time_limit=2)
                text = self.recognizer.recognize_google(audio, language="en-US").lower()
                
                if wake_word in text:
                    print(f"Wake word '{wake_word}' detected!")
                    return True
                
            except sr.WaitTimeoutError:
                pass
            except sr.UnknownValueError:
                pass
            except sr.RequestError as e:
                print(f"Speech recognition error: {e}")
            
            return False
    
    def listen_command(self) -> str:
        """Слушаем команду после wake word"""
        self.speak("Yes?", wait=True)
        
        with self.microphone as source:
            print("Listening for command...")
            self.recognizer.adjust_for_ambient_noise(source)
            
            try:
                audio = self.recognizer.listen(source, timeout=5, phrase_time_limit=10)
                text = self.recognizer.recognize_google(audio, language="en-US")
                print(f"You said: {text}")
                return text
                
            except sr.WaitTimeoutError:
                return ""
            except sr.UnknownValueError:
                self.speak("Sorry, I didn't catch that.")
                return ""
            except sr.RequestError as e:
                print(f"Speech recognition error: {e}")
                self.speak("Speech service is unavailable.")
                return ""
    
    def speak(self, text: str, wait: bool = False):
        """Произносим текст"""
        print(f"Jarvis: {text}")
        self.tts_engine.say(text)
        
        if wait:
            self.tts_engine.runAndWait()
        else:
            self.tts_engine.startLoop(False)
            self.tts_engine.iterate()
            self.tts_engine.endLoop()
    
    def run_voice_loop(self, assistant):
        """Основной цикл голосового интерфейса"""
        print("Voice loop started. Press Ctrl+C to exit.")
        
        try:
            while True:
                if self.listen_for_wake_word():
                    command = self.listen_command()
                    
                    if command:
                        response = assistant.chat(command)
                        self.speak(response)
                    
                    # Небольшая пауза между командами
                    time.sleep(1)
                
        except KeyboardInterrupt:
            print("\nVoice interface stopped.")

if __name__ == "__main__":
    from assistant import JarvisAssistant
    
    assistant = JarvisAssistant()
    voice = VoiceInterface()
    voice.run_voice_loop(assistant)

Теперь ваш Jarvis понимает голосовые команды! Скажите "Jarvis" (джа́рвис с английским акцентом), подождите ответа "Yes?" и произнесите команду.

6 Интегрируем календарь и другие сервисы

Настоящая сила ассистента - в интеграциях. Добавим работу с Google Calendar (самый популярный вариант).

Сначала нужно получить OAuth 2.0 credentials:

  1. Зайдите на Google Cloud Console
  2. Создайте новый проект или выберите существующий
  3. Включите Google Calendar API
  4. Создайте OAuth 2.0 Client ID типа "Desktop app"
  5. Скачайте credentials.json

Устанавливаем библиотеку для Google API:

pip install google-auth-oauthlib google-auth-httplib2 google-api-python-client

Добавляем в наш assistant.py новый инструмент:

# В начало файла добавляем импорт
import pickle
import os.path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# В класс JarvisAssistant добавляем метод для работы с календарем
def get_calendar_service(self):
    """Создает сервис для работы с Google Calendar"""
    SCOPES = ['https://www.googleapis.com/auth/calendar']
    creds = None
    
    # Файл token.pickle хранит access/refresh токены
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    
    # Если нет валидных токенов, запрашиваем авторизацию
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        
        # Сохраняем токены для следующего запуска
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)
    
    service = build('calendar', 'v3', credentials=creds)
    return service

def create_calendar_event(self, summary: str, start_time: str, end_time: str = None) -> str:
    """Создает событие в календаре"""
    try:
        service = self.get_calendar_service()
        
        # Парсим время (упрощенный вариант)
        # В реальном проекте нужно добавить нормальный парсинг дат
        from datetime import datetime, timedelta
        
        if not end_time:
            # По умолчанию событие длится 1 час
            start_dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
            end_dt = start_dt + timedelta(hours=1)
            end_time = end_dt.isoformat()
        
        event = {
            'summary': summary,
            'start': {
                'dateTime': start_time,
                'timeZone': 'Europe/Moscow',
            },
            'end': {
                'dateTime': end_time,
                'timeZone': 'Europe/Moscow',
            },
        }
        
        event = service.events().insert(calendarId='primary', body=event).execute()
        return f"Event created: {event.get('htmlLink')}"
        
    except HttpError as error:
        return f"An error occurred: {error}"
    except Exception as e:
        return f"Failed to create event: {e}"

# Добавляем новый инструмент в список self.tools
{
    "type": "function",
    "function": {
        "name": "create_calendar_event",
        "description": "Create a new event in Google Calendar",
        "parameters": {
            "type": "object",
            "properties": {
                "summary": {
                    "type": "string",
                    "description": "Event title or description"
                },
                "start_time": {
                    "type": "string",
                    "description": "Start time in ISO format (2024-01-31T14:00:00)"
                },
                "end_time": {
                    "type": "string",
                    "description": "End time in ISO format (optional)"
                }
            },
            "required": ["summary", "start_time"]
        }
    }
}

И добавляем обработку в метод process_response:

elif function_name == "create_calendar_event":
    result = self.create_calendar_event(
        arguments["summary"],
        arguments["start_time"],
        arguments.get("end_time")
    )

Теперь можно сказать: "Jarvis, create a meeting tomorrow at 2 PM called Team Standup". Модель поймет, преобразует "tomorrow at 2 PM" в ISO формат и создаст событие.

💡
Для нормального парсинга естественного языка дат и времени используйте библиотеку dateparser или аналоги. Модели LLM часто сами хорошо справляются с этим, но дополнительная проверка не помешает.

7 Автозагрузка и работа в фоне

Настоящий ассистент должен быть всегда наготове. Сделаем систему службой.

Для Linux создаем systemd сервис:

sudo nano /etc/systemd/system/jarvis.service

Содержимое:

[Unit]
Description=Jarvis AI Assistant
After=network.target

[Service]
Type=simple
User=your_username
WorkingDirectory=/path/to/your/jarvis
ExecStart=/usr/bin/python3 /path/to/your/jarvis/assistant.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Активируем:

sudo systemctl daemon-reload
sudo systemctl enable jarvis.service
sudo systemctl start jarvis.service

# Проверяем статус
sudo systemctl status jarvis.service

Для Windows используйте NSSM (Non-Sucking Service Manager) или создайте задачу в Планировщике заданий.

8 Что делать, когда что-то не работает

Я обещал рассказать про ошибки. Вот самые частые и как их фиксить:

Проблема Причина Решение
Ollama не запускается Порт 11434 занят Измените порт в .env: OLLAMA_BASE_URL=http://localhost:11435
Модель не загружается Не хватает памяти Используйте меньшую модель или увеличьте файл подкачки
Голос не распознается Проблемы с микрофоном Проверьте устройство ввода в системе, тест через `arecord` или `audacity`
Функции не вызываются Модель не поддерживает tool calling Используйте модель с поддержкой function calling (Gemma-2-2B или Qwen2.5-Coder)
Медленная работа CPU вместо GPU Проверьте, что CUDA установлена и Ollama использует GPU: `ollama run --gpu gemma2:2b`

Куда двигаться дальше?

Вы собрали базового Jarvis. Что дальше? Вот идеи для развития:

  • Добавьте больше интеграций: почта (IMAP), задачи (Todoist или Trello), умный дом (Home Assistant через MQTT). Если хотите более продвинутую систему автоматизации, посмотрите мой гайд про локальный голосовой ассистент с бесконечными инструментами на n8n.
  • Улучшите распознавание речи: перейдите на локальный Whisper.cpp вместо Google Speech Recognition для полной приватности.
  • Добавьте память: сохраняйте историю разговоров в базу данных, чтобы ассистент помнил контекст.
  • Сделайте веб-интерфейс: используйте Streamlit или FastAPI + React для красивого UI.
  • Настройте персонализацию: учите ассистента вашим привычкам и предпочтениям.

Если вам интересны более сложные архитектуры, где ИИ-агент управляет всем домом, рекомендую изучить HomeGenie v2.0 - полностью локальный агентный ИИ для умного дома.

Самый важный урок, который я усвоил за годы работы с локальными ИИ: начинайте с малого. Не пытайтесь сразу построить AI-монстра со всем функционалом в одной коробке. Сначала сделайте одну функцию, которая работает идеально. Потом добавьте вторую. И так далее.

Частые вопросы (которые мне задают в комментариях)

Можно ли запустить это на Raspberry Pi?

Да, но с оговорками. Используйте самые легкие модели (Phi-3.5-mini или Qwen1.5-0.5B) и ожидайте более медленную работу. У меня есть отдельный гайд про создание персонального ИИ-ассистента на Raspberry Pi 5 с конкретными настройками для слабого железа.

Как добавить поддержку русского языка?

Используйте русскоязычные модели (например, от Saiga или русскоязычные версии Qwen). В голосовом интерфейсе измените language="en-US" на language="ru-RU" в вызовах recognize_google(). Для синтеза речи на русском попробуйте pyttsx3 с русскими голосами или RHVoice.

Безопасно ли хранить токены Google Calendar локально?

Токены в token.pickle защищены не лучше, чем ваш пароль от компьютера. Если система скомпрометирована - токены тоже. Для повышения безопасности используйте шифрование файла или храните токены в зашифрованном виде. В продакшене лучше использовать OAuth с PKCE и короткоживущими токенами.

Можно ли коммерциализировать такого ассистента?

Технически - да. Но помните про лицензии моделей (не все свободны для коммерческого использования) и ограничения API (у Google Calendar есть квоты). Для бизнес-применений рекомендую изучить ИИ-агенты для бизнеса: практическое руководство по внедрению.

Последний совет перед тем, как вы начнете

Не пытайтесь сделать идеально с первого раза. Мой первый Jarvis умел только говорить "Hello World" и падал при любой попытке спросить погоду. Сейчас он управляет всем домом, но путь занял месяцы.

Начните с сегодняшнего дня. Скачайте Ollama, запустите модель, напишите первую функцию. Каждая работающая часть системы будет мотивировать вас двигаться дальше.

И помните: ваш Jarvis - это отражение ваших потребностей. Не копируйте слепо чужие решения. Добавляйте то, что нужно именно вам. Хотите, чтобы он читал вслух новости? Добавьте RSS парсер. Нужно следить за ценами на акции? Интегрируйте финансовый API.

Удачи в создании вашего цифрового дворецкого! И если что-то не работает - пишите в комментариях. Мы разберемся вместе.