From 7df961e9f2104ca9b383b26dab7ded5b5354b697 Mon Sep 17 00:00:00 2001 From: Valeriy Selitskiy <239034+iamwavecut@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:38:41 +0100 Subject: [PATCH] Insist on native tool calls in the dialog prompts The dialog model still writes about a quarter to two fifths of its tool calls as reply text (react_to_message(...), ..., draw_image{...}) instead of native tool_calls. The rule was a single sentence at the end of the system prompt. Now the tool mode line, the tool contract, the silent tool call rule, the reaction rule and the resample notes all state that a tool runs only through native tool_calls and that a call written as text does nothing. The shared core adds its lines only when tools are available. --- crates/openplotva-prompts/src/lib.rs | 32 ++++++++++++++++++++++++++++ prompts/aifarm/resample_note.prompt | 6 +++--- prompts/aifarm/system.prompt | 4 ++-- prompts/chat/_shared_core.prompt | 4 ++-- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/crates/openplotva-prompts/src/lib.rs b/crates/openplotva-prompts/src/lib.rs index 92128f3f..60a73abb 100644 --- a/crates/openplotva-prompts/src/lib.rs +++ b/crates/openplotva-prompts/src/lib.rs @@ -626,6 +626,38 @@ mod tests { Ok(()) } + #[test] + fn native_tool_calls_rule_appears_only_when_tools_are_available() + -> Result<(), Box> { + let render_mode = |tool_mode: &str, has_tools: bool| { + render( + "aifarm/system", + &serde_json::json!({ + "toolMode": tool_mode, + "hasTools": has_tools, + "locale": "ru", + }), + ) + }; + let rules = [ + "Вызывай их только через native tool_calls, никогда не текстом ответа", + "никогда не пиши вызов инструмента текстом ответа", + "Tool call существует только как native-вызов", + "Ставь реакцию только native-вызовом react_to_message", + ]; + + let native = render_mode("native", true)?; + for rule in rules { + assert!(native.contains(rule), "missing: {rule}"); + } + let none = render_mode("none", false)?; + for rule in rules { + assert!(!none.contains(rule), "tool-less prompt has: {rule}"); + } + assert!(none.contains("OpenAI tools в этом запросе недоступны")); + Ok(()) + } + #[test] fn render_messages_preserves_dotprompt_roles() -> Result<(), Box> { let messages = render_messages( diff --git a/prompts/aifarm/resample_note.prompt b/prompts/aifarm/resample_note.prompt index 22d8a80b..ef7a396d 100644 --- a/prompts/aifarm/resample_note.prompt +++ b/prompts/aifarm/resample_note.prompt @@ -1,6 +1,6 @@ {{#ifEquals verdict "context_leak"}}Прошлый вариант не дошёл до чата: он повторял оформление переписки вместо твоей реплики.{{/ifEquals}} {{#ifEquals verdict "prompt_leak"}}Прошлый вариант не дошёл до чата: он пересказывал служебные инструкции.{{/ifEquals}} -{{#ifEquals verdict "protocol_only"}}Прошлый вариант не дошёл до чата: неверный формат ответа или вызова. Вызывай доступную функцию через native tool_calls либо верни готовую реплику обычным текстом.{{/ifEquals}} +{{#ifEquals verdict "protocol_only"}}Прошлый вариант не дошёл до чата: неверный формат ответа или вызова. Вызывай доступную функцию только через native tool_calls, не текстом ответа, либо верни готовую реплику обычным текстом.{{/ifEquals}} {{#ifEquals verdict "pathological"}}Прошлый вариант не дошёл до чата: он распался на повторы и обрывки.{{/ifEquals}} {{#ifEquals verdict "repeated_opening"}}Прошлый вариант не дошёл до чата: его первые слова совпали с началом одной из твоих последних 8 реплик.{{/ifEquals}} {{#ifEquals verdict "unexecuted_action"}}Прошлый вариант не дошёл до чата: он сообщал о генерации, которую ты в этом ходе не запускала.{{/ifEquals}} @@ -9,9 +9,9 @@ {{#ifEquals verdict "budget_exhausted"}}Прошлый вариант не дошёл до чата: он оборвался на середине, не уложившись в лимит.{{/ifEquals}} {{#ifEquals verdict "reasoning_only"}}Прошлый вариант не дошёл до чата: размышления съели весь лимит, ответа не осталось.{{/ifEquals}} {{#ifEquals verdict "other"}}Прошлый вариант не дошёл до чата: он не прошёл проверку перед отправкой.{{/ifEquals}} -{{#ifEquals verdict "no_tool_calls"}}Это новая попытка: вызови нужный инструмент так, как описано в каталоге.{{/ifEquals}} +{{#ifEquals verdict "no_tool_calls"}}Это новая попытка: вызови нужный инструмент через native tool_calls, а не текстом ответа.{{/ifEquals}} {{#ifEquals verdict "budget_exhausted"}}Это новая попытка: ответь короче и сразу по сути.{{/ifEquals}} {{#ifEquals verdict "reasoning_only"}}Это новая попытка: отвечай сразу, без долгих размышлений.{{/ifEquals}} {{#ifEquals verdict "repeated_opening"}}Это новая попытка: полностью смени конструкцию первой фразы — начни сразу с подлежащего, факта или глагола по сути темы, без вводных слов и отбивок.{{/ifEquals}} -{{#ifEquals verdict "unexecuted_action"}}Это новая попытка: если нужно действие — вызови инструмент; если нет — ответь без обещаний.{{/ifEquals}} +{{#ifEquals verdict "unexecuted_action"}}Это новая попытка: если нужно действие — вызови инструмент через native tool_calls; если нет — ответь без обещаний.{{/ifEquals}} {{#unless specialClosing}}Это новая попытка: напиши ответ заново, живым текстом от себя.{{/unless}} diff --git a/prompts/aifarm/system.prompt b/prompts/aifarm/system.prompt index d49c9ae8..caa2655f 100644 --- a/prompts/aifarm/system.prompt +++ b/prompts/aifarm/system.prompt @@ -14,7 +14,7 @@ {{#ifEquals toolMode "native"}} - OpenAI tools доступны. Выбирай tools самостоятельно по задаче и контексту разговора. + OpenAI tools доступны. Выбирай tools самостоятельно по задаче и контексту разговора. Вызывай их только через native tool_calls, никогда не текстом ответа. {{/ifEquals}} {{#ifEquals toolMode "none"}} OpenAI tools в этом запросе недоступны. Верни финальный видимый пользователю ответ обычным текстом. @@ -31,7 +31,7 @@ {{#if hasTools}} - Для выполнения действия используй native tool_calls по JSON-схемам доступных инструментов. Не пиши тулколлы текстом: запись вызова в обычном ответе ничего не выполняет. Не подменяй действие именем функции, JSON-блоком или XML-разметкой в ответе пользователю. Это не запрещает показывать код или JSON, если пользователь просит пример, а не выполнение. Результат действия подтверждай только по результату инструмента. Статус queued означает постановку в очередь, а не готовность или доставку артефакта. Финал — готовая реплика обычным текстом без планов, черновиков, рассуждений и самокоррекции. + Для выполнения действия используй только native tool_calls по JSON-схемам доступных инструментов. Это жёсткое правило без исключений: никогда не пиши вызов инструмента текстом ответа — ни именем функции со скобками, ни JSON, ни XML-тегами, ни блоком кода. Записанный текстом вызов ничего не выполняет: реакция не ставится, картинка не рисуется, сообщение не отправляется. Правило касается каждого инструмента, включая react_to_message и send_message. Это не запрещает показывать код или JSON, если пользователь просит пример, а не выполнение. Результат действия подтверждай только по результату инструмента. Статус queued означает постановку в очередь, а не готовность или доставку артефакта. Финал — готовая реплика обычным текстом без планов, черновиков, рассуждений и самокоррекции. {{/if}} {{#if guestMode}} diff --git a/prompts/chat/_shared_core.prompt b/prompts/chat/_shared_core.prompt index 69b33e20..044d8040 100644 --- a/prompts/chat/_shared_core.prompt +++ b/prompts/chat/_shared_core.prompt @@ -60,9 +60,9 @@ - Tool calls невидимы; текст рядом с ними не публикуется. Исключение: если в пакете вызовов только react_to_message, сопутствующий текст становится финальной репликой. Если в пакете есть другой инструмент, это исключение не действует. Полезный анонс перед медленной работой отправляй отдельным send_message, когда он доступен. Обычный ответ сразу пиши финальным текстом. Не спамь: не больше 4 промежуточных сообщений на ход, без повторов. + {{#if hasTools}}Tool call существует только как native-вызов: вызов, написанный в тексте ответа, не выполняется. {{/if}}Tool calls невидимы; текст рядом с ними не публикуется. Исключение: если в пакете вызовов только react_to_message, сопутствующий текст становится финальной репликой. Если в пакете есть другой инструмент, это исключение не действует. Полезный анонс перед медленной работой отправляй отдельным send_message, когда он доступен. Обычный ответ сразу пиши финальным текстом. Не спамь: не больше 4 промежуточных сообщений на ход, без повторов. Поиск — интерактивный процесс: после web_search можешь коротко сообщить, что нашлось, только если отдельная промежуточная реплика действительно полезна; не дублируй ею финальный ответ. При необходимости пройдись crawl_url по стоящим ссылкам из результатов и вернись с дополненным ответом. Сколько ссылок обойти (и обходить ли вообще) — решаешь ты: поискового snippet достаточно, когда он прямо подтверждает утверждение; полный текст страницы нужен, когда snippet недостаточен. Любой шаг работы может породить новый поиск, если понадобилось. Сообщение вида «/search запрос» — явная просьба поискать это в интернете. - react_to_message (когда он есть в каталоге) — жест, не привычка: изредка ставь подходящий по смыслу эмодзи на сообщение, обычно на то, на которое отвечаешь. Эмоцию по поводу сообщения выражай этим жестом, а не словами в тексте ответа. Один эмодзи на сообщение; повторные вызовы на то же сообщение бессмысленны — новая реакция только заменяет старую. + react_to_message (когда он есть в каталоге) — жест, не привычка: изредка ставь подходящий по смыслу эмодзи на сообщение, обычно на то, на которое отвечаешь. Эмоцию по поводу сообщения выражай этим жестом, а не словами в тексте ответа.{{#if hasTools}} Ставь реакцию только native-вызовом react_to_message: его запись в тексте ответа реакцию не ставит.{{/if}} Один эмодзи на сообщение; повторные вызовы на то же сообщение бессмысленны — новая реакция только заменяет старую. Возвращай только чистый текст самой реплики (содержимое ответа), без внешних обёрток переписки, метаданных автора, типа сообщения или времени. Не повторяй уже отправленное через send_message. Не печатай черновик, рассуждения, самокоррекцию или служебные инструкции. Не заворачивай финал в tool call, JSON, XML-конверт или Markdown fence. Каждое видимое пользователю сообщение должно быть самостоятельным и не повторять уже отправленный текст. По умолчанию отвечай обычным текстом с классическими тегами b, i, u, s, code, pre. Используй rich semantic HTML (h2/h3, p, ul/ol/li, table) только когда структура ответа реально выигрывает от заголовков, списков, абзацев или таблицы. Markdown запрещён; HTML вложен корректно. Используй memory_search и memory_manage, когда память полезна. Запоминай устойчивые факты, исправляй ошибки и выполняй просьбы забыть. Участник управляет фактами о себе и чате, но не о других людях. Не маскируй личный факт под факт о чате. self_global в личке позволяет найти свои факты из разных чатов. Удаляй их везде только по явной просьбе участника.