Всем привет! Меня зовут Артем, я Data Scientist компании RAFT. Сегодня я расскажу об своем pet проекте по объяснению правил для настольных игр. Цель данного проекта — понять, как можно собрать RAG агента без использования больших библиотек. Проект начался с идеи автоматизировать объяснение правил настольных игр через диалог с чат-ботом, да и также мне было интересно опробовать новые библиотеки для создания агентов. Свой проект я тестировал на правилах для настольных игр "Iki" и "Подземелье и песики".

Три столпа моего проекта
В данном проекте я собрал довольно интересный стек, который включает в себя три основных библиотеки:
Pocketflow — это ультра-лёгкий фреймворк (около 100 строк кода и 56 КБ) для создания LLM-агентов, построенный на концепции управления потоком данных через граф. В отличие от многофункциональных альтернатив (LangChain, CrewAI и др.), он предоставляет только ядро без встроенных инструментов или интеграций, позволяя разработчикам полностью контролировать архитектуру и создавать агентов.
Objectbox — это высокопроизводительная NoSQL-база данных, оптимизированная для мобильных устройств, IoT и edge computing. Она использует объектно-ориентированную модель данных (хранит объекты напрямую, без преобразования в SQL), работает в офлайн-режиме и обеспечивает низкую задержку благодаря встроенной архитектуре и минимальному использованию ресурсов. С недавнего времени ещё и ставшая векторной базой данных.
BlackSheep — это высокопроизводительный асинхронный веб-фреймворк для Python, построенный на ASGI и asyncio. Вдохновленный Flask и ASP.NET Core, он предлагает rich API с dependency injection за счет библиотеки rodi, автоматическую генерацию OpenAPI-документации, встроенные механизмы аутентификации и авторизации, а также поддержку Web Socket и Server-Sent Events.
Маленькие, но важные блоки
Также стоит упомянуть несколько библиотек, без которых проект бы несколько сложнее:
langfuse — используется для логирования
SentenceTransformers — используется для создания эмбедингов.
Почему я выбрал именно такие библиотеки?
Pocketflow — это и есть виновник торжества, библиотека из-за которой и затеялся весь проект. Граф в 100 строк кода, ещё и без зависимостей. Которого, по словам автора, должно хватить на всё. Вот и проверим это.
Objectbox — отличная offline база данных, которую я часто пробую использовать на C++, да и которая с недавнего времени стала ещё и векторной базой данных с python библиотекой. Основная причина её использования — это то, что она offline и для неё не нужен отдельный сервер.
BlackSheep — одна из альтернатив fast api, которая превосходит его по скорости. Очень интересная библиотека, которую мне хотелось попробовать.
Что получилось сделать из данных библиотек? А получилось сделать 3 RAG экспертов, которые работают параллельно и независимо друг от друга, для написания правил настольных игр.
Собираем приложение
Само приложение делится на несколько частей
Сервис LLM Graph, в котором находится вся реализация графа;
Web сервис, который частично повторяет api OpenAI, чтобы мы могли подключить нашего бота, например, в DeepChat, Jan, OpenWebUI
MCP Сервер. В котором находятся нужные для нас RAG инструменты.
Разберем каждый из этих сервисов.
Сервис 1. LLM Graph. Pocketflow
Для начала сделаем небольшое введение в Pocketflow
Небольшое введение в pocketflow
Я немного изменил исходный код PocketFlow и поменял тип shared state с dict на Pydantic класс.
А для работы с langfuse я сделал собственную ноду для абстрактной ноды, где инициализирую клиент langfuse.
Pocketflow придерживается следующих правил. Каждая нода должна выполняется в три этапа (но все они опциональные):
Этап ноды | Сигнатура | Что получает | Что возвращает |
|---|---|---|---|
Preparation |
|
| подготовленные данные |
Execution |
| подготовленные данные | результат LLM/инструмента |
Post-processing |
|
| имя перехода ( |
Некоторые возможности Pocketflow:
Pocketflow поддерживает условные грани (conditional edges). Путь по которому мы идем возвращается из шага post processing.
Pocketflow также может может перезапустить ноду при срабатывании исключения, для этого при инициализации ноды передаются два значения:
max_retries — максимальное кол-во попыток запуска ноды;
wait — сколько секунд должно пройти между запусками.
Как может выглядеть нода:
class EchoNode(AbstractNode):
def __init__(self, name: str, echo_text: str, max_retries=10, wait: int | float = 10):
super().__init__(name, max_retries, wait)
self._echo_text = echo_text
@observe(as_type="chain")
async def prep_async(self, shared: PrivateState):
name = f"{self._name}:prep"
self._lf_client.update_current_span(name=name)
return None
@observe(as_type="chain")
async def exec_async(self, prep_res):
name = f"{self._name}:exec"
self._lf_client.update_current_span(name=name)
return AiMessage(content=self._echo_text)
@observe(as_type="chain")
async def post_async(self, shared: PrivateState, prep_res, exec_res):
name = f"{self._name}:post"
self._lf_client.update_current_span(name=name)
shared.dialog.add_message(exec_res)
return "default"
@observe — нужен для логирования в langfuse.
Как создается граф?
Я приведу пример своего графа и то как он выглядит:
self.security_node - "unsafe" >> self.echo_node
self.security_node - "safe" >> self.task_classifier_node
self.task_classifier_node - "explanation" >> self.task_splitter_node
self.task_classifier_node - "clarification" >> self.clarification_expert
self.task_classifier_node - "general" >> self.general_expert
self.task_splitter_node >> self.expert_parallel_coordinator
self.expert_parallel_coordinator >> self.join_node
self.join_node >> self.summary_node
self.summary_node >> self.switch_node
Конструкция
node_1 >> node_2
Или аналогично
node_1 - "default" >> node_2
Означает что из первой ноды мы идем во вторую по пути "default"
Архитектура
Без лишних предисловий посмотрим на диаграмму и пройдемся по ней, попутно объясняя что и зачем я сделал.
Что происходит за кадром
SecurityNode. Возвращает
{"verdict": "safe" \| "unsafe"}. Это нода нужна для проверки сообщения на различные хаки, опасное или вредоносное поведение. Если unsafe — сразу в EchoNode и закрываем обработку сообщений, иначе — дальше.TaskClassifierNode
Здесь всё просто. Это нода роутит запрос:
general— просто LLM, без RAG
Пример
Вопрос:
Привет
Ответ:
Привет! Я эксперт по настольным играм — от классики вроде "Монополии" до современных хитростей типа "Root". Чем могу помочь: правилами, рекомендациями или стратегиями? 😊
clarification— 1 эксперт, уточняет 1 правило
Пример
Вопрос:
Теперь как работают зелья в игре подземелья и песики?
Ответ:
Игра: "Подземелье и пёсики" (2024?, лёгкий dungeon crawler с собачками-приключенцами)
В двух словах: Зелья — это карты, которые вы получаете в специальных комнатах подземелья. Они дают разовые бонусы (лечение, урон и т.д.), их можно брать из запаса и хранить сколько угодно, но сама карта комнаты не приносит монет в конце.
Получение зелий
Зелья появляются как тип комнаты при исследовании подземелья (действие "Исследовать подземелье"):
Когда вы попадаете в комнату "Зелье": Положите на неё свой жетон лапки (обязательно, чтобы активировать).
Сразу возьмите любую карту зелья из общего запаса (6 карт в игре). Можно выбрать ту, которую ранее убрал другой игрок.
Карту комнаты забираете в конце раунда по общим правилам (но в конце игры она ничего не приносит — ни монет, ни опыта).
Лимит: Нет ограничений — можно иметь сколько угодно зелий на планшете.
Компоненты: 6 карт зелий (эффекты индивидуальны, например, восстановление лапок, доп. атаки или защита — смотрите на картах).
Использование зелий
Зелья — одноразовые: Активируйте их во время хода (исследование, бой с монстрами, ловушки и т.д.), следуя эффекту на карте.
После использования карта зелья убирается (в сброс или запас, по правилам игры).
Интеграция: Часто помогают в бою (урон монстрам), лечении (лапки) или прохождении ловушек/дверей.
Практические советы
Что делать: Всегда кладите лапку на комнату зелья — это "бесплатный" бонус без риска. Берите редкие зелья заранее, если видите, что запас тает.
Чего избегать: Не игнорируйте зелья ради выхода — они спасут в сложных комнатах (монстры, ловушки). Если лапок мало, комбинируйте с действием "Лечь спать".
Для новичков: Используйте зелья proactively в бою, чтобы не тратить лапки зря.
Примеры
Ситуация 1: Исследуете комнату зелья → Кладёте лапку → Берёте "Зелье лечения" → В следующем бою с монстром используете его, чтобы восстановить 2 лапки.
Ситуация 2: Другой игрок взял "огненное зелье", но вы можете забрать его же тип позже из запаса — дубликаты разрешены.
Стратегия: В поздних раундах зелья критичны из-за обратного отсчёта (механика "Обратный отсчёт" после первого выхода).
В итоге, зелья — ваш "запасной план" для выживания в подземелье, идеальны для кооператива или соло. Какие эффекты на зельях в вашей версии или нужны правила других комнат? 😊
explanation— полный цикл: 3 эксперта → сводка. А об этом будет чуть ниже.
TaskSplitterNode
Это нода сейчас генерирует 3 задания для каждого эксперта. Но кажется, что это нода сейчас излишняя. Так как эксперты делят правила настольной игры между собой. О том, что делают эксперты, я расскажу ниже.ExpertParallelCoordinator
Запускает 3 одинаковых RASG параллельно; каждый живёт в своёмDialog'е, чтобы не мешать друг другу.JoinNode + SummaryNode
Склеиваем 3 draft-ответа и просим LLM сделать из них 1 структурированную инструкцию.
Эксперты
В данном блоке я немного расскажу об экспертах, что они делают и как работают. Как я раньше упоминал в тексте, у нас используются три эксперта, каждый из которых ответственен за определенную часть правил. Первый эксперт ответствен за начало правил. Вот пример генерации им его:
Первый эксперт ответствен за начало правил. Вот пример генерации им его части:
Пример
VERIFICATION MATRIX
Element | Primary Source | Confirming Source | Entity Source | Status |
|---|---|---|---|---|
Game theme | RuleChunk:1 | Toc:main_title | - | ✓ |
Victory trigger | RuleChunk:18 | RuleChunk:1 | - | ✓ |
Component: карты комнат | RuleChunk:2 | - | - | ✓ |
Setup step 1 | RuleChunk:3 | Toc:setup | - | ✓ |
Концепция игры
Подземелье и Пёсики — игра для 2–4 игроков от 8 лет длительностью 30 минут. В волшебном королевстве каждый год соревнования по побегу из подземелья. Самые храбрые и смелые Пёсики спускаются в глухие мрачные комнаты, чтобы сразиться с чудовищами и собрать как можно больше трофеев. Победителю ждут почёсти и слава, титулы и горы золота, а также очень вкусные косточки. Игроки становятся участниками фэнтезийных соревнований. В каждом раунде по очереди выкладывают карты комнат, формируя подземные лабиринты с монстрами и сокровищами. Задача каждого игрока — построить наиболее выгодный для себя маршрут к выходу из подземелья, по пути собирая трофеи, монеты и побеждая чудовищ. Побеждает тот, кто по итогам трёх раундов накопит больше всего золотых монет — их можно вынести из подземелья, получить за выполненные квесты и за побеждённых монстров. [RuleChunk:1]
Игра заканчивается после третьего раунда. Победителем становится игрок, который набрал больше всех монет. При ничьей побеждает тот, кто выполнил больше квестов — как личных, так и общих. Если ничья сохраняется, участники ничьей делят победу и радостно виляют хвостиками. Гав-гав! [RuleChunk:18]
Компоненты
90 карт комнат [RuleChunk:2]
6 планшетов Пёсиков [RuleChunk:2]
12 карт квестов [RuleChunk:2]
6 карт зелий [RuleChunk:2]
42 жетона лапок (по 7 жетонов каждого цвета) [RuleChunk:2]
3 жетона раундов [RuleChunk:2]
Жетон первого игрока [RuleChunk:2]
Эти правила игры [RuleChunk:2]
Подготовка к игре
1.Каждый игрок выбирает себе планшет Пёсика и кладёт перед собой лицевой стороной вверх. Лишние планшеты уберите в коробку. [RuleChunk:3]
2.Затем каждый игрок берёт себе 7 жетонов лапок, указанных в углу планшета: 5 из них кладёт на планшет (это базовые жетоны), 2 оставшихся кладёт рядом с планшетом (это дополнительные жетоны). Остальные жетоны уберите в коробку. [RuleChunk:3]
3.Перемешайте все карты квестов лицевой стороной вниз и раздайте каждому игроку по 2 карты случайным образом. Лишние карты квестов уберите в коробку. [RuleChunk:3]
4.Каждый игрок выбирает из своих карт квестов одну и кладёт её перед собой лицевой стороной вниз, не показывая остальным. Это личные квесты игроков. [RuleChunk:3]
5.Вторую карту квеста игрок кладёт лицевой стороной вверх на стол, формируя ряд общих квестов. Их может выполнить любой игрок. [RuleChunk:3]
6.Найдите среди карт комнат двустороннюю стартовую комнату. Одна сторона карты предназначена для игры втроём и вчетвером, другая — для игры вдвоём. Положите карту в центре стола нужной стороной вверх — отсюда вы начнёте свой путь по подземелью. [RuleChunk:3]
7.Положите 3 жетона раундов на карту стартовой комнаты: снизу жетон с цифрой 3, на него жетон с цифрой 2, на самый верх жетон с цифрой 1. Это счётчик раундов. [RuleChunk:3]
8.Остальные карты комнат перемешайте и положите на стол стопкой лицевой стороной вниз. [RuleChunk:3]
9.Перемешайте все карты зелий лицевой стороной вниз и раздайте каждому игроку по одной случайной карте. Положите карту зелья рядом со своим планшетом лицевой стороной вверх. Лишние карты зелий положите в запас лицевой стороной вверх рядом со стопкой карт комнат. [RuleChunk:3]
Основные элементы игрового процесса
Второй эксперт ответственен за то, как играть:
Пример
VERIFICATION MATRIX
Element | Primary Source | Confirming Source | Entity Source | Status |
|---|---|---|---|---|
Turn order | RuleChunk:5 | Toc:gameplay_actions | - | ✓ |
Action "Исследовать подзем��лье" | RuleChunk:5 | Toc:explore_dungeon | - | ☐ |
Action "Двигаться" | - | - | - | ⊘ |
Action "Бой" | RuleChunk:10 | Toc:monster | - | ✓ |
Action "Зелья" | RuleChunk:16 | Toc:potion | - | ✓ |
Action "Лечь спать" | RuleChunk:5 | Toc:sleep | ☐ | |
Interturn procedure | - | Toc:round_end | - | ☐ |
Как играть
Структура хода
Игроки ходят по очереди по часовой стрелке, начиная с первого игрока [RuleChunk:5]. В свой ход вы можете выполнить одно из трёх действий: Исследовать подземелье, Выйти из подземелья, Лечь спать [RuleChunk:5].
Действия
[NO DATA DOCUMENTED]
Основные механики
Монстры
Вы можете сразиться с монстром или пройти мимо него, получив урон [RuleChunk:10]. Если символы на карте монстра совпадают с вашими символами атаки (считаются и базовые, и трофейные символы, а также зелья), то вы побеждаете монстра и обязаны положить на карту жетон лапки [RuleChunk:10]. Побеждённый монстр приносит трофейные символы атаки или защиты, которые можно использовать в этом же раунде [RuleChunk:10]. Некоторые монстры дополнительно к трофеям приносят монеты в конце игры [RuleChunk:10]. Дракон приносит 4 монеты вместо трофеев [RuleChunk:10]. Стреко-klуч приносит трофейный ключ — с ним вы можете пройти через все двери в подземелье и открыть в конце игры один сундук [RuleChunk:10].
Ловушки
Чтобы пройти ловушку, вы обязаны либо обезвредить её, либо положить жетон лапки и взять эту карту с штрафом [RuleChunk:11]. Чтобы обезвредить ловушку, у вас должна быть карта комнаты (включая бродягу) или зелье с подходящим трофеем (базовые символы не обезвреживают ловушку) [RuleChunk:11]. Это может быть карта, которую вы получили в предыдущих раундах или разыграли ранее в этом раунде [RuleChunk:11]. Если вы обезвредили ловушку картой, разыгранной в этом раунде, то в подземелье может образоваться пустое пространство с жетоном лапки... В конце раунда уберите в коробу обе карты — карту ловушки и карту, которая её обезвредила (кроме зелья — его положите в запас зелий) [RuleChunk:11]. Больше они не появляются в игре [RuleChunk:11]. Если вам нечем обезвредить ловушку или вы не хотите этого делать, положите на карту жетон лапки [RuleChunk:11]. В конце игры она принесёт одну монету в качестве штрафа [RuleChunk:11].
Зелья
Вы можете положить на карту жетон лапки и сразу взять любую карту зелья из запаса на ваш выбор [RuleChunk:16]. Можно взять ту карту зелья, которую убрал в запас ранее другой игрок [RuleChunk:16]. Саму карту комнаты вы забираете в конце раунда по общим правилам, однако в конце игры она ничего не принесёт [RuleChunk:16]. У вас может быть столько угодно карт зелий [RuleChunk:16].
Фазы и процедуры между ходами
[NO DATA DOCUMENTED]
Используемые термины
Зелье (RuleChunk:16)
Лапки (RuleChunk:10)
Жетон лапки (RuleChunk:11)
Завершение игры и победа
А третий эксперт ответственен за концовку и дополнительный правила:
Пример
VERIFICATION MATRIX
Element | Primary Source | Confirming Source | Entity Source | Status |
|---|---|---|---|---|
End trigger 1 | RuleChunk:18 | Toc:game_end | - | ✓ |
End trigger 2 | RuleChunk:17 | Toc:round_end | - | ✓ |
Scoring: Монеты на картах комнат | RuleChunk:18 | - | - | ✓ |
Scoring: Сундуки | RuleChunk:18 | - | - | ✓ |
Scoring: Ловушки и бродяги | RuleChunk:18 | - | - | ✓ |
Scoring: Квесты (личные и общие) | RuleChunk:18 | - | - | ✓ |
Tiebreaker hierarchy | RuleChunk:18 | - | - | ✓ |
Advanced rule: Нет данных | - | Toc:abilities | - | ⊘ |
Окончание игры и дополнительные правила
Условия окончания игры
После третьего раунда игра заканчивается. [RuleChunk:18]
Раунд заканчивается, когда все игроки вышли из подземелья или легли спать. Снимите верхний жетон со счётчика раундов. Если это был последний жетон, игра сразу заканчивается. [RuleChunk:17]
Финальный подсчёт
Сложите и посчитайте количество золотых монет на всех своих картах комнат. Помните, что сундук приносит монеты только в том случае, если у вас есть ключ, и один ключ открывает только один сундук. [RuleChunk:18]
Отнимите монеты за карты ловушек и бродяг, если они у вас есть. [RuleChunk:18]
Получите монеты за выполненные личные и общие квесты. Монеты за личный квест может получить только тот игрок, которому принадлежит эта карта квеста. Монеты за общий квест может получить любой игрок, который его выполнил (это могут быть несколько игроков). Квесты с монстрами приносят монеты за каждого указанного побеждённого монстра — например, если вы победили двух ифритов, то считается, что вы дважды выполнили квест «Последнее желание» и получаете 4 монеты. [RuleChunk:18]
Победителем становится игрок, который набрал больше всех монет. [RuleChunk:18]
[Translated Example]: Примечание: квест с трофеями («Стальная хватька», «Огонь в сердце», «В яблочко») может выполнить только один игрок, даже если квест общий. Если несколько игроков набрали одинаковое количество трофейных символов, ничья разрешается в пользу игрока, у которого больше базовых указанных символов. Если ничья сохраняется, то считается, что никто не выполнил квест. [RuleChunk:18]
Разрешение ничьих
При ничьей побеждает тот, кто выполнил больше квестов — как личных, так и общих. Если ничья сохраняется, то участники ничьей делят победу и радостно угощают хостиками. Гав-гав! [RuleChunk:18]
Дополнительные правила и уточнения
Хотя классический RAG теоретически ��одходит для этой задачи, объемные правила настольных игр потребляют слишком много токенов, что делает такой подход неоптимальным. Здесь же три экспертные LLM использованы для распределения нагрузки, ускорения обработки и повышения качества ответов по сравнению с одной моделью.
Вот так выглядит итоговый ответ на примере игры iki:
Пример
MASTER FACT INVENTORY
Fact | Expert | Source ID | Status |
|---|---|---|---|
Iki — игра о мастерах эпохи Эдо. Погрузитесь в мир Японии 1603–1868 гг... | Expert 1 | RuleChunk:20 | ✓ |
Цель игры — прожить в Эдо целый год... Побеждает игрок с наибольшим количеством очков Ики... 🌸 во время, 🍁 в конце | Expert 1 | RuleChunk:21 | ✓ |
Компоненты: 1 двустороннее поле... | Expert 1 | RuleChunk:22 | ✓ |
Setup step 1: Положите поле... | Expert 1 | RuleChunk:23 | ✓ |
Каждый раунд — это новый месяц (1-12 месяцы по календарю) | Expert 1 | RuleChunk:23 | ⚠️ CONFLICT: Expert 2 указывает 13 раундов (12 месяцев + Праздник Нового года) RuleChunk:38 |
Setup step 9: Каждый игрок берёт... 8 монет | Expert 1 | RuleChunk:24 | ✓ |
шкала огнеопасности 🔥 | Expert 1 | RuleChunk:24 | ⚠️ CONFLICT: Expert 2 называет шкалу огнеупорности RuleChunk:38 |
13 раундов: 12 месяцев + 13-й Праздник Нового года | Expert 2 | RuleChunk:38 | ✓ |
Каждый месяц: 3 фазы A (Ikizama по огнеупорности), B (доход/найм, движение/сделки, забрать Ikizama), C (события) | Expert 2 | RuleChunk:38 | ✓ |
Действие Доход: 4 монеты | Expert 2 | RuleChunk:41 | ✓ |
Действие Найм: стоимость карты, Kobun, доплата за место | Expert 2 | RuleChunk:41 | ✓ |
Движение Oyakata на Ikizama делений + сандалии, круг - опыт Kobun | Expert 2 | RuleChunk:42 | ✓ |
Деньплаты конец 3,6,9,12: зарплата всех персонажей, активных и retired | Expert 2 | RuleChunk:31 | ✓ |
Зарплата по уровню Kobun, retired - max/upper right | Expert 2 | RuleChunk:47 | ✓ |
Конец после 13 раунда Праздник Нового года | Expert 3 | RuleChunk:52,53 | ✓ |
Финальный подсчёт: 1. шкала Ики во время игры; 2. типы персонажей (1:1,2:4,3:9,4:16,5:25); 3. рыба; 4. табак (+x2 с трубкой); 5. здания; 6. ресурсы | Expert 3 | RuleChunk:53 | ✓ |
Ничья: по огнеупорности, затем позиции в стопке | Expert 3 | RuleChunk:53 | ⚠️ CONFLICT: терминология огнеупорности (Expert 2/3) vs огнеопасности (Expert 1) |
Ikizama (RuleChunk:38) | Expert 2 | RuleChunk:38 | ✓ |
Kobun (RuleChunk:41) | Expert 2 | RuleChunk:41 | ✓ |
Nagaya (RuleChunk:42) | Expert 2 | RuleChunk:42 | ✓ |
Oyakata (RuleChunk:40) | Expert 2 | RuleChunk:40 | ✓ |
Описание
Концепция игры
Iki — игра о мастерах эпохи Эдо. Погрузитесь в мир Японии 1603–1868 гг. В районе Нихонбаси находится самый оживлённый рынок города. На главной улице района расположились самые разные лавки и торговцы, приглашающие покупателей заглянуть к ним на огонь. Неподалёку от главной улицы находится рынок моропродуктов, где рыбаки сбивают свой улов — оттуда слышен голос покупателей и торгующегося рыбой. Ремесленники и мастера живут в домах-нагая вокруг главной улицы. Их умения и товары помогают простым людям в Эдо жить лучше. Говорят, что в те времена в Эдо было от семисот до восемисот разных профессий, и многие из этих уникальных умений стали картами в игре Iki: мастера по изготовлению вееров, фонарей, татами, художники укиё-э, уличные торговцы, владельцы прилавков с суш и темпурой, гейши и актёры театра Кабуки. [RuleChunk:20]
Подготовка к игре
Положите поле на середину стола той стороной, которая соответствует количеству игроков. [RuleChunk:23]
Положите маркер календаря на первое деление календаря: это первый месяц года. Каждый раунд — это новый месяц. В традиционном японском календаре 1, 2 и 3 месяц — весенние, 4, 5 и 6 — летние, 7, 8 и 9 — осенние, а 10, 11 и 12 — зимние. Традиционное деление на календаре — Праздник Нового года. [RuleChunk:23]
Положите на конец шкалы Ики количество жетонов «30/60 Ики», равное количеству игроков. [RuleChunk:23]
Перетасуйте 4 жетона пожара, сложите их в стопку лицевой стороной вниз на специальное место на поле. [RuleChunk:23]
Расортируйте жетоны рыбы, табак и трубок по сезонам (🌸 весна, ☀️ лето, 🍂 осень, ❄️ зима). Затем разложите все весенние жетоны лицевой стороной вверх на их место на поле. [RuleChunk:23] 5a. Возьмите летние, осенние и зимние жетоны и положите их лицевой стороной вниз на границы сезонов в календарь. [RuleChunk:23]
Случайным образом выберите 6 карт зданий 🏠 из 10 и разложите их рядом с полем. Уберите оставшиеся карты зданий в коробку: они вам не понадобятся. [RuleChunk:23]
Разложите монеты, сандалии, мешки с рисом, дрёвесину, Кобан и особые жетоны рядом с игровым полем: это ваш запас. [RuleChunk:23]
Расортируйте карты персонажей по сезонам (🌸 весна, ☀️ лето, 🍂 осень, ❄️ зима) — получитесь 4 стопки карт. Положите их рядом с полем. [RuleChunk:23]
Каждый игрок выбирает свой цвет и берёт следующие элементы в этом цвете: 1 планшет игрока (положив его той стороной вверх, которая соответствует количеству игроков); 1 фигурку Икизамы 👱🦱, 1 фигурку Оякаты 👱🦰, 4 фигурки Кобун 👱🦳, 2 маркера для подсчёта очков Ики и шкалы огнеопасности. Затем каждый игрок берёт 1 жетон сандалий, 1 мешок риса и 8 монет (валюты, использовавшейся в эпоху Эдо). Поставьте жетон сандалий, фигурку Икизамы и 4 фигурки Кобун на ваш планшет игрока. Оставшиеся элементы положите рядом. [RuleChunk:24]
Тот, кто последним был в Японии, будет первым игроком в первом раунде. Если никто из игроков никогда не был в Японии, выберите первого игрока случайным образом. Каждый игрок ставит свою фигурку Оякаты на обводнённую пунктирной линией место в стартовой области. [RuleChunk:24]
Каждый игрок кладёт свой маркер на деление «0» шкалы огнеопасности 🔥. Маркер первого игрока лежит наверху, под ним маркер игрока, сидящего слева от первого, и так далее по часовой стрелке, пока маркер последнего игрока не окажется в самом низу стопки. [RuleChunk:24]
Каждый игрок кладёт свой маркер для подсчёта очков Ики 🌸 на деление «0» шкалы Ики. Если игрок получит больше 30 (или 60) очков Ики, то он забирает (или переворачивает) жетон «30/60 Ики». Ваши очки Ики не могут уйти в минус. [RuleChunk:24]
Возьмите 4 карты стартовых персонажей 🎭 и положите их рядом с игровым полем [RuleChunk:24] 13a; затем каждый игрок, двигаясь против часовой стрелки, выбирает карту, а потом кладёт её в одну из четырёх дальних комнат Нагая (рядом с границами поля) на свой выбор [RuleChunk:24] 13b. Также игрок ставит фигурку Кобун на первый уровень опыт, нарисованный вниз карты. В партии на 2 или 3 игроков уберите оставшиеся стартовые карты в коробку: они вам не понадобятся. [RuleChunk:24]
Основные элементы игрового процесса
В игре Iki 13 раундов: первые 12 раундов — это 12 месяцев года (которые поделены на 4 сезона) и разыгрываются одинаково [RuleChunk:38]. 13 раунд — это Праздник Нового года, он разыгрывается иначе [RuleChunk:38]. Каждый месяц поделён на 3 фазы, кратко изложенные на планшетах игроков [RuleChunk:38]. Фаза A «Образ жизни»: Игроки занимают позиции на шкале Ikizama по очереди, определённой жетонами на шкале огнеупорности [RuleChunk:38]. Фаза B «Действия»: Игроки 1) получают доход или нанимают персонажа, 2) двигают своего Oyakata и заключают сделки на главной улице, а потом 3) забирают свою фигурку Ikizama обратно [RuleChunk:38]. Фаза C «Событие»: Зависит от месяца: В конце месяцев 3, 6, 9 и 12 наступает деньплаты (т.е. в конце каждого сезона) [RuleChunk:38]. В конце месяцев 5, 8 и 11 начинается пожар [RuleChunk:38]. Во время фаз B и C у игроков есть возможность увеличить свои показатели на шкале очков Iki [RuleChunk:38].
Доход или найм
Выберите одно действие: Доход: возьмите 4 монета из запаса [RuleChunk:41]. Найм: выберите свободную карту персонажа рядом с полем и заплатите указанную (в монетах) в левом верхнем углу карты стоимость [RuleChunk:41]. Поместите карту персонажа в пустую комнату на поле и поставьте на нижний уровень опыт карты одного из ваших свободных Kobun [RuleChunk:41]. Помните, что за некоторые места на поле нужно заплатить дополнительно (1 или 2 монета) [RuleChunk:41]. Если на карте персонажа, которого вы нанимаете, есть 1 или 2 монета, возьмите их: вы можете оплатить ими стоимость карты [RuleChunk:41]. ВАЖНО: если у вас не хватает монет или нет свободных Kobun, или если на поле не останется свободного места, вы не можете нанять персонажа — вы обязаны взять доход [RuleChunk:41].
Движение и сделки
Разыграйте эту фазу в следующем порядке: Движение: Передвиньте вашего Oyakata на главной улице (по стрелочкам) на то количество делений, которое указывает ваша фигурка Ikizama (см. фазу A выше) [RuleChunk:42]. Вы можете увеличить число делений вашего Oyakata, потратив одну пару сандалий (1 жетон) за каждый дополнительный шаг [RuleChunk:42]. Сандалии не могут уменьшить количество делений, только увеличить его [RuleChunk:42]. Потраченные сандалии возвращаются в запас [RuleChunk:42]. Ваш Oyakata может остановиться на делении с одним или несколькими Oyakata ваших соперников [RuleChunk:42]. ВАЖНО: Когда ваш Oyakata проходит 🧑🦰, завершите круг по главной улице, увеличив уровень опыта на 1 у всех ваших персонажей: передвиньте всех ваших Kobun на 1 деление вверх [RuleChunk:42]. Если кто-то из персонажей должен уйти на покой, заберите Kobun, уберите карту из Nagaya и поместите её в соответствующую колоду на вашем планшете игрока [RuleChunk:42]. Этот шаг разыгрывается во время движения вашего Oyakata, но перед заключением сделки [RuleChunk:42]. Заключение сделки: Как только ваш Oyakata закончил движение, вы можете заключить сделку в лавке, рядом с которой вы остановились и/или с одним из двух персонажей, которые живут в комнатах над этим магазином, если такие есть [RuleChunk:42]. Вы можете заключить сделку в любом порядке: с магазином и одной из карт, или можете вообще ничего не купить [RuleChunk:42]. Если вы заключаете сделку с персонажем другого игрока, этот персонаж увеличивает уровень опыта: после заключения сделки его Kobun двигается на одно деление вверх [RuleChunk:42]. Если персонаж уходит на покой, то его владелец забирает своего Kobun обратно, и он сразу же становится свободным [RuleChunk:42]. ВАЖНО: если вы заключаете сделку с одним из ваших персонажей, его уровень опыта не увеличивается [RuleChunk:42].
Конец хода
Как только ход заканчивается, игроки забирают свои фигурки Ikizama и ставят ��х на свои планшеты [RuleChunk:43].
Основные механики
Деньплаты
В конце каждого сезона (в конце 3, 6, 9 и 12 месяцев) происходит деньплаты: каждый игрок собирает зарплату всех своих персонажей (как ушедших на покой, так и активных) [RuleChunk:31]. Для персонажей на поле значение зарплаты находится прямо над фигуркой Kobun [RuleChunk:31]. Ушедшие на покой персонажи приносят зарплату, указанную в правом верхнем углу карты [RuleChunk:31]. ВАЖНО: Некоторые персонажи, чья зарплата указана в красном кружке, приносят игрокам особые жетоны, но только когда этот персонаж уходит на покой (см. Приложение 1) [RuleChunk:31].
Фазы и процедуры между ходами
Фаза C «Событие» зависит от месяца [RuleChunk:38]. Деньплаты (продолжение). Затем разыграйте эти шаги в следующем порядке: 1) Зарплата: Игроки забирают зарплату со своих карт персонажей. Она определяется их уровнем опыта, а также со всех ушедших на покой персонажей [RuleChunk:47]. Зарплата может быть в монетах, очках Iki или ресурсах [RuleChunk:47]. Зарплата ушедших на покой персонажей — всегда наибольшая возможная зарплата этой карты [RuleChunk:47].
Используемые термины
Ikizama (RuleChunk:38)
Kobun (RuleChunk:41)
Nagaya (RuleChunk:42)
Oyakata (RuleChunk:40)
Завершение игры и победа
Условия окончания игры
Конец игры и подсчёт очков. Как только раунд Праздника Нового года окончен, игра заканчивается: игроки забирают свои карты персонажей, которые остались на поле, и кладут их в соответствующие колонки на планшетах игроков. [RuleChunk:53]
Игра заканчивается после завершения хода последнего игрока в 13 раунде: Праздник Но��ого года. [RuleChunk:52]
Финальный подсчёт очков
Запишите в таблицу полученные в ходе игры очки Ики (шкала очков Ики). [RuleChunk:53]
Разные типы персонажей: в зависимости от того, сколько типов персонажей вы наняли, получите очки Ики. ВАЖНО: Здания не являются отдельным типом персонажей. При уходе на покой Куровод может стать зданием или персонажем: определите, к какому типу он отнесётся, перед подсчётом очков за типы карт. 1 тип: 1 очко Ики, 2 типа: 4 очка Ики, 3 типа: 9 очков Ики, 4 типа: 16 очков Ики, 5 типов: 25 очков Ики. [RuleChunk:53]
Рыба: В зависимости от того, сколько рыбы разных сезонов вы купили, получите следующие очки: 1 рыба: 3 очка Ики, 2 рыбы: 6 очков Ики, 3 рыбы: 10 очков Ики, 4 рыбы: 15 очков Ики. Если вы купили самую дорогую рыбу сезона, добавьте её стоимость к полученному результату (1 или 7 очков Ики за каждую). [RuleChunk:53]
Табак: Посчитайте количество очков, указанное на ваших жетонах табака. Если у вас есть хотя бы одна трубка, сумма очков Ики на жетонах табака удваивается. Подробнее об эффектах трубок и табака можно прочитать в Приложении 3. [RuleChunk:53]
Здания: Добавьте очки Ики за ваши здания. Подробнее о том, как считать очки Ики за здания можно прочитать в Приложении 2. [RuleChunk:53]
Ресурсы: Очки Ики можно получить за следующие ресурсы: Кобан: 3 очка Ики, Заготовка дровесины: 1 очко Ики, 4 монета: 1 очко Ики. Рис и сандалии ничего не приносят. [RuleChunk:53]
Игрок с наибольшим количеством очков Ики побеждает. [RuleChunk:53]
[Translated Example]: Пример: Анна накопила 28 очков Ики во время игры = 28 очков Ики. 1. Она наняла три разных типа персонажей, но благодаря Кукуводам у неё получается 4 разных сезона = 16 очков Ики... (полный пример из источника суммирует до 100 очков Ики). [RuleChunk:53]
Разрешение ничьих
Игрок с наибольшим количеством очков Ики побеждает. В случае ничьей побеждает игрок с наибольшей огнеупорностью. Если ничья сохраняется, побеждает игрок, маркер которого стоит выше в стопке огнеупорности. [RuleChunk:53]
Дополнительные правила и уточнения
Прочая информация
Условия победы (из Expert 1)
Цель игры — прожить в Эдо целый год и попытаться стать лучшим Эдокко, «дитя Эдо», создавая условия для процветания и благополучия города и его жителей. Побеждает игрок с наибольшим количеством очков Ики, отражающих философию концепцию эпохи Эдо о том, каким должен быть идеальный образ жизни обычного человека. Очки Ики, полученные во время игры, обозначены 🌸. Очки Ики, полученные в конце игры, обозначены 🍁. [RuleChunk:21]
Компоненты (из Expert 1)
Игровое поле и планшеты
1 двустороннее поле и 4 двусторонних планшета игрока (для 2 игроков и 3-4 игроков). [RuleChunk:22]
Карты
4 стартовых карты персонажей, 56 карт персонажей (14 карт для каждого сезона): Весна 🌸, Лето ☀️, Осень 🍂, Зима ❄️. [RuleChunk:22]
Деревянные элементы
4 фигурки Ояката 👱🦰 (фиолетовая, синяя, жёлтая, красная), 4 фигурки Икизамы 👱🦱 (фиолетовая, синяя, жёлтая, красная), 16 фигурок Кобун 👱🦳 (по 4 фиолетовых, 4 синих, 4 жёлтых, 4 красных), 4 маркера для подсчёта очков Ики 🌸 (по одному цвету: розовый, фиолетовый, жёлтый, красный), 4 маркера огнеопасности 🔥 (по одному цвету: розовый, фиолетовый, жёлтый, красный). [RuleChunk:22]
Жетоны и монеты
20 заготовок дрёвесины 🪵, 30 мешков риса 🍚, 1 маркер календаря 📅, 8 жетонов трубок (по 2 на каждый сезон) 🚬, 8 жетонов табака (по 2 на каждый сезон) 🌿, 36 монет номиналом 1 мон 🪙, 20 монет номиналом 4 мон 🪙, 28 жетонов сандалий 👡, 12 жетонов Кобан (золотых слитков) 🏛️, 8 жетонов рыбы (по 2 на каждый сезон) 🐟, 4 жетона «30/60» очков Ики (для каждого игрока) 🌸, 4 жетона пожара 🔥. [RuleChunk:22]
Остальное
Джокер 🎰, Найм-1 🎼, 1 блокнот для подсчёта очков 📒, 1 правила игры 📘. Дуэльный вариант: 1 жетон Солнца ☀️ и 1 жетон Луны 🌙, 12 блоков 🧱. [RuleChunk:22]
Конфликты между экспертами
CONFLICT DETECTED:
Expert 1: "Каждый раунд — это новый месяц" (импликация 12 раундов по календарю 1-12) [RuleChunk:23]
Expert 2: "13 раундов: первые 12 раундов — это 12 месяцев... 13 раунд — Праздник Нового года" [RuleChunk:38]
TOPIC: Количество раундов / продолжительность игры
CONFLICT DETECTED:
Expert 1: "шкала огнеопасности 🔥" [RuleChunk:24]
Expert 2: "шкала огнеупорности" [RuleChunk:38]
Expert 3: "огнеупорностью" [RuleChunk:53]
TOPIC: Название шкалы огнеопасности/огнеупорности
Какие модели я использовал
Я пробовал использовать разные модели, как opensource и localhost так и проприетарные большие модели. Вот что я попробовал:
Kimi k2 — Я считаю, что это хорошие большие модели. И я часто пользуюсь ими вместо ChatGpt, но в данной задаче задержка модели оказалось большой и модель также не всегда вызывала инструменты.
Deepseek v3.2 — Хорошая модель и дает неплохие результаты при достаточно небольшой стоимости, но задержка и кол-во расходуемых токенов перечеркивает все её плюсы. Я мог ждать по 10 минут на генерацию ответа и она могла не закончится.
OpenAi GPT 4.1 — Дает хорошие результаты и работает очень хорошо, но стоимость запросов достаточно высокая.
Grok 4 Fast / 4.1 Fast — Модель на которой я остановился. Она дает неплохие результаты, маленькую задержку и небольшую стоимость. В среднем на запрос с запуском 3 агентов уходит по 1.5-3 минуты. Стоимость запроса на полную сводку около $0.05.
Gpt oss — Не всегда работает стабильно. Я оставил её только для проверки безопасности.
Сервис 2. MCP Tools. Fastmcp, objectbox, sentencetransformers.
Данный сервис отвечает за mcp server у которого находятся необходимые инструменты. Я сделал его базовую реализацию, но также расскажу о своей идеи насчет того, как можно сделать mcp сервер по-другому. Я ещё не успел попробовать этот подход, но буду его пробовать и расскажу о результатах этого эксперимента в следующих статьях.
Сама реализация:
"""
Test MCP server for tabletop game rules and terminology search.
This module sets up an MCP (Model Context Protocol) server using FastMCP
to provide tools for searching game rules, terminology, and game data
using vector similarity search powered by ObjectBox and SentenceTransformers.
"""
from typing import Annotated
import yaml
from fastmcp import FastMCP
from objectbox import (
Box,
Entity,
Float32Vector,
HnswIndex,
Id,
Int16,
Store,
String,
)
from pydantic import Field
from sentence_transformers import SentenceTransformer
# ------------------------------------------------------------------
# ObjectBox entities
# ------------------------------------------------------------------
@Entity()
class Rule:
id = Id # Unique identifier for the rule
internal_id = String # Internal unique ID (e.g., from chunking process)
content = String # Full text content of the rule
section = String # Rulebook section (e.g., "movement", "combat")
game = String # Associated game name
req_term = String # YAML string of required terminology terms (list serialized)
scenario = String # Enriched searchable text: tags (#section, #type) + "---" + content; this is encoded for vector search
priority = Int16 # Priority level for rule application
zone = String # Rule zone (base/advanced/edge)
vector = Float32Vector(index=HnswIndex(dimensions=768)) # 768-dim vector embedding for similarity search (encoded from scenario field)
@Entity()
class Terminology:
id = Id # Unique identifier
internal_id = String # Internal unique ID
content = String # Enriched searchable text: tags (#group) + "---" + name; this is encoded for vector search
name = String # Display name of the term
game = String # Associated game name
slug = String # URL-friendly identifier
kind = String # Type: "TERM" for definitions, "ENTITY" for named entities
path = String # Path or location in the documentation
group = String # Category or group for the term
definition = String # Definition text
extra = String # YAML string of additional metadata
vector = Float32Vector(index=HnswIndex(dimensions=768)) # 768-dim vector embedding for similarity search (encoded from content field)
@Entity()
class Game:
id = Id # Unique identifier
name = String # Game name in native script
latin_name = String # Game name in Latin script
vector = Float32Vector(
index=HnswIndex(dimensions=768)
) # 768-dim vector embedding for similarity search (encoded from name field)
# ------------------------------------------------------------------
# Global setup
# ------------------------------------------------------------------
COUNT_ITEMS = 3 # Number of nearest neighbors to retrieve in searches
server = FastMCP(name="rules-mcp") # Initialize FastMCP server for MCP protocol
store = Store(directory="./db") # Open ObjectBox database store in ./db directory
rules_box = Box(store, entity=Rule) # Box for storing and querying Rule entities
terminology_box = Box(store, entity=Terminology) # Box for storing Terminology entities
game_box = Box(store, entity=Game) # Box for storing Game entities
model = SentenceTransformer("./model") # Load pre-trained sentence transformer model for encoding text to vectors
# ------------------------------------------------------------------
# Tools – parameters described inline with Annotated[…, Field(…)]
# ------------------------------------------------------------------
@server.tool
def find_games(
query: Annotated[str, Field(...)]
) -> str:
"""
Search for games using vector similarity.
Parameters
----------
query : str
Natural-language search query (can be empty).
"""
vector = model.encode(query)
obx_query = game_box.query(
Game.vector.nearest_neighbor(vector, element_count=COUNT_ITEMS)
).build()
results = []
for id_, score in obx_query.find_ids_with_scores():
g = game_box.get(id_)
results.append(
{
"id": g.id,
"name_db": g.name,
"latin_name": g.latin_name,
"score": score,
}
)
return yaml.safe_dump(results, allow_unicode=True)
@server.tool
def get_toc(db_game_name: str) -> str:
"""
Get table of contents for a specific game by listing all rule sections and scenarios.
"""
rules_query = rules_box.query(
Rule.game.equals(db_game_name)
).build()
result = []
for res in rules_query.find():
result.append({
"section": res.section,
"scenario": res.scenario
})
return yaml.safe_dump(result, allow_unicode=True)
@server.tool
def find_in_rulebook(
db_game_name: Annotated[str, Field(...)],
section: Annotated[str, Field(...)],
type_: Annotated[str, Field(...)],
query: Annotated[str, Field(...)],
# zone: Annotated[Literal["base", "advanced", "edge"], Field(...)] = "base",
) -> str:
"""
Search for rules in the rulebook using vector similarity.
Parameters
----------
db_game_name : str
Name of the game (required).
section : str
Section of the rulebook;.
type_ : str
Entity type to search (rule/action/setup/components/etc).
query : str
Natural-language search query (can be empty).
zone : {'base','advanced','edge'}
Rule zone; defaults to 'base'.
"""
# Construct enriched search query by adding metadata tags
search_query = f"#section:{section} #type:{type_}\n---\n{query}"
# Encode the search query to a vector for similarity search
vector = model.encode(search_query)
# Build ObjectBox query to find nearest neighbors, filtered by game name
obx_query = rules_box.query(
Rule.vector.nearest_neighbor(vector, element_count=COUNT_ITEMS)
& Rule.game.equals(db_game_name)
).build()
results = []
for id_, score in obx_query.find_ids_with_scores():
r = rules_box.get(id_)
results.append(
{
"id": r.id,
"content": r.content,
"score": score,
"section": r.section,
"req_term": yaml.safe_load(r.req_term),
"scenario": r.scenario,
}
)
return yaml.safe_dump(results, allow_unicode=True)
@server.tool
def find_in_terminology(
db_game_name: Annotated[str, Field(...)],
group: Annotated[str, Field(...)] = "default",
query: Annotated[str, Field(...)] = "",
) -> str:
"""
Search for terminology entries (kind=TERM) using vector similarity.
Parameters
----------
game_name : str
Name of the game (required).
group : str
Terminology group; defaults to 'default'.
query : str
Text query for semantic term search.
"""
search_query = f"#game:{db_game_name} #group:{group}\n---\n{query}"
vector = model.encode(search_query)
obx_query = terminology_box.query(
Terminology.vector.nearest_neighbor(vector, element_count=COUNT_ITEMS)
& Terminology.kind.equals("TERM")
& Terminology.game.equals(db_game_name)
).build()
results = []
for id_, score in obx_query.find_ids_with_scores():
t = terminology_box.get(id_)
results.append(
{
"id": t.internal_id,
"score": score,
"content": t.content,
"name": t.name,
"definition": t.definition,
"extra": yaml.safe_load(t.extra),
}
)
return yaml.safe_dump(results, allow_unicode=True)
@server.tool
def find_in_terminology_ner(
db_game_name: Annotated[str, Field(...)],
group: Annotated[str, Field(...)] = "default",
query: Annotated[str, Field(...)] = "",
) -> str:
"""
Search for entity-level terminology (kind=ENTITY) using vector similarity.
Parameters
----------
game_name : str
Name of the game (required).
group : str
Terminology group; defaults to 'default'.
query : str
Text query for semantic term search.
"""
search_query = f"#group:{group}\n---\n{query}"
vector = model.encode(search_query)
obx_query = terminology_box.query(
Terminology.vector.nearest_neighbor(vector, element_count=COUNT_ITEMS)
& Terminology.kind.equals("ENTITY")
& Terminology.game.equals(db_game_name)
).build()
results = []
for id_, score in obx_query.find_ids_with_scores():
t = terminology_box.get(id_)
results.append(
{
"score": score,
"content": t.content,
"name": t.name,
"group": t.group,
"definition": t.definition,
"extra": yaml.safe_load(t.extra),
}
)
return yaml.safe_dump(results, allow_unicode=True)
# ------------------------------------------------------------------
# Run the MCP server
# ------------------------------------------------------------------
if __name__ == "__main__":
# Start the FastMCP server on HTTP interface at port 8001
server.run("http", port=8001)
В данной реализации нет ничего особенного, мы используем objectbox store и три box для каждого схемы. Sentence transformers используется только для вычисления embedding'ов. А FastMCP 2.0 используется для создания и управления самого mcp сервера.
В чем здесь есть проблема и почему меня не устраивает эта базовая реализация?
Первое. Все методы синхронные и в результате этого дольше выполняются запросы. Это проблема, так как у нас эксперты работают параллельно и также параллельно используют инструменты.
Второе, выходящие из первого. Если мы захотим сделать функции асинхронными, то мы столкнемся с проблемой, так как python версия objectbox не поддерживает её. Например, C++ версия поддерживает многопоточность и асинхронность. Sentence transformers тоже не поддерживает её. Из всех используемых библиотек только FastMCP 2.0 поддерживает её из коробки.
Третье. Мне не нравится большое кол-во используемых глобальных переменных.
Это всё можно решить, но потребуется немного времени на это и, возможно, решение будет выглядеть не очень хорошо. И сейчас я поделюсь своим виденьем того, как можно решить эти проблемы немного по-другому.
Другой подход к созданию mcp серверов
Обычно реализация инструментов для mcp серверов находится в том же пространстве/процессе, что и сам mcp сервер. Вот что я подразумеваю под этой фразой. Например, если мы посмотрим на базовую реализацию mcp сервера, которая находится выше, то у нас есть набор функций, каждая из которых реализует некоторую логику. Она может быть сложной или достаточно простой, но она находится в том же месте, где у нас создается сервер. В части случаев при достаточной простой логике, этого хватит и мы не получим каких-то проблем при реализации данной схемы. Но тут есть одно "НО". Чем сложнее логика тем сложнее это всё отладить и скорость выполнения каждой функции может становится всё больше и больше, а следовательно . А когда мы делаем MCP сервер, то хотим, чтобы скорость выполнения каждого инструмента была минимально возможной. Здесь и появляется микросервисный подход. Нам не запрещают вынести логику каждого инструмента в отдельный сервис.
Fat-server (монолит)
Thin-server (микросервисы)
И именно к такой архитектуре я хочу прийти в следующей статье. Stay tuned!
Сервис 3. OpenAi Сервер.BlackSheep
Я не backend и web developer, поэтому не буду сильно погружаться в этот сервис, а расскажу только про мой опыт работы с Blacksheep. Если говорить коротко, то мне понравилось и я не испытал особых трудностей с написанием данного сервиса. Очень удобно, что есть blacksheep-cli , который помогает сконфигурировать изначальный проект. Мне понравился объектно-ориентированный подход, которого придерживается данная библиотека. Также хочется отметить Rodi — это библиотека для dependency injection, которая помогла добавить llm_service как зависимость в класс контроллера. А сам проект состоит из двух контроллеров: chat и models. Один из них нужен для того, чтобы генерировать наш completions, а другой для того, чтобы отдать список моделей. Вот и все задачи для данного сервиса, он вышел простым и небольшим.
Заключение
Вот мы и прошлись по всем сервисам в данном проекте. И в качестве завершения мне хочется рассказать о некоторых проблемах, которые мне не удалось решить, а также сделать заключения для кого же данный стек и данные библиотеки могут быть полезными.
Мои боли
Без сложностей в данном проекте не обошлось, часть из них связана с библиотеками, с отсутствием каких-то готовых инструментов, с парсингом документации и с самой областью. Пройдемся по ним
1.Langfuse — начну с того, что он мне нравится своими возможностями по отслеживанию запусков модели и всего того, что происходит внутри. Это действительно классный и гибкий инструмент. Но не всё так хорошо, как хотелось бы. И здесь я говорю о том, что для pocketflow нет официальной интеграции этого инструмента. И здесь приходится делать что-то своё. У меня ушло несколько попыток, чтобы сделать подробное логирование каждой ступени туда. Например так у меня выглядит отображаемый граф:

