engineering-ai-assistant / core /llm_engine.py
Милана
Первый релиз: интеллектуальный помощник инженера-строителя
96aaef2
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from typing import List, Dict, Optional
import re
class LLMEngine:
"""Движок для работы с локальной языковой моделью"""
def __init__(self, model_name: str = "mistralai/Mistral-7B-Instruct-v0.2", use_gpu: bool = None):
"""
Инициализация LLM
model_name: название модели (можно заменить на русскую Saiga)
use_gpu: использовать ли GPU (автоопределение если None)
"""
self.model_name = model_name
self.use_gpu = use_gpu if use_gpu is not None else torch.cuda.is_available()
self.model = None
self.tokenizer = None
self.is_loaded = False
# Альтернативные русские модели
self.russian_models = [
"IlyaGusev/saiga_mistral_7b",
"IlyaGusev/saiga_llama3_8b",
"mistralai/Mistral-7B-Instruct-v0.2"
]
def load_model(self):
"""Загружает модель в память"""
if self.is_loaded:
return True
print(f"🔄 Загрузка модели {self.model_name}...")
try:
# Загрузка токенизатора
self.tokenizer = AutoTokenizer.from_pretrained(
self.model_name,
trust_remote_code=True
)
# Настройка паддинга
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
# Загрузка модели
self.model = AutoModelForCausalLM.from_pretrained(
self.model_name,
torch_dtype=torch.float16 if self.use_gpu else torch.float32,
device_map="auto" if self.use_gpu else None,
trust_remote_code=True,
low_cpu_mem_usage=True
)
if not self.use_gpu:
self.model = self.model.to("cpu")
self.is_loaded = True
device = "GPU" if self.use_gpu else "CPU"
print(f"✅ Модель загружена на {device}")
return True
except Exception as e:
print(f"❌ Ошибка загрузки модели: {e}")
return False
def generate(self, prompt: str, context: str = None,
max_new_tokens: int = 500,
temperature: float = 0.7,
top_p: float = 0.95) -> str:
"""
Генерация ответа на основе промпта и контекста
"""
if not self.is_loaded:
if not self.load_model():
return "Ошибка: модель не загружена"
# Формирование полного промпта
if context:
full_prompt = f"""Ты — эксперт по строительной документации, специализирующийся на СП 61.13330.2012 "Тепловая изоляция оборудования и трубопроводов".
Контекст из документации:
{context}
Вопрос: {prompt}
Ответ (кратко, четко, со ссылками на нормативы):"""
else:
full_prompt = f"""Ты — эксперт по строительной документации. Отвечай кратко и по делу.
Вопрос: {prompt}
Ответ:"""
# Токенизация
inputs = self.tokenizer(
full_prompt,
return_tensors="pt",
truncation=True,
max_length=2048
)
if self.use_gpu:
inputs = {k: v.to("cuda") for k, v in inputs.items()}
# Генерация
with torch.no_grad():
outputs = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
do_sample=True,
pad_token_id=self.tokenizer.eos_token_id
)
# Декодирование
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
# Извлекаем только ответ (после "Ответ:")
if "Ответ:" in response:
response = response.split("Ответ:")[-1].strip()
# Очистка от повторений
response = self._clean_response(response)
return response
def _clean_response(self, text: str) -> str:
"""Очистка ответа от мусора"""
# Убираем повторяющиеся строки
lines = text.split('\n')
unique_lines = []
seen = set()
for line in lines:
line = line.strip()
if line and line not in seen and len(line) > 5:
seen.add(line)
unique_lines.append(line)
return '\n'.join(unique_lines[:20])
def answer_with_context(self, question: str, context: str,
sources: List[Dict] = None) -> Dict:
"""
Генерация ответа с использованием контекста из документации
"""
if not self.is_loaded:
self.load_model()
# Формируем контекст
context_text = context
if sources:
context_text += "\n\nИсточники:\n"
for src in sources[:3]:
context_text += f"- {src['doc_name']} (релевантность: {src['score']:.2f})\n"
context_text += f" {src['text'][:200]}...\n"
# Генерируем ответ
answer = self.generate(
prompt=question,
context=context_text,
max_new_tokens=500,
temperature=0.3
)
return {
'question': question,
'answer': answer,
'sources': sources or [],
'model': self.model_name
}
def unload(self):
"""Выгружает модель из памяти"""
if self.model is not None:
del self.model
del self.tokenizer
torch.cuda.empty_cache()
self.is_loaded = False
print("✅ Модель выгружена")