jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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:

  1. Model layer — vocabulary có EOS / end-of-turn token đặc biệt.
  2. API layermax_tokens, stop, response_format, tool schemas.
  3. Agent layer — vòng lặp của bạn interpret finish_reason và 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úcorchestrator 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:

TopicWhat to know
EOS ≠ periodModels can emit . and continue; EOS is a dedicated token ID
Chat templatesInstruction-tuned models use role-specific end markers (<|im_end|>, etc.)
”Natural” stopfinish_reason: "stop" covers both EOS and matched stop sequences
Suppressed EOSLow 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"hard failure với structured output trừ khi có chiến lược continuation rõ.

Chọn max_tokens trong vòng agent

Use caseTypical rangeNotes
Tool-call-only turn256–512Model emits short JSON + stop
ReAct reasoning step512–1024Thought + Action + Input
User-facing prose1024–4096Reserve headroom for citations
JSON schema extraction512–2048Size to worst-case schema
Summarization sub-agent1024–8192Match 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

PatternStop stringWhy
ReAct handoffObservation: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\nPrevent 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

  1. 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.
  2. Stop chồng — match đầu thắng; sắp thứ tự mảng có chủ đích.
  3. Stop rỗng — không tác dụng; dựa EOS hoặc max_tokens.
  4. 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_reasonMeaningAgent action
stopEOS or stop sequence matchedParse output; proceed to next loop phase
lengthmax_tokens exhaustedRetry with continuation or increase cap; flag if structured
tool_callsModel emitted native tool invocationExecute tools; append tool results; re-prompt
content_filterSafety system blocked outputLog, notify user, do not retry blindly
function_call (legacy)Older OpenAI tool formatMap 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:

  1. Đừng parse structured output đến khi stream kết thúc — hoặc đến khi stop sequence hiện đủ trong buffer.
  2. AbortController — hủy fetch khi user stop hoặc timeout; content partial có thể thiếu finish_reason.
  3. 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.
  4. 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.

TechniqueEffect
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 schemaConstrains format; pair with sized max_tokens
stop after required fieldsEnd JSON/tool block early
Post-hoc truncationLast 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_tokens theo worst case schema
  • Từ chối finish_reason: "length" — không JSON.parse blob 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:

  1. max_tokens mỗi turn — không null hoặc max provider trong production
  2. Budget token mỗi session — counter tích lũy qua iteration (Phần 1)
  3. Max iteration vòng lặp — giới hạn chu kỳ ReAct/planner bất kể content
  4. Timeout wall-clockAbortSignal trên fetch + deadline server
  5. Detector output lặp — loop n-gram giống → ép stop
  6. 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 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_tokens
  • finish_reason xử lý cho stop, 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_calls native 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

  1. Tokens & Context Windows
  2. Sampling: temperature, top_p, top_k
  3. Prompt Engineering for Agents
  4. Stopping Criteria & Output Control (current)
  5. Context Engineering & Memory
  6. Fine-tuning vs Prompting vs RAG
  7. Evaluating LLMs & Agents
  8. Choosing a Model
  9. Function Calling & Tool Use
  10. Agent Patterns: ReAct, Reflection, Planning