Помогите мне - новичку выстроить работу ИИ

Я только начинаю свой путь в работе с нейросетями. Но у меня возникают очевидные сложности, которые мне сложно решить самому, и я нуждаюсь в совете более опытных коллег :)

Сейчас у меня такая задача

У меня есть PDF файл (в перспективе их будет несколько сотен), я хочу развернуть локально какую-то модель, чтобы она отвечала мне строго по файлам

что я сделал:

я выбрал RAG подход, распарсил PDF файл (код приведу ниже), после чего я сформировал векорную БД

далее логика такая: пользователь делает запрос -> преобразуем его запрос в векторы -> получаем данные из векторной БД -> передаем LLM полученный контекст -> LLM дает релевантный ответ

Но я не могу определиться с тем, какая модель мне нужна и правильно ли я пользуюсь векторной БД, возможно чанки надо сделать больше или перекрытие. Важно, что вся информация в ПДФ файлах на русском (как я понял, что не каждая модель для этого подойдет), запросы пользователя, как и финальный промпт тоже на русском языке, ответ модели тоже на русском языке

сейчас я использовал llama3:8b , в целом мне нравится, как она отвечает, но проблема, как будто конеткст у нее неполный попадает, то сами ответы нерелевантные получаются, я бы сказал, что они слишком далекие от правды, возможно стоит использовать модель пожирнее или вообще что-то кроме llama3

буду безмерно благодарен за советы или ссылки на чтиво разного рода, которое могло бы помочь (английский или русский)

плюсом было бы то, что я мог бы запускать модель через ollama, так как наличие rest api для меня довольно таки важно

данную задумку я хочу реализовать на основе данных ФСНБ-2022, так как у них много данных как раз, эту идею мне подкинул ChatGPT :)

Сейчас я пытаюсь разобраться на основе одного файла прежде чем грузить все остальные

вот ссылка на скачивание этого файла с сайта Минстрой РФ

https://minstroyrf.gov.ru/trades/tree_download.php?folder=fsnb2022&ID=0

кто не доверяет ссылкам, то может сам найти файл

"Сборник ГЭСН 01 Земляные работы.pdf", данный файл находится в разделе ГЭСН

вот код парсера PDF pdf_parser.py

import pdfplumber
import json
from dataclasses import dataclass, asdict
from typing import List, Optional
from langchain.docstore.document import Document
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from tqdm import tqdm
import re

@dataclass
class Table:
    header: List[str]
    rows: List[List[str]]
    raw_text: str

@dataclass
class Section:
    title: str
    content: str
    tables: List[Table]
    subsections: List['Section']
    page_number: int

def clean_text(text: str) -> str:
    text = re.sub(r'-\n\s*', '', text)
    text = re.sub(r'\s+', ' ', text)
    return text

def process_table(table) -> Optional[Table]:
    if not table or not table[0]:
        return None
    
    header = [clean_text(cell) if cell else "" for cell in table[0]]
    rows = [[clean_text(cell) if cell else "" for cell in row] for row in table[1:]]
    
    raw_text = "\n".join([" | ".join(header)] + [" | ".join(row) for row in rows])
    
    return Table(header=header, rows=rows, raw_text=raw_text)

def identify_section(text: str) -> bool:
    section_patterns = [
        r'Таблица ГЭСН \d{2}-\d{2}-\d{3}',
        r'Состав работ:',
        r'Измеритель:'
    ]
    return any(re.search(pattern, text) for pattern in section_patterns)

