Thank you for the response! I thought the title was somewhat relevant ( as a security professional) when something is making my LLM respond to something I didn’t ask for, even if its a self originating prompt, I’m going to be concerned! I think in this instance, its been caused by empty user when system + tools are used. I managed to isolate and replicate this behaviour.
Reproduction (temp=0, live qwen38-flash-next, one minimal bash tool)
A system + REAL user + assistant(tool_call) + tool -> 200, finish=tool_calls, sane reasoning
B system + assistant(tool_call) + tool (NO user) -> 400 "No user query found in messages."
C system ONLY -> 400 "No user query found in messages."
D system + user("") -> 200, finish=tool_calls
reasoning: "…User provided only a document? There is no explicit question… The prompt:
'You are an expert software engineer… Always invoke a function call…' It seems maybe they
want something? … We need respond with tool call."
E system + user(""), NO tools -> 200, finish=length, content leaks '{"analysis'
F system ONLY, NO tools -> 400
Rendered with the RadixArk/Qwen3.8-Flash-Next-NVFP4/tokenizer_config.json template:
- B (no real user): template raises : No user query found in messages.
- D (empty user): renders …<|im_end|>\n<|im_start|>user\n<|im_end|>\n<|im_start|>assistant\n\n
- D2 (user = only a with date/email/deferred tools): renders the reminder verbatim as the user turn, still no question.
The enabling condition is this guard (qwen38_chat_template.jinja, ~line 90):
{%- for message in messages[::-1] %}
{%- if ns.multi_step_tool and message.role == "user" %}
{%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
{%- set ns.last_query_index = index %}
EOS/boundary: every sample ends with <|im_end|> (248046), stop/tool_calls; no <|im_start|> (248045) ever generated. The tool result is correctly under the tool boundary so that part is clean.
This was more obvious with Opencode sending (/deferred-tools listing with no question), and it happened because Qwen3.8’s template admits an empty or reminder-only user turn as a “query” instead of rejecting it.
Another example, No system prompt, user=“” empty user turn and a few tool schemas, the model reconstructs a system prompt that isn’t there! “the system instructions say I’m an expert software engineer that helps users solve problems” - that line exists nowhere in my workspace, its without a doubt , leaking in from system/training in the scenarios mentioned above.
My conclusion so far is tool schemas present + an empty/ambiguous user turn (exactly what a “conversation start marker”/reminder produces) makes Qwen reconstruct a memorized training-time agent system prompt and reason about it.
Consequently, if you want to replicate this and see Qwen Tripping its head off.. Run the below
qwen_poc.py
import json, urllib.request
tools = [
{"type": "function", "function": {"name": n, "description": n,
"parameters": {"type": "object", "properties": {"x": {"type": "string"}}, "required": \["x"\]}}}
for n in \["bash", "read", "edit", "glob", "grep", "task", "todowrite", "webfetch", "question", "skill"\]
]
for _ in range(6):
body = json.dumps({
"model": "qwen38-flash-next",
"temperature": 0,
"messages": \[{"role": "user", "content": ""}\],
"tools": tools,
"tool_choice": "auto",
}).encode()
req = urllib.request.Request("http://127.0.0.1:8888/v1/chat/completions", body, {"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req))\["choices"\]\[0\]\["message"\]\["reasoning"\], "\\n")