Structured Output от LLM в ZennoPoster: кейсы + связка с LM Studio

LaGir

Client
Регистрация
01.10.2015
Сообщения
255
Благодарностей
1 051
Баллы
93
Всем привет! :-)

Все мы привыкли, что языковые модели генерируют ответы в виде осмысленного человеческого текста, и чаще всего в таком виде его и используем. Для генерации текстов, статей, комментариев, иного контента, просто для ответов на что-либо. Также многие знают, что модели хорошо умеют в код и математику, хоть и используется это чаще для личных целей, а не для генерации контента. К чему я это – модели умеют генерировать не только человекоподобный текст, но и в строгом конкретном формате, например в валидном XML или JSON.
О такой фиче в этой статье мы и поговорим – о Structured Output (Структурированный вывод).
В контексте написания и использования шаблонов в ZennoPoster к ней можно найти массу полезнейших применений – и в этой статье мы в общем виде рассмотрим несколько кейсов-примеров.

Проще и нагляднее всего объяснить базу на примере задач парсинга.
Допустим, наша задача спарсить объявления о сдаче квартир, но не на досках объявлений, а в тематических группах в соцсетях (вк, тг и т.п.). Задача при таких условиях сложнее в том плане, что на таких площадках нет готовой разметки как на досках объявлениях, т.е. нет постоянных полей и элементов на странице, откуда сразу можно парсить конкретные и понятные данные – из этого элемента метраж квартиры, их этого – стоимость, из третьего – количество комнат и т.п. В соцсетях объявления оформлены просто в виде постов текста, а нам всё-таки при парсинге хотелось бы их разобрать в те же удобные таблички, как и при парсинге досок объявлений и специализированных сайтов.

И тут нам и помогает LLM с функцией вывода в Structured Output.
Зеннкой мы целиком парсим текст объявления, и просто отправляем его в LLM, вместе с заранее подготовленной JSON-схемой с нужными полями из объявления.
LLM читает объявление, "анализирует" его и рассовывает важную инфу из него строго по нужной нам JSON-схеме – и возвращает нам ответ в виде чистого валидного JSON.
Нам остаётся только разобрать этот JSON в ту же табличку, БД или иное место, куда нам надо. Обычным кодом, т.к. JSON будет в правильном виде – в этом и есть прелесть фичи Structured Output.

Допустим, мы обычным парсингом в ZennoPoster забрали в переменную такой текст объявления:
Код:
Сдаю однокомнатную квартиру на улице Кораблестроителей дом 25, метро Приморская. 
От метро 15 минут пешком. 1й этаж, ЗЗ квадратных метра. 
На кухне и в комнате недавно сделан ремонт. Новая мебель, хороший матрас, телевизор. 
Сдам по договору на 6 месяцев, дальше возможно продление. 
Стоимость З7 тысячи в месяц + ку (5000 зимой, 3000 летом). 
Залог З0 тысяч. Без комиссии. Порядочным и адекватным.
Нам надо вычленить из них параметры квартиры и положить в БД, чтобы можно было сравнивать с другими собранными квартирами.
Мы отправляем текст объявления модели и правила JSON-схемы, в которой хотим получить от неё ответ. И получаем именно то, что хотим:
JSON:
{
  "area": 33,
  "floor": 1,
  "rooms": 1,
  "address": "ул. Кораблестроителей, 25",
  "metro": "Приморская"
}
И всё, в таком структурированном виде отправить данные в БД – тривиальная задача.
Таким образом мы с помощью LLM разбираем тексты на составляющие без необходимости придумать суперсложный парсинг (на тех же регулярках например, как в старые времена), чтобы он работал на 100500 объявлений, которые могут писать люди.

Какие ещё могут быть кейсы в контексте автоматизации в ZennoPoster?

Заполнение форм на любых сайтах

Формы бывают на сайтах самые разные. Можно по старинке закодить заполнение наиболее часто встречаемые виды форм, попытаться сделать "универсальный заполнятор". Для создания такого шаблона нужно проделать довольно много работы – собрать популярные формы, спроектировать логику универсального заполнения, закодить это всё, проверить. И всё равно на практике в интеренете всегда найдутся формы, которые вы не предусмотрели.
А можно просто отправлять целиком формы (или даже HTML всей страницы) в LLM, и пусть она сама разбирает вёрстку каждой новой формы, а нам возвращает что-куда заполнять и куда тыкать. И это будет по-настоящему универсально, потому что примерно как настоящий человек разберётся как заполнять любую рандомную форму, так и ИИ может сделать то же самое.

Генерация контента с жёсткими полями под постинг
Допустим, мы генерируем статьи для сайта/площадки. и по API движка/площадки публикуем их. С помощью Structured Output мы можем сразу генерировать статью в формате, который требует API, и сразу с дополнительными полями типа заголовка, тегов и прочего. А не кодить самостоятельно всё это форматирование/обвязку.

