Mindket

用 Claude API 从零构建 AI 智能体

黑色横幅,标题是“Claude API 实战”,下面是“调用、工具、缓存与智能体循环”。

基于 Claude API 的 AI 智能体,本质上是一个循环。你发送对话和工具,运行 Claude 要求的工具,追加结果,再次调用,直到 stop_reason 不再是 tool_use。回合上限能挡住失控的循环,压缩能让历史保持在上下文窗口之内。本课用 Python 把这个循环写出来。

本节内容

  • stop_reason 是 tool_use 就继续循环,并且每个回合都追加完整的响应。
  • 自己限制回合数,因为客户端循环没有内置上限。
  • 把工具失败作为 is_error 结果返回,并检查模型选的每一个路径。
  • 用缓存和压缩应对上下文增长,再为循环选对工具。
全部课程
本页目录
  1. 开始之前
  2. 写出智能体循环
  3. 每个回合发生了什么
  4. 停止条件
  5. 上下文增长
  6. 什么时候不再手写循环
  7. Simon Willison 和 Anthropic 谈智能体循环
  8. 常见错误
  9. 自己试一试
  10. 接下来
  11. 自测一下
  12. 常见问题
  13. 资料来源

开始之前

这一课用到前面学的所有内容:第 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?"))
  1. 用较长的描述定义这两个工具,做法见第 2 课。
  2. 检查每一个路径。safe_path 会解析路径,拒绝 docs 之外的一切,因为参数是模型选的,你的代码是唯一的把关人。
  3. 只维护一个 messages 列表,每个回合都往里追加:先是完整的助手 response.content,再是一条包含所有 tool_result 的用户消息。
  4. 只要 stop_reason 不是 tool_use 就离开循环,并且仅在 end_turn 时返回文字。
  5. 数回合数。如果 for 循环用完了,else 分支会报告上限,而不是继续转下去。
  6. 运行它,留意缓存读取的数字。它从 0 开始,历史超过模型的最小长度(Opus 5.5 上是 512 个 token)后开始上升。

每个回合发生了什么

下面这段过程勾勒的是示例中那个问题的消息流向。它用来演示协议,不是一次真实运行的记录。

一个问题,三次调用
  1. Claude 要文件列表

    第一个响应以 stop_reason 为 tool_use 结束,里面是一次 list_files 调用。你的代码运行它,输出作为 tool_result 放进用户消息里送回去。

  2. Claude 打开一个文件

    第二个响应用那个列表里的一个路径调用 read_file。它的结果送回去之后,历史共有五条消息:任务、两个助手回合和两个结果回合。

  3. Claude 给出回答

    第三个响应的 stop_reason 是 end_turn,里面是一个文本块。循环退出,并返回这段文字。

  4. 每次调用都重发历史

    第三次请求比第一次大。有了顶层的 cache_control,前缀达到模型的最小长度之后,已经见过的部分就从缓存里读取。

停止条件

Claude 什么时候说完,由 API 决定。你什么时候放弃,由你决定。文档里的基础循环就是对 stop_reason 的一个 while,工具运行器的 max_iterations 也是可选的,所以没有自己计数器的循环,可能一直跑到上下文被占满。

数回合并设上限。如果你有预算,还要把每个响应的 usage 累加起来。停止原因页面里没有哪个停止原因会报告这两种限制,所以数回合和算 token 预算都得写在你的代码里。

停止原因出自停止原因页面,2026 年 11 月 4 日阅读。
停止信号来自哪里循环该怎么做
end_turnClaude 说完了返回文字
tool_useClaude 想用工具运行它,追加结果,再次调用
max_tokens回复达到了你的上限停下并报告,或者调高上限
refusalClaude 拒绝了读 stop_details,再换备用模型重试
model_context_window_exceeded上下文窗口占满了把回复当作被截断的,更早地压缩
pause_turn服务端工具循环达到了上限把响应原样送回去,再继续
回合数、token 预算、时钟你自己的代码停下并说明原因

上下文增长

每个回合都要重发整段历史,所以每次调用都比上一次大。增长主要来自工具结果:read_file 返回的文件,会留在之后的每一次请求里。示例把文件截断在 20,000 个字符,原因就在这里。提示词缓存让重复的部分更便宜,但不会让它变小。

大小的影响不止于窗口。压缩总览说,对话越长,回复质量越会下降,所以它提供了把较早的回合换成 Claude 在服务端写的摘要的做法。两种压缩目前都在 beta 阶段。

对这样的循环来说,阈值压缩改动更小:在普通请求上加一个参数,把带压缩块的响应整个放进历史就行。按需压缩适合必须自己挑时机的应用,文档说,凡是能用按需压缩的地方都该用它。

影响循环的几个数字

1M

每个当前模型的上下文 token 数

来源:Anthropic Docs: Models overview (2026年11月5日 查阅)

150,000

阈值压缩默认开始之前的输入 token 数

来源:Anthropic Docs: Compaction at a token threshold (2026年11月4日 查阅)

50,000

你能设置的最低触发值

