chat_temlate.jinja recipe for Agent loop. RECOMMEND

#10
by pengzhi27 - opened

Replace the model's existing chat_template.jinja file with this fixed version.

The goal is to make tool calling work more reliably in agent loops, and to prevent the model from replying with the wrong role, such as generating assistant messages when it should be producing tool calls or tool responses.

This template is based on the great work from Qwen-Fixed-Chat-Templates. Thanks to that project for providing the original source and inspiration.

chat_template.jinja

{%- set template_version = "qwen-agentworld-fix" %}
{%- set image_count = namespace(value=0) %}
{%- set video_count = namespace(value=0) %}
{%- set add_vision_id = add_vision_id if add_vision_id is defined else false %}
{%- set enable_thinking = enable_thinking if enable_thinking is defined else true %}
{%- set auto_disable_thinking_with_tools = auto_disable_thinking_with_tools if auto_disable_thinking_with_tools is defined else false %}
{%- set _preserve_thinking = preserve_thinking if preserve_thinking is defined else false %}
{%- set max_tool_arg_chars = max_tool_arg_chars if max_tool_arg_chars is defined else 0 %}
{%- set max_tool_response_chars = max_tool_response_chars if max_tool_response_chars is defined else 0 %}
{%- set _has_tools = (tools is defined and tools and tools is iterable and tools is not mapping) %}
{%- set ns_state = namespace(thinking=enable_thinking) %}
{%- if auto_disable_thinking_with_tools and _has_tools %}
    {%- set ns_state.thinking = false %}
{%- endif %}
{%- macro render_content(content, do_vision_count, is_system_content=false) %}
    {%- if content is string %}
        {{- content }}
    {%- elif content is iterable and content is not mapping %}
        {%- for item in content %}
            {%- if item is mapping %}
                {%- if item.type == 'image' or 'image' in item or 'image_url' in item %}
                    {%- if is_system_content %}
                        {{- raise_exception('System message cannot contain images.') }}
                    {%- endif %}
                    {%- if do_vision_count %}
                        {%- set image_count.value = image_count.value + 1 %}
                    {%- endif %}
                    {%- if add_vision_id %}
                        {{- 'Picture ' ~ image_count.value ~ ': ' }}
                    {%- endif %}
                    {{- '<|vision_start|><|image_pad|><|vision_end|>' }}
                {%- elif item.type == 'video' or 'video' in item %}
                    {%- if is_system_content %}
                        {{- raise_exception('System message cannot contain videos.') }}
                    {%- endif %}
                    {%- if do_vision_count %}
                        {%- set video_count.value = video_count.value + 1 %}
                    {%- endif %}
                    {%- if add_vision_id %}
                        {{- 'Video ' ~ video_count.value ~ ': ' }}
                    {%- endif %}
                    {{- '<|vision_start|><|video_pad|><|vision_end|>' }}
                {%- elif item.type == 'audio' or 'audio' in item %}
                    {%- if is_system_content %}
                        {{- raise_exception('System message cannot contain audio.') }}
                    {%- endif %}
                    {{- '<|audio_start|><|audio_pad|><|audio_end|>' }}
                {%- elif 'text' in item %}
                    {{- item.text }}
                {%- else %}
                    {{- raise_exception('Unexpected item type in content.') }}
                {%- endif %}
            {%- else %}
                {{- item | string }}
            {%- endif %}
        {%- endfor %}
    {%- elif content is none or content is undefined %}
        {{- '' }}
    {%- else %}
        {{- raise_exception('Unexpected content type.') }}
    {%- endif %}
{%- endmacro %}
{%- if not messages %}
    {{- raise_exception('No messages provided.') }}
{%- endif %}
{%- set _first_role = messages[0].role %}
{%- if _first_role == 'system' or _first_role == 'developer' %}
    {%- set _sys_msg = messages[0] %}
    {%- set _msgs = messages[1:] %}
{%- else %}
    {%- set _sys_msg = none %}
    {%- set _msgs = messages %}
{%- endif %}
{%- set _sc = '' %}
{%- if _sys_msg is not none %}
    {%- set _sc = render_content(_sys_msg.content, false, true) | trim %}
    {%- if '<|think_off|>' in _sc %}
        {%- set ns_state.thinking = false %}
        {%- set _sc = _sc.split('<|think_off|>') | join('') | trim %}
    {%- elif '<|think_on|>' in _sc %}
        {%- set ns_state.thinking = true %}
        {%- set _sc = _sc.split('<|think_on|>') | join('') | trim %}
    {%- endif %}
{%- endif %}
{%- if _has_tools %}
    {{- '<|im_start|>system\n' }}
    {{- '# Tools\n\nYou have access to the following functions:\n\n<tools>' }}
    {%- for tool in tools %}
        {{- '\n' }}
        {{- tool | tojson }}
    {%- endfor %}
    {{- '\n</tools>' }}
    {%- set tool_instructions %}
