
Полная версия
ChatGPT на вашем ноутбуке: бесплатно, анонимно, навсегда
import uvicorn
# Импортируем наш класс LocalModel и функцию загрузки модели
from local_model import LocalModel
from model_loader import download_model
# --- Модели данных, совместимые с OpenAI API ---
class ChatMessage(BaseModel):
"""Сообщение в диалоге."""
role: str # "system", "user", "assistant"
content: str
class ChatCompletionRequest(BaseModel):
"""Тело запроса, как в OpenAI."""
model: str = "local-model"
messages: List[ChatMessage]
temperature: Optional[float] = 0.7
max_tokens: Optional[int] = 512
stream: Optional[bool] = False
class ChatCompletionChoice(BaseModel):
"""Один вариант ответа."""
index: int
message: ChatMessage
finish_reason: Optional[str] = "stop"
class ChatCompletionResponse(BaseModel):
"""Полный ответ (не потоковый)."""
id: str
object: str = "chat.completion"
created: int
model: str
choices: List[ChatCompletionChoice]
# --- Инициализация FastAPI и модели ---
app = FastAPI(title="Local AI API", description="OpenAI-совместимый API для локальной LLM")
# Загружаем модель один раз при старте сервера
model_path = download_model()
print("Загрузка модели в память...")
local_model = LocalModel(model_path=model_path, n_threads=4, n_gpu_layers=0)
print("Модель готова к работе.")
# --- Вспомогательные функции ---
def messages_to_list(messages: List[ChatMessage]) -> List[Dict[str, str]]:
"""Преобразует Pydantic-сообщения в обычные словари для LocalModel."""
return [{"role": m.role, "content": m.content} for m in messages]
def generate_stream_response(messages: List[Dict[str, str]], temperature: float, max_tokens: int):
"""Генератор для потокового ответа в формате OpenAI (Server-Sent Events)."""
chat_id = f"chatcmpl-{uuid.uuid4().hex[:12]}"
created = int(time.time())
# Отправляем токены один за другим
for token in local_model.stream_chat(
messages=messages,
temperature=temperature,
max_tokens=max_tokens
):
chunk = {
"id": chat_id,
"object": "chat.completion.chunk",
"created": created,
"model": "local-model",
"choices": [
{
"index": 0,
"delta": {"content": token},
"finish_reason": None
}
]
}
yield f"data: {__import__('json').dumps(chunk)}\n\n"
# Финальный чанк с finish_reason
final_chunk = {
"id": chat_id,
"object": "chat.completion.chunk",
"created": created,
"model": "local-model",
"choices": [
{
"index": 0,
"delta": {},
"finish_reason": "stop"
}
]
}
yield f"data: {__import__('json').dumps(final_chunk)}\n\n"
yield "data: [DONE]\n\n"
# --- Основной endpoint ---
@app.post("/v1/chat/completions", response_model=None)
async def chat_completions(request: ChatCompletionRequest):
"""
Главный endpoint, совместимый с OpenAI Chat Completions API.
Поддерживает как обычный, так и потоковый режим.
"""
# Преобразуем сообщения
messages = messages_to_list(request.messages)
# Если запрошен потоковый режим
if request.stream:
return StreamingResponse(
generate_stream_response(
messages=messages,
temperature=request.temperature or 0.7,
max_tokens=request.max_tokens or 512
),
media_type="text/event-stream"
)
# Обычный режим
try:
response_text = local_model.chat(
messages=messages,
temperature=request.temperature or 0.7,
max_tokens=request.max_tokens or 512
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка генерации: {str(e)}")
# Формируем ответ в стиле OpenAI
response = ChatCompletionResponse(
id=f"chatcmpl-{uuid.uuid4().hex[:12]}",
created=int(time.time()),
model=request.model,
choices=[
ChatCompletionChoice(
index=0,
message=ChatMessage(role="assistant", content=response_text),
finish_reason="stop"
)
]
)
return response
@app.get("/v1/models")
async def list_models():
"""Возвращает список доступных моделей (для совместимости)."""
return {
"object": "list",
"data": [
{
"id": "local-model",
"object": "model",
"created": int(time.time()),
"owned_by": "local"
}
]
}
if __name__ == "__main__":
# Запускаем сервер на порту 8080
uvicorn.run(app, host="127.0.0.1", port=8080)
Разбор ключевых частей
Модели данных (Pydantic)
Мы используем pydantic.BaseModel для описания структуры запросов и ответов. Это гарантирует, что наш API будет строго соответствовать формату OpenAI. Любое приложение, которое умеет отправлять сообщения в ChatGPT, сможет работать и с нами: поля model, messages, temperature, max_tokens, stream идентичны оригиналу.
Потоковый ответ
Когда клиент запрашивает "stream": true, мы не должны возвращать обычный JSON. Вместо этого мы отправляем Server-Sent Events (SSE) — последовательность событий, разделённых префиксом data:. Каждое событие — это небольшой JSON-объект с очередным токеном. В конце мы отправляем data: [DONE], сигнализируя о завершении.
Функция generate_stream_response — это генератор, который yieldит строки. FastAPI оборачивает его в StreamingResponse с правильным media type, и клиент получает ответ по кусочкам, как в ChatGPT.
Загрузка модели при старте
Обратите внимание: LocalModel создаётся один раз, при запуске сервера. Модель загружается в память и остаётся там до выключения. Это значит, что каждый запрос обрабатывается мгновенно, без задержки на загрузку.
Тестирование
Запустите сервер:
bash
python openai_api.py
Вы увидите:
text
Загрузка модели в память...
Модель готова к работе.
INFO: Started server process [12345]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8080 (Press CTRL+C to quit)
Теперь протестируем API с помощью curl или Python. Сначала не-потоковый запрос:
python
# test_openai_api.py
import requests
response = requests.post(
"http://127.0.0.1:8080/v1/chat/completions",
json={
"model": "local-model",
"messages": [
{"role": "user", "content": "Привет! Кто ты?"}
],
"temperature": 0.7,
"max_tokens": 100,
"stream": False
}
)
print(response.json()["choices"][0]["message"]["content"])
Теперь с потоковой передачей:
python
import requests
response = requests.post(
"http://127.0.0.1:8080/v1/chat/completions",
json={
"model": "local-model",
"messages": [
{"role": "user", "content": "Расскажи анекдот"}
],
"stream": True
},
stream=True
)
for line in response.iter_lines():
if line:
line = line.decode("utf-8")
if line.startswith("data: ") and not line.endswith("[DONE]"):
import json
chunk = json.loads(line[6:])
token = chunk["choices"][0]["delta"].get("content", "")
if token:
print(token, end="", flush=True)
print()
Использование с официальной библиотекой OpenAI
Теперь самое интересное. Мы можем использовать библиотеку openai, которую ставят для работы с ChatGPT, просто указав свой базовый URL и любой ключ (он не проверяется):
bash
pip install openai
python
# test_with_openai_lib.py
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:8080/v1",
api_key="not-needed" # ключ обязателен для библиотеки, но наш сервер его игнорирует
)
# Не-потоковый запрос
response = client.chat.completions.create(
model="local-model",
messages=[{"role": "user", "content": "Что такое Python?"}],
temperature=0.7,
max_tokens=100
)
print(response.choices[0].message.content)
# Потоковый запрос
stream = client.chat.completions.create(
model="local-model",
messages=[{"role": "user", "content": "Напиши хокку о программировании"}],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()
Безопасность.
Наш сервер слушает только 127.0.0.1 (localhost). Это значит, что он недоступен из сети — только с вашего компьютера. Если вы захотите открыть его для других устройств, замените host="127.0.0.1" на host="0.0.0.0", но обязательно добавьте аутентификацию (например, проверку API-ключа). Без этого любой в вашей сети сможет пользоваться вашей моделью.
Возможные проблемы при запускеAPI.
Ошибка «Address already in use». Порт 8080, который мы используем по умолчанию, уже занят другой программой. Самое простое решение — указать другой порт при запуске: port=8081. Если хотите использовать именно 8080, найдите процесс, занимающий порт, и завершите его. На Windows это делается командой netstat -ano | findstr :8080, затем taskkill /PID номер_процесса /F.
Модель загружается при каждом запросе. Правильно настроенное приложение загружает модель один раз при старте сервера, и все последующие запросы используют уже загруженную модель. Если модель загружается заново при каждом запросе — проверьте код: объект LocalModel должен создаваться глобально, а не внутри функции-обработчика. Каждый новый экземпляр загружает модель в память заново, что занимает десятки секунд.
Потоковый вывод подвисает в середине ответа. Клиент может закрыть соединение, если долго не получает данные. Увеличьте max_tokens, чтобы модель не останавливалась раньше времени. Также попробуйте уменьшить temperature до 0.3–0.5 — при низких значениях модель генерирует более предсказуемый текст и реже «задумывается». Некоторые клиенты имеют встроенный таймаут — проверьте настройки на стороне клиента.
Клиент жалуется на неверный формат ответа. Ваш API должен возвращать ответ в точности как OpenAI: все поля id, object, created, model, choices обязательны. Если какого-то поля не хватает или оно названо иначе, клиент (например, библиотека openai) выдаст ошибку парсинга. Сверьте структуру ответа с эталонной — проще всего сравнить с реальным ответом от OpenAI.
Что мы получили.
Теперь у нас есть полноценный HTTP API, который:
· Работает локально без интернета.
· Совместим с OpenAI Chat Completions API.
· Поддерживает потоковую генерацию.
· Может быть использован с любыми инструментами, поддерживающими OpenAI API (а их сотни).
· Загружает модель один раз и держит в памяти.
Это огромный шаг. Мы превратили наш локальный ИИ в сервис, к которому можно подключаться из любого места на компьютере. В следующих частях мы будем наращивать функциональность (RAG, голос), и API будет расширяться вместе с моделью.
В Части 4 мы займёмся «глазами и памятью» — научим модель работать с вашими личными документами.
Часть 4. Зрение ипамять: обучаем модель вашим документам
Глава 4.1. Какработает RAG без облаков
Наша локальная модель уже умеет отвечать на вопросы — быстро, конфиденциально, без интернета. Но есть одна проблема, знакомая каждому, кто пользовался ChatGPT: модель знает только то, чему её обучили. Если вы спросите: «Какие задачи по проекту "Альфа" обсуждались на совещании 15 марта?», модель либо откажется отвечать, либо начнёт выдумывать. Она не читала ваши заметки, не видела ваши PDF-файлы, не имеет доступа к корпоративной документации.
Чтобы превратить общую модель в вашего личного эксперта, нам нужен механизм, который свяжет её с вашими документами. Этот механизм называется RAG — Retrieval-Augmented Generation, или «генерация, дополненная поиском». И мы реализуем его полностью локально, без единого запроса во внешний мир.
Проблема: модель не знает ваших документов
Языковая модель — это «слепок» знаний из интернета, книг и статей. Она обучалась на публичных данных и понятия не имеет о вашей частной информации. Можно было бы попытаться «дообучить» модель на своих документах, но это сложно, дорого по ресурсам и неудобно: при каждом обновлении документов придётся переучивать модель заново.
RAG решает проблему иначе. Вместо того чтобы вшивать знания в саму модель, мы научим её искать нужную информацию в ваших документах и подставлять её в запрос.
Аналогия: библиотекарь с фотографической памятью
Представьте, что у вас есть огромная библиотека с сотнями папок, договоров и отчётов. У вас нет времени читать их все, но у вас есть помощник — библиотекарь. Вы задаёте ему вопрос, он мгновенно находит три-четыре самых релевантных документа, кладёт их перед вами и говорит: «Вот что удалось найти по вашей теме». Вы читаете эти фрагменты и на их основе даёте точный ответ.
В архитектуре RAG:
· Вы — это пользователь, который задаёт вопрос.
· Библиотекарь — это система поиска (ретривер), которая ищет в базе знаний.
· Фрагменты документов, которые он вам принёс — это релевантные «чанки» (куски текста).
· Ваш мозг, который синтезирует ответ — это языковая модель (LLM).
Модель не обязана помнить содержимое всех документов. Она просто получает подсказку — и отвечает, опираясь на неё.
Как это работает: четыре шага
Разберём процесс по шагам. На каждом этапе мы будем использовать только локальные инструменты.
Шаг 1: Индексация (загрузка документов)
Сначала мы берём все ваши файлы — PDF, Word, текстовые заметки, Excel-таблицы — и превращаем их в единую базу знаний.
· Каждый документ разбивается на чанки (chunks) — небольшие фрагменты по 500–1000 символов. Почему не целиком? Потому что модель имеет ограниченное контекстное окно, и загружать весь 200-страничный договор в промпт невозможно. Чанки — это «абзацы» для быстрого поиска.
· Каждый чанк пропускается через embedding-модель — специальную нейросеть, которая превращает текст в вектор (список чисел). Вектор отражает смысл текста. Фразы «собака бежит» и «пёс несётся» получат близкие векторы, а «акции выросли» — совсем другой.
· Все векторы сохраняются в векторную базу данных. Это специальное хранилище, которое умеет искать не точные слова, а смысловую близость. Мы будем использовать ChromaDB — лёгкую, быструю и не требующую сервера.
Всё это делается один раз (или при обновлении документов) и занимает минуты даже на обычном ноутбуке.
Шаг 2: Поиск (ретрив)
Когда пользователь задаёт вопрос, например: «Какие гарантийные обязательства у нас перед клиентом "Ромашка"?», происходит следующее:
· Вопрос преобразуется в вектор той же embedding-моделью.
· Векторная база данных ищет среди миллионов чанков те, чьи векторы ближе всего к вопросу. Это занимает миллисекунды.
· Возвращаются, скажем, 4 самых релевантных чанка: абзац из договора с «Ромашкой», пункт о гарантиях из общего регламента и две записки с совещаний.
Шаг 3: Дополнение промпта (augmentation)
Найденные чанки вставляются в промпт для языковой модели вместе с вопросом пользователя. Выглядит это примерно так:
text
Ты — ассистент, который отвечает на вопросы на основе предоставленных документов.
Не выдумывай ничего, чего нет в документах. Если ответа нет, скажи "В документах не найдено".
Вопрос: Какие гарантийные обязательства у нас перед клиентом "Ромашка"?
Документы:
--- Документ 1 (Договор с ООО "Ромашка".pdf, раздел 7):
7.1. Исполнитель гарантирует устранение недостатков в течение 30 дней...
--- Документ 2 (Регламент гарантийного обслуживания.docx, пункт 3):
...
Ответ:
Модель видит конкретные факты и может на них опираться. Она не гадает — она анализирует предоставленный текст.
Шаг 4: Генерация ответа
Модель читает промпт, видит документы и генерирует ответ. Ответ может содержать цитаты: «Согласно пункту 7.1 договора с ООО "Ромашка"...» Это не магия — это просто хорошо сформулированный промпт с релевантной информацией.
Почему это работает без облаков
Все компоненты RAG в нашем проекте будут локальными:
1. Embedding-модель. Мы будем использовать sentence-transformers с моделями вроде intfloat/multilingual-e5-base или BAAI/bge-base-en. Они весят всего ~250–500 МБ и отлично работают на CPU.
2. Векторная база данных. ChromaDB хранит данные в папке на диске, не требует отдельного сервера и прекрасно интегрируется с Python.
3. Языковая модель. Наша Llama 3.1, работающая через LocalModel.
4. Оркестрация. Весь процесс — от получения вопроса до генерации ответа — будет управляться нашим Python-кодом, без внешних сервисов.
Ваши документы ни на одном этапе не покидают компьютер. Даже embedding-модель, которая превращает текст в векторы, работает локально.
RAG против «загрузки файла в ChatGPT»
Многие онлайн-сервисы предлагают «загрузить PDF и задать вопрос». Это работает, но:
· Конфиденциальность: документ отправляется на чужой сервер. Для личной переписки или корпоративных договоров это недопустимо.
· Ограничения: у сервисов часто есть лимиты на размер файла и количество запросов.
· Зависимость: без интернета сервис недоступен.
Наш локальный RAG лишён этих недостатков. Вы можете индексировать гигабайты документов и задавать вопросы в полной изоляции.
Ограничения RAG, о которых стоит знать
RAG — не волшебная палочка. Вот что нужно понимать заранее:
· Качество поиска. Если embedding-модель плохо понимает ваш язык или специфическую терминологию, релевантные чанки могут не найтись. Мы выберем мультиязычную модель, но в узких доменах может потребоваться тонкая настройка.
· Размер чанков. Слишком маленькие чанки теряют контекст (фраза «он сказал» бесполезна без предыдущего абзаца). Слишком большие — размывают релевантность и не влезают в контекстное окно. Мы найдём баланс в главе 4.4.
· Галлюцинации. Даже с документами модель может иногда «додумывать». Мы будем настраивать промпт так, чтобы она строго придерживалась источников.
Что мы построим в этой части
В следующих главах Части 4 мы шаг за шагом реализуем локальный RAG:
· В 4.2 выберем embedding-модель и научимся векторизовать текст.
· В 4.3 разберёмся с ChromaDB — создадим коллекцию, добавим векторы, выполним поиск.
· В 4.4 напишем функцию индексации папки с документами (PDF, DOCX, TXT).
· В 4.5 реализуем метод ask_documents() — он будет принимать вопрос, искать релевантные чанки, дополнять промпт и возвращать ответ с цитатами.
К концу этой части наш помощник обретёт «зрение и память». Он сможет работать с вашими личными данными так же уверенно, как с общими знаниями, и всё это — полностью офлайн.
Глава 4.2. Выбор локальной embedding-модели (BGE, multilingual-e5, jina)
В предыдущей главе мы разобрали, что RAG требует умения превращать текст в векторы — компактные числовые представления, отражающие смысл. Эту работу выполняет embedding-модель. Это отдельная нейросеть, которая специализируется не на генерации текста, а на его «понимании» и сравнении. Сегодня мы выберем такую модель, которая будет работать у нас локально, быстро и качественно, особенно с русским и английским языками.
Что делает embedding-модель
Embedding-модель принимает на вход текст (от одного слова до нескольких абзацев) и возвращает вектор — список чисел фиксированной длины, обычно от 384 до 1024 чисел. Главное свойство: тексты, близкие по смыслу, получают векторы, которые находятся близко друг к другу в этом многомерном пространстве. Измеряется близость косинусным расстоянием или скалярным произведением.
Например:
· «кошка спит на диване» и «кот дремлет на кушетке» — векторы почти совпадают.
· «кошка спит на диване» и «прибыль компании выросла» — векторы далеки друг от друга.
В мире открытых моделей для эмбеддингов сложилась тройка лидеров: BGE, multilingual-e5 и jina. Все они доступны на Hugging Face, работают через библиотеку sentence-transformers и могут быть запущены полностью офлайн.
Критерии выбора для нашей книги
Мы оцениваем модели по следующим критериям:
1. Качество русского языка. Наш помощник будет работать с русскоязычными документами. Модель должна хорошо понимать русский текст, включая деловую лексику и юридические формулировки.
2. Качество английского языка. Многие технические документы, статьи и переписка ведутся на английском. Хорошая мультиязычная поддержка обязательна.
3. Размер и скорость. Модель должна работать на CPU без GPU, занимать немного места (200–500 МБ) и быстро обрабатывать запросы. Помните: embedding-модель будет использоваться и при индексации (сотни документов), и при каждом поиске (доли секунды).
4. Совместимость. Модель должна без проблем загружаться через sentence-transformers, поддерживать токенизацию русского текста и работать на Windows, macOS и Linux.
Обзор кандидатов
BGE (BAAI General Embedding)
Семейство моделей от Пекинской академии искусственного интеллекта (BAAI). Актуальные версии: bge-base-en-v1.5 (английский), bge-m3 (мультиязычная).
· bge-base-en-v1.5: только английский, размер 438 МБ, размерность векторов — 768. Отличное качество на английском, но русский не поддерживает. Для нашей задачи не подходит.












