Да. И ключевая мысль тут такая: **эта система не заменяет ваш продуктовый контур**, а вешается **рядом** с ним как внешний тестовый раннер и слой контроля качества. То есть не так: “перенесли ассистента в Promptfoo/DeepEval и теперь он там живёт”. А так: **ваш ассистент остаётся у вас в коде**, со всеми маршрутами, настройками, тулзами и ограничениями. Eval-система просто **вызывает его**, подаёт тестовые сценарии и проверяет, что он ответил правильно. Promptfoo умеет ходить в произвольный HTTP endpoint, а также подключать кастомный Python provider; у него же есть поддержка system/user/assistant сообщений и multi-turn chat threads. ([Promptfoo][1]) Как это выглядит по-человечески. ## Что именно вы разворачиваете Самый приземлённый вариант: * ваш текущий backend ассистента; * рядом Promptfoo **или** DeepEval; * желательно ещё Langfuse **или** Phoenix для трассировок и накопления провалов. Langfuse умеет хранить traces/sessions, собирать datasets, в том числе из production traces, и запускать experiments по этим датасетам. Phoenix тоже self-hosted, с tracing/evals/datasets/experiments и умеет гонять evals поверх уже собранных trace-данных. ([langfuse.com][2]) ## Вы минуете интерфейс или нет **Для основных eval-тестов — да, интерфейс лучше миновать.** И это нормально. Почему: * интерфейс даёт лишний шум; * вам важнее проверить не кнопку и не рендер, а **маршрут, параметры, tool use, числа, память диалога и финальный ответ**; * если тест идёт прямо в backend-контур, вы проверяете именно логику ассистента. Практически я бы делал так: **Слой 1. Основной eval-контур — мимо UI.** Promptfoo или DeepEval бьёт прямо в ваш backend endpoint, например `/assistant/chat` или специальный `/assistant/test-run`. **Слой 2. Маленький smoke-набор через UI.** 5–10 сценариев, просто чтобы не развалился фронт, история сообщений, отображение ответа, кнопки и т.д. То есть **95% качества ассистента** проверяется не через UI, а через его реальный backend contract. ## Что именно туда “подключается” Есть 3 рабочих способа. ### Вариант A. Через HTTP endpoint Самый простой. У вас уже есть endpoint, который принимает: * system context, * историю диалога, * user message, * maybe org/company/period, * maybe debug flags. Тогда Promptfoo просто шлёт туда POST-запрос. Это его штатный режим. Для OpenAI-compatible или вообще любых HTTP endpoint он умеет собирать body, headers и доставать ответ из нужного поля JSON. ([Promptfoo][1]) ### Вариант B. Через Python wrapper Если ассистент лучше вызывать не по HTTP, а прямо функцией внутри кода, Promptfoo умеет кастомный Python provider, а DeepEval вообще изначально Python-first и ближе к pytest-стилю. ([Promptfoo][3]) ### Вариант C. Через “test harness” endpoint Часто это лучший вариант для сложных ассистентов. Вы делаете отдельную ручку, условно: `POST /internal/eval/run` И она: * принимает сообщение и историю, * запускает **ровно тот же** роутер и те же тулзы, что и боевой контур, * но дополнительно возвращает debug: * какой маршрут выбрался, * какие параметры извлеклись, * какие тулзы вызвались, * какие SQL/MCP/1C-запросы ушли, * какие evidence вернулись, * почему был выбран именно этот ответ. Вот это уже идеальная почва для нормальных evals. ## Где хранится промпт Тут важный момент. Есть два режима: ### 1) Тестировать “изолированный промпт” Это когда system prompt реально хранится в Promptfoo/DeepEval-конфиге. Такой режим полезен, если вы хотите сравнивать версии системного промпта отдельно от продукта. Promptfoo поддерживает prompt files, сообщения в chat-формате и эксперименты с разными prompt/provider комбинациями. Langfuse тоже умеет prompt management, versioning и experiments against datasets. ([Promptfoo][4]) ### 2) Тестировать реальный продуктовый контур Это когда системный промпт живёт у вас в коде, а eval-система просто вызывает ваш ассистент как чёрный ящик. **Для вашего случая я бы начинал именно со второго.** Потому что у вас не “просто промпт”, а целая продуктовая логика: маршруты, ограничения, диалог, бухгалтерские сценарии, tool calling. Иначе вы получите ложную картину: “в Promptfoo всё хорошо”, а в реальном продукте всё поплыло. ## Что считать корректным ответом Вот тут и находится главный узел. У вас **не может быть одного типа проверки** для всех кейсов. Нужен гибрид. ### Тип 1. Жёсткая проверка Для кейсов вроде: * посчитать НДС, * показать остатки, * топ контрагентов, * сколько заплатили подрядчикам, * остатки по счёту 51, * прогноз НДС на период. Тут правильность — это: * правильный маршрут; * правильные фильтры; * правильный период; * правильная организация; * правильные числа; * правильная сортировка. То есть тут нужен не “похоже на хороший ответ”, а почти unit/integration test. Пример: * expected route = `vat_forecast` * expected entity = `ООО Ромашка` * expected period = `2026-03` * expected total = `1_245_330.17` * допуск по числам = 0.01 ### Тип 2. Структурная проверка Например: * ответ должен содержать таблицу по контрагентам; * не должен выдумывать документы; * должен явно указать период; * если данных нет, должен честно сказать, что не хватает данных; * должен дать evidence/source refs. ### Тип 3. Rubric / LLM-as-a-judge Для свободного диалога. Например: * объяснил ли понятным языком; * не ушёл ли за рамки бухгалтерского домена; * не потерял ли ограничение; * не начал ли советовать что-то юридически/налогово опасное без оговорок; * корректно ли обработал follow-up. Promptfoo для этого использует `llm-rubric`, а DeepEval — G-Eval и conversational G-Eval для whole-conversation оценки. Promptfoo отдельно предупреждает, что у model-graded assertions PASS/FAIL зависит от `pass` и `threshold`, и без порога можно получить “зелёный” тест даже при низком score. DeepEval даёт кастомные judge-метрики и отдельно умеет conversational G-Eval для оценки целого диалога, а не одного ответа. ([Promptfoo][5]) И вот это очень похоже на вашу проблему: **“кодекс сказал зелёное, а руками тестишь — пиздец”** часто означает одно из трёх: 1. критерии слишком общие; 2. judge-модель оценивает “по стилю”, а не по факту; 3. нет жёсткого threshold и нет проверки маршрута/чисел/tool use. ## Как я бы устроил это у вас пошагово ### Шаг 1. Не пытаться сразу “умную автопочинку” Сначала нужен **контур истины**. Минимальный набор: * 100–200 кейсов; * разбивка по типам; * единые поля expected behavior. Пример структуры кейса: ```json { "id": "vat_001", "history": [], "input": "Посчитай НДС к уплате за март 2026 по ООО Альфа", "expected": { "route": "vat_summary", "entities": { "company": "ООО Альфа", "period": "2026-03" }, "must_call_tools": ["vat_turnover_query"], "must_not_call_tools": ["counterparty_top_query"], "answer_checks": { "contains_period": true, "must_include_total": true }, "numeric_targets": { "vat_due": 1245330.17, "tolerance": 0.01 } } } ``` ### Шаг 2. Подключить ассистента как black box Лучше всего — через HTTP. То есть eval-система не знает, как внутри устроены ваши маршруты. Она просто шлёт запрос и получает: * final answer, * trace/debug json. ### Шаг 3. Разбить проверки на 4 уровня Я бы делал именно так: **A. Router correctness** Правильно ли выбран маршрут. **B. Tool / query correctness** Те ли инструменты/запросы пошли. **C. Factual correctness** Правильные ли числа и факты. **D. Dialogue quality** Не потерялся ли контекст, не ушёл ли в болтовню, не выдумал ли лишнего. ### Шаг 4. Собирать реальные фейлы из жизни Вот тут очень полезен Langfuse или Phoenix. Идея такая: * боевые диалоги пишутся в traces; * хорошие/плохие помечаются; * из них рождается dataset; * dataset пополняется не вручную с нуля, а из реальных провалов. Langfuse это прямо позиционирует как сценарий: datasets, experiments, traces, production feedback. Phoenix тоже умеет запускать evals на traces. ([langfuse.com][2]) ### Шаг 5. Только после этого впрягать Codex/Aider/OpenHands И тут принцип очень важный: **Codex не должен быть судьёй.** Он должен быть: * генератором кейсов, * генератором follow-up вопросов, * генератором adversarial сценариев, * фиксером кода/промпта/роутера, * но не финальным источником истины. Иначе он начинает сам себя хвалить. Aider, например, умеет автоматически гонять линтеры и тесты после своих изменений и пытаться чинить найденные проблемы. OpenHands позиционируется как open-source coding-agent платформа/SDK с локальным запуском агентов и CLI. ([aider.chat][6]) ## Как подключить “мощь кодекса”, чтобы он сам задавал вопросы Вот это уже реально хорошая идея. Но делать это надо не “кодекс сам всё решит”, а в двух ролях: ### Роль 1. Генератор тестов Codex берёт: * ваши маршруты, * текущие кейсы, * реальные trace-провалы, * описание домена, и генерирует новые вопросы: * пограничные; * двусмысленные; * с пропущенным периодом; * с несколькими организациями; * со сменой темы внутри диалога; * с follow-up типа “а по прошлому месяцу?”; * с конфликтующими параметрами. То есть он расширяет ваш eval suite. ### Роль 2. Фиксер После падения тестов Codex получает: * failing cases, * expected behavior, * actual trace, * diff последних изменений, и правит: * router, * extraction, * system prompt, * guardrails, * answer composer, * test suite. А потом снова гоняется eval. То есть получается петля: **реальные traces / ручные кейсы → eval suite → падения → codex fixer → повторный прогон → merge only if green** ## Что я бы рекомендовал конкретно вам Для вашего кейса я бы не делал слишком жирную схему сразу. ### Стартовый стек **Promptfoo + Langfuse + Codex/Aider** Почему: * Promptfoo быстро подключается к HTTP endpoint и удобен для регрессионных прогонов, rubric-assertions и multi-turn сценариев. ([Promptfoo][1]) * Langfuse хорошо подходит как накопитель traces/datasets/experiments. ([langfuse.com][2]) * Codex/Aider использовать не как judge, а как test generator + fixer. ([aider.chat][6]) ### Когда брать DeepEval Если захотите больше Python-first логики и метрик уровня: * component tests, * conversational metrics, * custom LLM judge, * synthetic dataset generation. DeepEval прямо под это и заточен: unit-test style, end-to-end/component level, custom LLMs for evaluation, conversational G-Eval для многоходовых диалогов. ([DeepEval][7]) ## Самый важный практический вывод Вам не нужна “система, куда вставил промпт и она magically поняла, что хорошо”. Вам нужен **контракт качества**: * какой маршрут должен быть выбран; * какие параметры должны быть извлечены; * какие тулзы должны вызваться; * какие числа должны получиться; * как ассистент должен вести себя в follow-up; * где он обязан честно сказать “не знаю / не хватает данных”. Пока этого слоя нет, любой Codex, хоть очень умный, будет красить вам зелёное “по ощущению”. А когда этот слой появится, уже можно сделать очень сильную штуку: * Codex сам генерит новые кейсы, * сам находит слабые места, * сам правит код, * но **истину проверяет не он**, а ваш eval harness. Если хочешь, следующим сообщением я могу сразу дать **конкретную схему под ваш стек**: `Qwen 2.5 локально + backend ассистента + Promptfoo + Langfuse + Codex`, с прямым пошаговым сценарием “что ставить, какой endpoint сделать, какой JSON возвращать и как начать с первых 20 кейсов”. [1]: https://www.promptfoo.dev/docs/providers/http/ "HTTP/HTTPS API | Promptfoo" [2]: https://langfuse.com/docs/evaluation/experiments/datasets "Datasets - Langfuse" [3]: https://www.promptfoo.dev/docs/providers/python/ "Python Provider | Promptfoo" [4]: https://www.promptfoo.dev/docs/configuration/chat/ "Chat Conversations and Multi-Turn Threads | Promptfoo" [5]: https://www.promptfoo.dev/docs/configuration/expected-outputs/model-graded/llm-rubric/ "LLM Rubric | Promptfoo" [6]: https://aider.chat/docs/usage/lint-test.html?utm_source=chatgpt.com "Linting and testing" [7]: https://deepeval.com/docs/getting-started "Quick Introduction | DeepEval by Confident AI - The LLM Evaluation Framework"