Гулялка по сайтам с классификацией страниц
Можно замахнуться даже на что-то более масштабное, а-ля делегирование нагула кук для профилей. Например, заходим на рандомную страницу рандомного сайта, и отправляем её код/DOM целиком в LLM. А она пусть решает, что имеет смысл делать на этой страницы. Например, если это страница со статьёй, то нужно имитировать на ней чтение, если страница с капчей – надо её решить, если есть попап с формой – надо её заполнить. Мы заранее только прописываем в промпте для LLM какие вообще действия можно делать, и кодим блоки выполнения этих действий. А ходить по страницам и что на них делать – будет решать модель на основе их содержимого. Такая гулялка при должной реализации будет куда естественнее в плане поведения на сайтах, чем обычные боты.

Теперь немного о том, где и какие модели будем запускать в нашей демонстрации.

Подключение к LM Studio

В статьях к предыдущих конкурсах я уже неоднократно упоминал LM Studio в качестве локального способа запуска моделей и использования в ZennoPoster.
И в этот раз тоже будем подключаться к LM Studio – просто потому что это очень просто, локально и бесплатно.

Всё, что нам нужно сделать в самой программе – перейти на вкладку "Разработка", запустить сервер и выбрать модель (ну и скачать её предварительно, если раньше вы не пользовались LM Studio).

SO_1.png


Что тут стоит упомянуть – модель крайне желательно выбирать ту, которая нативно поддерживает фичу Structured Output.
Не каждая из моделей поддерживает её, хотя и работать скорее всего будет и с обычной не сильно хуже. Отличие в том, что если для LLM заявлена поддержка Structured Output, значит она специально дообучалась под этот формат вывода, и значит что вероятность ошибки в ответах кратно снижается.
Про поддержку пишут обычно в карточке модели, например на том же HuggingFace.

Также, чем "умнее" и современнее модель, и чем больше у неё параметров – тем лучше она будет справляться с нашими задачами. Разумеется, речь не про использование самых лучших и тяжелых локальных моделей, скорее про баланс – выбирать модель так, чтобы она не сильно нагружала вашу рабочую машину/сервер, и чтобы не была слишком "глупой" чтобы типовые разметки и задачи.

Вот пример кода подключения и запроса к модели в LM Studio:
Запрос к модели в LM Studio:
// LM Studio BaseUrl
// http://localhost:1234/v1/chat/completions
string apiUrl = project.Variables["ApiUrl"].Value;
string model  = project.Variables["Model"].Value;

string system = project.Variables["SystemPrompt"].Value;
string prompt = project.Variables["Prompt"].Value;

string schemaName = project.Variables["SchemaName"].Value;
string schemaJson = project.Variables["SchemaJson"].Value;

string formHtml = project.Variables["FormHtml"].Value;
// Вставляем взятую со страницы форму в промпт
prompt = prompt.Replace("<<<FORM_HTML>>>", formHtml);

// Собираем messages с промптом
var messages = new JArray
{
    new JObject { ["role"] = "system", ["content"] = system },
    new JObject { ["role"] = "user", ["content"] = prompt }
};
// Получаем schema object
var schemaObj = JObject.Parse(schemaJson);
// Формируем request object для запроса к модели
var requestObj = new JObject
{
    ["model"] = model,
    ["messages"] = messages,
    ["response_format"] = new JObject
    {
        ["type"] = "json_schema",
        ["json_schema"] = new JObject
        {
            ["name"] = schemaName,
            ["strict"] = "true",
            ["schema"] = schemaObj
        }
    },
    ["temperature"] = 0.2,
    ["stream"] = false
};

string body = requestObj.ToString(Formatting.None);

// POST-запрос к запущенной модели, получение ответа
var req = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(apiUrl);
req.Method = "POST";
req.ContentType = "application/json";
req.Headers["Authorization"] = "Bearer lm-studio";
using (var sw = new System.IO.StreamWriter(req.GetRequestStream()))
    sw.Write(body);
string raw = String.Empty;
using (var resp = (System.Net.HttpWebResponse)req.GetResponse())
using (var sr = new System.IO.StreamReader(resp.GetResponseStream()))
    raw = sr.ReadToEnd();

// Достаём ответ (content) из ответа API
var jo = JObject.Parse(raw);
// Здесь строка с JSON по схеме
string contentJson = (string)jo["choices"][0]["message"]["content"];
// Сохраняем в переменную проекта
project.Variables["jsonStructuredOutput"].Value = contentJson;
Этим кодом мы отправляем html некоей формы с сайта, промпт и схему JSON, в формате которой модель должна ответить.
От сервера LM Studio приходит ответ модели, который мы помещаем в переменную проекта jsonStructuredOutput. Далее можно разбирать этот ответ, делать что нам нужно исходя из начальной задачи.

Что нужно отправлять модели

