File size: 6,550 Bytes
417b2ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183

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("✅ Модель выгружена")