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

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

Мне нужен был не просто бот-напоминалка, а второй пилот — умный, конфиденциальный и мультимодальный 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 и мультимодальности для решения реальных и очень личных проблем, сохраняя при этом контроль, конфиденциальность и надёжность.

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

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


  1. TomskDiver
    29.10.2025 14:56

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


  1. Salamander174
    29.10.2025 14:56

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


  1. aladkoi
    29.10.2025 14:56

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