В примере подключения у нас используется несколько сущностей-заготовок, которые возможно не всем будет очевидно.
По-хорошему нам всегда надо отправлять 3 вещи.
  1. Промпт. Тестовое описание, что должна сделать модель. Промпт зависит от каждой задачи/кейса.
  2. Входные данные, которые надо обработать модели. Как правило, во многих кейсах это HTML либо отдельного элемента или целого блока на странице, либо код/DOM страницы целиком. Входные данные вставляем в промпт, например с помощью макроса и его замены.
  3. Схема JSON. Под каждую задачу/кейс надо составить структуру JSON, в который мы хотим принимать ответы от LLM. Это надо обязательно, т.к. иначе модель не будет знать в какой структуре отвечать, а нам не будет понятно, как разбирать её ответ. Если вы не знаете, как создать схему, посмотрите примеры ниже, или же сразу попросите помочь условный чатгпт или любую другую нейронку.

Ну а пайплайн у нас будет примерно такой, соответственно:
  1. Парсим в ZP обычными способами нужные блоки данных со страницы (или берём в переменную всю страницу).
  2. Отправляем модели Промпт+Данные+Схему с включённой опцией Structured Output.
  3. Парсим и заодно проверяем уверенность модели в ответе.
  4. Если модель не уверена в ответе, либо разбор JSON падает или нас не устраивает – просим модель починить и отправляем на доработку.
  5. Если с ответом сразу или после "починки" всё ок – делаем всё что нужно обычными средствами (например, на основе ответа модели кликаем сышкой по нужным местам на странице).
Как вы заметили, мы тут сразу добавили обработку возможных ошибок/проблем, что будет крайне полезным на реальных проектах.
Подробнее пример реализации таких проверок и "починок" можно посмотреть в прикреплённом шаблоне.


Кейсы

Давайте рассмотрим конкретные кейсы использования Structured Output, а также примеры промптов, схем и кода для них.

1. Умное заполнение неизвестной формы

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

В нашем случае нам надо брать весь блок form и отправлять модели, а она нам будет в JSON отвечать, какие поля и чем именно заполнять, а также куда тыкать для отправки формы. Чем заполнять – берём данные из текущего профиля ZennoPoster.

JSON:
{
  "type": "object",
  "properties": {
    "fields": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "label": { "type": "string" },
          "nameAttr": { "type": "string" },
          "inputType": { "type": "string" },
          "requiredGuess": { "type": "boolean" },
          "valueSource": {
            "type": "string",
            "enum": ["profile.email","profile.phone","profile.firstName","profile.lastName","custom","skip"]
          },
          "value": { "type": "string" },
          "xpath": { "type": "string" },
          "confidence": { "type": "number" }
        },
        "required": ["valueSource","xpath","confidence"]
      }
    },
    "submit": {
      "type": "object",
      "properties": {
        "xpath": { "type": "string" },
        "confidence": { "type": "number" }
      },
      "required": ["xpath","confidence"]
    },
    "confidence": { "type": "number" },
    "warnings": { "type": "array", "items": { "type": "string" } },
    "evidence": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["fields","submit","confidence","warnings","evidence"]
}
HTML формы:
<<<FORM_HTML>>>

Данные профиля:
email = {project.Profile.Email}
phone = {project.Profile.Phone}
firstName = {project.Profile.FirstName}
lastName = {project.Profile.LastName}

Найди поля, которые имеет смысл заполнить пользователю.
- Игнорируй captcha/hidden/readonly/служебные поля
- Для каждого поля дай XPath и valueSource (из профиля или custom)
- XPath составляй только короткие уникальные, например: //input[@id='email']
- Найди XPath кнопки отправки формы (submit)

Верни только JSON по схеме.
JSON:
{
  "fields": [
    {
      "label": "Email",
      "nameAttr": "email",
      "inputType": "email",
      "requiredGuess": true,
      "valueSource": "profile.email",
      "value": "",
      "xpath": "//input[@type='email' or contains(@name,'mail')]",
      "confidence": 0.86
    },
    {
      "label": "First name",
      "nameAttr": "first_name",
      "inputType": "text",
      "requiredGuess": false,
      "valueSource": "profile.firstName",
      "value": "",
      "xpath": "//input[contains(@name,'first') or contains(@id,'first')]",
      "confidence": 0.7
    }
  ],
  "submit": {
    "xpath": "//button[@type='submit' or contains(.,'Send') or contains(.,'Отправить')]",
    "confidence": 0.78
  },
  "confidence": 0.8,
  "warnings": ["Проверьте, что форма не требует подтверждения чекбокса согласия (privacy/terms)."],
  "evidence": ["input type=email найден", "button type=submit найден"]
}
C#:
// Ответ от LLM
string json = project.Variables["jsonStructuredOutput"].Value;

var jo = JObject.Parse(json);
double conf = (double)jo["confidence"];
var warnings = jo["warnings"]?.Select(x => (string)x).ToArray() ?? new string[0];
if (warnings.Length > 0)
    project.SendInfoToLog("Предупреждения от LLM: " + string.Join(" | ", warnings), true);

