Агентные системы на Org-mode: от цепочек до графов - практический гайд 2026 | AiManual
AiManual Logo Ai / Manual.
09 Фев 2026 Гайд

Построение агентных систем с нуля: туториал на Org-mode от линейных цепочек до графового управления

Пошаговый туториал по созданию AI-агентов с нуля на Org-mode. Литературное программирование, графовое управление, MedMCQA датасет. Код и примеры для 2026 года.

Почему все делают агентов неправильно (и как исправить это с помощью Org-mode)

Откройте любой гайд по созданию AI-агентов. Что вы увидите? Тысячи строк кода на Python, запутанные классы, сложные абстракции. Авторы гордятся тем, что их код "масштабируемый" и "производственный". А на деле получается монстр, который ломается при первом же изменении требований.

Проблема в подходе. Вы пишете код, который описывает поведение агента. А нужно писать документацию, которая становится кодом. Разница колоссальная.

К февралю 2026 года большинство фреймворков для агентов превратились в слона: тяжелые, медленные, неповоротливые. LangChain? CrewAI? Они решают проблемы, которых у вас нет. А реальные проблемы - отладка, понимание потока данных, изменение логики - остаются.

Org-mode - это редактор в Emacs для литературного программирования. Вы пишете текст с кодом внутри. Текст объясняет, код выполняется. Звучит просто? Это революционно.

Литературное программирование: когда документация становится исполняемой

Дональд Кнут придумал литературное программирование в 1984 году. Идея: программа должна читаться как книга. Сначала объяснение, потом код. Сначала "почему", потом "как".

За 40 лет идея не прижилась. Пока не появились LLM.

Современные модели (я использую GPT-4.5-Turbo на 09.02.2026) отлично понимают структурированный текст. Они могут читать вашу документацию и выполнять инструкции. Org-mode превращает эту возможность в суперсилу.

💡
Org-babel - система в Org-mode для выполнения кода прямо в документации. Пишете блок кода на Python, нажимаете C-c C-c - код выполняется. Результат появляется ниже. Вся история выполнения сохраняется в одном файле.

Начинаем с простого: агент для медицинских вопросов

Возьмем MedMCQA - датасет с медицинскими вопросами и вариантами ответов. Наша задача: создать агента, который анализирует вопрос, ищет информацию, выбирает ответ.

Типичный подход: написать класс MedicalAgent с методами analyze, research, decide. Мой подход: написать Org-файл.

1 Создаем базовую структуру агента

Откройте новый файл medical-agent.org. Начните с заголовков:

* Медицинский агент для MedMCQA
** Конфигурация
** Вопросы и ответы
** Логика агента
** Результаты

В разделе Конфигурация определите настройки:

#+begin_src python :exports none :session main
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

client = OpenAI(
    api_key=os.getenv('OPENAI_API_KEY'),
    base_url=os.getenv('OPENAI_BASE_URL', 'https://api.openai.com/v1')
)

MODEL = "gpt-4.5-turbo"  # Актуально на 09.02.2026
TEMPERATURE = 0.1
MAX_TOKENS = 2000
#+end_src

Не храните API-ключи в коде. Используйте .env файл. В 2026 году утечки ключей все еще самая частая причина проблем.

2 Линейная цепочка: простейший агент

Сначала сделаем агента, который работает пошагово:

#+begin_src python :exports none :session main
def linear_agent(question, options):
    """Простейший агент: вопрос -> анализ -> ответ"""
    
    # Шаг 1: Анализ вопроса
    analysis_prompt = f"""
    Проанализируй медицинский вопрос:
    {question}
    
    Определи:
    1. Тему вопроса (кардиология, неврология и т.д.)
    2. Ключевые термины
    3. Что именно спрашивается
    """
    
    analysis = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": analysis_prompt}],
        temperature=TEMPERATURE,
        max_tokens=MAX_TOKENS
    ).choices[0].message.content
    
    # Шаг 2: Выбор ответа
    decision_prompt = f"""
    На основе анализа выбери правильный ответ:
    
    Анализ: {analysis}
    
    Вопрос: {question}
    
    Варианты:
    {chr(10).join([f'{i+1}. {opt}' for i, opt in enumerate(options)])}
    
    Верни только номер правильного варианта (1, 2, 3 или 4).
    """
    
    decision = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": decision_prompt}],
        temperature=TEMPERATURE,
        max_tokens=50
    ).choices[0].message.content
    
    return {
        "analysis": analysis,
        "decision": decision,
        "question": question
    }
