Перейти к основному содержимому
CodeAlive 3.0: от context engine к агенту для исследования кода
CodeAlive

Ко всем статьям

Каноникализация аргументов для небольших агентных LLM: практический AX-паттерн

В нашей системе наблюдаемости раз за разом всплывала одна и та же ошибка вызова инструмента.

В одной выборке из продакшена мы насчитали десять таких случаев на быстром агентном пути с qwen3.6-35b-a3b. В каждом из них биндер аргументов .NET отклонял вызов ещё до запуска инструмента.

Трассировки показали отклонённые аргументы:

json
{  "data_source_names": "dotnet/eshop"}

Схема ожидала:

json
{  "data_source_names": ["dotnet/eshop"]}

Модель выбрала правильный инструмент и правильный источник данных. Валидный вызов от ошибки отделяла одна пара квадратных скобок.

Это классическая проблема интеграции с AI-агентами: намерение верное, данные почти верные, а детерминированная граница превращает небольшой промах в исключение. Интерфейс для агента должен подстраиваться под него настолько, насколько позволяет семантика. Остальная система по-прежнему должна получать одну каноническую форму.

Мы подстроились под модель

Напрашивалось объявить data_source_names как string | string[]. Но публичную схему мы оставили строгой: string[]. Остальные части системы не должны знать об особенностях конкретной модели.

Вместо этого мы добавили одну политику каноникализации на границе вызова функции:

  • читаем JSON Schema инструмента;
  • если схема требует array<string>, а модель прислала скалярную строку, превращаем её в массив из одного элемента;
  • остальные значения не трогаем;
  • записываем исправление в телеметрию и вызываем инструмент как обычно.

Для нас это и есть Agent Experience (AX). Схема остаётся точной, а граница вызова исправляет безобидное расхождение в форме данных.

Граница намеренно узкая. Мы не разбиваем строки по запятым, не переименовываем параметры и не приводим несвязанные типы. Каноникализация здесь безопасна, потому что "dotnet/eshop" и ["dotnet/eshop"] означают для инструмента одно и то же.

Результат

После добавления middleware мы повторно прогнали записанные вызовы через настоящий биндер фреймворка. Доля сбоев из-за расхождения между скалярной строкой и string[] составила 0%.

Middleware читает схему каждой функции, поэтому правило работает для любого параметра array<string>, а не для жёстко заданного списка имён.

Минимальная реализация для Microsoft Agent Framework

Middleware Microsoft Agent Framework перехватывает вызов функции до биндинга аргументов. Этот минимальный вариант превращает скалярную строку в массив только тогда, когда объявленная схема ожидает массив строк.

csharp
using System.Text.Json;using Microsoft.Agents.AI;using Microsoft.Extensions.AI;
static async ValueTask<object?> CanonicalizeArgumentsAsync(    AIAgent _,    FunctionInvocationContext context,    Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next,    CancellationToken cancellationToken){    JsonElement schema = context.Function.JsonSchema;    if (!schema.TryGetProperty("properties", out JsonElement properties))    {        return await next(context, cancellationToken);    }
    AIFunctionArguments? repaired = null;
    foreach (JsonProperty property in properties.EnumerateObject())    {        if (!IsStringArray(property.Value)            || !context.Arguments.TryGetValue(property.Name, out object? raw)            || !TryReadString(raw, out string? value))        {            continue;        }
        repaired ??= new AIFunctionArguments(context.Arguments)        {            Services = context.Arguments.Services,            Context = context.Arguments.Context,        };        repaired[property.Name] = new[] { value };    }
    if (repaired is not null)    {        context.Arguments = repaired;    }
    return await next(context, cancellationToken);}
static bool IsStringArray(JsonElement schema) =>    HasType(schema, "array")    && schema.TryGetProperty("items", out JsonElement items)    && HasType(items, "string");
static bool HasType(JsonElement schema, string expected) =>    schema.TryGetProperty("type", out JsonElement type)    && (type.ValueKind == JsonValueKind.String        ? type.GetString() == expected        : type.ValueKind == JsonValueKind.Array          && type.EnumerateArray().Any(item => item.GetString() == expected));
static bool TryReadString(object? raw, out string? value){    value = raw switch    {        string text => text,        JsonElement { ValueKind: JsonValueKind.String } json => json.GetString(),        _ => null,    };    return value is not null;}
static AIAgent WithArgumentCanonicalization(AIAgent agent) =>    agent.AsBuilder()        .Use(CanonicalizeArgumentsAsync)        .Build();

Когда каноникализации недостаточно

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

Вместо этого нужно вернуть структурированный результат валидации, сказать модели, что именно изменить, и дать ей повторить вызов. Такой repair-цикл станет темой следующей статьи.

Приложение: окружение примера

Код выше проверен с:

  • Microsoft.Agents.AI 1.13
  • Microsoft.Extensions.AI 10.7

Документация: function-calling middleware в Agent Framework и FunctionInvokingChatClient.

Agent Experience (AX)

Дайте агентам всю кодовую базу

Проиндексируйте первый репозиторий за минуты — или попробуйте демо без регистрации.