def extract_sections(pdf_path) -> List[Section]:
    sections = []
    current_section = None
    
    with pdfplumber.open(pdf_path) as pdf:
        for page in tqdm(pdf.pages, desc="Processing pages"):
            text_lines = page.extract_text_lines()
            tables = page.extract_tables()
            
            # Process text lines
            current_text = ""
            for line in text_lines:
                text = clean_text(line['text'])
                
                if identify_section(text):
                    if current_section:
                        if current_text:
                            current_section.content += f"\n{current_text}"
                        sections.append(current_section)
                    
                    current_section = Section(
                        title=text,
                        content=text,
                        tables=[],
                        subsections=[],
                        page_number=page.page_number
                    )
                    current_text = ""
                else:
                    current_text += f"\n{text}"
            
            if current_section and current_text:
                current_section.content += f"\n{current_text}"
            
            # Process tables for current section
            if current_section and tables:
                for table in tables:
                    processed_table = process_table(table)
                    if processed_table:
                        current_section.tables.append(processed_table)
    
    if current_section:
        sections.append(current_section)
    
    return sections

def section_to_document(section: Section) -> Document:
    content = f"""
Title: {section.title}
Content: {section.content}
Tables:
{chr(10).join(table.raw_text for table in section.tables)}
"""
    return Document(
        page_content=content,
        metadata={
            "page_number": section.page_number,
            "title": section.title,
            "table_count": len(section.tables)
        }
    )

def build_vector_db(sections: List[Section], index_path="smeta_faiss_index"):
    documents = [section_to_document(section) for section in sections]
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=3000, chunk_overlap=750)
    docs_split = text_splitter.split_documents(documents)
    
    # embeddings = HuggingFaceEmbeddings(model_name="ai-forever/sbert_large_mt_nlu_ru")
    embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
    # embeddings = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-base")
    vector_db = FAISS.from_documents(docs_split, embeddings)
    vector_db.save_local(index_path)
    return vector_db

def main():
    pdf_path = "Сборник ГЭСН 01 Земляные работы.pdf"
    sections = extract_sections(pdf_path)
    
    with open("extracted_sections.json", "w", encoding="utf8") as f:
        json.dump([asdict(section) for section in sections], f, indent=2, ensure_ascii=False)
    
    vector_db = build_vector_db(sections, index_path="faiss_index_sectioned_all-MiniLM-L6-v2")

if __name__ == "__main__":
    main()

а вот код бизнес логики, где уже пользователь делает запрос, его обработка и тд

import json
from typing import List, Dict, Any
import requests
from dataclasses import dataclass
from langchain_community.vectorstores import FAISS
from langchain_huggingface.embeddings import HuggingFaceEmbeddings

@dataclass
class GesnFormat:
    """Формат ответа ГЭСН"""
    price_number: str = ""
    price_name: str = ""
    works: List[str] = None
    resources: List[Dict[str, str]] = None

    def __post_init__(self):
        if self.works is None:
            self.works = []
        if self.resources is None:
            self.resources = []

    def to_dict(self) -> dict:
        return {
            "price_number": self.price_number,
            "price_name": self.price_name,
            "works": self.works,
            "resources": [
                {
                    "resource_name": res.get("resource_name", ""),
                    "resource_number": res.get("resource_number", "")
                }
                for res in self.resources
            ]
        }

