? Идея, Которая Важнее Кода

Мой отец — человек, переживший несколько сложнейших операций на сердце. Жизнь с хроническим заболеванием — это бесконечный поток анализов, заключений и схем приёма лекарств. Находясь далеко (я живу во Вьетнаме), я постоянно волновался: не забудет ли он про дозу, правильно ли понял назначение, задал ли все нужные вопросы врачу?

Мне нужен был не просто бот-напоминалка, а второй пилот — умный, конфиденциальный и мультимодальный AI-Кардиолог. Ассистент, который знает его анамнез наизусть, понимает голосовые команды и может "прочитать" фотографию свежего анализа.

Я решил собрать полноценный автономный агент с возможностью вызова внешних инструментов (Tool-Calling) и локальной базой знаний (RAG), но без использования громоздких фреймворков вроде LangChain или LlamaIndex.

? Архитектура: Планировщик и Жесткий Контроль

Моя цель: максимальная надёжность, локальность данных и предсказуемость.

Ядро системы — это Полноценный Tool-Calling Pipeline на GPT-4o-mini, который работает в два этапа: Планирование и Генерация.

Компонент

Технология

Роль в системе

Планировщик

GPT-4o-mini (OpenRouter)

Принимает решение: local_rag, internet_search или none.

База знаний (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)

? Итог: Что получилось

Я создал автономного медицинского ассистента, который:

  1. Всегда помнит его личную историю, анализы и заметки.

  2. Умеет искать актуальную информацию в сети.

  3. Понимает любой ввод: текст, фото или голос.

  4. Сам выбирает, что делать с помощью двухэтапного Tool-Calling.

Это не просто код, это часть заботы. Проект показал, как можно использовать современные возможности LLM, RAG и мультимодальности для решения реальных и очень личных проблем, сохраняя при этом контроль, конфиденциальность и надёжность.

Надеюсь, мой опыт вдохновит и вас на создание социально-значимых проектов, где код служит самой важной цели.

Комментарии (5)


  1. TomskDiver
    29.10.2025 14:56

    Надеюсь этот агент не убьёт вашего отца. Как-то переживательно даже.


  1. Salamander174
    29.10.2025 14:56

    Хоть бы отредачили полный копи паст от чата без надстройки, даже можно не читать


  1. aladkoi
    29.10.2025 14:56

    Такое Claude напишет минут за 10 при правильной постановке задачи.


  1. Andrey2894
    29.10.2025 14:56

    Такое впечатление что продукта не существует. Не рассказано о целях и роли RAG - для чего он нужен в вашей системе. По стеку(ChromaDB + SentenceTransformer) видно, что вы используете семантический поиск. Про него нет ни слова. Отсутствует описание логики извлечения информации для медицинской системы. Если ваша система по запросу ищет ближний вектор, то есть шанс вернуть не ту информацию, которая нужна в данном запросе, что может навредить человеку.
    Чем обусловлен выбор тессеракта, который не может справиться с хорошо структурированными медицинскими файлами, в которых есть таблицы, разметки и т д? Ничего не сказано про неправильное распознавание символов им. Если тессеракт неправильно распознает символ, LLM придется работать с неверными данными(что в свою очередь угроза жизни вашему отцу).

    Присоединяюсь к комментариям выше.


  1. Stasiao Автор
    29.10.2025 14:56

    Это моя первая статья поэтому не стал расписывать подробно всё. Но если будет интересно читателям то в следующей статье распишу подробнее.