Files
RAG_Helper/yandex.py
T
2025-11-01 21:28:55 +05:00

229 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# medical_rag.py
import os
import json
import tiktoken
from typing import List, Tuple
import chromadb
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction
from llama_cpp import Llama
class MedicalRAG:
def __init__(
self,
model_path: str,
corpus_path: str = "rag_corpus.json",
db_path: str = "./chroma_db",
embedding_model_name: str = "cointegrated/rubert-tiny2",
top_k: int = 3,
n_ctx: int = 4096, # Увеличил контекст для YandexGPT
n_threads: int = 4,
token_multiplier: int = 5 # Множитель токенов для ответа
):
self.corpus_path = corpus_path
self.top_k = top_k
self.token_multiplier = token_multiplier
# === Инициализация токенизатора ===
print("Инициализация токенизатора...")
try:
self.encoding = tiktoken.get_encoding("cl100k_base") # Совместим с большинством моделей
except:
print("⚠️ Не удалось загрузить токенизатор, используется базовый подсчет символов")
self.encoding = None
# === Эмбеддинги ===
print("Загрузка эмбеддинг-модели...")
self.embedding_function = SentenceTransformerEmbeddingFunction(
model_name=embedding_model_name,
device="cpu"
)
# === ChromaDB ===
print("Инициализация ChromaDB...")
self.client = chromadb.PersistentClient(path=db_path)
self.collection = self.client.get_or_create_collection(
name="medical_anamnesis",
embedding_function=self.embedding_function,
metadata={"hnsw:space": "cosine"}
)
if self.collection.count() == 0:
print("Коллекция пуста. Загрузка данных...")
self._load_corpus()
else:
print(f"Коллекция уже содержит {self.collection.count()} записей.")
# === LLM (YandexGPT) ===
if not os.path.exists(model_path):
raise FileNotFoundError(
f"Модель не найдена: {model_path}\n"
"Скачайте YandexGPT-5-Lite-8B-instruct-Q4_K_M.gguf и поместите в ./models/"
)
print("Загрузка языковой модели (YandexGPT)...")
self.llm = Llama(
model_path=model_path,
n_ctx=n_ctx,
n_threads=n_threads,
verbose=False
)
print("✅ Система готова к работе!")
def _load_corpus(self):
with open(self.corpus_path, "r", encoding="utf-8") as f:
data = json.load(f)
self.collection.add(
documents=[item["full"] for item in data],
metadatas=[{"short": item["short"]} for item in data],
ids=[f"id_{i}" for i in range(len(data))]
)
print(f"✅ Загружено {len(data)} записей.")
def count_tokens(self, text: str) -> int:
"""Подсчет токенов в тексте"""
if self.encoding:
return len(self.encoding.encode(text))
else:
# Фолбэк: приблизительный подсчет (1 токен ≈ 4 символа)
return len(text) // 4
def build_prompt_with_token_management(self, short_note: str, max_context_tokens: int = 3000) -> Tuple[str, int]:
"""Строит промпт с управлением токенами"""
examples = self.retrieve(short_note)
# Системное сообщение
system_msg = (
"На основе приведённых клинических примеров напиши развёрнуто жалобы пациента, грамотно с медицинской точки зрения. "
"Напиши жалобы в одно предложение, одной строкой. "
"Не пиши вводных слов и фраз. Только жалобы пациента. "
"Неуместно писать диагнозы и план лечения. "
"Расшифруй все сокращения. "
"Отвечай сразу без размышлений."
)
# Считаем токены системного сообщения и короткой заметки
system_tokens = self.count_tokens(system_msg)
note_tokens = self.count_tokens(short_note)
# Доступное количество токенов для примеров
available_tokens = max_context_tokens - system_tokens - note_tokens - 100 # Запас на форматирование
# Отбираем примеры, которые помещаются в контекст
selected_examples = []
current_tokens = 0
for example in examples:
example_tokens = self.count_tokens(example)
if current_tokens + example_tokens <= available_tokens:
selected_examples.append(example)
current_tokens += example_tokens
else:
print("⛔️ RAG: Token limit exceeded.")
break
context = "\n\n".join([f"Пример: {ex}" for ex in selected_examples])
user_msg = f"""Примеры развёрнутых описаний:
{context}
Жалобы пациента: "{short_note}"
"""
# 🔑 Формат промпта для YandexGPT
prompt = (
f"<|im_start|>system\n{system_msg}<|im_end|>\n"
f"<|im_start|>user\n{user_msg}<|im_end|>\n"
"<|im_start|>assistant\n"
)
prompt_tokens = self.count_tokens(prompt)
return prompt, prompt_tokens
def retrieve(self, query: str, n: int = None) -> List[str]:
n = n or self.top_k
results = self.collection.query(query_texts=[query], n_results=n)
return results["documents"][0]
def generate(self, short_note: str) -> str:
prompt, prompt_tokens = self.build_prompt_with_token_management(short_note)
# Вычисляем max_tokens на основе длины промпта
available_tokens = 4096 - prompt_tokens - 50 # Запас
max_tokens = min(prompt_tokens * self.token_multiplier, available_tokens)
print(f"📊 Токены: промпт={prompt_tokens}, макс.ответ={max_tokens}")
output = self.llm(
prompt,
max_tokens=max_tokens,
temperature=0.1,
stop=["<|im_end|>"],
echo=False
)
result = output["choices"][0]["text"].strip()
return result
def __call__(self, short_note: str) -> str:
return self.generate(short_note)
# === Отключаем телеметрию Chroma ===
os.environ["CHROMA_TELEMETRY"] = "false"
import time
# === Запуск ===
if __name__ == "__main__":
rag = MedicalRAG(
model_path="./models/YandexGPT-5-Lite-8B-instruct-Q4_K_M.gguf",
n_ctx=8192 # Увеличиваем контекст для YandexGPT
)
# Промты для тестирования
test_notes = [
"Кашель сухой, температура 38",
"А.д. 140, заложенность ушей, частичная потеря слуха",
"а.д. 140/80, т.36.6, выделения из левого уха гнойные",
"ушные палочки 5 лет, снижение слуха 2 года"
]
for note in test_notes:
print(f"\n📥 Кратко: {note}")
t1 = time.time()
result = rag(note)
elapsed_time = time.time() - t1
print(f"⏱ Время выполнения: {elapsed_time:.2f} сек")
if result:
print(f"📤 Развёрнуто:\n{result}")
else:
print("❌ Пустой ответ от модели.")
print("" * 60)
"""
📥 Кратко: Кашель сухой, температура 38
📊 Токены: промпт=392, макс.ответ=1960
📤 Развёрнуто:
Пациент жалуется на сухой кашель и повышение температуры до 38°C.
────────────────────────────────────────────────────────────
📥 Кратко: А.д. 140, заложенность ушей, частичная потеря слуха
📊 Токены: промпт=379, макс.ответ=1895
📤 Развёрнуто:
Пациент жалуется на повышенное артериальное давление 140 мм рт. ст., заложенность ушей и частичную потерю слуха.
────────────────────────────────────────────────────────────
📥 Кратко: а.д. 140/80, т.36.6, ОСГО л. уха
📊 Токены: промпт=405, макс.ответ=2025
❌ Пустой ответ от модели.
────────────────────────────────────────────────────────────
📥 Кратко: ушные палочки 5 лет, снижение слуха 2 года
📊 Токены: промпт=430, макс.ответ=2150
📤 Развёрнуто:
Пациент жалуется на использование ушных палочек в течение пяти лет и снижение слуха, которое наблюдается в течение двух лет.
"""
# YandexGPT-5-Lite-8B-instruct-Q4_K_M 7-30 сек