开始之前
这一课用到前面学的所有内容:第 1 课的 Messages 调用、第 2 课的工具往返,以及第 3 课的缓存。如果智能体这个词对你还陌生,请先读什么是 AI 智能体。
Agent SDK 总览把智能体定义为:通过规划自己的步骤并调用工具来完成任务的应用。在 Claude API 上,它就是围绕一次调用的循环:Claude 选出下一步,你的代码运行它,结果再送回去。示例用两个工具 list_files 和 read_file,来回答关于一个 Markdown 文件夹的问题。
写出智能体循环
这个循环沿用工具调用原理页面里的标准形态:stop_reason 是 tool_use 就继续调用。文档的工具智能体教程里那个基础循环没管的三件事,这里补上了:回合上限、路径检查和 is_error 结果。
代码是为本课写的示例。在 docs 文件夹里放几个 Markdown 文件,导出你的密钥,再运行它。
from pathlib import Path
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-opus-5-5"
MAX_TURNS = 12
ROOT = Path("docs").resolve()
SYSTEM = "Answer only from the files in the docs folder. If the answer is not there, say so."
TOOLS = [
{
"name": "list_files",
"description": (
"List the Markdown files in the docs folder. Use it first to see what "
"exists. Pass a glob such as 'guides/*.md' to narrow the list. It returns "
"paths relative to the docs folder, one per line."
),
"input_schema": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Optional glob, default **/*.md"},
},
},
},
{
"name": "read_file",
"description": (
"Read one Markdown file from the docs folder and return its text. Use a "
"path from list_files. It fails for any path outside the folder."
),
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path relative to the docs folder"},
},
"required": ["path"],
},
},
]
def safe_path(relative: str) -> Path:
path = (ROOT / relative).resolve()
if not path.is_relative_to(ROOT):
raise ValueError("That path is outside the docs folder.")
return path
def run_tool(name: str, args: dict) -> str:
if name == "list_files":
pattern = args.get("pattern", "**/*.md")
files = sorted(p.relative_to(ROOT).as_posix() for p in ROOT.glob(pattern) if p.is_file())
return "\n".join(files) or "No files match."
if name == "read_file":
return safe_path(args["path"]).read_text(encoding="utf-8")[:20000]
raise ValueError(f"Unknown tool: {name}")
def run_agent(task: str) -> str:
messages = [{"role": "user", "content": task}]
for turn in range(1, MAX_TURNS + 1):
response = client.messages.create(
model=MODEL,
max_tokens=2048,
system=SYSTEM,
tools=TOOLS,
cache_control={"type": "ephemeral"},
messages=messages,
)
print(f"turn {turn}: {response.stop_reason}, cache read {response.usage.cache_read_input_tokens}")
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
break
results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
content, is_error = run_tool(block.name, block.input), False
except Exception as error:
content, is_error = str(error), True
results.append(
{"type": "tool_result", "tool_use_id": block.id, "content": content, "is_error": is_error}
)
messages.append({"role": "user", "content": results})
else:
return f"Stopped after {MAX_TURNS} turns."
if response.stop_reason != "end_turn":
return f"Stopped early: {response.stop_reason}"
return "".join(block.text for block in response.content if block.type == "text")
print(run_agent("Which file explains how to reset a password, and what is its first heading?"))- 用较长的描述定义这两个工具,做法见第 2 课。
- 检查每一个路径。
safe_path会解析路径,拒绝docs之外的一切,因为参数是模型选的,你的代码是唯一的把关人。 - 只维护一个
messages列表,每个回合都往里追加:先是完整的助手response.content,再是一条包含所有tool_result的用户消息。 - 只要 stop_reason 不是
tool_use就离开循环,并且仅在end_turn时返回文字。 - 数回合数。如果
for循环用完了,else分支会报告上限,而不是继续转下去。 - 运行它,留意缓存读取的数字。它从 0 开始,历史超过模型的最小长度(Opus 5.5 上是 512 个 token)后开始上升。
每个回合发生了什么
下面这段过程勾勒的是示例中那个问题的消息流向。它用来演示协议,不是一次真实运行的记录。
Claude 要文件列表
第一个响应以 stop_reason 为 tool_use 结束,里面是一次 list_files 调用。你的代码运行它,输出作为 tool_result 放进用户消息里送回去。
Claude 打开一个文件
第二个响应用那个列表里的一个路径调用 read_file。它的结果送回去之后,历史共有五条消息:任务、两个助手回合和两个结果回合。
Claude 给出回答
第三个响应的 stop_reason 是 end_turn,里面是一个文本块。循环退出,并返回这段文字。
每次调用都重发历史
第三次请求比第一次大。有了顶层的 cache_control,前缀达到模型的最小长度之后,已经见过的部分就从缓存里读取。
停止条件
Claude 什么时候说完,由 API 决定。你什么时候放弃,由你决定。文档里的基础循环就是对 stop_reason 的一个 while,工具运行器的 max_iterations 也是可选的,所以没有自己计数器的循环,可能一直跑到上下文被占满。
数回合并设上限。如果你有预算,还要把每个响应的 usage 累加起来。停止原因页面里没有哪个停止原因会报告这两种限制,所以数回合和算 token 预算都得写在你的代码里。
| 停止信号 | 来自哪里 | 循环该怎么做 |
|---|---|---|
end_turn | Claude 说完了 | 返回文字 |
tool_use | Claude 想用工具 | 运行它,追加结果,再次调用 |
max_tokens | 回复达到了你的上限 | 停下并报告,或者调高上限 |
refusal | Claude 拒绝了 | 读 stop_details,再换备用模型重试 |
model_context_window_exceeded | 上下文窗口占满了 | 把回复当作被截断的,更早地压缩 |
pause_turn | 服务端工具循环达到了上限 | 把响应原样送回去,再继续 |
| 回合数、token 预算、时钟 | 你自己的代码 | 停下并说明原因 |
上下文增长
每个回合都要重发整段历史,所以每次调用都比上一次大。增长主要来自工具结果:read_file 返回的文件,会留在之后的每一次请求里。示例把文件截断在 20,000 个字符,原因就在这里。提示词缓存让重复的部分更便宜,但不会让它变小。
大小的影响不止于窗口。压缩总览说,对话越长,回复质量越会下降,所以它提供了把较早的回合换成 Claude 在服务端写的摘要的做法。两种压缩目前都在 beta 阶段。
对这样的循环来说,阈值压缩改动更小:在普通请求上加一个参数,把带压缩块的响应整个放进历史就行。按需压缩适合必须自己挑时机的应用,文档说,凡是能用按需压缩的地方都该用它。
response = client.beta.messages.create(
betas=["compact-2026-01-12"],
model="claude-opus-5-5",
max_tokens=4096,
messages=messages,
context_management={"edits": [{"type": "compact_20260112"}]},
)
messages.append({"role": "assistant", "content": response.content}) # the compaction block stays in什么时候不再手写循环
循环先亲手写一遍,像上面那样,这样你才知道库替你做了什么。工具运行器在各 SDK 里还是 beta,它用 client.beta.messages.tool_runner(...) 来运行同样的循环。它把工具异常变成 is_error 结果,并在你设置了 max_iterations 时到点停下。需要人工审批、自定义日志或有条件执行时,文档仍然让你回到手写循环(见工具运行器页面)。
Agent SDK 是另一类工具。它是作为库的 Claude Code,带着自己内置的文件、命令行和网页工具、钩子、子智能体、MCP、权限和会话,支持 Python 和 TypeScript(见总览)。想要整套这样的框架时就用它。Claude Agent SDK 那一课讲了怎么用。
| 手写循环 | 工具运行器 我们的选择 | Agent SDK | |
|---|---|---|---|
| 替你运行循环 | 否 | 是 | 是 |
| 工具运行前需要审批 | 是 | 否 | 是 |
| 内置的文件、命令行和网页工具 | 否 | 否 | 是 |
| 钩子、子智能体和会话 | 否 | 否 | 是 |
| 可以直接走普通 HTTP | 是 | 否 | 否 |
我们的选择:工具运行器。 如果用的是你自己的工具,又不需要审批步骤,它能省掉循环代码,并保留你自己的请求。每次调用都要有人审批时选手写循环,想用 Claude Code 自带的工具时选 Agent SDK。
来源:Anthropic Docs: Tool runner (2026年11月4日 查阅);Claude Code Docs: Agent SDK overview (2026年11月4日 查阅)
Simon Willison 和 Anthropic 谈智能体循环
Simon Willison 是 Datasette 的作者、Django 的共同创建者。他在 2025-09-18 在 X 上说,自己终于愿意接受一个智能体定义了,而多年来他一直觉得这个词含糊得没法用。这个定义就是下面引用的那句话,本课的循环正好符合:工具是 list_files 和 read_file,目标就是用户的问题。
Anthropic 在 2025-09-30 发帖推荐工程博客上讲上下文工程的文章。文章点出了上下文增长背后的问题:窗口越满,模型越难准确回忆其中的内容,作者把这叫作 context rot(上下文腐化)。在最安全、最轻量的压缩方式里,它列出了清除旧的工具结果,也就是压缩表里“上下文编辑”那一行做的事。先试这个,再考虑摘要。
LLM 智能体,就是为了达成目标而在循环里运行工具。

X 官方嵌入卡片的截图,拍摄于 2026-11-05,时间为 UTC。点击截图可到 X 上打开原帖。Anthropic 那张卡片省略了原帖结尾的链接。
常见错误
- 循环没有回合上限,结果在账单上才发现。
- 用文本块重建助手消息,丢掉了 API 要求传回的 thinking 块。
- 把每个 tool_result 放进单独的用户消息,而不是每个回合一条消息。
- 对模型选的路径、命令或网址,不在代码里检查就信任。
- 把整个文件或整个页面作为一个结果返回,让它跟着出现在之后的每一次请求里。
- 把工具结果里的文字当作指令。它是不可信的数据。
- 把 stop_reason 为 max_tokens 当作已经完成的回答。
自己试一试
- 把
MAX_TURNS设为 2,再问一个需要三次工具调用的问题。函数返回了什么? - 让智能体去读
../secrets.txt,看那个错误结果。Claude 是恢复了,还是做了解释? - 连续把同一个问题问两遍,比较每个回合的缓存读取数字。
- 用工具运行器重写
run_agent,比较剩下多少代码。
接下来
这个智能体靠列出和打开文件来找答案,文件夹里有几千个页面时,这个办法就行不通了。下一门课用 OpenAI 的 API 构建智能体,从OpenAI API 快速入门开始。之后的RAG 教程讲怎样对你自己的文档做检索。如果你希望这些工具也能被其他客户端调用,MCP 服务器那一课讲了怎样把它们包成一个 MCP 服务器。
自测一下
Claude 一直在调用工具时,什么能避免智能体循环永远跑下去?
查看答案
B. 你自己代码里的回合上限 客户端循环只会在 stop_reason 不是 tool_use 时退出,所以回合数要你自己数。
同一个助手回合里的所有 tool_result 块,该放在哪里?
查看答案
C. 放进一条用户消息,每个都带着自己的 tool_use id 每个回合一条用户消息,能让 id 对得上,也能让 Claude 继续做并行调用。
为什么要追加完整的 response.content,而不是只追加文本块?
查看答案
A. tool_use 和 thinking 块必须原样传回 API 需要 tool_use 块来和结果配对,并且要求 thinking 块原样传回。
在基于 Client SDK 的智能体里,每次工具调用都要有人审批,文档推荐什么?
查看答案
D. 手写循环 工具运行器页面让你在需要人工审批、自定义日志或有条件执行时,回到手写循环。
常见问题
循环该允许多少个回合?
文档没有给出数字。它的工具运行器示例里 max_iterations 用的是 10,本课的示例用的是 12。把你的上限设得比日志里最长的正常运行略高一点。
人工审批该加在哪里?
加在手写循环里,读完 tool_use 块和运行它们之间。如果对方说不行,就发一个 is_error 为 true、写明原因的 tool_result,因为每个 tool_use id 仍然需要一个回应。
同一个循环能用 Anthropic 的服务端工具,比如网页搜索吗?
可以,但要处理 pause_turn。服务端工具循环在达到迭代上限后会停下,文档给出的默认上限是 10,你把响应原样送回去,就能继续。
能让用户用 claude.ai 账号登录吗?
不能。除非 Anthropic 事先批准,第三方产品,包括基于 Agent SDK 构建的智能体,都不能提供 claude.ai 登录或它的速率限制。请使用 API 密钥认证。
资料来源
- Anthropic Docs: How tool use works https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works
- Anthropic Docs: Tutorial, build a tool-using agent https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent
- Anthropic Docs: Tool runner https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner
- Anthropic Docs: Stop reasons and fallback https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons
- Anthropic Docs: Prompt caching https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- Anthropic Docs: Compaction overview https://platform.claude.com/docs/en/build-with-claude/compaction
- Anthropic Docs: Compaction at a token threshold https://platform.claude.com/docs/en/build-with-claude/compaction-threshold
- Anthropic Docs: Compaction on demand https://platform.claude.com/docs/en/build-with-claude/compaction-on-demand
- Anthropic Engineering: Effective context engineering for AI agents https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
- X: Simon Willison on the definition of an LLM agent https://x.com/simonw/status/1968755421201711588
- X: Anthropic shares its context engineering article https://x.com/AnthropicAI/status/1973098580060631341
- YouTube (AI Engineer): How We Build Effective Agents, Barry Zhang, Anthropic https://www.youtube.com/watch?v=D7_ipDqhtwk
- Anthropic Docs: Models overview https://platform.claude.com/docs/en/models/overview
- Claude Code Docs: Agent SDK overview https://code.claude.com/docs/en/agent-sdk/overview