#+end_src

Проверим работу:

#+begin_src python :exports results :session main
# Тестовый вопрос из MedMCQA
test_question = "У пациента с хронической болезнью почек отмечается гиперкалиемия. Какой препарат противопоказан?"
test_options = [
    "Фуросемид",
    "Спиронолактон",
    "Гидрохлоротиазид",
    "Амилорид"
]

result = linear_agent(test_question, test_options)
print(f"Анализ: {result['analysis'][:200]}...")
print(f"Ответ: {result['decision']}")
#+end_src

Это работает. Но есть проблема: что если анализ недостаточен? Нужна проверка. Что если нужно уточнить вопрос? Нужна обратная связь.

Линейная цепочка ломается при сложных задачах. Как в статье "Как победить контекстный блот и 'зону тупости' в агентах" - простые цепочки не справляются со сложностью.

Переходим к графовому управлению

Графовое управление - это когда агент не просто идет от A к B к C. Он может вернуться, выбрать другой путь, параллелить задачи.

В Org-mode это реализуется через заголовки и свойства. Каждый заголовок - узел графа. Свойства - условия перехода.

3 Создаем граф агента

Определим узлы нашего графа:

* Граф медицинского агента
:PROPERTIES:
:graph_start: yes
:END:

** Анализ вопроса
:PROPERTIES:
:next_nodes: Проверка_достаточности, Поиск_доп_инфо
:condition: always
:END:

** Проверка достаточности
:PROPERTIES:
:next_nodes: Выбор_ответа, Уточняющий_вопрос
:condition: confidence > 0.7
:END:

** Поиск дополнительной информации
:PROPERTIES:
:next_nodes: Проверка_достаточности
:condition: always
:END>

** Уточняющий вопрос
:PROPERTIES:
:next_nodes: Анализ_ответа
:condition: always
:END>

** Выбор ответа
:PROPERTIES:
:next_nodes: Конец
:condition: always
:END>

** Конец
:PROPERTIES:
:graph_end: yes
:END:

Теперь реализуем исполнение графа:

#+begin_src python :exports none :session main
class GraphNode:
    def __init__(self, name, properties):
        self.name = name
        self.properties = properties
        self.next_nodes = properties.get('next_nodes', '').split(',')
        self.condition = properties.get('condition', 'always')
        
    def should_execute(self, context):
        """Проверяет условие выполнения узла"""
        if self.condition == 'always':
            return True
        
        # Пример: проверка уверенности
        if 'confidence' in self.condition:
            required_conf = float(self.condition.split('>')[1].strip())
            return context.get('confidence', 0) > required_conf
        
        return True

def execute_graph(question, options):
    """Исполняет граф агента"""
    
    # Загружаем граф из Org-файла
    # В реальности парсим файл, здесь упрощенный пример
    nodes = {
        'Анализ_вопроса': GraphNode('Анализ_вопроса', {
            'next_nodes': 'Проверка_достаточности,Поиск_доп_инфо',
            'condition': 'always'
        }),
        'Проверка_достаточности': GraphNode('Проверка_достаточности', {
            'next_nodes': 'Выбор_ответа,Уточняющий_вопрос',
            'condition': 'confidence > 0.7'
        }),
        'Выбор_ответа': GraphNode('Выбор_ответа', {
            'next_nodes': 'Конец',
            'condition': 'always'
        })
    }
    
    context = {'question': question, 'options': options, 'confidence': 0}
    current_node = 'Анализ_вопроса'
    execution_path = []
    
    while current_node != 'Конец':
        node = nodes[current_node]
        execution_path.append(current_node)
        
        if node.should_execute(context):
            # Выполняем логику узла
            if current_node == 'Анализ_вопроса':
                context = execute_analysis(context)
            elif current_node == 'Проверка_достаточности':
                context = execute_sufficiency_check(context)
            elif current_node == 'Выбор_ответа':
                context = execute_decision(context)
            
            # Выбираем следующий узел
            if node.next_nodes:
                # Здесь может быть сложная логика выбора
                # Для простоты берем первый
                current_node = node.next_nodes[0]
        else:
            # Условие не выполнено, идем по альтернативному пути
            if len(node.next_nodes) > 1:
                current_node = node.next_nodes[1]
            
    return {
        'context': context,
        'execution_path': execution_path,
        'final_answer': context.get('answer')
    }
