Демократизация AI-кодинга: почему мы построили C#-слой синтеза для лёгких локальных моделей
Проблема, о которой молчат евангелисты AI
Индустрия любит рассказывать про GPT-4o и Claude, которые пишут идеальный код по одному промпту. Но большинство инди-разработчиков не сидят на подписках за $20-200 в месяц и не гоняют агентов через облачные API с оплатой за токен. У них есть RTX 3050 Ti с 4 ГБ VRAM, желание автоматизировать рутину в Unity Editor — и ноль бюджета на эксперименты.
Мы в MLLM Studio решили эту задачу не через "подождём, пока модели станут умнее", а через инженерию на стороне клиента. Результат — Ather, локальный AI-агент для Unity, который работает на моделях типа Qwen 2.5 Coder 3B или Llama 3.2 3B и при этом выдаёт стабильный, компилируемый C#-код внутри редактора. Без единого цента на API-ключи.
Это статья о том, как мы это сделали.
Почему маленькие модели "ломаются" именно в Unity
3B-параметрические модели — это не urезанные GPT-4. Это принципиально другой класс инструментов: быстрые, бесплатные, работающие офлайн на слабом железе, но статистически менее устойчивые к формату вывода. В обычном чат-боте это незаметно. В Unity Editor — фатально.
Мы выделили два системных паттерна отказа, которые не лечатся "более хорошим промптом":
1. Escape-мутация JSON-полезной нагрузки.
Малые модели регулярно "перестраховываются" и экранируют управляющие символы там, где это не нужно. < и > превращаются в \u003c и \u003e, кавычки дублируются, генерик-типы C# (List<Vector3>) превращаются в JSON-калеку. Для человека это забавная деталь. Для компилятора Roslyn — синтаксическая ошибка, которая останавливает весь пайплайн генерации.
Малые модели регулярно "перестраховываются" и экранируют управляющие символы там, где это не нужно. < и > превращаются в \u003c и \u003e, кавычки дублируются, генерик-типы C# (List<Vector3>) превращаются в JSON-калеку. Для человека это забавная деталь. Для компилятора Roslyn — синтаксическая ошибка, которая останавливает весь пайплайн генерации.
2. Смерть в момент Domain Reload.
Unity Editor пересобирает домен приложения при каждой компиляции C#-скриптов. Это выгружает всё состояние в памяти — включая контекст выполняющегося AI-агента. Если агент писал код, ждал ответа модели или выполнял tool-call именно в момент, когда Unity триггерит recompile — процесс просто обрывается. Агент "забывает", что он делал, и пользователь получает зависший интерфейс без объяснений.
Unity Editor пересобирает домен приложения при каждой компиляции C#-скриптов. Это выгружает всё состояние в памяти — включая контекст выполняющегося AI-агента. Если агент писал код, ждал ответа модели или выполнял tool-call именно в момент, когда Unity триггерит recompile — процесс просто обрывается. Агент "забывает", что он делал, и пользователь получает зависший интерфейс без объяснений.
Больше модель, больше VRAM — не решает ни одну из этих двух проблем. Это не про интеллект модели. Это про архитектуру среды, в которой эта модель работает.
Решение: C#-слой синтеза, а не более умный промпт
Наш подход был осознанно контрарным. Вместо того чтобы бесконечно "уговаривать" модель через prompt engineering выдавать идеальный формат, мы построили детерминированный C#-слой между выводом LLM и Unity API. Мы назвали его Synthesizer/Sanitizer — по аналогии с тем, как компилятор синтезирует AST из токенов, а не доверяет программисту писать идеальный байт-код руками.
Три ключевых компонента:
Domain Reload Survival
Перед тем как Unity начинает пересборку домена, Ather сериализует полное состояние агента — историю диалога, текущий шаг ReAct-цикла, незавершённые tool-calls — в ScriptableObject, привязанный к AssetDatabase. Это делается через AssemblyReloadEvents.beforeAssemblyReload. После пересборки, в AssemblyReloadEvents.afterAssemblyReload, состояние десериализуется, и агент продолжает работу с того шага, на котором остановился — без потери контекста и без повторного запроса к модели.
Для пользователя это выглядит как магия: он видит, что Unity перекомпилировался, а агент просто... продолжил работать. Для нас это была самая скучная и самая важная часть системы — потому что без неё локальный агент, работающий с кодом, не может существовать в принципе.
The Code Sanitizer
Это лёгкий C#-фильтр, который перехватывает вывод модели до записи файла на диск и применяет детерминированные правила восстановления:
- Разэкранирование Unicode- и HTML-последовательностей (\u003c → <, < → <, и так далее) в контексте, где ожидается валидный C#-синтаксис.
- Нормализация вложенных generic-конструкций, которые модель могла разорвать при экранировании кавычек.
- Валидация через легковесный парсинг Roslyn до физической записи файла — если санитайзер не может гарантировать компилируемость, файл не пишется, а запускается повторный, более узкий запрос к модели с точечным описанием ошибки.
Важно: это не "исправление плохого кода". Санитайзер не трогает логику, не переписывает архитектуру, не принимает решений за модель. Он решает ровно одну задачу — гарантирует, что то, что модель имела в виду, дойдёт до диска в синтаксически валидном виде.
The ReAct Loop Wrapper
Малые модели хуже держат многошаговый формат "Thought → Action → Observation" на длинных контекстах — они склонны "срезать" промежуточные шаги или смешивать рассуждение с финальным ответом. Мы вынесли принуждение к этому формату из промпта на уровень C#: обёртка перехватывает вывод модели после каждого шага, парсит его по строгой грамматике, и если модель нарушила формат — не идёт дальше по пайплайну, а возвращает модели короткий corrective prompt с точным указанием, какой блок отсутствует.
Это превращает "мягкое пожелание" в промпте в жёсткий контракт на уровне кода. Модель может ошибиться сколько угодно раз — пайплайн просто не пропустит невалидный шаг дальше.
Zero-Impact на тяжёлые облачные модели
Закономерный вопрос: не мешает ли этот слой более мощным моделям — Claude 3.5, GPT-4o — которые и так редко ошибаются в формате?
Ответ — нет, и это принципиальный момент архитектуры. Sanitizer и ReAct Wrapper работают по паттерну "проверка без побочных эффектов": если вывод модели уже валиден, все проверки проходят мгновенно и без единой модификации данных. Санитайзер не переписывает корректный код — он просто подтверждает, что переписывать не нужно, и пропускает его дальше.
Для тяжёлых моделей весь слой действует как невидимая страховочная сетка: она не тратит заметные ресурсы, не меняет поведение, не накладывает дополнительных ограничений на creative freedom модели. Она просто есть — на случай редкого сбоя даже у мощной модели (обрыв сети, урезанный ответ по лимиту токенов, редкая галлюцинация формата).
Это осознанное архитектурное решение: мы не строили систему "для слабых моделей с костылями". Мы построили единый пайплайн, который одинаково корректно ведёт себя на всём спектре — от 3B на потребительской видеокарте до топовых облачных API.
Пайплайн целиком
┌──────────────────────────────────────────────────────────────────────┐
│ MLLM STUDIO PROCESSING PIPELINE │
└──────────────────────────────────────────────────────────────────────┘
[ Unity Editor Context ]
│
▼
┌─────────────────┐
│ Prompt Wrapper │ контекст задачи + системная роль +
│ │ ReAct-грамматика (Thought/Action/Obs)
└────────┬─────────┘
│
▼
┌─────────────────────┐
│ Local RAG / TOON │ компрессия релевантного контекста проекта
│ Compression │ (структура сцены, API-сигнатуры, история)
└────────┬─────────────┘
│
▼
┌──────────────────────┐
│ Model Execution │ Qwen2.5-Coder-3B / Llama-3.2-3B
│ (llama.cpp backend) │ ...или Claude 3.5 / GPT-4o (облако)
└────────┬───────────────┘
│
▼
┌──────────────────────┐
│ ReAct Loop Wrapper │──── формат нарушен? ──┐
│ (грамматика C#) │ │
└────────┬────────────────┘ │
│ валиден │
▼ │
┌──────────────────────┐ │
│ Code Sanitizer │──── не компилируется? ─┤
│ (Unicode/HTML fix + │ │
│ Roslyn pre-check) │ │
└────────┬───────────── ┘ │
│ валиден │
▼ │
┌──────────────────────┐ │
│ Domain Reload Guard │ сериализация состояния │
│ (beforeAssemblyReload)│ перед компиляцией │
└────────┬───────────── ┘ │
│ │
▼ │
┌──────────────────────┐ │
│ Unity Compilation │ │
│ (Roslyn / IL2CPP) │ │
└────────┬───────────── ┘ │
│ │
▼ │
┌──────────────────────┐ │
│ afterAssemblyReload │ восстановление состояния│
│ Resume Agent │ агента, продолжение шага│
└────────┬───────────── ┘ │
│ │
▼ │
[ Файл записан, ✔ ] │
│
┌──────────────────────────────────────────┘
▼
┌──────────────────────┐
│ Corrective Prompt │ точечный запрос модели:
│ Loop │ "укажи, что было не так"
└────────┬───────────── ┘
│
└──────────► обратно в Model Execution
Что это значит для инди-разработчиков
Мы не пытаемся утверждать, что 3B-модель "не хуже" GPT-4o — это было бы нечестно. Она делает больше логических ошибок, у неё меньше контекстного окна, она хуже держит сложную многофайловую архитектуру. Но с правильным C#-слоем вокруг неё она становится надёжным инструментом для узкого класса задач: генерация boilerplate-компонентов, рефакторинг по шаблону, автогенерация Editor-утилит, точечные правки по описанию.
Это не история про "AI заменит программиста". Это история про то, что барьер входа в AI-ассистированную разработку — не мощность модели, а качество инженерии вокруг неё. Санитайзер, который стоит нам нескольких сотен строк C#, окупается тем, что делает бесплатную локальную модель практически неотличимой по надёжности от платного облачного API — в тех задачах, где это действительно важно.
Мы продолжаем работу над Ather и рады любому фидбеку от тех, кто пробует запускать локальные модели внутри Unity на своём железе.