2026-06-11 · harness

第 2 章:跑起你的第一个 agent 回路

用 Claude Agent SDK 的 query() 跑起最小的 agent 回路,看清它替你跑的那台引擎:感知、调用工具、观察、决策。

小满 · 取物长廊

到现在为止,都是你把文件喂给小满。今天,它要自己去取。

草稿章节。跑通格式用的第一版,正式索引前会再打磨。

本章目标

用 Claude Agent SDK 跑起你的第一个真正的 agent 回路。还是贯穿全书的那个 PR 审查 agent,你会写下审查契约、只给它一个 Read 工具、用 query() 把它跑起来,然后看它自己去读 src/payments/refund.py、再交回一份审查清单。读完你能看清每个 agent 框架内部跑的是什么,也就是微软「Tool Use Design Pattern」那一课讲的四步循环:模型感知、决定用哪个工具、工具运行、模型观察结果,然后循环。

第 1 章里,你写的审查契约默认 diff 已经粘好了。这一章变了一点:agent 自己去取上下文。不再是你粘文件,而是它请求去读。就这一个改变(模型主动请求工具,而不是由你把一切预先喂齐),把一个 prompt 变成了一个 agent。而 query() 替你把这个循环跑完,你只需把目标、工具和约束交给它。

前置准备

  • 完成第 1 章:审查契约、项目脚手架和可用的 API key(ANTHROPIC_API_KEY)。
  • 装好 Claude Agent SDK:Python 用 pip install claude-agent-sdk(需 Python 3.10+,CLI 已随包自带),TypeScript 用 npm install @anthropic-ai/claude-agent-sdk(需 Node 18+)。
  • 一个可以安全折腾的仓库,里面有那个要审查的文件 src/payments/refund.py

动手做

1. 把审查契约写成 system prompt,并只给一个工具

用第 1 章那份审查契约当系统提示词。关键的一步是 allowed_tools:它把会被自动放行的工具收成一个 Read,于是这个回路只能读文件,不能写,也不能跑命令。工具越少,回路就越好懂、越安全。

import anyio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock, ToolUseBlock

CONTRACT = """你是一个 PR 审查者。要评判任何文件,必须先用 Read 读它的内容。
读够了就输出审查清单:severity、file:line、一句话风险。绝不臆造你没读过的代码。"""

options = ClaudeAgentOptions(
    system_prompt=CONTRACT,
    allowed_tools=["Read"],   # 只自动放行读文件这一个工具
    max_turns=5,              # 给回路封顶
    cwd="./repo",             # 把它锁在仓库目录里
)
import { query } from "@anthropic-ai/claude-agent-sdk";

const CONTRACT = `你是一个 PR 审查者。要评判任何文件,必须先用 Read 读它的内容。
读够了就输出审查清单:severity、file:line、一句话风险。绝不臆造你没读过的代码。`;

const options = {
  systemPrompt: CONTRACT,
  allowedTools: ["Read"], // 只自动放行读文件这一个工具
  maxTurns: 5,            // 给回路封顶
  cwd: "./repo",          // 把它锁在仓库目录里
};

2. 用 query() 跑起来,给它一个目标

query() 接受一个目标和这套选项,返回一串消息。目标就是触发指令:它点名审查什么,但故意不带文件正文。query() 替你把整个回路跑完,你不用自己写「发请求、判断、执行工具、回灌、再发」那一圈。

async def main():
    async for message in query(
        prompt="审查 src/payments/refund.py 的 bug 和风险。",
        options=options,
    ):
        handle(message)   # 下一步定义

anyio.run(main)
const q = query({
  prompt: "审查 src/payments/refund.py 的 bug 和风险。",
  options,
});

for await (const message of q) {
  handle(message); // 下一步定义
}

3. 遍历消息流,看见这四步

回路跑的时候,query() 一条条吐出消息。你要做的只是读它们。一条 AssistantMessage 里可能有两种块:一个 ToolUseBlock(模型请求调用工具,比如 Read),或一个 TextBlock(模型的文字答复)。这一处分支就是 agent 的核心,其余全是管道,而管道由 SDK 替你接好了。