// 1) Валидация уверенности модели в ответе
if (conf < 0.6)
{
    project.SendInfoToLog("Низкая уверенность модели по форме. Повторно отправим более узкий HTML/уточним промпт.", true);
    // Можгл вырезать более точный контейнер формы и повторить запрос.
    // Если второй раз тоже плохо — fallback ниже.
    throw new Exception("Выход по красной ветке");
}

var tab = instance.ActiveTab;
// 2) Заполнение полей в цикле
foreach (var f in jo["fields"])
{
    string valueSource = (string)f["valueSource"];
    if (valueSource == "skip") continue;

    string xpath = (string)f["xpath"];
    if (string.IsNullOrWhiteSpace(xpath)) continue;
    
    string value = String.Empty;
    switch (valueSource)
    {
        case "profile.email": value = project.Profile.Email; break;
        case "profile.phone": value = project.Profile.Phone; break;
        case "profile.firstName": value = project.Profile.Name; break;
        case "profile.lastName": value = project.Profile.Surname; break;
        case "custom": value = (string)f["value"]; break;
    }
    if (string.IsNullOrWhiteSpace(value)) continue;
    
    project.SendInfoToLog($"valueSource: {valueSource} xpath: {xpath} value: {value}", true);

    var el = tab.FindElementByXPath(xpath, 0);
    if (el.IsNull || el.IsVoid) continue;

    tab.FullEmulationMouseMoveToHtmlElement(el);
    tab.FullEmulationMouseClick("left", "click");
    
    project.SendInfoToLog($"SetValue: {value}", true);
    
    // el.Click();
    el.SetValue(value, "SuperEmulation");
}
// 3) Отправка формы
string submitXpath = (string)jo["submit"]["xpath"];
double submitConf = (double)jo["submit"]["confidence"];

var btn = tab.FindElementByXPath(submitXpath, 0);
// Fallback: если submit не найден — пробуем простую эвристику
if (btn.IsNull || submitConf < 0.55)
{
    project.SendInfoToLog("Submit не найден/неуверенно. Fallback: ищу button[type=submit] и input[type=submit].", true);
    btn = tab.FindElementByXPath("//button[@type='submit']|//input[@type='submit']", 0);
}
if (!btn.IsNull && !btn.IsVoid) btn.Click();


2. Умный парсер списка объявлений/постов

Похожая на предыдущую задачу, в плане того, что нужна универсальность.
С помощью LLM и Structured Output мы её и достигаем.

