Главная / Статьи / Безопасный компилятор контекста для ИИ-агентов на .NET 10

Безопасный компилятор контекста для ИИ-агентов на .NET 10

Собирайте контекст модели в .NET 10 с метками доверия, проверкой свежести, редактированием данных, карантином инъекций, детерминированным бюджетом токенов и тестируемым манифестом SHA-256.

2001 слов

Когда агент ведёт себя плохо в продакшене, обычно винят модель. Но часто вред нанесён раньше, когда приложение склеило инструкции, результаты поиска, память и недоверенный веб-текст в одну привилегированную строку. Эта строка — всё, что модель знает для следующего инференса. Если устаревшая политика побеждает актуальную, загруженная страница несёт инструкции, данные клиента пересекают границу или малоценная история съедает контекстное окно, более хитрый промпт сверху архитектуру не исправит.

Решение — относиться к этому шагу сборки как к настоящему ПО. Каждый вход получает идентичность и уровень доверия. Политики решают, что достаточно свежо и что нужно скрыть. Фиксированный бюджет заставляет делать явный выбор. Результат воспроизводим и сопровождается доказательствами того, что было включено, обрезано, отредактировано, помещено в карантин или отброшено. Ниже эта идея превращается в небольшой сервис на .NET 10.

Почему простой конкатенации мало

Инженерные материалы Anthropic описывают context engineering как курирование всего состояния контекста для каждого инференса, что выходит далеко за пределы шлифовки формулировок одного промпта. Системные инструкции, инструменты, найденные данные, память и история сообщений конкурируют за один конечный ресурс, и большие окна не убирают проблем релевантности и загрязнения.

Безопасность обостряет задачу отбора. В статье OpenAI от марта 2026 года о противодействии prompt injection отмечается, что сильные атаки всё больше похожи на социальную инженерию, поэтому один лишь классификатор входа не является полной защитой. Там же предлагается мыслить источниками и стоками: где проникает недоверенное влияние и до какой опасной возможности оно может добраться. Работа Microsoft Research над PlugMem добавляет угол надёжности: сырой поиск по памяти может завалить агента длинным малоценным контекстом.

Практический вывод: прежде чем данные попадут к модели, им нужны контракт приёма, проверки политик, бюджет и происхождение.

Форма компилятора

TrustedContextCompiler предоставляет один эндпоинт:

POST /v1/context/compile

Запрос несёт задачу, доверенные системные инструкции и список идентифицированных источников, каждый из которых объявляет свой тип, уровень доверия, приоритет и время получения. В ответ приходят собранный контекст, грубая оценка токенов, отпечаток SHA-256 ровно этого текста и манифест с одним решением на каждый источник. Конвейер обработки выглядит так:

flowchart LR
    A[Retrieved sources] --> V[Validate ID and freshness]
    V --> T[Apply trust policy]
    T --> Q[Quarantine suspicious untrusted data]
    T --> R[Redact sensitive values]
    R --> B[Deterministic budget allocator]
    B --> C[Compiled context]
    B --> M[Decision manifest]
    C --> F[SHA-256 fingerprint]

В структуре проекта политика отделена от транспорта:

TrustedContextCompiler/
├── src/TrustedContextCompiler.Core/
├── src/TrustedContextCompiler.Api/
├── tests/TrustedContextCompiler.Tests/
├── samples/compile-request.json
├── docs/verification.md
├── Dockerfile
└── TrustedContextCompiler.slnx

Основная библиотека не зависит ни от какой модели. Это сделано намеренно: политика контекста должна быть детерминированной и тестируемой без трат на токены и ожидания провайдера.

Идентичность и доверие для каждого источника

Анонимные строки не принимаются. Каждый вход типизирован:

public enum TrustLevel
{
    Trusted = 0,
    Internal = 1,
    User = 2,
    Untrusted = 3
}
public sealed record ContextSource(
    string Id,
    string Kind,
    string Content,
    TrustLevel Trust,
    int Priority,
    DateTimeOffset RetrievedAt);

Id связывает вывод с доказательствами поиска. Kind разделяет политики, тикеты, память, веб-страницы и другие типы источников. Priority — ограниченный вход для политики, а RetrievedAt питает решение о свежести.

Метка доверия должна приходить из кода приёма, а не из самого документа, чтобы веб-страница не могла объявить себя доверенной. Строка внутренней базы данных тоже не доверена автоматически. Метка должна описывать, кто может влиять на содержимое и какой путь проверки его породил.

