Итак, в первой части я сделал подход к RAG для нашей небольшой компании с большим кол-вом документов на wiki, и множеством переписок в Slack.
Стек технологий: Python, ChromaDB, простой SentenceTransformer(«all-MiniLM-L6-v2»), Slack API, OpenAI API, Google Gemini API, YandexGPT API, Sber Gigachat API.
Что уже работает?
-
Данные можно собрать с Wiki запуском скрипта WikiToJson.
-
Затем данные можно загрузить в векторную базу ChromaDB, с помощью скрипта JsonToChromaDB.
-
Скрипт SlackBot запускается, подключается к нужным каналам в Slack и готов отвечать на вопросы на основании контекста из базы ChromaDB. Напомню, в векторной базе ChromaDB хранятся сами тексты и их векторное представление, которое позволяет быстро искать схожие по тексту с вопросом ответы.
Пункты 1 и 2 планируем выполнять overnight(ночью). Пункт 3 будет постоянно запущен в облаке. Переключать бота можно между Gemini, OpenAI, YandexGPT, Gigachat API. Пока находимся в режиме тестирования системы, поэтому используем бесплатные AI API.
При первом тестировании (на документах с wiki, где находятся бизнес- и техническая документация на английском) лучше всего себя показала модель gemini-2.0-flash
от Google. Хуже всего локальные модели, типа mistral-7b-instruct-v0.1.Q4_K_M.gguf
. Локальные модели работают медленно, и в ответе много мусора. Возможно, их нужно сильно тюнить, но при текущих ценах на cloud AI, я решил полностью сосредоточиться на облачных решениях.
Что удалось добавить с прошлого раза?
Добавил feed из Slack, то есть скрипт, который парсит заданный канал Slack в json, а затем в ChromaDB. Здесь всё стало значительно сложнее, потому что объем переписки в одном только support-канале оказался больше, чем вся наша wiki. Поэтому из-за throttle limit в Slack (ограничение на кол-во запросов в секунду), его парсинг идет значительно дольше (десятки минут вместо 2–3 минут для Wiki).
Скрипт парсинга Slack канала (сообщения + ответы):
import os import json from slack_sdk import WebClient from slack_sdk.errors import SlackApiError # Set your Slack bot token SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN") CHANNEL_NAME = "team-sqa-r2d2" # Replace with your Slack channel name DATA_FOLDER = "./Data" client = WebClient(token=SLACK_BOT_TOKEN) # Ensure data folder exists os.makedirs(DATA_FOLDER, exist_ok=True) def get_channel_id(channel_name): try: response = client.conversations_list() for channel in response["channels"]: print(f"Found channel: {channel['name']} (ID: {channel['id']})") # Debug if channel["name"] == channel_name: return channel["id"] except SlackApiError as e: print(f"Error fetching channel list: {e.response['error']}") return None def fetch_messages(channel_id): messages = [] cursor = None while True: try: response = client.conversations_history(channel=channel_id, cursor=cursor, limit=200) messages.extend(response["messages"]) if not response.get("has_more"): break cursor = response.get("response_metadata", {}).get("next_cursor") except SlackApiError as e: print(f"Error fetching messages: {e.response['error']}") break return messages def fetch_replies(channel_id, thread_ts): replies = [] try: response = client.conversations_replies(channel=channel_id, ts=thread_ts) replies = response.get("messages", [])[1:] # Skip the first message (original post) except SlackApiError as e: print(f"Error fetching replies: {e.response['error']}") return replies def save_message_to_json(message): message_id = message.get("ts", "unknown") filename = os.path.join(DATA_FOLDER, f"{message_id}.json") with open(filename, "w", encoding="utf-8") as f: json.dump(message, f, ensure_ascii=False, indent=4) def process_messages(channel_id, messages): for msg in messages: if "thread_ts" in msg and msg["thread_ts"] == msg["ts"]: msg["replies"] = fetch_replies(channel_id, msg["ts"]) save_message_to_json(msg) if __name__ == "__main__": print("Fetching channel ID...") CHANNEL_ID = get_channel_id(CHANNEL_NAME) CHANNEL_ID = "C02G28PRZFH" # product-bug-reporting if not CHANNEL_ID: print("Channel not found!") else: print(f"Channel ID: {CHANNEL_ID}") print("Fetching messages...") messages = fetch_messages(CHANNEL_ID) print("Processing and saving messages...") process_messages(CHANNEL_ID, messages) print("Done!")
Затем просто загружаем JSON в ChromaDB. По аналогии с ‘WikiToJson’ в первой статье.
На удивление, всё та же модель gemini-2.0-flash
, даёт очень полезные рекомендации по запросам в support канале. Причина, возможно, в том, что запросы, хоть и отличаются по описанию, по факту всё крутятся вокруг одних и тех же проблем. К примеру:
-
Клиент не может загрузить данные для обработки
-
Клиент не может увидеть свои данные
-
Приложение у клиента падает
-
Клиент не может войти
Важно: тут пришлось поиграть с промтами. От них очень сильно зависит, насколько ‘хорошо’ модель ответит на запрос в support-канале.
Наиболее удачный prompt для анализа запросов в support-канале:
prompt = f"Use the following context to answer the question:\n{context}\nQuestion: {query}\n"\ "Do not tell what you going to do. Use plain text in reply. Try to be very short, ask important questions only, or accept the report as valid bug."\ "In case of accepting as bug, in short, define Severity and list steps to repro."\ "Define team that should take care of bug, if there are still questions to clarify then assign to SQA team."\ "If it is a valid bug related to backed/web/emails, then assign to Backend team. Assign to iOS team if it is App bug.\nAnswer:"
Пример запроса в support-канале:
LOW MEMORY WARNING WHILE SCANNING, CAPTURED SCANS WON'T OPEN IN DJANGO (Not sure who is best to handle this, so my apologies for any incorrect tags.) Customer maegan@sogliastudios.com reached out after she received low memory warnings when scanning for her SketchUp order. She says she received the warnings towards the end of her site visit when capturing the final scans; crawlspace scan 4, garage scan 5 and garage scan 6
Пример ответа бота:
Valid bug. Severity: High (data loss, app crash) Steps to reproduce: 1. Scan large areas or complex scenes. 2. Monitor memory usage during the scan. 3. Observe if low memory warnings appear and if the app crashes or scan files become corrupted. Team: iOS
Для нас важно, чтобы проблема была быстро классифицирована по двум параметрам:
-
Критичность
-
Кто должен посмотреть (команда)
Как видно, бот достаточно точно определил как критичность, так и команду.
Второй промт для целей преобразования потока мыслей PM (заказчиков) в чёткое ТЗ пока не удалось подобрать, но эта задача будет посложнее.
Пока что бот для support-канала работает в режиме RAG, то есть он просто берёт вопрос и, используя все доступные знания, классифицирует этот запрос.
Дальше планируется создать реального AI-агента, который будет:
-
Прояснять детали запроса, беседуя с customer support (здесь подойдут threads от OpenAI, либо придётся использовать LangChain).
-
Иметь API для доступа к логам, для проверки работоспособности основных систем.
-
Через JIRA API заводить подтверждённый баг.
Это большая ветка работ для создания реального автономного AI-агента. Для п2-3 планирую попробовать OpenAI Function Calling.
OpenAI Function Calling — это возможность взаимодействовать с внешними функциями через API OpenAI. Когда модель генерирует ответ, она может автоматически или по запросу вызвать внешние функции, передавая им необходимые параметры. Это позволяет интегрировать модель с реальными системами, базами данных или сторонними сервисами для выполнения динамических задач, таких как получение данных, выполнение операций или взаимодействие с другими API.
Как улучшить качество выборки данных из векторной базы?
Сейчас выбираются top-5 похожих по тексту документов. Для каталога знаний в режиме вопрос-ответ, вроде, работает достаточно хорошо.
Для более сложных запросов, например преобразования потока мыслей PM в чёткое ТЗ, обойтись просто тюнингом промта, скорее всего, не получится.
План работ таков:
-
Поиграться с разбиением документов на чанки по разным принципам. Сейчас принцип:
одна страница из wiki = один вектор
.Один месседж из Slack = один вектор
. Планирую применить spaCy в Python. -
Добавить
Named Entity Recognition
, то есть определение сущностей из текста, для добавления меток к векторам с целью улучшения качества поиска top-векторов при запросе. -
Перейти с векторной БД на базу графов, например, neo4j. Цель — делать выборку не по совпадению векторов, а по сложным взаимосвязям между документами (или чанками). Это большая задача.


Я не понял, могу ли я жить без LangGraph и даже без LangChain, если буду использовать threads от OpenAPI?
Сложно всё это уложить в голове, поэтому сейчас рисую общую схему работы системы в Miro. Если интересно — поставьте лайк, и я выложу её в следующей статье.
ссылка на оригинал статьи https://habr.com/ru/articles/889376/
Добавить комментарий