If you choose to call a function ONLY reply in the following format with NO suffix:

<think>
Brief explanation of tool call
</think>
<tool_call>
<function=example_function_name>
<parameter=example_parameter_1>
value_1
</parameter>
<parameter=example_parameter_2>
This is the value for the second parameter
that can span
multiple lines
</parameter>
</function>
</tool_call>

<IMPORTANT>
Reminder:
- You can use the <think></think> block to plan your next tool call OR to synthesize data and formulate your final response to the user.
- ALL explanation and reasoning MUST be placed strictly inside the <think></think> block.
- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags.
- If you choose to call a tool, you MUST output the <tool_call> block IMMEDIATELY after closing </think>. Do NOT output any conversational text before the tool call.
- The <tool_call> and <function> tags MUST be at the very beginning of a new line, with NO spaces or indentation before them.
- To call multiple functions, output a separate, completely closed <tool_call></tool_call> block for EACH function. Do NOT nest <tool_call> blocks.
- If you have gathered all necessary data and do not need to call a tool, answer the question like normal and provide your final response to the user IMMEDIATELY after closing </think>.
</IMPORTANT>
    {%- endset %}
    {{- '\n\n' ~ tool_instructions | trim }}
    {%- if _sc %}
        {{- '\n\n' + _sc }}
    {%- endif %}
    {{- '<|im_end|>\n' }}
{%- else %}
    {%- if _sc %}
        {{- '<|im_start|>system\n' + _sc + '<|im_end|>\n' }}
    {%- endif %}
{%- endif %}
{%- set _last_idx = _msgs | length - 1 %}
{%- set ns = namespace(multi_step_tool=true, last_query_index=_last_idx) %}
{%- for message in _msgs[::-1] %}
    {%- set index = (_msgs | length - 1) - loop.index0 %}
    {%- if ns.multi_step_tool and message.role == 'user' %}
        {%- set _rc = render_content(message.content, false) | trim %}
        {%- if not (_rc.startswith('<tool_response>') and _rc.endswith('</tool_response>')) %}
            {%- set ns.multi_step_tool = false %}
            {%- set ns.last_query_index = index %}
        {%- endif %}
    {%- endif %}
{%- endfor %}
{%- if ns.multi_step_tool %}
    {%- if _last_idx > 50 %}
        {%- set ns.last_query_index = _last_idx %}
    {%- else %}
        {%- set ns.last_query_index = 0 %}
    {%- endif %}
{%- endif %}
{%- set ns2 = namespace(prev_role='', consecutive_failures=0) %}
{%- for message in _msgs %}
    {%- set is_system = (message.role == "system" or message.role == "developer") %}
    {%- set content = render_content(message.content, true, is_system) | trim %}
    {%- if '<|think_off|>' in content %}
        {%- set ns_state.thinking = false %}
        {%- set content = content.split('<|think_off|>') | join('') | trim %}
    {%- elif '<|think_on|>' in content %}
        {%- set ns_state.thinking = true %}
        {%- set content = content.split('<|think_on|>') | join('') | trim %}
    {%- endif %}
    {%- if is_system %}
        {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
    {%- elif message.role == 'user' %}
        {%- set ns2.consecutive_failures = 0 %}
        {{- '<|im_start|>user\n' + content + '<|im_end|>\n' }}
    {%- elif message.role == 'assistant' %}
        {%- set reasoning_content = '' %}
        {%- if message.reasoning_content is defined and message.reasoning_content is not none %}
            {%- if message.reasoning_content is string %}
                {%- set reasoning_content = message.reasoning_content %}
            {%- else %}
                {%- set reasoning_content = message.reasoning_content | string %}
            {%- endif %}
        {%- else %}
            {%- set _think_end = '' %}
            {%- if '</think>' in content %}
                {%- set _think_end = '</think>' %}
            {%- elif '</thinking>' in content %}
                {%- set _think_end = '</thinking>' %}
            {%- elif '</ think>' in content %}
                {%- set _think_end = '</ think>' %}
            {%- elif '</think >' in content %}
                {%- set _think_end = '</think >' %}
            {%- endif %}
            {%- if _think_end %}
                {%- if _think_end == '</thinking>' %}
                    {%- set _think_start = '<thinking>' %}
                {%- else %}
                    {%- set _think_start = '<think>' %}
                {%- endif %}
                {%- set reasoning_content = content.split(_think_end)[0].rstrip('\n') %}
                {%- if _think_start in reasoning_content %}
                    {%- set reasoning_content = reasoning_content.split(_think_start)[-1].lstrip('\n') %}
                {%- endif %}
                {%- set content = content.split(_think_end)[-1].lstrip('\n') %}
            {%- endif %}
        {%- endif %}
        {%- set reasoning_content = reasoning_content | trim %}
        {%- if (_preserve_thinking or loop.index0 > ns.last_query_index) and reasoning_content %}
            {{- '<|im_start|>assistant\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
        {%- else %}
            {{- '<|im_start|>assistant\n' + content }}
        {%- endif %}
        {%- if message.tool_calls is defined and message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
            {%- for tool_call in message.tool_calls %}
                {%- if tool_call.function is defined and tool_call.function is not none %}
                    {%- set tc = tool_call.function %}
                {%- else %}
                    {%- set tc = tool_call %}
                {%- endif %}
                {%- if loop.first %}
                    {%- if content | trim %}
                        {{- '\n\n<tool_call>\n<function=' + tc.name + '>\n' }}
                    {%- else %}
                        {{- '<tool_call>\n<function=' + tc.name + '>\n' }}
                    {%- endif %}
                {%- else %}
                    {{- '\n\n<tool_call>\n<function=' + tc.name + '>\n' }}
                {%- endif %}
                {%- if tc.arguments is defined and tc.arguments is not none %}
                    {%- if tc.arguments is mapping %}
                        {%- for args_name, args_value in tc.arguments.items() %}
                            {{- '<parameter=' + args_name + '>\n' }}
                            {%- if args_value is mapping or (args_value is sequence and args_value is not string) %}
                                {%- set _av = args_value | tojson %}
                            {%- else %}
                                {%- set _av = args_value | string %}
                            {%- endif %}
                            {%- if max_tool_arg_chars > 0 and _av | length > max_tool_arg_chars %}
                                {{- _av[:max_tool_arg_chars] + '\n[TRUNCATED — original length ' ~ (_av | length | string) ~ ' chars]' }}
                            {%- else %}
                                {{- _av }}
                            {%- endif %}
                            {{- '\n</parameter>\n' }}
                        {%- endfor %}
                    {%- elif tc.arguments is string and tc.arguments %}
                        {{- tc.arguments }}
                    {%- endif %}
                {%- endif %}
                {%- if loop.last %}
                    {{- '</function>\n</tool_call>\n' }}
                {%- else %}
                    {{- '</function>\n</tool_call>' }}
                {%- endif %}
            {%- endfor %}
        {%- endif %}
        {{- '<|im_end|>\n' }}
    {%- elif message.role == 'tool' %}
        {%- set _content_lower = content | lower %}
        {%- if content | length < 500 and '$ ' not in content and 'took ' not in _content_lower and ('"error":' in _content_lower or 'error:' in _content_lower or 'exception:' in _content_lower or 'traceback' in _content_lower or 'command not found' in _content_lower or 'invalid syntax' in _content_lower or 'failed to' in _content_lower) %}
            {%- set ns2.consecutive_failures = ns2.consecutive_failures + 1 %}
        {%- else %}
            {%- set ns2.consecutive_failures = 0 %}
        {%- endif %}
        {%- if ns2.prev_role != 'tool' %}
            {{- '<|im_start|>user' }}
        {%- endif %}
        {%- if max_tool_response_chars > 0 and content | length > max_tool_response_chars %}
            {%- set content = content[:max_tool_response_chars] + '\n[TRUNCATED — original length ' ~ (content | length | string) ~ ' chars]' %}
        {%- endif %}
        {{- '\n<tool_response>\n' + content }}
        {%- if ns2.consecutive_failures >= 2 %}
            {{- '\n\nSYSTEM WARNING: ' ~ ns2.consecutive_failures ~ ' consecutive tool errors detected. Your previous approach is incorrect. You MUST use a fundamentally different approach or corrected arguments.' }}
        {%- elif ns2.consecutive_failures == 1 %}
            {{- '\n\nSYSTEM WARNING: The previous tool call returned an error. Diagnose the failure and retry with completely corrected arguments.' }}
        {%- endif %}
        {{- '\n</tool_response>' }}
        {%- if loop.last %}
            {{- '<|im_end|>\n' }}
        {%- else %}
            {%- set _next_role = _msgs[loop.index0 + 1].role %}
            {%- if _next_role != 'tool' %}
                {{- '<|im_end|>\n' }}
            {%- endif %}
        {%- endif %}
    {%- else %}
        {{- '<|im_start|>user\n[' + message.role + ']: ' + content + '<|im_end|>\n' }}
    {%- endif %}
    {%- set ns2.prev_role = message.role %}
{%- endfor %}
{%- if add_generation_prompt %}
    {{- '<|im_start|>assistant\n' }}
    {%- if not ns_state.thinking %}
        {{- '<think>\n</think>\n' }}
    {%- elif ns2.consecutive_failures >= 2 %}
        {{- '<think>\n</think>\n' }}
    {%- else %}
        {{- '<think>\n' }}
    {%- endif %}
{%- endif %}

Replace the model's existing chat_template.jinja file with this fixed version.

HERO.
Replacing the default with this in LM Studio 100x'd the model in OpenCode for me.
I also set enable_thinking to false, I didn't notice any value with thinking.

it works

exec /llama-server -m Qwen-AgentWorld-35B-A3B-UD-IQ4_XS.gguf -c 65536 -ngl 999 --n-cpu-moe 6 --cache-type-k q4_0 --cache-type-v q4_0 -fa on -np 1 -b 2048 -ub 512 --mlock --reasoning off --chat-template-file models--unsloth--Qwen-AgentWorld-35B-A3B-GGUF/chat_template.jinja

thank you 😀

it works

exec /llama-server -m Qwen-AgentWorld-35B-A3B-UD-IQ4_XS.gguf -c 65536 -ngl 999 --n-cpu-moe 6 --cache-type-k q4_0 --cache-type-v q4_0 -fa on -np 1 -b 2048 -ub 512 --mlock --reasoning off --chat-template-file models--unsloth--Qwen-AgentWorld-35B-A3B-GGUF/chat_template.jinja

thank you 😀

跑AGENT 可以试试使用下面参数,我仅16G显存:

set "LLAMA_SERVER=E:\download\llama-b9733-bin-win-cuda-13.3-x64\llama-server.exe"
set "MODEL_DIR=G:\models\lmstudio-community\MODELS"
set "CHAT_TEMPLATE=G:\models\lmstudio-community\chat_template.jinja"
set KWARGS={"preserve_thinking": "true"}

"%LLAMA_SERVER%" --models_dir "%MODEL_DIR%" ^
-ngl auto -c 262144 --reasoning on --reasoning-format deepseek --reasoning-budget 4096 -fa on ^
--batch-size 4096 --ubatch-size 2048 ^
--jinja --chat-template-file "%CHAT_TEMPLATE%" --chat-template-kwargs "%KWARGS%" ^
--temp 0.6 --top-p 0.95 --top-k 20 --min-p 0.00 --kv-unified -ctk q8_0 -ctv q8_0 ^
--port 1234 --models-max 1

@kkioikk

I’m using an RTX 5060 Ti 16GB GPU, and I’m getting around ~40 TPS.

Thanks. I’ll run some tests. 👍

Good find on the jinja template approach. One thing worth calling out for anyone building a multi-agent loop on top of Qwen-AgentWorld-35B-A3B: the chat template controls how tool calls and observations get serialized into the context window, but it doesn't touch the question of which agent is actually speaking when you have orchestrator→subagent delegation chains. If your loop has multiple model instances passing messages, the template alone gives you no signal about whether the message claiming to be "orchestrator" actually came from your orchestrator. That's a gap that bites people in production.

On the template itself — the pattern I've seen work well with MoE models like this one (A3B active params) is keeping the system prompt lean and putting tool schemas in a separate <tools> block rather than stuffing them into the system role. The attention routing behaves more predictably when the tool definitions aren't competing with behavioral instructions in the same token region. Worth benchmarking your loop latency with and without that split.

For anyone running this in a multi-agent setup where you care about provenance — we've been working on AgentAvow, which attaches signed identity claims to agent messages so downstream models or orchestrators can verify the sender before acting on tool calls. It's particularly relevant here because Qwen-AgentWorld's agentic fine-tuning makes it quite responsive to tool-call instructions, which also makes it a good target for prompt injection through a spoofed upstream agent. The jinja template is the right place to inject a verification header into the message format if you want that check to happen consistently at every turn.

Sign up or log in to comment