def handle(message):
    if isinstance(message, AssistantMessage):
        for block in message.content:
            if isinstance(block, ToolUseBlock):
                print(f"[调用工具] {block.name} {block.input}")   # 感知 -> 决定用 Read
            elif isinstance(block, TextBlock):
                print(block.text)                                # 观察结果后的答复
function handle(message) {
  if (message.type === "assistant") {
    for (const block of message.message.content) {
      if (block.type === "tool_use") {
        console.log(`[调用工具] ${block.name}`, block.input); // 感知 -> 决定用 Read
      } else if (block.type === "text") {
        console.log(block.text);                              // 观察结果后的答复
      }
    }
  }
}

4. 给回路设上限,并把它关在仓库里

两道护栏不是可选项。max_turns 给回路设上限:一个迷糊的模型会无限请求读取,到上限就停。allowed_tools 只放行 Read,再加 cwd 把它锁在仓库目录里,模型就碰不到磁盘上别的东西。给它的能力越窄,你越敢用它,这一点会一直延续到第 11 章的护栏。

习得「自己伸手」小满不用再等你把 diff 递到手上,它能自己调用 Read 工具、读进文件里取上下文了。

一条真实 trace

在退款文件上跑这个回路,消息流长这样,四步、一次工具调用:

[调用工具] Read {'file_path': 'src/payments/refund.py'}    <- 感知 + 决定
(SDK 执行 Read,把文件内容回灌给模型)                      <- 工具运行 + 观察
- [ ] (high) refund.py:13 : `<` 拒绝了金额恰好等于总额的退款;
      全额退款静默失败。                                     <- 没有再调工具,回路结束

如何验证

  • 跑一次,确认它恰好调用一次 Read,然后用第 1 章的清单形状给出答复。
  • 把每条消息打印出来,你应当依次看到:工具请求(Read)、然后是最终的文字审查。
  • allowed_tools 改成 [](一个工具都不给),再跑。模型读不了文件,要么如实说「我没法读」,要么干净地停下,而不是臆造代码,这正好印证了契约里「绝不臆造你没读过的代码」那句。
  • max_turns 设成 1,给它一个含糊目标(「审查所有东西」)。看它在封顶处停下,这就证明了护栏为何必要。

习得「读懂回路」你能把每条消息打印出来,看清它感知、调用、观察、决策这四步,从此任何 agent 出错你都问得出「那一步消息流里有什么」。

原理

query() 替你跑的,其实就是一个很简单的调度器:把工具列给模型、执行模型挑中的那个、把结果作为新上下文回灌,循环到模型不再调工具为止。运行时里没有藏着什么智能,唯一的「思考」发生在模型那一端。各种框架(包括这个 SDK)会加上重试、并行工具调用、流式、会话存储,但它们包的都是同一个四步循环。懂了这个,你就能用一个问题调试任何 agent:它出错那一步时,消息流里到底有什么?

小结

你现在跑通了每个 agent 都在跑的四步:感知、调用工具、观察、决策。你没有自己手写回路,因为 SDK 替你跑了,但你看清了它跑的是什么。审查 agent 现在能自己取上下文,不用别人喂一段 diff 给它。下一章讲上下文工程:当那串消息越来越长,怎么决定里面放什么(哪些文件、各放多少、丢掉什么),让回路在长审查里不犯糊涂。

常见坑

  • 没有迭代上限。 不设 max_turns,迷糊的模型会无限循环。务必设一个。
  • 工具放得太宽。 默认情况下 agent 能用整套 Claude Code 工具(含 Write、Bash)。一个只读审查任务,就把 allowed_tools 收成 ["Read"],别让它能改你的代码。
  • 不限定工作目录。 不设 cwd,模型可能读到仓库以外的文件。把它锁在项目目录里。
  • 只读最终文字,忽略工具块。 调试时你要的恰恰是中间的 ToolUseBlock,它告诉你模型到底请求了什么、在哪一步。

一扇一直锁着的门,咔哒松开了。小满第一次自己走进存放文件的地方取东西,收工还怯生生补一句:「……还有两个文件我没敢看,要不要?」你没教过它问这个。取物长廊,亮了。

刚点亮 取物长廊 · 地图已点亮 3 / 16

门后,是连它自己都还读不懂的记忆迷宫。下一站:记忆迷宫。

来源

  1. Claude Agent SDK 文档(Python) · official
下一章 · 第 3 章 上下文工程