Почему ваша локальная нейросеть глупа без интернета
Вы скачали 7-миллиардную модель, запустили ее в Ollama или LM Studio, и она бодро отвечает на вопросы про философию Канта. Но стоит спросить "Какая погода в Москве?" или "Что пишут на Хабре про Docker?", и вы получаете либо бред, либо вежливый отказ. Типичный ответ: "Как локальная модель, я не имею доступа к актуальным данным из интернета". Звучит знакомо?
Это главная проблема локальных LLM. Они заморожены во времени, как муха в янтаре. Дата их последнего обновления - это дата тренировочного сета. Для реальной работы с актуальной информацией им нужен доступ в интернет. Но просто открыть порт и сказать "иди погуляй" - не вариант. Безопасность, контроль, стабильность - все это рушится.
Министр (Ministral) - это не конкретная модель, а подход: легковесная LLM в контейнере с инструментами для работы с внешним миром. Мы будем использовать Mistral 7B как пример, но принципы работают с любой локальной моделью.
Что мы на самом деле строим: не просто доступ, а контролируемый шлюз
Проблема в том, что большинство гайдов предлагают либо запустить модель с флагом --network=host (кошмар безопасности), либо настроить сложные VPN-туннели (кошмар администрирования). Мы пойдем другим путем: создадим изолированную среду, где модель может безопасно запрашивать внешние ресурсы через контролируемый прокси.
Архитектура выглядит так:
- Локальная LLM (Mistral 7B) в Docker-контейнере
- Traefik как обратный прокси и роутер
- Изолированная Docker-сеть для коммуникации
- Инструменты для веб-скрапинга внутри контейнера
Ключевая идея: модель не "выходит в интернет" напрямую. Она отправляет запросы к специальным endpoint'ам, которые уже обрабатывают веб-запросы и возвращают чистый контент.
1 Подготовка: почему Docker, а не голый Python
Я знаю, о чем вы думаете: "Зачем городить Docker, если можно просто установить requests и BeautifulSoup?". Потому что через месяц вы забудете, какие зависимости установили, а через два - почему скрипт перестал работать. Docker дает предсказуемость и изоляцию.
Сначала установите Docker Desktop если у вас Windows/Mac, или просто Docker Engine на Linux. Для пользователей WSL2 - включите интеграцию в настройках Docker Desktop.
# Проверяем установку
$ docker --version
Docker version 24.0.7, build afdd53b
# Проверяем, что Docker Daemon работает
$ docker run hello-world
# Если видите "Hello from Docker!" - все готово
Внимание пользователям WSL2: не запускайте Docker внутри WSL без Docker Desktop. Интеграция через Docker Desktop дает стабильную работу с сетью и volumes. Самый частый грабель - конфликт сетевых драйверов.
2 Dockerfile: собираем контейнер с мозгами и руками
Создаем директорию проекта и пишем Dockerfile. Мы не просто упаковываем модель - мы даем ей инструменты для работы с внешним миром.
# Dockerfile
FROM python:3.11-slim
# Устанавливаем системные зависимости для сборки
RUN apt-get update && apt-get install -y \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
# Устанавливаем инструменты для работы с интернетом
RUN pip install --no-cache-dir \
requests \
beautifulsoup4 \
markdownify \
fastapi \
uvicorn \
python-multipart \
ollama
# Создаем рабочую директорию
WORKDIR /app
# Копируем скрипты
COPY web_tools.py .
COPY app.py .
# Открываем порт для FastAPI
EXPOSE 8000
# Запускаем приложение
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Теперь создаем web_tools.py - набор инструментов, которые наша модель будет использовать через LangChain или прямое вызовы:
# web_tools.py
import requests
from bs4 import BeautifulSoup
from markdownify import markdownify as md
import json
class WebTools:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (compatible; LocalLLM-Bot/1.0)'
})
def fetch_url(self, url: str) -> str:
"""Получает содержимое URL и конвертирует в Markdown"""
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
# Парсим HTML
soup = BeautifulSoup(response.text, 'html.parser')
# Удаляем ненужные элементы
for element in soup(['script', 'style', 'nav', 'footer']):
element.decompose()
# Конвертируем в markdown
text_content = md(str(soup))
return text_content[:5000] # Ограничиваем объем
except Exception as e:
return f"Ошибка при получении {url}: {str(e)}"
def search_duckduckgo(self, query: str) -> str:
"""Ищет через DuckDuckGo Instant Answer API"""
try:
response = self.session.get(
'https://api.duckduckgo.com/',
params={'q': query, 'format': 'json', 'no_html': '1'}
)
data = response.json()
result = []
if data.get('AbstractText'):
result.append(f"Краткий ответ: {data['AbstractText']}")
if data.get('RelatedTopics'):
for topic in data['RelatedTopics'][:3]:
if 'Text' in topic:
result.append(topic['Text'])
return '\n'.join(result) if result else 'Информация не найдена'
except Exception as e:
return f"Ошибка поиска: {str(e)}"
3 FastAPI-сервер: мост между моделью и внешним миром
Теперь создаем app.py - FastAPI сервер, который будет принимать запросы от модели и выполнять веб-запросы:
# app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from web_tools import WebTools
import subprocess
import json
app = FastAPI(title="Local LLM Internet Gateway")
tools = WebTools()
class WebRequest(BaseModel):
url: str
class SearchRequest(BaseModel):
query: str
class LLMRequest(BaseModel):
prompt: str
use_web: bool = False
@app.get("/health")
def health_check():
return {"status": "healthy", "service": "llm-gateway"}
@app.post("/fetch")
def fetch_web_content(request: WebRequest):
"""Получить содержимое веб-страницы"""
content = tools.fetch_url(request.url)
return {"url": request.url, "content": content}
@app.post("/search")
def search_web(request: SearchRequest):
"""Поиск информации в интернете"""
results = tools.search_duckduckgo(request.query)
return {"query": request.query, "results": results}
@app.post("/ask")
async def ask_llm(request: LLMRequest):
"""Задать вопрос LLM с возможностью поиска в интернете"""
# Если нужен поиск в интернете
if request.use_web and "погода" in request.prompt.lower():
search_result = tools.search_duckduckgo(request.prompt)
enhanced_prompt = f"Вопрос: {request.prompt}\nКонтекст из интернета: {search_result}"
else:
enhanced_prompt = request.prompt
# Вызываем локальную LLM через Ollama
try:
result = subprocess.run(
['ollama', 'run', 'mistral', enhanced_prompt],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
return {"response": result.stdout.strip()}
else:
return {"error": result.stderr}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
4 Docker Compose: оркестрация всего зоопарка
Один контейнер - это скучно. Давайте запустим полноценный стек с Traefik в качестве обратного прокси:
# docker-compose.yml
version: '3.8'
services:
traefik:
image: traefik:v3.0
container_name: llm-gateway-proxy
command:
- --api.insecure=true
- --providers.docker=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
ports:
- "80:80"
- "443:443"
- "8080:8080" # Dashboard
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- llm-network
llm-gateway:
build: .
container_name: llm-internet-gateway
labels:
- "traefik.enable=true"
- "traefik.http.routers.llm-gateway.rule=Host(`llm.localhost`)"
- "traefik.http.routers.llm-gateway.entrypoints=web"
- "traefik.http.services.llm-gateway.loadbalancer.server.port=8000"
volumes:
- ./models:/app/models
environment:
- OLLAMA_HOST=host.docker.internal
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- llm-network
depends_on:
- traefik
networks:
llm-network:
driver: bridge
5 Запуск и тестирование: момент истины
Собираем и запускаем:
# Собираем образ
$ docker-compose build
# Запускаем стек
$ docker-compose up -d
# Проверяем, что все запустилось
$ docker-compose ps
NAME STATUS PORTS
llm-gateway-proxy Up 2 minutes 0.0.0.0:80->80/tcp
llm-internet-gateway Up 2 minutes 8000/tcp
# Проверяем health check
$ curl http://localhost/health
{"status":"healthy","service":"llm-gateway"}
Теперь тестируем доступ в интернет через нашу шлюз:
# Получаем содержимое страницы
$ curl -X POST http://localhost/fetch \
-H "Content-Type: application/json" \
-d '{"url":"https://habr.com/ru/articles/"}' \
| jq '.content | .[0:200]'
И самое интересное - спрашиваем у модели с использованием интернета:
# Задаем вопрос с поиском в интернете
$ curl -X POST http://localhost/ask \
-H "Content-Type: application/json" \
-d '{"prompt":"Какая погода в Москве сегодня?","use_web":true}' \
| jq '.response'
Ошибки, которые совершают все (и как их избежать)
| Ошибка | Почему происходит | Как исправить |
|---|---|---|
| Connection refused от Ollama | Контейнер не видит host.docker.internal | В Docker Compose добавить extra_hosts и установить Ollama на хосте |
| SSL ошибки при запросах | Устаревшие CA сертификаты в контейнере | В Dockerfile: RUN apt-get update && apt-get install -y ca-certificates |
| Traefik не видит контейнер | Отсутствует volume с docker.sock | Проверить, что /var/run/docker.sock правильно подключен |
| Медленные ответы от модели | Контейнеру не хватает ресурсов | В docker-compose.yml добавить deploy.resources.limits |
Что дальше: от простого шлюза к полноценной инфраструктуре
Сейчас у вас работает базовый шлюз. Но настоящая магия начинается, когда вы интегрируете это с другими инструментами:
- LangChain: Создайте CustomTool для веб-поиска и добавьте в цепочку
- RAG с актуальными данными: Используйте полученный контент для пополнения векторной базы
- Автоматизация: Настройте периодический сбор данных с любимых сайтов
- Безопасность: Добавьте авторизацию и rate limiting в Traefik middleware
Если вы хотите пойти дальше, посмотрите мой гайд про идеальный стек для self-hosted LLM, где я показываю, как подключить локальную модель к IDE и CLI-инструментам.
Помните: эта архитектура - компромисс между безопасностью и функциональностью. Модель все еще изолирована, но может запрашивать данные через контролируемые endpoint'ы. Это лучше, чем давать ей прямой доступ в интернет.
Самый частый вопрос: а зачем все это, если есть ChatGPT?
Потому что контроль. Потому что приватность. Потому что стоимость. Когда вы отправляете запрос в облако, вы не контролируете, что происходит с вашими данными. Когда вы платите за API-вызовы, каждый вопрос имеет цену. Когда вы зависите от доступности чужого сервиса, вы зависите от его прихотей.
Локальная LLM с доступом в интернет - это золотая середина. Вы получаете актуальность облачных моделей с приватностью локальных. И самое главное - вы учитесь строить системы, а не просто потреблять сервисы.
Теперь ваша модель не просто архив знаний. Она живой инструмент, который может читать новости, проверять факты, анализировать тренды. И все это - не выходя из вашей Docker-сети.