class SmetaService:
    def __init__(self, vector_db_path: str, llm_url: str = "http://localhost:11434/api/generate"):
        """
        Инициализация сервиса
        
        Args:
            vector_db_path: путь к векторной БД
            llm_url: URL для LLM API
        """
        self.embeddings = HuggingFaceEmbeddings(
            # model_name="ai-forever/sbert_large_mt_nlu_ru"
            model_name="all-MiniLM-L6-v2" 
        )
        
        self.vector_db = FAISS.load_local(
            vector_db_path,
            self.embeddings,
            allow_dangerous_deserialization=True
        )
        
        self.llm_url = llm_url

    def search_vector_db(self, query: str, k: int = 5) -> List[str]:
        """
        Поиск в векторной базе
        
        Args:
            query: запрос для поиска
            k: количество результатов
            
        Returns:
            List[str]: список найденных документов
        """
        try:
            results = self.vector_db.similarity_search(query, k=k)
            return [doc.page_content for doc in results if hasattr(doc, 'page_content')]
        except Exception as e:
            print(f"Ошибка поиска в векторной БД: {e}")
            return []

    def query_llm(self, prompt: str, temperature: float = 0.3) -> str:
        """
        Запрос к LLM модели
        
        Args:
            prompt: промпт для модели
            temperature: температура генерации
            
        Returns:
            str: ответ модели
        """
        payload = {
            "model": "llama3:8b",
            "prompt": prompt,
            "temperature": temperature,
        }
        headers = {"Content-Type": "application/json"}
        
        try:
            response = requests.post(
                self.llm_url,
                json=payload,
                headers=headers,
                stream=True
            )
            response.raise_for_status()
            
            full_response = ""
            print("Генерация ответа:")
            for line in response.iter_lines():
                if line:
                    try:
                        data = json.loads(line.decode("utf-8"))
                        fragment = data.get("response", "")
                        full_response += fragment
                        print(fragment, end='', flush=True)
                        if data.get("done", False):
                            break
                    except json.JSONDecodeError as e:
                        print(f"\nОшибка декодирования ответа: {e}")
                        continue
            print()
            return full_response
            
        except requests.exceptions.RequestException as e:
            print(f"Ошибка запроса к LLM: {e}")
            return ""
    
    def prepare_prompt(self, context: List[str], query: str, output_format: str = "json") -> str:
    
        if output_format == "json":
            format_example = {
                "price_number": "<номер расценки ГЭСН>",
                "price_name": "<название расценки ГЭСН>",
                "works": ["<название работы ГЭСН>"],
                "resources": [
                    {
                        "resource_name": "<название ресурса>",
                        "resource_number": "<код ресурса>"
                    }
                ]
            }
            
            return f"""Контекст:
    {chr(10).join(context)}

    Запрос: {query}

    Ответь только в формате JSON без каких-либо дополнительных пояснений или текста:
    {json.dumps([format_example], ensure_ascii=False, indent=2)}

    Важно: 
    1. Весь ответ должен быть только валидным JSON
    2. Не добавляй никакого текста до или после JSON
    3. Не добавляй пояснений или примечаний
    4. Если нужно указать несколько расценок, добавь их в массив
    """
        else:
            return f"""Контекст:
    {chr(10).join(context)}

    Запрос: {query}

    Предоставь информацию в следующем формате:

    Расценка ГЭСН: (номер)
    Название: (название расценки)
    Состав работ:
    - (работа 1)
    - (работа 2)
    ...
    Ресурсы:
    - [код ресурса] название ресурса
    """

    def generate_response(self, user_query: str, k: int = 5, output_format: str = "json") -> Any:
        full_query = f"выведи расценки ГЭСН, состав работ, ресурсы для: {user_query}"
        context = self.search_vector_db(full_query, k)
        
        if not context:
            return [GesnFormat().to_dict()] if output_format == "json" else "Не найдено подходящих расценок"
        
        prompt = self.prepare_prompt(context, user_query, output_format)
        response = self.query_llm(prompt)
        
        if output_format == "json":
            try:
                result = json.loads(response)
                if isinstance(result, list):
                    return result
                return [result]
            except json.JSONDecodeError as e:
                print(f"Ошибка парсинга JSON: {e}")
                return [GesnFormat().to_dict()]
        else:
            return response

def main():
    service = SmetaService("faiss_index_sectioned_all-MiniLM-L6-v2")
    
    while True:
        try:
            user_query = input(">>> Запрос (или 'exit' для выхода): ")
            if user_query.lower() == 'exit':
                break
                
            result = service.generate_response(user_query, k=10)
            
            # print(json.dumps(result, ensure_ascii=False, indent=2))
            
        except KeyboardInterrupt:
            break
        except Exception as e:
            print(f"Произошла ошибка: {e}")

if __name__ == "__main__":
    main()


Ответы (0 шт):