#+end_src

Преимущество графа: визуальная понятность. Откройте Org-файл, сразу видно все возможные пути. Хотите изменить логику? Просто поменяйте свойства узла.

💡
В 2026 году появились инструменты визуализации графов прямо в Org-mode. Можете увидеть поток данных, статистику выполнения, узкие места. Это как Ralph Loop в Trello, но встроенное в среду разработки.

Структурированный вывод: заставляем LLM возвращать данные, а не текст

Самая большая проблема с LLM - неструктурированный вывод. Модель возвращает текст, который нужно парсить. Ошибки парсинга ломают всю систему.

Решение: заставляем модель возвращать JSON с четкой схемой. В 2026 году все современные модели (GPT-4.5, Claude 3.7, Gemini 2.0) отлично работают с JSON.

#+begin_src python :exports none :session main
def structured_analysis(question):
    """Анализ вопроса со структурированным выводом"""
    
    prompt = f"""
    Проанализируй медицинский вопрос и верни результат в JSON.
    
    Вопрос: {question}
    
    Верни JSON со следующей структурой:
    {{
      "topic": "основная тема",
      "subtopics": ["подтема1", "подтема2"],
      "key_terms": ["термин1", "термин2"],
      "question_type": "диагностика/лечение/этиология",
      "complexity": 1-5,
      "confidence": 0.0-1.0
    }}
    
    Только JSON, без дополнительного текста.
    """
    
    response = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": prompt}],
        temperature=TEMPERATURE,
        response_format={"type": "json_object"}  # Критически важно!
    )
    
    import json
    result = json.loads(response.choices[0].message.content)
    return result
#+end_src

Параметр response_format={"type": "json_object"} появился в OpenAI API в 2024 году. В 2026 это стандарт. Без него модель может вернуть текст перед JSON или после, что сломает парсинг.

Интеграция с внешними инструментами: когда агенту нужны "руки"

Чистые LLM знают много, но не все. Иногда нужно:

  • Поискать в медицинской базе данных
  • Посчитать дозировку препарата
  • Проверить взаимодействия лекарств

В Org-mode каждый инструмент - отдельный блок кода. Агент решает, какой инструмент вызвать.

#+begin_src python :exports none :session main
# Медицинская база знаний (упрощенная)
MEDICAL_KB = {
    "спиронолактон": {
        "category": "калийсберегающий диуретик",
        "contraindications": ["гиперкалиемия", "болезнь Аддисона"],
        "mechanism": "антагонист альдостерона"
    },
    "фуросемид": {
        "category": "петлевой диуретик",
        "contraindications": ["анафилаксия к сульфонамидам"],
        "mechanism": "ингибитор Na-K-2Cl котранспортера"
    }
}

def search_medical_kb(term):
    """Поиск в медицинской базе знаний"""
    return MEDICAL_KB.get(term.lower(), {})

def calculate_dosage(weight_kg, drug, indication):
    """Расчет дозировки препарата"""
    # Упрощенный расчет
    dosages = {
        "фуросемид": {"отёк": 0.5, "гипертензия": 0.25},
        "спиронолактон": {"отёк": 1.0, "гипертензия": 0.5}
    }
    
    base_dose = dosages.get(drug, {}).get(indication, 0)
    return base_dose * weight_kg
#+end_src

Теперь агент может использовать эти инструменты:

#+begin_src python :exports none :session main
def agent_with_tools(question):
    """Агент с доступом к инструментам"""
    
    # Анализ вопроса
    analysis = structured_analysis(question)
    
    # Если в вопросе есть препараты - ищем в БЗ
    tools_used = []
    for term in analysis['key_terms']:
        if term in MEDICAL_KB:
            info = search_medical_kb(term)
            tools_used.append({"tool": "medical_kb", "term": term, "result": info})
    
    # Если есть дозировка - рассчитываем
    if 'дозировка' in question.lower() or 'доза' in question.lower():
        # Здесь был бы более сложный парсинг
        # Для примера фиктивные значения
        dosage = calculate_dosage(70, 'фуросемид', 'отёк')
        tools_used.append({"tool": "dosage_calculator", "result": dosage})
    
    return {
        "analysis": analysis,
        "tools_used": tools_used,
        "has_sufficient_data": len(tools_used) > 0
    }
