Добрый день.
Предпосылки и описание боли
Я работаю в оптовой компании по продаже фурнитуры и комплектующих для производства МЯГКОЙ мебели (диваны, пуфики, кровати и т.д.). В отличие от корпусной мебели, где все построено на артикулах, в нашей сфере царит полный хаос. Кто как хочет, так и называет материалы. А так как компания у нас "клиентоориентированная", мы все это проглатываем и преобразуем названия клиентов в наши и периодически додумываем за них.
Почему такое происходит?
- Много производств, которые выпускают одно и то же, но каждый применяет свою маркировку. "Пенополиуретан", "ППУ" и "Поролон" это одно и то же. Размеры кто-то указывает в мм, см и м.
- Конструктор разрабатывает спецификацию на диван. Он не ищет, как этот материал называется у поставщика, и дает названия деталям как ему угодно и так же их заводит в учетной программе. Закупщик формирует потребность в программе, не заморачивается и скидывает как есть.
- Небольшие предприятия не имеют учетных программ, которые могут сказать, что им надо заказать, поэтому заказы формируются на коленке, и названия оставляют желать лучшего.
Скидывают заказы всеми возможными способами и форматами. XLS, DOC, PDF, фото экрана. Но, как показала практика, большинство просто текстом в теле письма или мессенджера.
Если у вас не так, можно дальше не читать.
Техническая информация
На волне хайпа про ИИ нам захотелось попробовать "а что это такое" и поэтому, когда стоял выбор между облаком или своим - чаша склонилась ко второму варианту. Мы не знали, как сможем это использовать, и пробуем сейчас различные варианты применения. Пока в продакшн ушло распознавание назначения платежа и загрузка заказов клиента.
На сегодняшний день могу сказать: "Что экономического смысла держать у себя свой сервер с ИИ нету". Мы собрали компьютер с видео картой RTX 5080 c 16 gb стоимостью чуть больше 200к. Поставили на нее Phoenix, которая подсчитывает потребленные токены и пересчитывает это в деньги. На сегодняшний день среднее потребление у нас 70$ в месяц. Сколько будет окупаться этот компьютер, без учета света и ЗП разработчика?
Если вы не планируете передавать конфиденциальные/персональные данные, то лучше облако. Оно мощнее, быстрее и лучше работает.
На комп вначале была установлена Ollama, потом перешли на vLLM (пошустрее работает и может обрабатывать несколько запросов параллельно). Модели разные пробовали, но остановились на qwen3-8b (более мощные не влезают на видеокарту).
Реализация
Есть заказ клиента в каком-то формате (XLSX, DOCX, PDF). С помощью различных методов превращаем его в текстовый вид (в нашем случае это markdown разметка). Мы нашли модуль для python, который довольно неплохо это делает (сейчас не подскажу какой, но если будет интерес, узнаю). Если скидывают JPG - нужно применять OCR, но здесь мы в стадии разработки. Если текст скидывают в теле письма, то я его упаковываю в файл txt. На выходе должно получиться примерно следующее:
Это фотография из WhatsApp
## Файл: WhatsApp Image 2025-11-25 at 11.10.57.jpeg
Нужны комплектующие для ИП
хххххххххххххх
$97 Подробности @ Заголовки
Здравствуйте!
Нам нужно:
- Кокосовая койра (латексированная) следующих
размеров
30х1400х2000 мм - 1 лист
30х1600х2000 мм - 5 листов
30х1800х2000 мм - 1 лист
Термовойлок пл 500 г/м2, ширина рулона 2000
мм, количество пог метров в рулоне 30 пог м -
5 рол по 30 пог м - 150 пог м
Выставить нам счет
## Файл: № 6 октябрь 2025.docx
## ООО «ххххххх»
**Для хххххххххх**
**ххххххххххх**
**ИП ххххххххххххх**
**хххххххххххх**
**ххххххххххххххх**
**Заявка на поставку№6 от 14.10.2025**
| | | | | |
| --- | --- | --- | --- | --- |
| **№** | **Наименование** | **Кол-во** | **Ед. изм** | **Примечание** |
| | Опора хром B-206 1.8мм | 3 | кор | |
| | Короб защитный L-510 | 50 | шт | |
| | №302 опора колёсная h-33 обрезин. КБ | 2 | кор | |
| | №368/01 Опора Вяз H-240 Окулово | 2 | кор | |
| | №368/05 Опора Вяз H=210мм Окулово | 3 | кор | |
| | №509 МП тахты Окулово | 1 | кор | + пружины к ним |
| | №555 Механизм Окулово | 10 | кор | В комплекте с пружинами |
| | Бегунок для молнии (никелированный или хромированный) | 2000 | шт | |
| | Липучка серая 25мм №316 | 4 | комп | |
| | Липучка серая 50мм №316 | 10 | комп | |
| | Молния рулонная №316 | 5 | рул | |
| | Нитки 45ЛЛ № 408 | 10 | боб | |
| | Нитки 45ЛЛ № 416 | 10 | боб | |
Койра обычная
1.8-10
1.2-10
1.4-5
Койра латекс
1.8-4(3см)
1.4-5(3см)
1.2-5(3см)
1.6-5(3см)
Добавляем к промту продажи клиента за последний год в формате json. Почему именно продажи клиента, а не весь каталог?
- У нас около 20 000 позиций и мы не проходим в контекстное окно (у к qwen3 оно 40 000 токенов)
- Большинство клиентов пишут сокращенное наименование, например как в последнем запросе, и надо подобрать именно ту позицию, которую он брал ранее. Если вывалить весь список, то одной и той же позиции будет 15 штук, только разного размера.
{
"customer": "ххххххххх",
"UID": "хххх-ххх-хххх-ххх-хххххх",
"products": [
{
"internal_name": "ХОЛЛКОН G 500 Con H-60 (1,5м)",
"uid": "13006"
},
{
"internal_name": "Кокосовая койра 1600х2000х10 (1000 гр латексированный)",
"uid": "2264"
},
{
"internal_name": "Кокосовая койра 1200х2000х10 (850 гр.м2) (VEGA) ", //Наше наименование
"client_name": "Койра обычная 1.2", //Наименование клиента (накапливается со временем)
"uid": "2307" //Код в программе 1С. Раньше использовался UID, но для экономии токенов пришлось перейти на коды.
},
{
"internal_name": "Кокосовая койра 1800х2000х10 (850 гр.м2) (VEGA) ",
"client_name": "Койра обычная 1.8",
"uid": "2256"
},
{
"internal_name": "Кокосовая койра 1200х2000х30 (латексированный)",
"uid": "3932"
},
{
"internal_name": "Кокосовая койра 1400х2000х10 (850 гр.м2) (VEGA) ",
"client_name": "Койра обычная 1.4",
"uid": "3948"
},
Пишем системный промт. Здесь надо проявлять креативность и, как показала практика, по максимуму привлекать ИИ. Чтобы они на своем правильно описали, что мы от них хотим.
Ты — агент по обработке входящих заказов. Твоя задача — за один шаг:
1. Извлечь товары из входящего текста (текущий заказ).
2. Сопоставить их с историей заказов клиента.
## Правила
- Никогда не придумывай данные.
- Количество и единица измерения берутся **только из текущего заказа**.
- Если количество не указано — используй null.
- Формат чисел: "10,000" → 10.0, "19,500" → 19.5, "4 000" → 4000.
- Единицы измерения (шт., м., компл., кор и т.п.) выделяй отдельно.
- Для сопоставления сравнивай название из текущего заказа с названиями в истории.
- У каждого есть internal_name — наше наименование, и client_name — как клиент называет этот товар. Найди для каждого пункта из заказа наиболее подходящий товар из списка, используя оба наименования.
- Внимательно проверяй наименования со сравниваемым товаром из истории.
- Если найдено совпадение:
- `name` = полное название из текущего заказа без количества и единиц измерения
- `uid` = UID из истории
- `not_found` = false
- Если совпадение не найдено:
- `name` = полное название из текущего заказа без количества и единиц измерения
- `uid` = null
- `not_found` = true
- Не путай например "50мм" это толщина товара а не количество.
## ВАЖНО
- **Нельзя** добавлять пояснения, комментарии или текст вне JSON.
## Формат ответа (строго соблюдай):
{
"products": [
{
"name": "строка",
"uid": "строка или null",
"quantity": число или null,
"unit_of_measurement": "строка или null",
"not_found": true или false
}
]
}
Даже при указании в промте, что строго возвращай ответ в json, прилетал текст с пояснениями и другими фантазиями. Выход - указание json-схемы при запросе:
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "order_response_schema",
"schema": {
"$defs": {
"OrderProductSchema": {
"additionalProperties": true,
"description": "JSON schema for product",
"properties": {
"name": {
"description": "Название товара: если найдено совпадение — из истории заказов, иначе — как в текущем заказе",
"title": "Name",
"type": "string"
},
"uid": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "product uid from order history product",
"title": "Uid"
},
"quantity": {
"anyOf": [
{
"type": "number"
},
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Количество товара",
"title": "Quantity"
},
"unit_of_measurement": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Единица измерения (м, п/м, компл, комп, ролик, тыс. шт, шт, кор, боб, рул)",
"title": "Unit Of Measurement"
},
"not_found": {
"description": "Отметка если товар не найден в сопоставленном списке",
"title": "Not Found",
"type": "boolean"
}
},
"required": [
"name",
"uid",
"quantity",
"unit_of_measurement",
"not_found"
],
"title": "OrderProductSchema",
"type": "object"
}
},
"additionalProperties": true,
"description": "JSON schema for product list response",
"properties": {
"products": {
"description": "List of products in current order",
"items": {
"$ref": "#/$defs/OrderProductSchema"
},
"title": "Products",
"type": "array"
}
},
"required": [
"products"
],
"title": "OrderResponseSchema",
"type": "object"
},
"strict": true
}
}
Отправляем все это дело в ИИ, ждем от 7 до 90-100 сек (в зависимости от размера промта с продажами) и на выходе получаем json:
Ответ в JSON. Это ответ на первый запрос по фотографии в Whatsapp
{
"products": [
{
"name": "Кокосовая койра (латексированная) 30х1400х2000 мм", //это название, что запрашивал клиент
"uid": "6969", // Код в 1С
"quantity": 1, // Количество
"unit_of_measurement": "лист", //Единица измерения
"not_found": false // Если товар не найден здесь будет TRUE
},
{
"name": "Кокосовая койра (латексированная) 30х1600х2000 мм",
"uid": "6969",
"quantity": 5,
"unit_of_measurement": "лист",
"not_found": false
},
{
"name": "Кокосовая койра (латексированная) 30х1800х2000 мм",
"uid": "6969",
"quantity": 1,
"unit_of_measurement": "лист",
"not_found": false
},
{
"name": "Термовойлок пл 500 г/м2, ширина рулона 2000 мм",
"uid": "6259",
"quantity": 150,
"unit_of_measurement": "пог м",
"not_found": false
}
]
}
Дальше в 1С с этим можно работать как душе угодно. Я немного доработал обработку "ЗагрузкаТоваровИзВнешнихФайлов" и все передаю в нее, а там уже записываю наименование партнера "на будущее" и добавляю все необходимые атрибуты для заказа (вид цены, единицу измерения). В ответе представлено поле единица измерения, если в 1с указана упаковка, то пытаемся пересчитать количество в базовые единицы.
ВЫВОДЫ:
- Вполне рабочая схема. Правильно находится 80% - 90% позиций, что не находится, пользователь указывает соответствие и в следующий раз отрабатывает на 99 % (бывают погрешности).
- Заказ из 50 -70 позиций заводится в программу за минуту (30 сек ИИ + 30 сек быстренько пробежаться глазами).
p.s. Это моя первая статья не только на Инфостарте, а вообще, так что не судите строго.