Дубликаты ID отклоняются, как и отсутствующие поля и приоритеты вне диапазона от 0 до 100. Ранний отказ здесь важен: манифест, построенный на неоднозначных идентичностях, даёт ложную уверенность.

Инструкции получают бюджет первыми

Прежде чем рассматривать найденные данные, доверенные инструкции и задача получают выделенную часть бюджета:

var instructionBlock =
    quot;<trusted-instructions>\n" +
    quot;{request.SystemInstructions.Trim()}\n" +
    quot;Task: {request.Task.Trim()}\n" +
    quot;</trusted-instructions>";
var instructionTokens = EstimateTokens(instructionBlock);if (instructionTokens > options.ReservedInstructionTokens)
{
    throw new ArgumentException(
        "Instructions exceed the reserved instruction budget.",
        nameof(request));
}

Это не даёт лавине результатов поиска тихо вытеснить управляющий слой. А ещё не позволяет раздутому системному промпту съесть весь бюджет данных без видимой ошибки конфигурации.

Токены считаются по простой оценке — четыре символа на токен:

public static int EstimateTokens(string value) =>
    Math.Max(1, (value.Length + 3) / 4);

Это не токенизатор уровня биллинга. В продакшене стоит использовать токенизатор выбранной модели и оставлять запас на обёртки сообщений и кодирование конкретного провайдера. Детерминированное приближение всё равно годится для тестирования поведения политик, если его честно так и называть.

Отбрасываем устаревший контекст

Источник может быть корректным и всё же неподходящим для задачи, потому что он старый. Каждый сравнивается с настраиваемым максимальным возрастом:

if (now - source.RetrievedAt >
    TimeSpan.FromMinutes(options.MaxSourceAgeMinutes))
{
    manifest.Add(Record(
        source,
        ContextDecision.Stale,
        0,
        0,
        hash,
        "Source exceeded the freshness policy."));
    continue;
}

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

Важное проектное решение — устаревшие источники остаются в манифесте. Они исчезают из контекста модели, но сохраняются в операционной записи.

Карантин попыток инъекций в недоверенных данных

Через эвристику инъекций проходят только источники Untrusted. Она ищет узкие паттерны: попытки переопределить инструкции с более высоким приоритетом, извлечь секреты или протащить разметку в стиле ролей.

if (source.Trust == TrustLevel.Untrusted &&
    SafetyFilters.Injection().IsMatch(source.Content))
{
    manifest.Add(Record(
        source,
        ContextDecision.Quarantined,
        0,
        0,
        hash,
        "Untrusted content matched an " +
        "instruction-injection heuristic."));
    continue;
}

Совпадение отправляет в карантин весь источник. Вырезать одно предложение было бы хрупко, поэтому компилятор жертвует частью полноты ради решения, которое легко объяснить.

Это регулярное выражение ни в коем случае не "ИИ-файрвол".

Редактирование до того, как модель что-то увидит

Источники, пережившие карантин, преобразуются до распределения бюджета. Пример распознаёт адреса электронной почты, Bearer-токены и значения, похожие на номера платёжных карт:

public static (string Value, int Count) Redact(string value)
{
    var count = 0;
    value = Email().Replace(value, _ =>
    {
        count++;
        return "[EMAIL]";
    });    value = Bearer().Replace(value, _ =>
    {
        count++;
        return "[SECRET]";
    });    value = Card().Replace(value, _ =>
    {
        count++;
        return "[CARD]";
    });    return (value, count);
}

Поскольку редактирование происходит до включения, исходное значение никогда не попадает в скомпилированный контекст. Манифест хранит лишь число замен и хеш SHA-256 исходного источника.

Эти паттерны — примеры, а не полноценная DLP-система. Международным идентификаторам, номерам счетов, специфичным для компании секретам и персональным данным в свободной форме нужны подходящие детекторы. Хеш содержимого к тому же может выдать значение с низкой энтропией, если его угадать, поэтому там, где эта угроза важна, используйте HMAC с защищённым ключом.

Детерминированный бюджет

Кандидаты сортируются по приоритету от высшего, затем по более сильному доверию, затем по ID источника:

foreach (var candidate in candidates
    .OrderByDescending(item => item.Source.Priority)
    .ThenBy(item => item.Source.Trust)
    .ThenBy(item => item.Source.Id, StringComparer.Ordinal))
{
    // Allocate the remaining budget.
}