JSON:
{
  "type": "object",
  "properties": {
    "listContainerXpath": { "type": "string" },
    "itemXpath": { "type": "string" },
    "fields": {
      "type": "object",
      "properties": {
        "titleXpath": { "type": "string" },
        "priceXpath": { "type": "string" },
        "urlXpath": { "type": "string" },
        "imageXpath": { "type": "string" }
      },
      "required": ["titleXpath","urlXpath"]
    },
    "pagination": {
      "type": "object",
      "properties": {
        "nextPageXpath": { "type": "string" }
      },
      "required": ["nextPageXpath"]
    },
    "confidence": { "type": "number" },
    "warnings": { "type": "array", "items": { "type": "string" } },
    "evidence": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["listContainerXpath","itemXpath","fields","pagination","confidence","warnings","evidence"]
}
HTML блока выдачи:
<<<LIST_HTML>>>

Нужно извлечь поля: title, price, url, image (если есть).
Дай XPath:
- listContainerXpath
- itemXpath (так, чтобы можно было получить коллекцию карточек)
- поля внутри карточки (titleXpath/priceXpath/urlXpath/imageXpath)
- пагинация: nextPageXpath

Верни только JSON по схеме.
JSON:
{
  "listContainerXpath": "//div[contains(@class,'catalog') or contains(@class,'results')]",
  "itemXpath": ".//div[contains(@class,'card') or contains(@class,'item')]",
  "fields": {
    "titleXpath": ".//h2|.//h3|.//a[contains(@class,'title')]",
    "priceXpath": ".//*[contains(@class,'price') or contains(.,'₽') or contains(.,'$')]",
    "urlXpath": ".//a[@href][1]",
    "imageXpath": ".//img[@src][1]"
  },
  "pagination": {
    "nextPageXpath": "//a[contains(.,'Next') or contains(.,'Следующая') or contains(@aria-label,'Next')]"
  },
  "confidence": 0.73,
  "warnings": ["priceXpath может цеплять лишний текст — лучше нормализовать regex’ом."],
  "evidence": ["Найден повторяющийся паттерн карточек (div.card/div.item)"]
}
C#:
// Ответ от LLM
string json = project.Variables["jsonStructuredOutput"].Value;

var jo = JObject.Parse(json);
double conf = (double)jo["confidence"];
if (conf < 0.6)
{
    project.SendInfoToLog("Низкий confidence по парсеру выдачи. Вырезаем более точный контейнер и повторяем.", true);
    // Retry: вместо всего main — взять ближайший контейнер вокруг 5-10 карточек.
}

var tab = instance.ActiveTab;
string listXp = (string)jo["listContainerXpath"];
string itemXp = (string)jo["itemXpath"];

var listEl = tab.FindElementByXPath(listXp, 0);
if (listEl == null)
{
    project.SendInfoToLog("Не найден контейнер выдачи. Fallback: пробуем искать карточки по глобальному XPath.", true);
    listEl = tab.FindElementByXPath("//body", 0); // грубый fallback
}

string titleXp = (string)jo["fields"]["titleXpath"];
string priceXp = (string)jo["fields"]["priceXpath"];
string urlXp   = (string)jo["fields"]["urlXpath"];

var items = listEl.FindChildrenByXPath(itemXp);
if (items.Count < 1)
{
    project.SendInfoToLog("Не найдено карточек. Пробуем типовые XPath для карточек.", true);
    items = tab.FindElementsByXPath("//article|//div[contains(@class,'card') or contains(@class,'item')]");
}

foreach (var it in items)
{
    string title = it.FindChildByXPath(titleXp, 0)?.GetAttribute("innerText");
    string url = it.FindChildByXPath(urlXp, 0)?.GetAttribute("href");
    string price = it.FindChildByXPath(priceXp, 0)?.GetAttribute("innerText");

    // Тут обычно: нормализация price regex'ом + запись в таблицу/файл
    project.SendInfoToLog($"ITEM => {title} | {price} | {url}", false);
}
// Пагинация
string nextXp = (string)jo["pagination"]["nextPageXpath"];
var nextBtn = tab.FindElementByXPath(nextXp, 0);
if (!nextBtn.IsVoid) nextBtn.Click();

3. Гулялка с классификацией страниц

Тот самый кейс, который мы упоминали в начале статьи – что делать на странице решает LLM, в зависимости от содержимого этой самой страницы.
Тоже момент по универсальности. Не надо по шаблону тыкать те же проверки на капчу. Если капча на странице вдруг появится перед очередным действием – LLM нам об этом отрапортует и выберет ветку блока решения капчи в шаблоне, например.

JSON:
{
  "type": "object",
  "properties": {
    "pageType": {
      "type": "string",
      "enum": ["ok","login","captcha","rate_limit","blocked","error","unknown"]
    },
    "recommendedAction": {
      "type": "string",
      "enum": ["continue","relogin","solve_captcha","change_proxy","wait_and_retry","retry","skip","stop"]
    },
    "confidence": { "type": "number" },
    "warnings": { "type": "array", "items": { "type": "string" } },
    "evidence": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["pageType","recommendedAction","confidence","warnings","evidence"]
}
URL: <<<URL>>>
Title: <<<TITLE>>>
Text snippet:
<<<TEXT_SNIPPET>>>

Определи состояние страницы (ok/login/captcha/rate_limit/blocked/error/unknown)
и что делать дальше (continue/relogin/solve_captcha/change_proxy/wait_and_retry/retry/skip/stop).

Верни только JSON по схеме.
JSON:
{
  "pageType": "rate_limit",
  "recommendedAction": "wait_and_retry",
  "confidence": 0.82,
  "warnings": ["Похоже на временное ограничение. Не усугубляйте частыми ретраями."],
  "evidence": ["фраза 'Too many requests'", "код/текст про ограничение запросов"]
}
C#:
// Ответ от LLM
string json = project.Variables["jsonStructuredOutput"].Value;

var jo = JObject.Parse(json);
string pageType = (string)jo["pageType"];
string action = (string)jo["recommendedAction"];
double conf = (double)jo["confidence"];

if (conf < 0.55)
{
    project.SendInfoToLog("Классификация неуверенная. Fallback: простые проверки по ключевым словам.", true);
    throw new Exception("Выход по красной ветке");
    // Пример грубого fallback:
    // if (text.Contains("captcha") || text.Contains("Cloudflare")) pageType="captcha";
    // if (text.Contains("login") || text.Contains("войти")) pageType="login";
}
project.SendInfoToLog($"Тип страницы: {pageType}, действие: {action}, уверенность модели: {conf}", true);

switch (action)
{
    case "continue":
        // продолжаем основной сценарий
        break;
    case "relogin":
        // перейти к блоку залогина
        break;
    case "solve_captcha":
        // Решить капчу, или остановить/уведомить
        break;
    case "change_proxy":
        // смена прокси, затем повтор
        break;
    case "wait_and_retry":
        Thread.Sleep(15000);
        // затем обновить страницу/повторить действие
        break;
    case "skip":
        // пропустить текущую цель
        break;
    case "stop":
        // остановить шаблон
        throw new Exception("Stop by classifier");
}

project.Variables["action"].Value = action;


4. Извлечение контактов/реквизитов/соцсетей со страницы

Ещё одна задача по парсингу, но другого плана.
Тут кстати можно прикрутить дополнительное применение способностей LLM. Мы можем например просить модель не просто собирать телефоны со страницы, а сразу возвращать их в определённом формате (например XXX-XX-XX). Отпадает необходимость самостоятельно потом нормализовывать спаршенные данные.

JSON:
{
  "type": "object",
  "properties": {
    "company": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "address": { "type": "string" },
        "inn": { "type": "string" },
        "ogrn": { "type": "string" }
      },
      "required": ["name","address"]
    },
    "contacts": {
      "type": "object",
      "properties": {
        "emails": { "type": "array", "items": { "type": "string" } },
        "phones": { "type": "array", "items": { "type": "string" } },
        "socials": {
          "type": "object",
          "properties": {
            "telegram": { "type": "string" },
            "whatsapp": { "type": "string" },
            "vk": { "type": "string" },
            "instagram": { "type": "string" }
          }
        }
      },
      "required": ["emails","phones","socials"]
    },
    "confidence": { "type": "number" },
    "warnings": { "type": "array", "items": { "type": "string" } },
    "evidence": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["company","contacts","confidence","warnings","evidence"]
}
URL: <<<URL>>>
HTML:
<<<CONTACTS_HTML>>>

Извлеки и нормализуй:
- company: name, address, inn, ogrn (если есть)
- contacts: emails[], phones[], socials{telegram,whatsapp,vk,instagram}

Верни только JSON по схеме.
JSON:
{
  "company": {
    "name": "Example Studio",
    "address": "Москва, ул. Примерная, 10",
    "inn": "7701234567",
    "ogrn": ""
  },
  "contacts": {
    "emails": ["info@example.com", "sales@example.com"],
    "phones": ["+7 (999) 123-45-67"],
    "socials": {
      "telegram": "https://t.me/example",
      "whatsapp": "",
      "vk": "https://vk.com/example",
      "instagram": ""
    }
  },
  "confidence": 0.84,
  "warnings": ["Телефон не приведён к E.164 — при необходимости нормализуйте."],
  "evidence": ["Найдены mailto:", "Найдены ссылки на соцсети в футере"]
}
C#:
// Ответ от LLM
string json = project.Variables["jsonStructuredOutput"].Value;

var jo = JObject.Parse(json);
double conf = (double)jo["confidence"];

string name = (string)jo["company"]["name"];
string addr = (string)jo["company"]["address"];

var emails = jo["contacts"]["emails"].Select(x => (string)x).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToArray();
var phones = jo["contacts"]["phones"].Select(x => (string)x).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToArray();

if (conf < 0.6)
{
    project.SendInfoToLog("Низкий уверенность по сбору контактов, прерываемся или пробуем спарсить по-другому", true);
    // Тут можно начать заново, спарсив по другому XPath/регулярке страничку и снова передать LLM
    throw new Exception("Выход по красной ветке");
}

// Пример: сохраняем в переменные/списки
project.Variables["CompanyName"].Value = name;
project.Variables["CompanyAddress"].Value = addr;
project.Lists["Emails"].AddRange(emails);
project.Lists["Phones"].AddRange(phones);
// Соцсети
string tg = (string)jo["contacts"]["socials"]["telegram"] ?? "";
project.Variables["Telegram"].Value = tg;


Тут мы затронули 4 простых кейса и примеры промптов и схем к ним. Однако, в прикреплённом шаблоне и видео упоминаются ещё несколько.




Надёжность и решение проблем

Выше мы уже упоминали, что хоть и модели с поддержкой Structured Output отлично справляются с соответствующими задачами, проверять ответы всегда надо.
Поэтому по-хорошему надо заранее предусмотреть возможные проблемы и пути их решения.
  • Если модель помимо прочего создаёт для вас пути XPath – проверяйте их перед использований. Если по XPath нашлось 0 (или наоборот слишком много) элементов – просить модель починить.
  • Если ваш код не может нормально спарсить JSON от модели – опять же отправляем модели обратно с просьбой починить.
  • Если параметр собственной уверенности модели низкий (confidence) – пробуем по возможности изменить входные данные и отправляем модели на вторую попытку.
  • Если какие-то из обязательных полей в JSON пустые или с некорректными данными – отправляем модели обратно с просьбой починить.
  • Если ещё какие-либо проблемы, или не получается с N-ой попытки – придумываем запасной сценарий решения штатными средствами "по старинке", либо просто адекватно сворачиваем выполнение. Про штатные средства такой пример: допустим, у модели никак не получается составить нормальный XPath для конкретного поля. В этом случае можно продумать несколько заготовленных универсальных путей, и запускать их если модель не справляется.

В принципе, на этом и закончим. Областей применения для этой фичи в действительности несравнимо больше, и можно сильно упростить себе разработку шаблонов вот таким вот образом, плюс найти новые применения или даже ниши. Задачка вам на подумать. :-)
 