#+end_src

Этот подход похож на архитектуру Plan-Code-Execute, но проще. Каждый инструмент - независимый блок. Легко добавлять новые, легко тестировать.

Параллельное выполнение: когда один агент - это мало

Некоторые задачи можно распараллелить:

  • Один агент ищет в PubMed
  • Второй проверяет клинические рекомендации
  • Третий анализирует аналогичные случаи

В Org-mode это делается через таблицы и параллельное выполнение блоков:

* Параллельные агенты

#+name: parallel-tasks
| Задача               | Агент      | Входные данные |
|----------------------+------------+----------------|
| Поиск исследований   | research   | вопрос         |
| Проверка руководств  | guidelines | тема           |
| Анализ случаев       | cases      | ключевые_термины |

#+begin_src python :var tasks=parallel-tasks :exports none
import concurrent.futures

def run_parallel_agents(tasks):
    results = {}
    
    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        future_to_agent = {}
        
        for task in tasks:
            agent_name = task['Агент']
            input_data = task['Входные данные']
            
            if agent_name == 'research':
                future = executor.submit(search_research, input_data)
            elif agent_name == 'guidelines':
                future = executor.submit(search_guidelines, input_data)
            elif agent_name == 'cases':
                future = executor.submit(search_cases, input_data)
            
            future_to_agent[future] = agent_name
        
        for future in concurrent.futures.as_completed(future_to_agent):
            agent_name = future_to_agent[future]
            try:
                results[agent_name] = future.result()
            except Exception as e:
                results[agent_name] = {'error': str(e)}
    
    return results
#+end_src

Подробнее о параллельных агентах читайте в статье про параллельное выполнение coding-агентов.

Ошибки, которые все совершают (и как их избежать)

Ошибка Почему происходит Как исправить
Агент зацикливается Нет проверки на максимальное количество шагов Добавить счетчик итераций в контекст графа
LLM возвращает не JSON Забыли response_format или модель "творческая" Использовать response_format и парсить с try/except
Контекст переполняется Каждый шаг добавляет данные, ничего не удаляет Реализовать стратегию summarization или окно контекста
Инструменты не вызываются Агент не понимает, когда нужен инструмент Явно указывать в промпте условия вызова инструментов

Собираем все вместе: полная система на одном листе

Красота Org-mode в том, что вся система помещается в один файл:

* Медицинская агентная система
:PROPERTIES:
:author: Ваше имя
:date: 2026-02-09
:version: 1.0
:END:

** Конфигурация
*** Настройки API
#+begin_src python ...
*** Модели и параметры
#+begin_src python ...

** Граф выполнения
*** Узлы и связи
*** Условия переходов

** Инструменты
*** База знаний
*** Калькуляторы
*** Поисковые системы

** Агенты
*** Основной аналитик
*** Специализированные агенты
*** Координатор

** Тесты
*** Примеры вопросов
*** Ожидаемые ответы
*** Фактические результаты

** Мониторинг
*** Логи выполнения
*** Метрики качества
*** Визуализация графов

Откройте этот файл через месяц. Вы сразу поймете, как работает система. Что изменилось? Какие узлы чаще всего выполняются? Где ошибки?

Попробуйте сделать то же самое с Python-файлами. Удачи.

Что дальше? Эволюция вместо революции

Вы построили агентную систему. Она работает. Что делать теперь?

Не переписывайте с нуля. Используйте эволюционный подход:

  1. Добавляйте метрики качества в каждый узел графа
  2. Записывайте, какие пути чаще приводят к правильным ответам
  3. Экспериментируйте с разными моделями для разных узлов
  4. Автоматизируйте тестирование на новых данных

Через месяц у вас будет не просто агент. У вас будет система, которая эволюционирует. Она подскажет, где слабые места. Где нужно добавить инструменты. Где упростить логику.

Это и есть будущее агентных систем. Не монолитные фреймворки. Не тысячи строк кода. А живые, документированные, эволюционирующие системы в одном Org-файле.

Начните с простого линейного агента. Добавьте один узел графа. Потом еще один. Через неделю у вас будет система, которая решает реальные задачи. Через месяц - система, которая учится на своих ошибках.

И все это без единого класса AgentFactoryManagerCoordinator.