Compare commits
4 Commits
435d4dfbc2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
f5d3229b7c
|
|||
|
74b0f4ba55
|
|||
|
4748403378
|
|||
|
f1aa9f2324
|
42
compose.yml
42
compose.yml
@@ -9,25 +9,25 @@ services:
|
||||
- "${OLLAMA_PORT:-11434}:11434"
|
||||
restart: "no"
|
||||
|
||||
ai-qdrant:
|
||||
container_name: ai-qdrant
|
||||
image: qdrant/qdrant
|
||||
env_file: .env
|
||||
ports:
|
||||
- "${QDRANT_PORT:-6333}:6333"
|
||||
volumes:
|
||||
- ./.data/qdrant/storage:/qdrant/storage
|
||||
restart: "no"
|
||||
profiles: ["rag"]
|
||||
# ai-qdrant:
|
||||
# container_name: ai-qdrant
|
||||
# image: qdrant/qdrant
|
||||
# env_file: .env
|
||||
# ports:
|
||||
# - "${QDRANT_PORT:-6333}:6333"
|
||||
# volumes:
|
||||
# - ./.data/qdrant/storage:/qdrant/storage
|
||||
# restart: "no"
|
||||
# profiles: ["rag"]
|
||||
|
||||
ai-webui:
|
||||
container_name: ai-webui
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./.data/webui:/app/backend/data
|
||||
ports:
|
||||
- "${OWEBUI_PORT:-9999}:8080"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: "no"
|
||||
# ai-webui:
|
||||
# container_name: ai-webui
|
||||
# image: ghcr.io/open-webui/open-webui:main
|
||||
# env_file: .env
|
||||
# volumes:
|
||||
# - ./.data/webui:/app/backend/data
|
||||
# ports:
|
||||
# - "${OWEBUI_PORT:-9999}:8080"
|
||||
# extra_hosts:
|
||||
# - "host.docker.internal:host-gateway"
|
||||
# restart: "no"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Чек-лист по построению RAG
|
||||
|
||||
* [ ] Определиться с форматом входных данных
|
||||
* [ ] Очистить входные данные, обеспечив метаданными
|
||||
* [ ] Подобрать модель эмбеддинга
|
||||
* [ ] Подобрать размер чанка и перекрытия для эмбеддинга
|
||||
* [ ] Подобрать место хранения (векторная СУБД)
|
||||
* [ ] Подобрать модель ранжирования
|
||||
* [ ] Подобрать модель генерации
|
||||
* [ ] Подобрать для неё системный промпт (для встраивания найденных чанков, грамотного их цитирования)
|
||||
* [ ] Подобрать параметры:
|
||||
* [ ] top_k (количество чанков для поиска при эмбеддинге)
|
||||
* [ ] top_n (остаток найденных чанков после ранжирования)
|
||||
* [ ] temperature (степень фантазии)
|
||||
* [ ] top_p (???)
|
||||
* [ ] другие?
|
||||
* [ ]
|
||||
@@ -8,7 +8,7 @@
|
||||
cd ..; ./up; cd -
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install beautifulsoup4 markdownify sentence-transformers qdrant-client langchain transformers
|
||||
pip install beautifulsoup4 markdownify sentence-transformers qdrant-client langchain transformers ollama
|
||||
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
|
||||
./download.sh 123456789 # <<== pageId страницы в Confluence
|
||||
python3 convert.py
|
||||
@@ -66,7 +66,7 @@ rag/
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source ./venv/bin/activate
|
||||
pip install beautifulsoup4 markdownify sentence-transformers qdrant-client langchain transformers
|
||||
pip install beautifulsoup4 markdownify sentence-transformers qdrant-client langchain transformers ollama
|
||||
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
|
||||
```
|
||||
|
||||
@@ -260,3 +260,9 @@ python3 rag.py --help
|
||||
Этот проект -- пазл, который позволяет пошагово, по косточкам понять и настроить работу RAG.
|
||||
|
||||
Частично (в качестве агентов) в проекте участвовали модели семейств qwen, clause и chatgpt.
|
||||
|
||||
## Дополнительные материалы
|
||||
|
||||
* https://github.com/ollama/ollama/blob/main/docs/api.md
|
||||
* https://habr.com/ru/articles/881268/
|
||||
* https://habr.com/ru/companies/oleg-bunin/articles/835910/
|
||||
|
||||
132
rag/mindmap.puml
Normal file
132
rag/mindmap.puml
Normal file
@@ -0,0 +1,132 @@
|
||||
@startmindmap RAG
|
||||
title Mindmap по построению RAG
|
||||
header
|
||||
https://git.axenov.dev/anthony/ollama
|
||||
endheader
|
||||
|
||||
* RAG
|
||||
|
||||
** Подготовка сырых данных
|
||||
*** Количество
|
||||
****_ Больше => сложнее
|
||||
*** Формат
|
||||
**** HTML
|
||||
*****_ Очистка
|
||||
**** Markdown
|
||||
**** JSON
|
||||
**** PDF
|
||||
***** OCR
|
||||
****** Проблема плохого текста\n(сканы, картинки, фото)
|
||||
****** Проблема разметки текста\n(колонки, обтекание картинок)
|
||||
**** Проблема наличия таблиц
|
||||
*****_ Оставить
|
||||
*****_ Удалить
|
||||
*****_ Конвертировать
|
||||
******_ Markdown
|
||||
*******_ Таблица
|
||||
*******_ Список
|
||||
******_ CSV
|
||||
*** Качество
|
||||
**** Очистка
|
||||
*****_ Картинки
|
||||
*****_ Бессмысленный текст
|
||||
*****_ Разметка HTML, XML, ...
|
||||
**** Метаданные
|
||||
*****_ Название
|
||||
*****_ Ссылка
|
||||
*****_ Дата
|
||||
*****_ Автор
|
||||
*****_ ...
|
||||
**** Семантика и смысл текстов
|
||||
***** Разделение на осмысленные наборы
|
||||
|
||||
** Встраивание данных
|
||||
*** Векторизация
|
||||
**** Подбор модели эмбеддинга *
|
||||
***** Проблема русского языка
|
||||
**** Подбор способа разделения
|
||||
***** Fixed-length chunking\n(строго по символам)
|
||||
******_ самый простой и быстрый
|
||||
******_ хорош для длинных текстов
|
||||
******_ рвёт тексты с потерей связи
|
||||
***** Semantic chunking\n(по смысловым блокам)
|
||||
******_ посложнее
|
||||
******_ сохраняет логику, даёт больше смысла
|
||||
******_ лучше поиск
|
||||
******_ чанки могут быть разных размеров
|
||||
***** Structural chunking\n(по структуре текстов)
|
||||
******_ самый сложный и медленный
|
||||
******_ сохраняет контекст
|
||||
******_ хорош для сложных и технических текстов
|
||||
******_ требует структурированные входные данные
|
||||
**** Подбор размера чанка
|
||||
*****_ уменьшение
|
||||
******_ меньше смысла
|
||||
******_ больше нерелевантных результатов и галлюцинаций
|
||||
******_ быстрее поиск
|
||||
******_ хуже ответ
|
||||
*****_ увеличение
|
||||
******_ больше смысла
|
||||
******_ больше размер контекста
|
||||
******_ медленнее поиск
|
||||
******_ хуже ответ
|
||||
**** Подбор перекрытия чанков
|
||||
*****_ уменьшение
|
||||
******_ хуже смысловая связь документов
|
||||
******_ более уникальные чанки
|
||||
*****_ увеличение
|
||||
******_ лучше смысловая связь документов
|
||||
******_ сильнее дублирование частей чанков
|
||||
*** Индексация
|
||||
**** Метод хранения чанков
|
||||
*****_ chromadb (примитивно на базе sqlite, но медленно)
|
||||
*****_ qdrant (быстро, но немного усложняет деплой)
|
||||
*****_ postgres + pgvector (сложнее)
|
||||
*****_ ...
|
||||
|
||||
** Классификация\n(Classification)
|
||||
*** Подбор модели классификации
|
||||
**** Проблема русского языка
|
||||
|
||||
** Поиск и встраивание\n(Embedding)
|
||||
***: Подбор модели эмбеддинга *
|
||||
<i>та же, что на этапе векторизации</i>;
|
||||
***: Подбор top_k
|
||||
<i>количество чанков для поиска</i>;
|
||||
|
||||
** Ранжирование\n(Re-ranking)
|
||||
*** Подбор модели реранкинга
|
||||
' **** Проблема русского языка
|
||||
***: Подбор top_n
|
||||
<i>количество лучших чанков после реранка</i>;
|
||||
|
||||
** Генерация ответа
|
||||
*** Подбор модели генерации
|
||||
**** Проблема размера модели\n(млрд параметров)
|
||||
*****_ меньше
|
||||
******_ требует меньше ресурсов (RAM, CPU, GPU)
|
||||
******_ выше скорость ответа (TPS)
|
||||
******_ ниже качество (мешанина токенов, путает язык, игнорирует инструкции)
|
||||
*****_ больше
|
||||
******_ требует больше ресурсов (RAM, CPU, GPU)
|
||||
******_ ниже скорость ответа (TPS)
|
||||
******_ выше качество, но может быть избыточно, в зависимости от домена
|
||||
*** Подбор системного промпта
|
||||
**** Проблема русского языка
|
||||
*****_ модель может его не понимать/генерировать
|
||||
*****_ язык сложнее, занимает больше токенов в контексте
|
||||
**** Проблема размера контекста
|
||||
*****: Проблема Lost-in-the-middle
|
||||
----
|
||||
Исследование:
|
||||
* коротко https://huggingface.co/papers/2307.03172
|
||||
* целиком https://arxiv.org/abs/2307.03172
|
||||
;
|
||||
******_ ...
|
||||
*****_ Сдвиг контекстного окна
|
||||
*****_ Сжатие контекста
|
||||
*** Подбор настроек генерации
|
||||
****_ temperature
|
||||
****_ top_p?
|
||||
****_ ...
|
||||
@endmindmap
|
||||
161
rag/rag.py
161
rag/rag.py
@@ -1,16 +1,13 @@
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
from qdrant_client import QdrantClient
|
||||
from sentence_transformers import SentenceTransformer, CrossEncoder
|
||||
import ollama
|
||||
|
||||
DEFAULT_CHAT_MODEL = "openchat:7b"
|
||||
DEFAULT_EMBED_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
|
||||
# DEFAULT_RANK_MODEL = "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
|
||||
DEFAULT_RANK_MODEL = "cross-encoder/ms-marco-MiniLM-L-6-v2"
|
||||
# DEFAULT_RANK_MODEL = "cross-encoder/ms-marco-TinyBERT-L-2-v2"
|
||||
DEFAULT_MD_FOLDER = "data"
|
||||
DEFAULT_OLLAMA_URL = "http://localhost:11434"
|
||||
DEFAULT_QDRANT_HOST = "localhost"
|
||||
@@ -40,17 +37,26 @@ class RagSystem:
|
||||
self.qdrant_port = qdrant_port
|
||||
self.chat_model = chat_model
|
||||
self.emb_model = SentenceTransformer(embed_model)
|
||||
self.qdrant = QdrantClient(host=args.qdrant_host, port=args.qdrant_port)
|
||||
self.qdrant = QdrantClient(host=qdrant_host, port=qdrant_port)
|
||||
self.use_rank = use_rank
|
||||
if self.use_rank:
|
||||
self.rank_model = CrossEncoder(rank_model)
|
||||
self.conversation_history = []
|
||||
self.load_chat_model()
|
||||
self.ollama = ollama.Client(base_url=ollama_url)
|
||||
|
||||
def check_chat_model(self):
|
||||
models = self.ollama.list()
|
||||
return any(model.name == self.chat_model for model in models)
|
||||
|
||||
def install_chat_model(self, model: str = DEFAULT_CHAT_MODEL):
|
||||
try:
|
||||
result = self.ollama.pull(model)
|
||||
print(f"Модель {model} установлена успешно")
|
||||
except Exception as e:
|
||||
print(f"Ошибка установки модели: {str(e)}")
|
||||
|
||||
def load_chat_model(self):
|
||||
url = f"{self.ollama_url}/api/generate"
|
||||
body = {"model": self.chat_model}
|
||||
requests.post(url, json=body, timeout=600)
|
||||
self.ollama.generate(model=self.chat_model, keep_alive=True)
|
||||
|
||||
def search_qdrant(self, query: str, doc_count: int = DEFAULT_TOP_K, collection_name = DEFAULT_QDRANT_COLLECTION):
|
||||
query_vec = self.emb_model.encode(query, show_progress_bar=False).tolist()
|
||||
@@ -86,85 +92,71 @@ class RagSystem:
|
||||
return ranked_docs[:top_n]
|
||||
|
||||
def generate_answer(self, sys_prompt: str, user_prompt: str):
|
||||
url = f"{self.ollama_url}/api/generate"
|
||||
body = {
|
||||
"model": self.chat_model,
|
||||
"system": sys_prompt,
|
||||
"prompt": user_prompt,
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": 0.5,
|
||||
# "top_p": 0.2,
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post(url, json=body, timeout=900)
|
||||
if response.status_code != 200:
|
||||
return f"Ошибка генерации ответа: {response.status_code} {response.text}"
|
||||
self.response = response.json()
|
||||
return self.response["response"]
|
||||
try:
|
||||
with self.ollama.generate(
|
||||
model=self.chat_model,
|
||||
prompt=sys_prompt + "\n" + user_prompt,
|
||||
options={
|
||||
"temperature": 0.5,
|
||||
},
|
||||
stream=False,
|
||||
) as generator:
|
||||
response = next(generator)
|
||||
if response.error:
|
||||
raise RuntimeError(f"Ошибка генерации: {response.error}")
|
||||
self.last_response = response
|
||||
return response.output
|
||||
except Exception as e:
|
||||
print(f"Ошибка генерации ответа: {str(e)}")
|
||||
return str(e)
|
||||
|
||||
def generate_answer_stream(self, sys_prompt: str, user_prompt: str):
|
||||
url = f"{self.ollama_url}/api/generate"
|
||||
body = {
|
||||
"model": self.chat_model,
|
||||
"system": sys_prompt,
|
||||
"prompt": user_prompt,
|
||||
"stream": True,
|
||||
"options": {
|
||||
"temperature": 0.5,
|
||||
# "top_p": 0.2,
|
||||
},
|
||||
}
|
||||
resp = requests.post(url, json=body, stream=True, timeout=900)
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"Ошибка генерации ответа: {resp.status_code} {resp.text}")
|
||||
|
||||
answer = ""
|
||||
self.response = None
|
||||
for chunk in resp.iter_lines():
|
||||
if chunk:
|
||||
try:
|
||||
decoded_chunk = chunk.decode('utf-8')
|
||||
data = json.loads(decoded_chunk)
|
||||
if "response" in data:
|
||||
yield data["response"]
|
||||
answer += data["response"]
|
||||
if "done" in data and data["done"] is True:
|
||||
self.response = data
|
||||
break
|
||||
elif "error" in data:
|
||||
answer += f" | Ошибка стриминга ответа: {data['error']}"
|
||||
break
|
||||
except json.JSONDecodeError as e:
|
||||
answer += f" | Ошибка конвертации чанка: {chunk.decode('utf-8')} - {e}"
|
||||
except Exception as e:
|
||||
answer += f" | Ошибка обработки чанка: {e}"
|
||||
try:
|
||||
generator = self.ollama.generate(
|
||||
model=self.chat_model,
|
||||
prompt=sys_prompt + "\n" + user_prompt,
|
||||
options={
|
||||
"temperature": 0.5,
|
||||
},
|
||||
stream=True,
|
||||
)
|
||||
answer = ""
|
||||
for response in generator:
|
||||
if response.data:
|
||||
yield response.data
|
||||
answer += response.data
|
||||
if response.done:
|
||||
self.last_response = response
|
||||
break
|
||||
return answer
|
||||
except Exception as e:
|
||||
print(f"Ошибка стриминга: {str(e)}")
|
||||
return str(e)
|
||||
|
||||
def get_prompt_eval_count(self):
|
||||
if not self.response:
|
||||
if not hasattr(self, "last_response"):
|
||||
return 0
|
||||
return self.response["prompt_eval_count"]
|
||||
return self.last_response.prompt_eval_count or 0
|
||||
|
||||
def get_prompt_eval_duration(self):
|
||||
if not self.response:
|
||||
if not hasattr(self, "last_response"):
|
||||
return 0
|
||||
return self.response["prompt_eval_duration"] / (10 ** 9)
|
||||
return self.last_response.prompt_eval_duration / (10 ** 9)
|
||||
|
||||
def get_eval_count(self):
|
||||
if not self.response:
|
||||
if not hasattr(self, "last_response"):
|
||||
return 0
|
||||
return self.response["eval_count"]
|
||||
return self.last_response.eval_count or 0
|
||||
|
||||
def get_eval_duration(self):
|
||||
if not self.response:
|
||||
if not hasattr(self, "last_response"):
|
||||
return 0
|
||||
return self.response["eval_duration"] / (10 ** 9)
|
||||
return self.last_response.eval_duration / (10 ** 9)
|
||||
|
||||
def get_total_duration(self):
|
||||
if not self.response:
|
||||
if not hasattr(self, "last_response"):
|
||||
return 0
|
||||
return self.response["total_duration"] / (10 ** 9)
|
||||
return self.last_response.total_duration / (10 ** 9)
|
||||
|
||||
def get_tps(self):
|
||||
eval_count = self.get_eval_count()
|
||||
@@ -219,6 +211,10 @@ class App:
|
||||
use_rank = self.args.use_rank,
|
||||
chat_model = self.args.chat_model
|
||||
)
|
||||
if not self.rag.check_chat_model():
|
||||
print(f"Установка модели {self.args.chat_model} ...")
|
||||
self.rag.install_chat_model(self.args.chat_model)
|
||||
self.rag.load_chat_model()
|
||||
self.print_v(text=f"Модели загружены. Если ответ плохой, переформулируйте запрос, укажите --chat-model или улучшите исходные данные RAG")
|
||||
|
||||
def init_query(self):
|
||||
@@ -234,8 +230,9 @@ class App:
|
||||
self.query = input(">>> ").strip()
|
||||
|
||||
def process_help(self):
|
||||
print("<<< Команды итерактивного режима:")
|
||||
print("<<< Команды интерактивного режима:")
|
||||
print("save -- сохранить диалог в файл")
|
||||
print("stats -- статистика последнего ответа")
|
||||
print("exit -- выход\n")
|
||||
self.query = None
|
||||
self.args.query = None
|
||||
@@ -341,19 +338,23 @@ Context:
|
||||
|
||||
def process_query(self, sys_prompt: str, user_prompt: str, streaming: bool = DEFAULT_STREAM):
|
||||
answer = ""
|
||||
# try:
|
||||
if streaming:
|
||||
self.print_v(text="\nГенерация потокового ответа (^C для остановки)...\n")
|
||||
print(f"<<< ", end='', flush=True)
|
||||
for token in self.rag.generate_answer_stream(sys_prompt, user_prompt):
|
||||
answer += token
|
||||
print(token, end='', flush=True)
|
||||
try:
|
||||
for token in self.rag.generate_answer_stream(sys_prompt, user_prompt):
|
||||
answer += token
|
||||
print(token, end='', flush=True)
|
||||
except KeyboardInterrupt:
|
||||
print("\n*** Генерация ответа прервана")
|
||||
return answer
|
||||
else:
|
||||
self.print_v(text="\nГенерация ответа (^C для остановки)...\n")
|
||||
answer = self.rag.generate_answer(sys_prompt, user_prompt)
|
||||
print(f"<<< {answer}\n")
|
||||
# except RuntimeError as e:
|
||||
# answer = str(e)
|
||||
try:
|
||||
answer = self.rag.generate_answer(sys_prompt, user_prompt)
|
||||
except KeyboardInterrupt:
|
||||
print("\n*** Генерация ответа прервана")
|
||||
return ""
|
||||
|
||||
print(f"\n===================================================")
|
||||
return answer
|
||||
|
||||
Reference in New Issue
Block a user