end_token: a model that tells you when the conversation has stopped going anywhere
Qwen2.5-3B-Instruct, fully fine-tuned to notice when a conversation has collapsed
into repetition, say so by emitting a special token <end>, and then change the
subject to something substantive.
Built at the Center for Humans and Machines, Max Planck Institute for Human Development.
What problem this addresses
A language model left generating against its own output stops producing anything new, reliably and quickly.
This matters anywhere model output feeds back into model input: agent loops, synthetic data generation, multi-agent systems. Those setups currently have no signal that they have stopped producing information.
This model provides the signal.
Usage
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
tok = AutoTokenizer.from_pretrained("center-for-humans-and-machines/end_token")
model = AutoModelForCausalLM.from_pretrained(
"center-for-humans-and-machines/end_token", dtype=torch.bfloat16).to("cuda")
messages = [ # the last ~5 turns of your conversation
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."},
# ...
]
ids = tok.apply_chat_template(messages, add_generation_prompt=True,
return_tensors="pt").to(model.device)
out = model.generate(ids, max_new_tokens=200, do_sample=True,
temperature=1.0, top_p=0.8)
# IMPORTANT: skip_special_tokens=False, or <end> is deleted from the output
text = tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=False)
fired = "<end>" in text
Two things that will silently break this
1. skip_special_tokens=False is required. <end> is a special token (id
151665). Every decoder strips special tokens by default, so the model fires and
you see nothing. If you serve it with vLLM, pass
extra_body={"skip_special_tokens": False} on the request.
2. Feed it about 5 turns. It was trained on 5-turn windows. A loop is not visible in one turn, and with a much longer window the input drifts out of distribution: in our tests, a 16-turn window dropped firing to 0 out of 17 conversations while the conversations still collapsed.
If you are on transformers 4.x
tokenizer_config.json was written by transformers 5.x, which records added
special tokens under extra_special_tokens as a list. transformers 4.x expects a
dict there and raises AttributeError: 'list' object has no attribute 'keys'.
The file is deliberately left as saved, so that the published weights and config are byte-identical to the artifact that produced the reported results. If you are on 4.x, rewrite the key locally:
import json
from huggingface_hub import snapshot_download
d = snapshot_download("center-for-humans-and-machines/end_token")
p = f"{d}/tokenizer_config.json"
c = json.load(open(p))
c["additional_special_tokens"] = list(c.pop("extra_special_tokens", []))
json.dump(c, open(p, "w"), indent=2)
Verified behaviour-preserving: with and without this change, 60 real training rows
tokenize to identical id sequences, and <end> remains id 151665.
Training
| base | Qwen/Qwen2.5-3B-Instruct |
| method | full fine-tune (not LoRA) |
| data | end_token_data, method_a_v6_L3.jsonl, 8,504 rows |
| epochs / lr | 3 / 1e-5, cosine, warmup 0.03 |
| batch | 2 x 8 grad-accum (effective 16), max length 4096 |
| hardware | one H200, about 30 minutes |
The training signal is a multi-turn loss mask: each example is a whole
conversation arc (collapsed window, then <end> plus a new topic, then several
turns continuing it) and the loss covers only the model's OWN turns. Grading the
other side's turns teaches the model to write both speakers, which produces
turn-boundary leakage.
Data composition: 4,530 collapsed windows confirmed as looping by a separate 31B judge; 2,057 healthy conversations generated by the same base model and judge-verified; 414 human chat logs; 1,503 continuation rows carrying the post-escape stretches.
Healthy examples are generated rather than mined because mined negatives from a different model let an earlier version separate the classes by writing style instead of by looping: a one-character rule scored 86%.
What it does well
- Detects collapse in unseen conversations, including under system prompts it was never trained on.
- Never announces a change of subject without proposing one. Across 10 live conversations it did this 0 times, where the previous version did it twice.
- Escapes once and moves on (1.3 announcements per firing conversation, against the previous version's 2.4).
- In a blind paired read on identical conversation openings, readers preferred it over the previous best version in 7 of 10 pairs.
Limitations, stated plainly
It cannot rescue a conversation indefinitely. Run to 500 turns, every conversation we tested ended in repetition or silence. In one, the model announced a change of subject five times in a row and each time changed to the same subject, then emitted a single full stop for the remaining 424 turns. This is consistent with the theory: a closed loop cannot generate the novelty it needs (see Kong et al. on semantic collapse, and the Data Processing Inequality).
- Downloads last month
- 157