373 lines
18 KiB
Plaintext
373 lines
18 KiB
Plaintext
Да. И ключевая мысль тут такая: **эта система не заменяет ваш продуктовый контур**, а вешается **рядом** с ним как внешний тестовый раннер и слой контроля качества.
|
||
|
||
То есть не так: “перенесли ассистента в 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"
|