Разрешение ничьих по ID — не украшение. Без него два результата поиска с одинаковыми оценками могли бы давать разные промпты, ключи кеша и поведение модели в зависимости от того, в каком порядке их вернули база данных или сеть.

У каждого источника есть максимальная квота. Не помещающийся источник обрезается до остатка бюджета, а тот, у которого не помещается даже обёртка, отбрасывается. Оба исхода видны в манифесте. Каждый источник оборачивается своими ID, типом и метаданными доверия:

<source id="ticket-42" kind="ticket" trust="internal">
Contact [EMAIL]. Refund requested: $125.
</source>

Маркеры помогают с разделением и наблюдаемостью, но внутри модели они не криптографическая граница. Модель всё равно может неверно прочитать данные или последовать им, поэтому контроль инструментов должен исходить из того, что некоторые атаки на контекст сработают.

Отпечаток артефакта

После детерминированной сборки компилятор хеширует точный UTF-8-текст контекста:

var context = string.Join("\n\n", sections);
var fingerprint = Convert.ToHexString(
    SHA256.HashData(Encoding.UTF8.GetBytes(context)));

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

Манифест отсортирован по ID источника, чтобы сравнения оставались стабильными. Для каждого источника он фиксирует доверие, решение, оценку включённых токенов, число замен, хеш исходного содержимого и причину.

Слой ASP.NET Core

Minimal API добавляет то, что нужно сервису-компилятору в продакшене:

  • Строго типизированные опции с проверкой при старте
  • Строковые enum в JSON-контракте, доступном вызывающим
  • Ограничитель частоты с фиксированным окном
  • Health checks, а ошибки возвращаются как Problem Details
  • Структурированные логи на генераторах исходного кода, которые содержат счётчики и никогда не содержат контекст
  • Отмена запросов плюс стандартные лимиты запросов ASP.NET Core

Аутентификация намеренно не включена. Прежде чем эндпоинт станет кому-то доступен, добавьте идентичность рабочей нагрузки или сервиса и авторизацию по арендаторам, типам источников и профилям политик.

Тесты, проверяющие решения

Пять тестов покрывают основные утверждения:

  1. Недоверенный источник, похожий на инструкцию, попадает в карантин и отсутствует в контексте.
  2. Email и номера карт заменяются до включения.
  3. Подача входов в обратном порядке даёт тот же контекст и отпечаток.
  4. Когда бюджет заканчивается, источник с меньшим приоритетом обрезается или отбрасывается.
  5. Устаревший источник есть в манифесте, но не в контексте.

Тест детерминизма короткий и очень важный:

var left = compiler.Compile(Request(first, second));
var right = compiler.Compile(Request(second, first));
Assert.Equal(left.Fingerprint, right.Fingerprint);
Assert.Equal(left.Context, right.Context);

Решение запускалось на .NET 10 под Windows. Restore прошёл успешно, сборка Release не дала ни предупреждений, ни ошибок, и все пять тестов прошли. Затем живая проверка запустила API и отправила один запрос из трёх источников: доверенной политики, внутреннего тикета с адресом электронной почты и недоверенной веб-заметки с атакой через инструкцию.

Ошибки, которых стоит избегать

  • Позволять источнику самому выбирать уровень доверия
  • Считать теги-разделители песочницей
  • Логировать весь контекст ради удобства
  • Использовать оценку поиска как неограниченный авторитет
  • Молча отбрасывать источники без манифеста
  • Называть эвристический детектор полной защитой от инъекций
  • Выдавать приблизительный подсчёт токенов за цифру для биллинга

Ещё одно правило: не настраивайте контекст в отрыве от инструментов. Недоверенное предложение опасно ровно настолько, насколько опасна возможность, которой оно может управлять, а загрузка публичного документа — совсем другой сток, чем отправка данных клиентов, поэтому каждому нужна своя авторизация.

Что это даёт

Контекст — больше чем безобидный текст: всё, что в него попадает, становится миром, в котором думает агент, и недоверенная идея может управлять тем, что он сделает дальше. Такой компилятор не избавит от prompt injection. Зато он убирает случайности. У источников есть идентичность, доверие задаётся вне содержимого, старые данные отклоняются, чувствительные значения преобразуются, бюджеты предсказуемы, подозрительные входы сидят в карантине, а каждое решение можно проверить. Context engineering перестаёт быть искусством промптов и становится бэкенд-дисциплиной, которую можно тестировать. А ревьюеры получают конкретный артефакт для изучения, когда поведение агента меняется между релизами.