反模式:解析文本判断完成
了解扫描输出寻找“done”为何脆弱且错误。
反模式:解析文本判断完成 是 CoddyKit 上的免费 Claude Architect 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Claude Architect 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Claude Architect 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
The Trap
You build an agent. It runs tools, thinks, replies. Now you need to know when it's done. A tempting shortcut: scan the model's text for a word like done, finished, or complete, and stop the loop when you see it.
This is the anti-pattern Parsing Text for Completion. It feels intuitive, but it is fragile and wrong. The Claude API already hands you a precise, structured completion signal. In this lesson you'll learn why text-scanning breaks and what to use instead.
How the Agentic Loop Really Works
The agentic loop is driven by one field on the response: stop_reason. The loop is simple and deterministic:
- Send the request (with the full message history every turn).
- Inspect
stop_reason. - If it is
tool_use, run the tools, append the results to history, and repeat. - If it is
end_turn, the task is complete. Stop.
The model itself tells you, through structured metadata, whether it wants to keep going or is finished. You never have to guess by reading prose.
while True:
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break # complete
# else: tool_use -> run tools, append results, loopThe Four Stop Reasons
Terminate on the signal, not on the words. Claude returns one of four stop reasons:
end_turn— Claude finished naturally. The task is complete.tool_use— Claude wants a tool run; execute it and continue the loop.max_tokens— the output was truncated by the token cap.stop_sequence— a configured stop sequence was hit.
Each is unambiguous and machine-readable. Compare that to scanning free text, where the meaning depends entirely on how the model happened to phrase itself this time.
Why Text-Scanning Is Fragile
Natural language is not a control protocol. The same completed task can end a dozen different ways:
- "All done!"
- "That completes the migration."
- "I've finished the analysis."
- "Everything is in place now."
Your keyword check for done matches the first, maybe the second, and silently misses the rest. The loop never stops. You burn tokens and time, or hit a hard cap, on a task that was actually finished turns ago.
# ANTI-PATTERN: brittle keyword scan
if "done" in response_text.lower():
break # misses "finished", "complete", "all set"...False Positives Are Worse
Missing the signal wastes resources. The opposite failure is more dangerous: stopping too early.
Suppose the user asks Claude to "check whether the deployment is done." Claude might reply, mid-investigation, "Let me verify the deploy is done before I continue." Your scanner sees done and kills the loop — abandoning the task halfway, before any tool ran or any answer was produced.
The model uses words like done conversationally. A keyword match cannot tell narration apart from a genuine completion signal. stop_reason can.
The Model Keeps No State
Here is the deeper reason text-scanning is the wrong layer. The Claude API is stateless: the model keeps no memory between requests. You send the entire conversation — system, every prior messages turn, tool results — on every single call.
So "is the agent done?" is a question about the loop you control, answered by the structured stop_reason the API returns — not something to be reverse-engineered from prose. The control plane (your loop) and the content plane (the model's text) are separate. Keep completion logic in the control plane.
# Stateless: full history goes up every turn
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
system=system_prompt,
tools=tools,
messages=messages, # ENTIRE history, every call
)
messages.append({"role": "assistant", "content": response.content})The Correct Termination Check
Drive the loop off stop_reason alone. When it is tool_use, run the requested tools and append their results to the history with the matching tool_use_id. When it is end_turn, you are done.
This works no matter how Claude phrases its final message — "All set," "Finished," or nothing at all. The decision is made by metadata you can trust, not by string matching you have to babysit.
while True:
response = client.messages.create(
model="claude-opus-4-8", max_tokens=16000,
tools=tools, messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
results = run_tools(response.content) # each with tool_use_id
messages.append({"role": "user", "content": results})
continue
if response.stop_reason == "end_turn":
break # done -- no text parsing anywhereDecisions Are Model-Driven
A key architectural principle: in an agentic loop, the flow is decided by the model, surfaced through stop_reason. You don't dictate "after 3 tools, you must be finished." Claude signals when it needs another tool and when it's done.
Reserve hard-coded control for things that need a guarantee — a deterministic invariant your business requires. Completion detection is not one of those things; the API already provides it cleanly. Hand-rolling it with text parsing replaces a reliable signal with a guess.
Iteration Caps Are a Safety Net
"Fine," you say, "I'll just stop after N iterations." Careful: an iteration cap is a safety net, not the primary stop mechanism. It exists to bound runaway loops and protect against bugs — not to decide that work is complete.
If your loop relies on the cap to terminate normally, you have the same disease as text-scanning: you're substituting an arbitrary heuristic for the real, model-driven signal. Terminate on stop_reason first; let the cap catch only the pathological case.
MAX_ITERS = 20 # safety net, NOT the primary stop
for _ in range(MAX_ITERS):
response = client.messages.create(...)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
break # primary: model-driven
# ... handle tool_use ...
else:
log.warning("hit iteration cap -- investigate, don't trust as 'done'")Handle the Other Stop Reasons
A robust loop branches on every stop reason, not just the happy path. In particular, max_tokens means the response was cut off — it is not a completion. Treating a truncated turn as "done" silently ships half an answer.
When you see max_tokens, the fix is to raise max_tokens or switch to streaming for large outputs, then continue — never to break out of the loop as if the task succeeded.
if response.stop_reason == "end_turn":
break
elif response.stop_reason == "tool_use":
messages.append({"role": "user", "content": run_tools(response.content)})
elif response.stop_reason == "max_tokens":
# truncated, NOT complete -- raise cap or stream, then continue
raise OutputTruncated("increase max_tokens or stream")Need a Structured Done Flag? Use a Tool
Sometimes you genuinely want the model to report a structured outcome — a status, a confidence, a summary — when it finishes. The answer is still not to parse prose. Give Claude a tool (or a JSON Schema via structured outputs) and let it emit a typed result.
With tool_choice: "any" you can force the model to call some tool, guaranteeing structured output instead of free text. The loop still terminates on stop_reason; the tool just carries the machine-readable verdict. Structure replaces string-matching everywhere it matters.
tools = [{
"name": "submit_result",
"description": "Report the final task outcome.",
"input_schema": {
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["success", "failed"]},
"summary": {"type": "string"},
},
"required": ["status", "summary"],
},
}]
# tool_choice={"type": "any"} forces a structured call, not proseQuick Check
Test your judgment on a realistic design decision.
Key Takeaways
You now know why scanning output for completion is the wrong tool for the job:
- Terminate on
stop_reason, never on text.end_turn= complete;tool_use= run tools and continue. - Text-scanning is fragile both ways — it misses real completions (varied phrasing) and triggers false positives (conversational use of "done").
- The API is stateless and model-driven. Send full history every turn; let the structured signal, not prose, drive the loop.
- Iteration caps are a safety net, not the primary stop. Handle
max_tokensas truncation, not success. - Need a structured outcome? Use a tool or JSON Schema (force it with
tool_choice: "any") — never parse free text.
常见问题解答
「反模式:解析文本判断完成」课时是免费的吗?
是的 — 「反模式:解析文本判断完成」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Claude Architect 课程的其余内容,请升级到 CoddyKit PRO。 Claude Architect 课程共包含 4 节课。
「反模式:解析文本判断完成」这节课中我会学到什么?
了解扫描输出寻找“done”为何脆弱且错误。 你通过在浏览器中直接运行的动手代码来练习 Claude Architect,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Claude Architect 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Claude Architect 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「反模式:解析文本判断完成」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Claude Architect 课中编写并运行代码吗?
能。每节 Claude Architect 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 核心循环
- 根据 stop_reason 终止
- 反模式:解析文本判断完成
- 反模式:任意设置迭代上限