Вложения

Последнее редактирование модератором:

Darvel

Client
Регистрация
17.11.2013
Сообщения
124
Благодарностей
37
Баллы
28
Проект крутой, но модели пока очень тупые, коряво пишут, с ошибками, могут сильно тупить, выдавать кракозябры и т.п Пару недель назад тестил все самые последние - пока даже близко не GPT3, а прям хорошая это 4о, ну и 5-ая. Куда профитнее сделать авторизацию в какой-нибудь квен или дипсик, в идеале сделать резерв, чтобы если вдруг что-то с одним случилось - писали во второй(отказоустойчивость). Да, чуть дольше, но качество просто в разы лучше. А так, молодец, когда модели года через 2-3 хотя бы до уровня гпт 3 дотянут - будет юзабельно. Ну если сделать поправку на то, что можно в LM подключать API, то это уже дело другое, но тут уже не бесплатно получается, и тогда уже проще просто делать запрос по API непосредственно к GPT и парстить ответ.
 
  • Спасибо
Реакции: seodamage

LaGir

Client
Регистрация
01.10.2015
Сообщения
255
Благодарностей
1 051
Баллы
93
Ну если сделать поправку на то, что можно в LM подключать API, то это уже дело другое, но тут уже не бесплатно получается, и тогда уже проще просто делать запрос по API непосредственно к GPT и парстить ответ.
Переделать под обращение к API проприетарных моделей, вместо обращения к локальному API LM Studio – дело нескольких минут. Для прода, т.е. для уже готового отлаженного проекта я поддерживаю подход использования легких фронтирных моделей, типа Gemini Flash, Claude Haiku. Это и дёшево, и сами такие последние модели отличные, и не надо иметь машину/сервер с кучей [видео]памяти.
Однако же на локальных моделях проще заниматься именно разработкой и отладкой, плюс это бесплатно. Поэтому в качестве локального и сервера и поставщика моделей выбрана LM Studio. Ну и опять же она знакома многим людям по другим статьям (не только моим) и по другим вариантам использования.
Проект крутой, но модели пока очень тупые, коряво пишут, с ошибками, могут сильно тупить, выдавать кракозябры и т.п Пару недель назад тестил все самые последние - пока даже близко не GPT3, а прям хорошая это 4о, ну и 5-ая. Куда профитнее сделать авторизацию в какой-нибудь квен или дипсик, в идеале сделать резерв, чтобы если вдруг что-то с одним случилось - писали во второй(отказоустойчивость). Да, чуть дольше, но качество просто в разы лучше. А так, молодец, когда модели года через 2-3 хотя бы до уровня гпт 3 дотянут - будет юзабельно.
Не знаю что у вас за кейсы, и насколько хорош ваш подход к тестированию. Но мой личный опыт использования мелких моделей с опенсорса совершенно другой, как и тот, что видел-читал у других в сети, кто использует опенсорс в проде по тем или иным причинам.
Если на тему того, догнали ли средние модели линеек Qwen 3 и Gemma 3 уровня gpt-4o в большинстве среднестатистических задач – дискутировать ещё можно. То вот сравнение с более ранними проприетарными моделями как правило не в их пользу будет. А уж сравнение с gpt-3 совсем ни в какие ворота.

