Головна / Статті / Безпечний компілятор контексту для ШІ-агентів на .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 перестає бути мистецтвом промптів і стає бекенд-дисципліною, яку можна тестувати. А рев'юери отримують конкретний артефакт для вивчення, коли поведінка агента змінюється між релізами.