ChatGPT на вашем ноутбуке: бесплатно, анонимно, навсегда
ChatGPT на вашем ноутбуке: бесплатно, анонимно, навсегда

Полная версия

Настройки чтения
Размер шрифта
Высота строк
Поля
На страницу:
5 из 9

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. Отличное качество на английском, но русский не поддерживает. Для нашей задачи не подходит.

На страницу:
5 из 9