Stopping Criteria & Output Control — When Generation Ends and What to Do About It
EOS tokens, max_tokens, stop sequences, and finish_reason handling for production LLM agents — streaming, truncation, and runaway cost guards.
Generation không dừng khi model “cảm thấy xong” theo nghĩa con người — nó dừng khi một điều kiện cơ học kích hoạt.
Mỗi token agent phát ra đều qua cổng stopping: EOS token, trần max_tokens, chuỗi stop khớp, ranh giới tool-call, hoặc safety filter.
Senior engineer coi finish_reason như contract hạng nhất — không phải trivia log — vì nó quyết định parse JSON, gọi tool, retry, hay append continuation prompt.
Bài này là Phần 4 loạt Building AI Agents: output kết thúc thế nào, điều khiển ra sao, và vòng lặp agent phụ thuộc dừng đúng ranh giới.
Vòng generation trong một câu
Model autoregressive sample từng token một đến khi điều kiện stop kích hoạt; API wrapper surface kết quả thành finish_reason.
prompt tokens → [sample] → token₁ → [sample] → token₂ → … → STOP
↑
EOS | max_tokens | stop seq | tool_calls | content_filter
Ba lớp quan trọng cho agent:
- Model layer — vocabulary có EOS / end-of-turn token đặc biệt.
- API layer —
max_tokens,stop,response_format, tool schemas. - Agent layer — vòng lặp của bạn interpret
finish_reasonvà quyết bước tiếp.
Phần 1–3 đã cover budget (token), phân phối sampling, và cấu trúc prompt. Bài này khép vòng khi generation kết thúc và orchestrator phải làm gì tiếp.
Demo tương tác: xem stopping kích hoạt từng token
Demo dưới mô phỏng stream token canned với max_tokens, stop sequence, và EOS tự nhiên — không API key, không network.
Mở demo đầy đủ:
Thử preset ReAct tool handoff: model dừng trước Observation: để runtime inject tool output — pattern mọi agent dùng tool đều dựa vào.
EOS và end-of-turn token
Khi training, model học EOS token (hoặc họ end-of-turn marker) báo “completion này đã xong”.
Khi model sample EOS lúc inference, generation dừng và API báo finish_reason: "stop".
Vocabulary (simplified):
… " the" " cat" " sat" <|endoftext|> " on" …
↑
sampling this → generation ends
Nuances quan trọng cho agent engineer:
| Topic | What to know |
|---|---|
| EOS ≠ period | Models can emit . and continue; EOS is a dedicated token ID |
| Chat templates | Instruction-tuned models use role-specific end markers (<|im_end|>, etc.) |
| ”Natural” stop | finish_reason: "stop" covers both EOS and matched stop sequences |
| Suppressed EOS | Low temperature + forced prefixes can reduce early EOS — watch max_tokens |
Mental model: EOS là return statement của model. Stop sequence là breakpoint bạn chèn trước khi model tới return.
Một số API expose end_turn riêng (Anthropic) vs gom vào stop (OpenAI).
Chuẩn hóa trong agent SDK để code downstream branch theo ý định ngữ nghĩa, không phải string literal vendor.
function normalizeFinishReason(raw, provider) {
if (provider === "anthropic" && raw === "end_turn") return "natural_stop";
if (raw === "stop") return "natural_stop";
if (raw === "length") return "truncated";
if (raw === "tool_calls") return "tool_pending";
if (raw === "content_filter") return "blocked";
return "unknown";
}
max_tokens và max_completion_tokens
max_tokens (OpenAI legacy) và max_completion_tokens (param thống nhất mới) giới hạn độ dài output — không phải input.
Khi chạm trần giữa generation, API cắt và trả finish_reason: "length".
{
"choices": [{
"message": { "role": "assistant", "content": "Here is a detailed expl" },
"finish_reason": "length"
}],
"usage": { "completion_tokens": 8 }
}
Vì sao agent không được bỏ qua length
Output bị cắt invalid về cấu trúc thường xuyên hơn là “đủ dùng”:
- Nửa JSON object → parse fail
- Argument function dở → lỗi tool call
- Code block cắt cụt → lỗi syntax khi chạy
- ReAct
Action Input:dở → payload tool sai
Quy tắc: Coi
finish_reason: "length"là hard failure với structured output trừ khi có chiến lược continuation rõ.
Chọn max_tokens trong vòng agent
| Use case | Typical range | Notes |
|---|---|---|
| Tool-call-only turn | 256–512 | Model emits short JSON + stop |
| ReAct reasoning step | 512–1024 | Thought + Action + Input |
| User-facing prose | 1024–4096 | Reserve headroom for citations |
| JSON schema extraction | 512–2048 | Size to worst-case schema |
| Summarization sub-agent | 1024–8192 | Match target summary length |
Budget theo context window Phần 1: input_tokens + max_completion_tokens ≤ context_limit.
max_tokens lớn không ép verbose — nó đặt trần, không phải mục tiêu — nhưng reserve headroom billing.
Continuation sau truncation
Khi truncation không chấp nhận được, append continuation prompt và gọi lại:
async function generateUntilComplete(client, messages, opts) {
let content = "";
for (let attempt = 0; attempt < opts.maxContinuations; attempt++) {
const res = await client.chat.completions.create({
...opts,
messages: [
...messages,
...(content ? [{ role: "assistant", content }, { role: "user", content: "Continue exactly where you left off." }] : []),
],
});
const delta = res.choices[0].message.content ?? "";
content += delta;
if (res.choices[0].finish_reason !== "length") return { content, finish_reason: res.choices[0].finish_reason };
}
throw new Error("Exceeded max continuation attempts");
}
Bảo vệ continuation: mỗi retry re-tokenize full prefix (chi phí Phần 1) và có thể loop nếu model liên tục chạm cùng trần.
Stop sequence: breakpoint điều khiển agent
Param stop (string hoặc mảng tối đa bốn sequence) báo API dừng generation khi output tích lũy chứa match.
{
"model": "gpt-4o",
"messages": [{ "role": "user", "content": "Plan a trip to Hanoi." }],
"stop": ["Observation:", "\n\nUser:"],
"max_tokens": 512
}
Include vs exclude stop text
OpenAI loại stop sequence khớp khỏi message.content mặc định.
Một số runtime local include.
Parser phải biết provider dùng behavior nào — bug string off-by-one trong ReAct parser thường từ đây.
Generated (internal): "Action Input: {\"city\":\"Hanoi\"}\nObservation:"
stop = "Observation:"
OpenAI content: "Action Input: {\"city\":\"Hanoi\"}\n" ← stop excluded
Use case stop sequence cho agent
| Pattern | Stop string | Why |
|---|---|---|
| ReAct handoff | Observation: | Model plans + acts; runtime injects observation |
| Multi-agent routing | \n\nAssistant B: | Prevent role bleed between agents |
| JSON fence | "```" or \n} | End structured block before prose |
| Few-shot delimiter | \n\n---\n\n | Prevent model from inventing new examples |
| Human-in-the-loop | \n\nAWAITING_APPROVAL: | Pause before irreversible action |
ReAct loop (simplified):
LLM → Thought/Action/Action Input [stop: "Observation:"]
↓ finish_reason: stop
Runtime → execute tool
↓
Runtime → append "Observation: {result}"
↓
LLM → next Thought …
Mẹo thiết kế: Ưu tiên stop sequence hiếm trong prose thường nhưng rõ trong prompt template.
Observation:an toàn trong ReAct vì bạn không bao giờ yêu cầu model emit observation — chỉ runtime làm.
Gotcha stop sequence
- Biên tokenization — stop string cắt qua token có thể chưa match đến khi đủ chuỗi; thường ổn nhưng thêm latency 0–N token.
- Stop chồng — match đầu thắng; sắp thứ tự mảng có chủ đích.
- Stop rỗng — không tác dụng; dựa EOS hoặc max_tokens.
- Stop vs tool_calls — function calling native thay text Action/Input; stop vẫn hữu ích cho hybrid hoặc parser legacy.
Giá trị finish_reason và ma trận xử lý
Provider hội tụ enum nhỏ; chuẩn hóa một chỗ.
| finish_reason | Meaning | Agent action |
|---|---|---|
stop | EOS or stop sequence matched | Parse output; proceed to next loop phase |
length | max_tokens exhausted | Retry with continuation or increase cap; flag if structured |
tool_calls | Model emitted native tool invocation | Execute tools; append tool results; re-prompt |
content_filter | Safety system blocked output | Log, notify user, do not retry blindly |
function_call (legacy) | Older OpenAI tool format | Map to tool_calls handling |
function handleCompletion(result, handlers) {
const reason = result.choices[0].finish_reason;
const msg = result.choices[0].message;
switch (reason) {
case "stop":
return handlers.onNaturalStop(msg.content);
case "length":
return handlers.onTruncated(msg.content);
case "tool_calls":
return handlers.onToolCalls(msg.tool_calls);
case "content_filter":
return handlers.onBlocked();
default:
return handlers.onUnknown(reason, msg);
}
}
content_filter trong production
Đừng nuốt generation bị block im lặng. Log sự kiện (không lưu nội dung harmful), hiện message an toàn cho user, tránh retry giống hệt ngay lập tức gây rate limit moderation.
tool_calls vs text stop
Agent hiện đại nên ưu tiên native tool calling (Phần 9) khi API emit tool_calls structured với finish_reason: "tool_calls".
ReAct text với stop sequence vẫn có giá trị cho model không có function calling tin cậy hoặc debug interpretability.
Streaming vs non-streaming: dừng giữa stream
Non-streaming: nhận một object cuối với finish_reason sau khi generation xong.
Streaming: token đến dần; chunk cuối mang finish_reason.
Stream chunks:
data: {"choices":[{"delta":{"content":"Thought"}}]}
data: {"choices":[{"delta":{"content":": check"}}]}
…
data: {"choices":[{"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Hệ quả cho UX và điều khiển agent:
- Đừng parse structured output đến khi stream kết thúc — hoặc đến khi stop sequence hiện đủ trong buffer.
- AbortController — hủy fetch khi user stop hoặc timeout; content partial có thể thiếu
finish_reason. - Phát hiện stop giữa stream — mirror behavior API: kiểm buffer tích lũy cho stop string phía client nếu cần dispatch tool sớm.
- TTFT vs total time — streaming cải thiện latency cảm nhận; logic stopping giống nhau.
async function streamWithStop(client, params, onToken) {
const stream = await client.chat.completions.create({ ...params, stream: true });
let buffer = "";
let finishReason = null;
for await (const chunk of stream) {
const choice = chunk.choices[0];
if (choice.finish_reason) finishReason = choice.finish_reason;
const text = choice.delta?.content ?? "";
if (!text) continue;
buffer += text;
onToken(text, buffer);
for (const stop of params.stop ?? []) {
if (stop && buffer.includes(stop)) {
finishReason = "stop";
return { content: buffer.split(stop)[0], finish_reason: finishReason };
}
}
}
return { content: buffer, finish_reason: finishReason ?? "stop" };
}
Điều khiển verbosity và độ dài (ngoài max_tokens)
max_tokens là trần cứng; prompting và sampling định hình độ dài điển hình.
| Technique | Effect |
|---|---|
| System instruction: “Answer in ≤3 sentences” | Soft cap; model may still hit max_tokens on ramble |
| Lower temperature (Part 2) | Less digression; slightly shorter outputs |
| Structured output / JSON schema | Constrains format; pair with sized max_tokens |
stop after required fields | End JSON/tool block early |
| Post-hoc truncation | Last resort; loses semantic completeness |
Với agent user-facing, kết hợp giới hạn prompt mềm với max_tokens cứng và xử lý length rõ ràng.
System: Respond in at most 150 words. If you need more, ask a clarifying question.
API: max_completion_tokens: 300 ← hard backstop (~2× soft target)
Agent: if finish_reason === "length" → "Response was cut short; retry with narrower question"
Hoàn thành structured output
JSON mode, response_format, và constrained decoding giảm nhưng không loại hết rủi ro truncation.
Checklist cho generation ràng buộc schema:
- Đặt
max_tokenstheo worst case schema - Từ chối
finish_reason: "length"— khôngJSON.parseblob cắt - Dùng lỗi validation provider làm tín hiệu retry
- Log schema version mỗi lần parse
function parseStructured(content, finishReason) {
if (finishReason === "length") {
throw new TruncatedGenerationError("Output truncated before schema complete");
}
try {
return JSON.parse(content);
} catch (err) {
throw new MalformedJsonError("Invalid JSON despite natural stop", { cause: err });
}
}
Khi provider có strict schema mode, ưu tiên hơn instruction “return JSON” — stopping và validation được decoder enforce một phần.
Bảo vệ runaway generation và chi phí
Agent lặp; thiếu điều kiện stop có thể đốt hàng nghìn token mỗi user message.
Phòng thủ nhiều lớp:
- max_tokens mỗi turn — không
nullhoặc max provider trong production - Budget token mỗi session — counter tích lũy qua iteration (Phần 1)
- Max iteration vòng lặp — giới hạn chu kỳ ReAct/planner bất kể content
- Timeout wall-clock —
AbortSignaltrên fetch + deadline server - Detector output lặp — loop n-gram giống → ép stop
- Cảnh báo chi phí — bất thường completion_tokens mỗi request
const LIMITS = {
maxTokensPerTurn: 1024,
maxTokensPerSession: 32_000,
maxAgentSteps: 12,
wallClockMs: 120_000,
};
async function runAgentLoop(ctx) {
while (ctx.steps < LIMITS.maxAgentSteps && ctx.sessionTokens < LIMITS.maxTokensPerSession) {
const res = await generate(ctx, { max_tokens: LIMITS.maxTokensPerTurn, signal: ctx.signal });
ctx.sessionTokens += res.usage.completion_tokens;
ctx.steps += 1;
if (res.finish_reason === "tool_calls") {
await executeTools(res);
continue;
}
if (res.finish_reason === "stop") return res.message.content;
if (res.finish_reason === "length") throw new TruncatedGenerationError();
}
throw new AgentBudgetExceededError();
}
Bài học production: Sự cố agent đắt nhất không phải prompt tệ — là vòng lặp không giới hạn không max_tokens và không step cap.
Vòng agent phải dừng turn để gọi tool
Agent dùng tool phụ thuộc ranh giới turn. Generation phải dừng trước khi model bịa kết quả tool.
Hai kiến trúc:
A) Native tool_calls (preferred)
LLM → finish_reason: tool_calls
Runtime → execute → append tool message → LLM
B) Text ReAct + stop sequences
LLM → Thought/Action/Input [stop: "Observation:"]
Runtime → parse Action → execute → append Observation → LLM
Với (B), stop sequence là contract giữa output LLM và injection runtime.
Nếu model emit Observation: trước khi stop, nó có thể bịa tool output — failure mode phổ biến trong ReAct thiếu ràng buộc.
Giảm thiểu:
- System prompt: “Không bao giờ viết Observation; hệ thống cung cấp”
- Stop tại
Observation:(loại khỏi content) - Validate Action parse với allowlist trước khi execute
Tổng hợp: checklist stopping
Trước khi ship vòng agent, xác minh:
- Mọi LLM call đặt
max_tokens/max_completion_tokensrõ finish_reasonxử lý chostop,length,tool_calls,content_filter- Stop sequence khớp prompt template và behavior include/exclude provider
- Streaming parser đợi chunk terminal hoặc stop match xác nhận
- Structured output cắt trigger retry hoặc lỗi — không parse im lặng
- Giới hạn token và step cấp session enforce ngoài model
- Turn tool dùng
tool_callsnative hoặc text stop đã verify — không Action loop không giới hạn
Kết luận: Stopping criteria là control plane của generation. Sampling (Phần 2) quyết token nào; stopping quyết khi nào chuỗi kết thúc — agent quyết bước tiếp theo.
Tiếp theo
Phần 5 chuyển từ điều khiển output single-turn sang context engineering multi-turn — giữ, nén, và retrieve gì qua các bước agent.
Continue to Context Engineering & Memory.
Loạt bài Building AI Agents
- Tokens & Context Windows
- Sampling: temperature, top_p, top_k
- Prompt Engineering for Agents
- Stopping Criteria & Output Control (current)
- Context Engineering & Memory
- Fine-tuning vs Prompting vs RAG
- Evaluating LLMs & Agents
- Choosing a Model
- Function Calling & Tool Use
- Agent Patterns: ReAct, Reflection, Planning