? Идея, Которая Важнее Кода
Мой отец — человек, переживший несколько сложнейших операций на сердце. Жизнь с хроническим заболеванием — это бесконечный поток анализов, заключений и схем приёма лекарств. Находясь далеко (я живу во Вьетнаме), я постоянно волновался: не забудет ли он про дозу, правильно ли понял назначение, задал ли все нужные вопросы врачу?
Мне нужен был не просто бот-напоминалка, а второй пилот — умный, конфиденциальный и мультимодальный AI-Кардиолог. Ассистент, который знает его анамнез наизусть, понимает голосовые команды и может "прочитать" фотографию свежего анализа.
Я решил собрать полноценный автономный агент с возможностью вызова внешних инструментов (Tool-Calling) и локальной базой знаний (RAG), но без использования громоздких фреймворков вроде LangChain или LlamaIndex.
? Архитектура: Планировщик и Жесткий Контроль
Моя цель: максимальная надёжность, локальность данных и предсказуемость.
Ядро системы — это Полноценный Tool-Calling Pipeline на GPT-4o-mini, который работает в два этапа: Планирование и Генерация.
Компонент |
Технология |
Роль в системе |
Планировщик |
GPT-4o-mini (OpenRouter) |
Принимает решение: |
База знаний (RAG) |
ChromaDB + SentenceTransformer (локально) |
Хранит историю болезни, анализы, заметки. |
Веб-поиск |
DuckDuckGo Search (DDGS) |
Предоставляет актуальные медицинские данные из сети. |
Мультимодальность |
Tesseract OCR + AssemblyAI STT |
Понимание фотоанализов и голосовых сообщений. |
Метаданные |
SQLite |
Надёжное хранение истории чата и метаинформации о документах. |
1. Ядро: Двухэтапный Tool-Calling
Вместо того чтобы надеяться на то, что модель сама "вспомнит" или "погуглит", я заставляю её выбрать инструмент, прежде чем давать ответ.
Шаг A. Планирование (Forced JSON)
Мы передаём модели текущий вопрос и историю диалога. Самое важное: мы заставляем её вернуть ответ в строгом формате JSON:
JSON
{
"tool": "local_rag,internet_search",
"query": "последний уровень холестерина и побочные эффекты статинов"
}
Преимущества JSON: Это делает пайплайн невероятно надёжным. При ошибке парсинга я выполняю Graceful Fallback — автоматически переключаюсь на local_rag как на самый безопасный вариант.
Шаг Б. Сбор Контекста
После получения плана мы выполняем поиск:
Локальный RAG: Используем поисковый запрос (
queryиз JSON) для извлечения релевантных личных данных из ChromaDB.Веб-поиск: Используем DuckDuckGo Search с региональными настройками (
region='ru-ru') для получения актуальной информации.
Все найденные данные объединяются в один системный промпт для финальной модели. При этом я использую жёсткую маркировку (например, === ИНФОРМАЦИЯ ИЗ ВЕБ-ПОИСКА ===), чтобы модель чётко разделяла личные факты и общие знания.
? Погружение в Код: Ключевые Функции
Вот как выглядит ядро пайплайна на Python.
A. Функция Планирования (chat_with_assistant)
Эта функция объединяет планирование и генерацию. Обратите внимание, как мы фиксируем текущую дату в промпте — это критически важно для медицинского ассистента при расчёте сроков действия рецептов или возраста пациента.
Python
# ГЛАВНАЯ ФУНКЦИЯ: Tool Calling (Планирование) с фиксацией даты
def chat_with_assistant(user_id: int, message_text: str) -> str:
# ? ИСПРАВЛЕНИЕ: Получаем текущую дату и время
current_datetime_str = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
PLANNING_PROMPT = f"""
Проанализируй следующий вопрос... Текущая дата: {current_datetime_str}.
Тебе нужно решить, какой инструмент необходим...
# ... (Остальная часть промпта) ...
Вопрос пациента: "{message_text}"
"""
# --- ВЫЗОВ 1: ПЛАНИРОВАНИЕ (Формат JSON) ---
data_plan = {
"model": "gpt-4o-mini",
# ...
"response_format": {"type": "json_object"} # Принудительный JSON
}
# ... (Обработка ответа, извлечение tools и query) ...
# --- ВЫПОЛНЕНИЕ ПЛАНА (Сбор контекста) ---
# ... (Вызовы retrieve_relevant и search_internet) ...
# --- ВЫЗОВ 2: ГЕНЕРАЦИЯ ОТВЕТА ---
if context_parts:
# Жёсткая инструкция для финального ответа
STRICT_INSTRUCTION = "\n\nВНИМАНИЕ! ... Твой ответ ОБЯЗАН быть основан на информации из раздела 'ИНФОРМАЦИЯ ИЗ ВЕБ-ПОИСКА'. ..."
context_joined = "\n\n=== РЕЛЕВАНТНЫЙ КОНТЕКСТ (ОБЯЗАТЕЛЬНО ИСПОЛЬЗУЙ) ===\n" + "\n\n---\n\n".join(context_parts)
full_system_prompt = SYSTEM_PROMPT + STRICT_INSTRUCTION + context_joined
# ... (Добавление истории и отправка финального запроса) ...
Б. Конфиденциальность: RAG без облаков
Вся медицинская история хранится локально с помощью ChromaDB и SentenceTransformer.
Python
# ---------------------------
# ChromaDB Embedder Initialization
# ---------------------------
try:
# ... (импорт и инициализация) ...
LOCAL_EMBEDDING_MODEL_NAME = "all-MiniLM-L6-v2"
CHROMA_EMBEDDER = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=LOCAL_EMBEDDING_MODEL_NAME,
device='cpu' # Ключевой момент: все локально!
)
# ... (создание коллекции) ...
except Exception as e:
logger.error("Ошибка при инициализации SentenceTransformer: %s", e)
В. Полная Мультимодальность (Голос и Фото)
Это делает ассистента удобным для человека, не слишком активно пользующегося новыми технологиями.
1. OCR для Анализов (Tesseract)
Фотографии документов переводятся в текст, затем отправляются в LLM на расшифровку и сохраняются в RAG.
Python
@bot.message_handler(content_types=['photo'])
def handle_photo(message):
# ... (скачивание файла) ...
raw_text = extract_text_from_image_bytes(downloaded)
# Анализ и саммаризация текста нейросетью
summary = analyze_medical_text(raw_text)
# Сохраняем в RAG
add_to_chroma(doc_id, summary, metadata)
# ... (ответ пользователю) ...
2. Голосовое управление (AssemblyAI)
Я добавил логику автоматического распознавания намерения для голосовых сообщений, начинающихся со слова "запомни".
Python
@bot.message_handler(content_types=['voice'])
def handle_voice(message):
# ... (транскрипция с AssemblyAI) ...
# ? ЛОГИКА АВТОМАТИЧЕСКОГО ЗАПОМИНАНИЯ ДЛЯ ГОЛОСА
if transcribed_text.lower().startswith("запомни"):
memory_text = transcribed_text[len("запомни"):].strip()
if memory_text:
# Сохраняем данные в RAG и прерываем обычный диалог
add_to_chroma(doc_id, memory_text, metadata)
# ...
return
# Если это не команда "запомни", передаем в основную логику чата
resp = chat_with_assistant(message.chat.id, transcribed_text)
bot.reply_to(message, resp)
? Итог: Что получилось
Я создал автономного медицинского ассистента, который:
Всегда помнит его личную историю, анализы и заметки.
Умеет искать актуальную информацию в сети.
Понимает любой ввод: текст, фото или голос.
Сам выбирает, что делать с помощью двухэтапного Tool-Calling.
Это не просто код, это часть заботы. Проект показал, как можно использовать современные возможности LLM, RAG и мультимодальности для решения реальных и очень личных проблем, сохраняя при этом контроль, конфиденциальность и надёжность.
Надеюсь, мой опыт вдохновит и вас на создание социально-значимых проектов, где код служит самой важной цели.
Комментарии (3)

Salamander174
29.10.2025 14:56Хоть бы отредачили полный копи паст от чата без надстройки, даже можно не читать
TomskDiver
Надеюсь этот агент не убьёт вашего отца. Как-то переживательно даже.