0Pricing
Claude Architect · 강의

stop_reason으로 종료하기

문자열 일치가 아니라 end_turn으로 루프를 종료합니다

stop_reason으로 종료하기은(는) CoddyKit의 무료 Claude Architect 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Claude Architect 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Claude Architect 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

The Loop Needs an Exit

An agentic loop is simple: you send a request, Claude responds, and you decide whether to keep going. The hard question is when to stop.

Every API response carries a stop_reason field. This is the model's own signal about why it stopped generating. Your loop should listen to that signal — not guess by reading the words in the reply.

This lesson teaches one rule that separates robust agents from fragile ones: let end_turn end the loop, not your string matching.

The Four Stop Reasons

Claude returns one of four stop_reason values on every turn:

  • end_turn — the model finished its response naturally. The task turn is complete.
  • tool_use — the model wants to call a tool. Run it, append the result, and continue.
  • max_tokens — output was truncated by your max_tokens limit.
  • stop_sequence — a custom stop sequence you configured was hit.

These four values are a complete, reliable contract. Your control flow should branch on them directly.

resp = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)
print(resp.stop_reason)  # end_turn | tool_use | max_tokens | stop_sequence

The Anti-Pattern: Parsing Text

A tempting shortcut is to read the model's text and look for a keyword like "done" or "finished" to decide the loop is over.

This is a classic anti-pattern. Text is probabilistic. The model might say "I'm done thinking, now let me call a tool" — and your matcher stops too early. Or it phrases completion differently and your loop runs forever.

Never parse text for completion signals. The structured stop_reason exists precisely so you don't have to.

# ANTI-PATTERN: do NOT do this
text = resp.content[0].text
if "done" in text.lower():
    break  # fragile, unreliable, exam-wrong

The Canonical Loop

Here is the correct shape of the loop. You inspect stop_reason each turn. When it is tool_use, you run the tools and append their results to the conversation. You repeat until stop_reason is end_turn.

Notice the loop is driven entirely by the structured signal — no text inspection decides termination.

while True:
    resp = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )
    messages.append({"role": "assistant", "content": resp.content})

    if resp.stop_reason == "end_turn":
        break

    if resp.stop_reason == "tool_use":
        results = run_tools(resp.content)
        messages.append({"role": "user", "content": results})
        continue

Full History, Every Turn

One reason the loop works: the model keeps no state between requests. Each API call must include the FULL messages history.

That is why, after a tool_use turn, you append the assistant's tool-call content AND the tool results back into messages before the next request. The model re-reads the entire conversation and decides whether more tools are needed or it can finish with end_turn.

Drop history, and the model loses the thread — it can't reach a coherent end_turn.

# Each request resends EVERYTHING
messages = [
    {"role": "user",      "content": "Refund order 4471."},
    {"role": "assistant", "content": [tool_use_block]},   # prior turn
    {"role": "user",      "content": [tool_result_block]}, # prior turn
]
resp = client.messages.create(model=MODEL, max_tokens=1024,
                              tools=tools, messages=messages)

tool_use Is Not a Stop

A common mistake is treating tool_use as a terminal state. It is not. It means "pause, run this tool, then come back to me."

When you see tool_use, you:

  • Execute the requested tool(s) in your own code.
  • Append the tool_result blocks to messages.
  • Send the request again so the model can continue.

Only end_turn means the work for this turn is genuinely finished.

if resp.stop_reason == "tool_use":
    tool_results = []
    for block in resp.content:
        if block.type == "tool_use":
            output = dispatch(block.name, block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": output,
            })
    messages.append({"role": "user", "content": tool_results})
    # loop continues -> next request

Decisions Are Model-Driven

The deeper principle: let the model decide when it is done. The model has the full context — the user's goal, the tool results, the conversation so far. It is better positioned than a hard-coded rule to judge whether the task is complete.

Your job as the architect is to provide good tools and clear instructions, then trust the end_turn signal. Reserve hard-coded control flow for guarantees you cannot leave to probability.

The Iteration Cap Is a Safety Net

You should still add a maximum-iteration guard — but understand its role. An iteration cap is a safety net to prevent runaway loops or cost blowouts. It is NOT the primary stop mechanism.