Тут имеет смысл чем-то конкретным подкрепить свои слова. Тесты в своих кейсах я по понятным причинам не могу показать, а релизы и тесты этих моделей по бенчмаркам слишком разнесены по времени ввиду их дат выхода. Поэтому решил сходить глянуть на лидерборд LMArena: https://lmarena.ai/ru/leaderboard/text
И вот например как выглядят позиции моделей.

Gemma 3:
Снимок экрана 2025-12-24 171446.png
GPT 4 и 4 Turbo:
Снимок экрана 2025-12-24 171522.png
GPT 4o:
Снимок экрана 2025-12-24 171550.png
 
  • Спасибо
Реакции: volody00 и VladV777

Darvel

Client
Регистрация
17.11.2013
Сообщения
124
Благодарностей
37
Баллы
28
Переделать под обращение к API проприетарных моделей, вместо обращения к локальному API LM Studio – дело нескольких минут. Для прода, т.е. для уже готового отлаженного проекта я поддерживаю подход использования легких фронтирных моделей, типа Gemini Flash, Claude Haiku. Это и дёшево, и сами такие последние модели отличные, и не надо иметь машину/сервер с кучей [видео]памяти.
Однако же на локальных моделях проще заниматься именно разработкой и отладкой, плюс это бесплатно. Поэтому в качестве локального и сервера и поставщика моделей выбрана LM Studio. Ну и опять же она знакома многим людям по другим статьям (не только моим) и по другим вариантам использования.

Не знаю что у вас за кейсы, и насколько хорош ваш подход к тестированию. Но мой личный опыт использования мелких моделей с опенсорса совершенно другой, как и тот, что видел-читал у других в сети, кто использует опенсорс в проде по тем или иным причинам.
Если на тему того, догнали ли средние модели линеек Qwen 3 и Gemma 3 уровня gpt-4o в большинстве среднестатистических задач – дискутировать ещё можно. То вот сравнение с более ранними проприетарными моделями как правило не в их пользу будет. А уж сравнение с gpt-3 совсем ни в какие ворота.

