Какую архитектуру лучше использовать при разработке экспертной системы?

Столкнулся с проблемой выбора архитектуры при разработке экспертной системы. Сама система основывается на 30-страничном алгоритме, на каждом листе от 30 до 100 вопросов.

Рассматривалось два способа:

Способ 1 - создать сущность вопрос, и сущность лист, который содержит список этих вопросов, и двигаться от вопроса к вопросу.

class Question:
    def __init__(self, number, result_true, result_false):
        self.number = number
        self.result_true = result_true
        self.result_false = result_false
        
list1 = [Question(1, 2, 3), Question(2, 4, 5), ...]

Способ 2 - Создать набор правил для каждого результата, и задать каждый результат как набор правил

result1 = 'Высокий риск' if params['temperature'] >50 and params['humidity']>80 and...

Оба способа мне не нравятся с точки зрения масштабирования, то есть, при внесении изменения в алгоритм, придется менять достаточно большое количество правил/кода. Способ 2 также не подходит из-за размера системы(более 400 результатов, каждый основывается на 30+ параметрах).

Есть ли какой то более продвинутый с точки зрения теории алгоритмов способ представления архитектуры экспертной системы, чтобы она была максимально дружелюбна к редактированию и масштабированию, то есть добавлению/удалению/редактированию вопросов(узлов)?

Примечание: Проект реализуется на Python(библиотеки и фреймворки можно использовать любые). Сами правила планируется хранить в БД PostgreSQL

Типы вопросов которые могут быть в системе:

  1. Утвердительные (ДА/НЕТ)
  2. Количественные (a > b)
  3. Проверка вхождения в набор (a in [a,b,c])

Упрощенный пример системы: введите сюда описание изображения

UPD Способ 3 (на данный момент приоритетный)

1) Классифицировать алгоритм по листам

list_tree = {
    'question': 'Температура > 50?',
    'yes': {
        'question': 'Влажность > 80?',
        'yes': 'Лист 1',
        'no': 'Лист 2'
    },
    'no': 'Лист 3'
}

def traverse_tree(tree, facts):
    if isinstance(tree, dict):
        answer = facts.get('температура', 0) > 50
        branch = tree['yes'] if answer else tree['no']
        return traverse_tree(branch, facts)
    else:
        return tree
        
def get_result_number(leaf_name):
    return results_mapping.get(leaf_name)

facts = {'температура': 55, 'влажность': 85}

print(traverse_tree(list_tree, facts))

2) Аналогичным способом классифицировать листы на результаты

Почему классификация происходит поэтапно: как раз по причине повышения удобства модификации узлов(в рамках одной сущности это делать явно проще, и с меньшим риском ошибки). Вопросы, факты, промежуточные результаты хранятся в БД. Тут я их написал в качестве примера.


Ответы (2 шт):

Автор решения: CrazyElf

Да ну сделайте просто JSON примерно такой структуры:

[
    {
       "Id": 1,
       "Question": "Тут вопрос 1",
       "True": 2,
       "False": 3
    },
    {
       "Id": 2,
       "Question": "Тут вопрос 2",
       "True": 4,
       "False": 4
    },
    {
       "Id": 3,
       "Question": "Тут вопрос 3",
       "True": 5,
       "False": 42
    }
]

Просто структура:

  • id вопроса
  • вопрос
  • на какой id перейти если да
  • на какой id перейти если нет

Просто, понятно. Хотя если нужно будет вставить куда-то вопрос... Ну, нумерация же не обязана быть подряд, можете произвольные большие id для вставки вопросов использовать. Или вообще использовать UUID вместо числового id, или вообще словесный id.

Программно это не сложно будет реализовать совсем. Кстати, можно и "русифицировать" названия полей, если захочется, тогда совсем понятный конфиг будет. Что-то такое (с учётом добавленного примера):

[
    {
       "Номер": 1,
       "Вопрос": "Тут вопрос 1",
       "ДаПереход": 2,
       "НетПереход": 3
    },
    {
       "Номер": 2,
       "Вопрос": "Тут вопрос 2",
       "ДаРезультат": "Результат 1",
       "НетПереход": 4
    },
    {
       "Номер": 3,
       "Вопрос": "Тут вопрос 3",
       "ДаПереход": 5,
       "НетРезультат": "Результат 2"
    }
]

То есть можно не все поля чтобы были всегда. Либо результат, либо переход.

→ Ссылка
Автор решения: Solt

Действительно, тут вырисовывается не столько код, сколько проектирование приличной БД. А код в итоге простой получится, причём для разных целей отдельный - для отображения и заполнения, для отрисовки графов/цепочек, для аналитики, админка/редактор, итд.

А таблицы мне видятся следующие:

1. Вопросы

Поля:

  • id - уникальный идентификатор вопроса
  • number - порядковый номер в рамках опросника
  • page - не вижу смысла разбивки на листы, но если логика требует - пусть будет.
  • description - текст вопроса
  • type - тип данных. Зачем ограничиваться булевым да/нет, это же могут быть диапазоны, пороговые значения, даты, даже тексты, если придумать, как их оценивать.
  • group - принадлежность к группе. Опять таки, не будем ограничиваться одним опросником - пусть их будет много. Конечно, можно в отдельной таблице описания опросников сочинить.

2. Связи/переходы

  • id - уникальный идентификатор записи

  • question_id - идентификатор вопроса. Для одного вопроса может быть много записей с разными правилами ответов - например если это диапазоны, или отдельные значения с переходами в разные места.

  • rule - правило перехода, тут надо посочинять правила, зависящие от типов, например '>','<' для порогов, '=' для значений типа да/нет, или равно/не равно, '()','[)','(]','[]' для диапазонов итд. Ну или словами какими-то 'equal/range,less,more, default...'.

  • value - значение для сравнения с ответом по заданному правилу

  • destination_id - идентификатор вопроса, на который надо перейти при попадании под критерий

3.Ответы

  • id - уникальный идентификатор ответа

  • session_id - идентификатор экземпляра опросника

  • question_id - идентификатор вопроса

  • value(answer) - ответ

  • rule_id - идентификатор правила, под которое попал (необязательное поле, можно потом рассчитать, но удобнее хранить)

  • user_id - если ответы в рамках сессии дают разные пользователи, то их id. Если опрашивается один - можно вынести в таблицу сессий.

Ну и дополнительные информационные справочники к которым могут быть привязаны вышеописанные записи:

  • questionaries(groups) - описание вопросников или групп вопросов.
  • sessions - описание сессий/экземпляров вопросников с привязкой к датам, опрашиваемым пользователям, и т.д.
  • users - таблица юзеров

Думаю, по таблицам и полям становится понятно, как использовать таблицы:

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

Всё остальное мне кажется очевидным, но это субъективно.

Ну и, конечно, всё зависит от объемов. Если не сильно большие, можно всё это сохранить в JSON/XML/YAML/CSV, да как угодно. Но БД удобнее, гибче и быстрее.

→ Ссылка