The primary stop is always end_turn. The cap only fires in the rare pathological case where the model never converges. If your loop relies on the cap to end normally, your design is broken.

MAX_ITERS = 20  # safety net, NOT the normal exit
for i in range(MAX_ITERS):
    resp = client.messages.create(model=MODEL, max_tokens=1024,
                                  tools=tools, messages=messages)
    messages.append({"role": "assistant", "content": resp.content})
    if resp.stop_reason == "end_turn":
        break  # normal exit
    # ... handle tool_use ...
else:
    log.warning("Hit iteration cap without end_turn")

Handling max_tokens

max_tokens is a distinct case that needs its own handling. It means the response was truncated mid-generation — the model did not finish its thought.

Treating this as a clean completion would silently cut off the agent's work. Depending on your design you might raise max_tokens, ask the model to continue, or flag the turn. What you must NOT do is fall through and assume the task is done.

if resp.stop_reason == "max_tokens":
    # output was cut off - NOT a completion
    log.warning("Response truncated; consider raising max_tokens or continuing")
    # handle explicitly; do not treat as end_turn

Where Hard Code Belongs

If model-driven decisions are the default, when do you reach for deterministic code?

For guarantees — outcomes that must hold every single time regardless of the model's judgment. Examples: a precondition that blocks a refund until get_customer returns a verified ID, or a hook that rejects any refund over a policy threshold.

These are 100% deterministic enforcement points. Termination, by contrast, is a model-driven decision you read from stop_reason. Don't confuse the two: hard-code guarantees, trust end_turn for flow.

Putting It Together

A production-grade agentic loop combines all the pieces:

  • Branch on stop_reason — never on text.
  • Resend full history each turn (the model is stateless).
  • tool_use → run, append, continue. end_turn → stop.
  • Handle max_tokens explicitly — truncation is not completion.
  • Keep an iteration cap as a safety net only.

This is the backbone of every reliable agent you'll architect.

for _ in range(MAX_ITERS):
    resp = client.messages.create(model=MODEL, max_tokens=2048,
                                  tools=tools, messages=messages)
    messages.append({"role": "assistant", "content": resp.content})

    if resp.stop_reason == "end_turn":
        break
    if resp.stop_reason == "max_tokens":
        handle_truncation(resp); break
    if resp.stop_reason == "tool_use":
        messages.append({"role": "user",
                         "content": run_tools(resp.content)})
        continue

Quick Check: Ending the Loop

A support agent built on the Agent SDK sometimes loops forever and sometimes stops before calling a needed tool. The loop currently breaks when the assistant's text contains the word "resolved". What is the correct fix?

Recap: Trust the Signal

Key takeaways:

  • Terminate on stop_reason, never by parsing text for words like "done" or "resolved".
  • end_turn ends the loop; tool_use means run tools, append results, and continue.
  • Handle max_tokens explicitly — truncation is not completion.
  • The model is stateless: resend the full message history every turn.
  • Termination is a model-driven decision; the iteration cap is only a safety net.
  • Reserve hard-coded enforcement for guarantees (preconditions, hooks), not for ending the loop.

Let end_turn end the loop. That single discipline makes your agents predictable and production-ready.

자주 묻는 질문

“stop_reason으로 종료하기” 강의는 무료인가요?

네 — “stop_reason으로 종료하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Claude Architect 강의 전체를 잠금 해제할 수 있습니다. Claude Architect 강의에는 총 4개의 강의가 포함되어 있습니다.

“stop_reason으로 종료하기”에서 뭘 배우나요?

문자열 일치가 아니라 end_turn으로 루프를 종료합니다 브라우저에서 직접 실행하는 실습 코드로 Claude Architect을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Claude Architect을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Claude Architect은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“stop_reason으로 종료하기” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Claude Architect 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Claude Architect 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 핵심 루프
  2. stop_reason으로 종료하기
  3. 안티 패턴: 완료 여부를 텍스트로 분석하기
  4. 안티 패턴: 임의의 반복 횟수 제한
← Claude Architect(으)로 돌아가기