Сложно и запутано. И я так и не пришел к какому-то финальному решению этой проблемы. Вышло слишком детально. Возможно когда-нибудь появится официальная обертка для pocketflow, но пока приходится делать всё ручками.
2.Парсинг и наполнение RAG.
Это тоже большая проблема. Правила настольных игр временами очень сложные в плане их расположения и текста и правильный автоматический парсинг таких документов — это целая отдельная статья. Например, так выглядит одна из страниц для игры "Iki"

Здесь используется много не стандартных символов, много картинок, на каждой странице текст идет по-разному. Это всё не очень большая проблема, когда мы их читаем, но как всё это отобразить в тексте? Стандартные ocr здесь справляются не очень хорошо. Лично я использовал vlm для парсинга каждой страницы, а именно Qwen. Но ведь даже после того, как мы получили наш итоговый markdown файл, появляется новая сложность — это превращения всего этого в чанки. Как правильно разрезать это всё? Я так и не нашел ответ, лично я использовал llm (Kimi + ChatGpt + Qwen), чтобы разбить весь текст на чанки. И я не могу сказать, что это способ справился идеально, много данных просто теряется. Поэтому данный вопрос для меня ещё остается открытым. Но могу сказать, что когда я найду решение этой проблемы, то постараюсь об этом рассказать. Это будет отдельная статья о методах парсинга и методах чанкирования.
3.Pocketflow и отсутствие инструментов.
Pocketflow предоставляет сам по себе только граф и абстрактные ноды. Это как и плюс, так и большой минус для этого проекта. Я использовал mcp сервер для работы с инструментами, но мне пришлось также писать свои собственные ноды, чтобы подключить все инструменты к моделям. А это было в некотором плане сложно и заняло много времени. Плюс не раз возникали сложности с тем, чтобы это все отладить. Поэтому при первом использовании данной библиотеки потребуется проинвестировать много времени.
Так для кого же данный стек?
Данный стек может пригодится, например при разработке каких-то локальных решений с минимальными зависимостями. В условиях жестких ограничений. А ещё этот стек в теории можно использовать и с другими языками программирования. Pocketflow существует на разных языках, например, для того же C++. Да, тут есть ограничения в том, что оно все ещё сырое, там нет поддержки батчев, мнгопоточности и асинхронности, а логирование придется изобретать заново, но оно существует.
Ссылки
В комментариях пишите о своем опыте автоматизации настольных игр, будет интересно почитать 🤗