来源:Anthropic Docs: Compaction at a token threshold (2026年11月4日 查阅)

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
出自压缩总览、阈值页面和按需页面,2026 年 11 月 4 日阅读。
选项谁决定时机会发生什么
按 token 阈值压缩API,按你设的触发值在普通请求里做摘要。beta 请求头 compact-2026-01-12,编辑类型 compact_20260112
按需压缩你的应用单独发一个把 compaction 设为 summarize 的请求,返回一个摘要块,放在 messages 的最前面。beta 请求头 compact-2026-09-04
上下文编辑你设定的规则清除旧的工具结果或旧的 thinking 块,而不是做摘要

什么时候不再手写循环

循环先亲手写一遍,像上面那样,这样你才知道库替你做了什么。工具运行器在各 SDK 里还是 beta,它用 client.beta.messages.tool_runner(...) 来运行同样的循环。它把工具异常变成 is_error 结果,并在你设置了 max_iterations 时到点停下。需要人工审批、自定义日志或有条件执行时,文档仍然让你回到手写循环(见工具运行器页面)。

Agent SDK 是另一类工具。它是作为库的 Claude Code,带着自己内置的文件、命令行和网页工具、钩子、子智能体、MCP、权限和会话,支持 Python 和 TypeScript(见总览)。想要整套这样的框架时就用它。Claude Agent SDK 那一课讲了怎么用。

在 Claude 上运行智能体的三种方式
手写循环工具运行器 我们的选择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 智能体,就是为了达成目标而在循环里运行工具。

Simon Willison, Datasette 作者、Django 共同创建者,引文为译文 · X (2026年11月5日 查阅)
Simon Willison(@simonw)2025 年 9 月 18 日在 X 上发帖:我愿意接受一个我认为已被广泛认同、足以派上用场的“智能体”定义。LLM 智能体就是为了达成目标而在循环里运行工具。他还说,多年来他一直认为这个词含糊得无可救药。
Simon Willison 在 X 上,2025 年 9 月 18 日
Anthropic(@AnthropicAI)2025 年 9 月 30 日在 X 上发帖:Anthropic 工程博客新文章。大多数开发者都听说过提示词工程,但要让 AI 智能体发挥最大作用,你需要上下文工程。我们解释它是怎么回事。
Anthropic 在 X 上,2025 年 9 月 30 日
AI Engineer 的 YouTube 频道上,Anthropic 的 Barry Zhang 做的演讲,标题是 How We Build Effective Agents。

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 服务器。

自测一下

  1. Claude 一直在调用工具时,什么能避免智能体循环永远跑下去?

    查看答案

    B. 你自己代码里的回合上限 客户端循环只会在 stop_reason 不是 tool_use 时退出,所以回合数要你自己数。

  2. 同一个助手回合里的所有 tool_result 块,该放在哪里?

    查看答案

    C. 放进一条用户消息,每个都带着自己的 tool_use id 每个回合一条用户消息,能让 id 对得上,也能让 Claude 继续做并行调用。

  3. 为什么要追加完整的 response.content,而不是只追加文本块?

    查看答案

    A. tool_use 和 thinking 块必须原样传回 API 需要 tool_use 块来和结果配对,并且要求 thinking 块原样传回。

  4. 在基于 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 密钥认证。

资料来源

  1. Anthropic Docs: How tool use works (2026年11月4日 查阅)https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works
  2. Anthropic Docs: Tutorial, build a tool-using agent (2026年11月4日 查阅)https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent
  3. Anthropic Docs: Tool runner (2026年11月4日 查阅)https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner
  4. Anthropic Docs: Stop reasons and fallback (2026年11月4日 查阅)https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons
  5. Anthropic Docs: Prompt caching (2026年11月4日 查阅)https://platform.claude.com/docs/en/build-with-claude/prompt-caching
  6. Anthropic Docs: Compaction overview (2026年11月4日 查阅)https://platform.claude.com/docs/en/build-with-claude/compaction
  7. Anthropic Docs: Compaction at a token threshold (2026年11月4日 查阅)https://platform.claude.com/docs/en/build-with-claude/compaction-threshold
  8. Anthropic Docs: Compaction on demand (2026年11月4日 查阅)https://platform.claude.com/docs/en/build-with-claude/compaction-on-demand
  9. Anthropic Engineering: Effective context engineering for AI agents (2026年11月5日 查阅)https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
  10. X: Simon Willison on the definition of an LLM agent (2026年11月5日 查阅)https://x.com/simonw/status/1968755421201711588
  11. X: Anthropic shares its context engineering article (2026年11月5日 查阅)https://x.com/AnthropicAI/status/1973098580060631341
  12. YouTube (AI Engineer): How We Build Effective Agents, Barry Zhang, Anthropic (2026年11月5日 查阅)https://www.youtube.com/watch?v=D7_ipDqhtwk
  13. Anthropic Docs: Models overview (2026年11月5日 查阅)https://platform.claude.com/docs/en/models/overview
  14. Claude Code Docs: Agent SDK overview (2026年11月4日 查阅)https://code.claude.com/docs/en/agent-sdk/overview

分享本节课

看看成品智能体是怎样上架的

在 Mindket 浏览 AI 智能体和它们的设置说明,再把你在这里做出的智能体也上架。

浏览 AI 智能体

本节课的 Markdown 版本 · Claude API 实战课:从零构建 AI 智能体 · 全部课程