Тут имеет смысл чем-то конкретным подкрепить свои слова. Тесты в своих кейсах я по понятным причинам не могу показать, а релизы и тесты этих моделей по бенчмаркам слишком разнесены по времени ввиду их дат выхода. Поэтому решил сходить глянуть на лидерборд LMArena: https://lmarena.ai/ru/leaderboard/text
Тесты тестами - там использовалось на очень мощном железе, в вакууме и заготовленными промптами заранее, которые довольно хорошо отрабатывают + в целом оценка модели по параметрам. Я же проверял на практике на 7800x3d, 3090 и 64 оперативы. И могу сказать, что они периодически пишут некоторые слова транслитом/кракозябрами, типо иврита или тайского, теряют контекст при большом, но разрешенном объеме текста и т.п. Я задавал базовые для моей работы запросы вроде перевода текста, суммаризации статьи, переписать статью, добавив пару новых фактов, разные заголовки, составить промпты и т.п. - то, что я использую каждый день в GPT. И уровень пока не для стабильной работы. Мне тут скинули, что вышла новая модель, которая реально крутая и локальная, но пока не тестил ее, но все предыдущие, включая квен, дипсик и другие, которые в топе загрузок - пока не удовлетворяют минимальным требованиям для стабильной работы, а уж тем более в автоматическом режиме.
Я больше писал это как совет для тех, кто скачает и захочет работать с шаблоном, что лучше сделать запросы к апи, например GPT или любой, какая нравится, а не через локальные, если нужна точность и без косяков и перепроверок.
 

LaGir

Client
Регистрация
01.10.2015
Сообщения
255
Благодарностей
1 051
Баллы
93
Я больше писал это как совет для тех, кто скачает и захочет работать с шаблоном, что лучше сделать запросы к апи, например GPT или любой, какая нравится, а не через локальные, если нужна точность и без косяков и перепроверок.
Спасибо. Тогда прокомментирую ваше сообщение, но не как ответ вам, а скорее как тоже дополнительную информацию именно по использованию локальных моделей.
Возможно, кому-то из читающих пригодится, т.к. для немалого числа задач и локальных с головой хватает, а галлюцинации полностью и в проприетарных пока не победили. Плюс, мало ли что грядующий год готовит, в каких-то местах вполне может появится инет чисто по белым спискам.

Я же проверял на практике на 7800x3d, 3090 и 64 оперативы.
Тут отмечу, что мощность железа не влияет на качество ответов моделей, по сути лишь на скорость генерации токенов. Объём памяти и видеопамяти, соответственно – на то, какого размера модель и с каким размером контекста получится запустить.
Именно на качество ответов напрямую влияет выбор модели, какая именно версия используется (базовая, файнтюн и т.д.), оригинальная или квантованная. Часто в софте для инференса по умолчанию предлагается скачать/запустить модель с квантованием в 4 бит (Q4_K_M как правило). Это сильно экономит память и снижает требования по её объёму, но качество модели падает куда в меньшей процентовке, а то и вовсе на уровне погрещности. Однако – качество ответов всё же падает, и на некоторых задачах и с некоторыми моделями это прям сильно ощущается. Иными словами, если не знать таких особенностей, можно легко забраковать отдельную модель или например посчитать, что её создатели врут и накручивают бенчмарки.
И могу сказать, что они периодически пишут некоторые слова транслитом/кракозябрами, типо иврита или тайского, теряют контекст при большом, но разрешенном объеме текста и т.п.
Тут прокоментирую, что в большой мере этим страдают китайские модели. Из остальных – в большинстве своём это касается просто маленьких, или квантованных слишком сильно (на каком именно кванте начнут появлятся артефакты – как правило зависит от конкретной модели, т.е. без тестов ).
Из хороших моделей тут опять же вспомню гугловские Gemma 3, ни разу не видел у них таких проблем, плюс многие считают именно эти модели лучшими для работы/генерации контента конкретно на русском языке.

У китайских моделей, кстати, проблемы с языками бывают и на больших моделях, которыми пользуются у них в чате/через API.
Например, занятный момент был, что вышедшая на рубеже 2024/2025 Deepseek v3 отлично работала с русским языком, а вот после её обновления до v3.1 летом 25-го пользователи стали жаловаться, что у модели стали проскакивать некоторые слова в ответах на других языках. Т.е. модель стала лучше, но похоже как-то сказалось, что её ещё больше затачивали под "родную" аудиторию на китайском языке, что в какой-то мере негативно повлияло на использование на русском.


В прошлый раз ещё кстати не упоминал модели gpt-oss, которые выпустили в опенсорс OpenAI этим летом. Тоже весьма хорошие модели, младшая причём умещается в 12 Гб видеопамяти. Проблем с русским, как и других артефактов не замечал. Из минусов в использовании и подключении к Zenno скорее то, что это модель для ризонинга, т.е. заточена под узкий "не творческий" спектр задач, который при подключении к ZP вряд ли кто использует.
 
  • Спасибо
Реакции: seodamage и volody00

Кто просматривает тему: (Всего: 1, Пользователи: 0, Гости: 1)