AI Agent 教程

第4章:query() —— Agent 的启动按钮

一句话:掌握 SDK 的核心函数 query() 的所有用法,从此你就能让 Agent 听你指挥干活了。

本章目标

前置知识


4.1 query() 是什么?

整个 Claude Agent SDK 就围绕一个函数 —— query()

没错,就一个函数。你学会了它,就学会了整个 SDK 的 80%。

打个比方

query() 就像洗衣机上的"启动"按钮:

  1. 你先把衣服放进去(设置 prompt —— 告诉 Agent 要干什么)
  2. 选好洗衣程序(设置 options —— 用什么模型、给什么工具、权限怎么配)
  3. 按下启动按钮(调用 query()
  4. 洗衣机开始自动运转(Agent 进入 Agentic Loop —— 思考、行动、检查、再思考...)
  5. 你可以随时看进度(通过 for await 接收消息)
  6. 洗完了会通知你(收到 ResultMessage)

函数签名

import { query } from "@anthropic-ai/claude-code";

function query({
  prompt,
  options
}: {
  prompt: string | AsyncIterable<SDKUserMessage>;
  options?: Options;
}): Query

返回值 Query 是一个 AsyncGenerator<SDKMessage, void>,说人话就是:它会一条一条地"吐出"消息,你用 for await 去接就行。

它的工作流程

你调用 query()
    │
    ▼
SDK 创建一个 Agent 循环
    │
    ▼
┌──────────────────────────────┐
│  Agent 循环开始运转:         │
│                              │
│  1. 发 SystemMessage(init) │  ← 告诉你"我准备好了"
│  2. Claude 思考,给回复      │  ← AssistantMessage
│  3. 需要用工具吗?           │
│     ├── 是 → 执行工具        │
│     │   → 把结果交给 Claude  │
│     │   → 回到第2步          │
│     └── 否 → 任务完成        │
│  4. 发 ResultMessage         │  ← 告诉你"我做完了"
│                              │
└──────────────────────────────┘

整个过程全自动,你只需要坐等消息就行。


4.2 最小可运行示例

示例1:最简单的 —— 让 Agent 说句话

// 文件:hello-agent.ts
import { query } from "@anthropic-ai/claude-code";

async function main() {
  // 调用 query(),启动 Agent
  for await (const message of query({
    prompt: "用一句话介绍你自己",
    options: {
      allowedTools: [],         // 不给任何工具,纯聊天
      maxTurns: 1,              // 只跑一轮
      permissionMode: "default" // 默认权限模式
    }
  })) {
    // 只关心 Agent 的回复
    if (message.type === "assistant") {
      // message.message.content 是一个数组,里面有 text 块
      for (const block of message.message.content) {
        if (block.type === "text") {
          console.log("Agent 说:", block.text);
        }
      }
    }
  }
}

main().catch(console.error);

逐行解释:

干什么
import { query } 从 SDK 导入唯一的核心函数
for await (const message of query({...})) 启动 Agent,并逐条接收消息
prompt: "用一句话介绍你自己" 告诉 Agent 要做什么
allowedTools: [] 不给工具,Agent 只能"说话"不能"动手"
maxTurns: 1 只允许一个回合
message.type === "assistant" 过滤出 Agent 的回复消息
block.type === "text" 过滤出文本内容(不是工具调用)

运行它:

npx ts-node hello-agent.ts

你会看到 Agent 回复一句自我介绍,然后程序结束。

示例2:给 Agent 一些工具

// 文件:agent-with-tools.ts
import { query } from "@anthropic-ai/claude-code";

async function main() {
  for await (const message of query({
    prompt: "请查看当前目录下有哪些文件,然后告诉我",
    options: {
      allowedTools: ["Bash", "Glob", "Read"],  // 给三个工具
      permissionMode: "bypassPermissions",       // 自动放行(测试用)
      maxTurns: 5                                // 最多跑5轮
    }
  })) {
    if (message.type === "assistant") {
      for (const block of message.message.content) {
        if (block.type === "text") {
          console.log(block.text);
        }
        if (block.type === "tool_use") {
          console.log(`[调用工具] ${block.name}(${JSON.stringify(block.input)})`);
        }
      }
    }

    if (message.type === "result") {
      console.log("\n--- 任务完成 ---");
      if (message.subtype === "success") {
        console.log("花费:", message.total_cost_usd, "美元");
        console.log("用了", message.num_turns, "个回合");
      }
    }
  }
}

main().catch(console.error);

这次 Agent 有工具了!它可能会:

  1. 先用 Bash 执行 ls 命令
  2. 看到文件列表后,用自然语言告诉你结果

你能在终端里实时看到它的思考和行动过程。


4.3 理解消息类型

query() 会吐出好几种消息。最重要的就三种,我们一个一个来看。

消息流的整体顺序

SystemMessage (type: "system", subtype: "init")
    ↓
AssistantMessage (type: "assistant")    ← 可能有多条
    ↓
AssistantMessage (type: "assistant")    ← Agent 继续思考和行动
    ↓
ResultMessage (type: "result")          ← 最后一条

4.3.1 SystemMessage —— "我准备好了"

这是你收到的第一条消息,Agent 用它告诉你:"我启动了,这是我的基本信息。"

TypeScript 类型定义:

type SDKSystemMessage = {
  type: "system";
  subtype: "init";
  uuid: string;
  session_id: string;        // 会话 ID,很重要!后面恢复会话要用
  apiKeySource: string;       // API Key 来源
  cwd: string;               // 工作目录
  tools: string[];            // 可用工具列表
  mcp_servers: {              // MCP 服务器状态
    name: string;
    status: string;
  }[];
  model: string;              // 使用的模型
  permissionMode: string;     // 权限模式
  slash_commands: string[];   // 可用的斜杠命令
  output_style: string;       // 输出风格
}

实际拿到的数据长什么样?

// 打印 SystemMessage
if (message.type === "system" && message.subtype === "init") {
  console.log("会话 ID:", message.session_id);
  console.log("工作目录:", message.cwd);
  console.log("可用工具:", message.tools);
  console.log("使用模型:", message.model);
  console.log("权限模式:", message.permissionMode);
  console.log("MCP 服务器:", message.mcp_servers);
}

输出示例:

会话 ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
工作目录: /home/user/my-project
可用工具: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"]
使用模型: claude-sonnet-4-20250514
权限模式: bypassPermissions
MCP 服务器: []

这条消息有什么用?

4.3.2 AssistantMessage —— Agent 的"实时汇报"

这是最常见的消息,每次 Claude 思考完或者要调用工具时,你都会收到一条。

TypeScript 类型定义:

type SDKAssistantMessage = {
  type: "assistant";
  uuid: string;
  session_id: string;
  message: {                    // Anthropic API 的原始消息格式
    role: "assistant";
    content: ContentBlock[];    // 内容块数组
    stop_reason: string;        // 停止原因
    usage: Usage;               // Token 用量
  };
  parent_tool_use_id: string | null;  // 如果是子 Agent,这里有值
}

重点来了:content 数组

content 里面可以有两种东西:

1. 文本块(TextBlock)—— Agent 在"说话":

{
  type: "text",
  text: "让我来看看当前目录有哪些文件..."
}

2. 工具调用块(ToolUseBlock)—— Agent 在"动手":

{
  type: "tool_use",
  id: "toolu_abc123",
  name: "Bash",
  input: {
    command: "ls -la"
  }
}

处理 AssistantMessage 的完整代码:

if (message.type === "assistant") {
  for (const block of message.message.content) {
    switch (block.type) {
      case "text":
        // Agent 在说话
        console.log("[思考]", block.text);
        break;

      case "tool_use":
        // Agent 在调用工具
        console.log(`[行动] 调用 ${block.name}`);
        console.log(`  参数: ${JSON.stringify(block.input, null, 2)}`);
        break;
    }
  }

  // 还可以看看为什么停下来
  const stopReason = message.message.stop_reason;
  if (stopReason === "end_turn") {
    console.log("(Agent 说完了)");
  } else if (stopReason === "tool_use") {
    console.log("(Agent 要用工具,循环继续)");
  }
}

4.3.3 ResultMessage —— "我做完了"

这是你收到的最后一条消息。Agent 用它告诉你:任务完成了(或者出错了)。

ResultMessage 有两种变体:

变体1:成功

{
  type: "result",
  subtype: "success",
  uuid: string,
  session_id: string,
  duration_ms: number,          // 总耗时(毫秒)
  duration_api_ms: number,      // API 调用耗时
  is_error: false,
  num_turns: number,            // 跑了几个回合
  result: string,               // 最终结果文本
  total_cost_usd: number,       // 总花费(美元)
  usage: {                      // Token 用量统计
    input_tokens: number,
    output_tokens: number,
    cache_creation_input_tokens: number,
    cache_read_input_tokens: number
  },
  modelUsage: { ... },          // 按模型分的用量
  permission_denials: [],       // 被拒绝的权限列表
  structured_output?: unknown   // 结构化输出(如果配了的话)
}

变体2:失败

{
  type: "result",
  subtype: "error_max_turns"                    // 超过最大回合数
         | "error_during_execution"             // 执行过程中出错
         | "error_max_budget_usd"               // 超过预算
         | "error_max_structured_output_retries", // 结构化输出重试次数超限
  uuid: string,
  session_id: string,
  duration_ms: number,
  duration_api_ms: number,
  is_error: true,
  num_turns: number,
  total_cost_usd: number,
  usage: { ... },
  modelUsage: { ... },
  permission_denials: [],
  errors: string[]              // 错误信息列表
}

处理 ResultMessage 的代码:

if (message.type === "result") {
  if (message.subtype === "success") {
    console.log("任务成功完成!");
    console.log("结果:", message.result);
    console.log("花费:", message.total_cost_usd.toFixed(4), "美元");
    console.log("耗时:", (message.duration_ms / 1000).toFixed(1), "秒");
    console.log("回合数:", message.num_turns);
    console.log("Token 用量:", JSON.stringify(message.usage));

    // 如果有结构化输出
    if (message.structured_output) {
      console.log("结构化数据:", message.structured_output);
    }
  } else {
    console.error("任务失败!");
    console.error("失败类型:", message.subtype);
    if ("errors" in message) {
      console.error("错误详情:", message.errors);
    }
  }
}

消息类型速查表

类型 type 值 什么时候出现 你要关注什么
SystemMessage "system" 最开始 session_idtoolsmodel
AssistantMessage "assistant" 中间(多次) content 数组中的 text 和 tool_use
ResultMessage "result" 最后 subtypetotal_cost_usdresult

4.4 配置选项大全(Options)

query() 的第二个参数 options 是一个对象,里面有很多配置项。下面我按"使用频率"从高到低来讲。

4.4.1 allowedTools —— 给 Agent 什么工具

allowedTools: string[]

大白话: 决定 Agent 能用哪些工具。不给工具,Agent 就只能说话不能干活。

常用工具列表:

工具名 干什么的
"Read" 读文件
"Write" 写文件
"Edit" 编辑文件
"Bash" 执行 Shell 命令
"Glob" 按模式搜索文件名
"Grep" 搜索文件内容
"WebSearch" 搜索网页
"WebFetch" 抓取网页内容
"Task" 启动子 Agent
"TodoRead" 读取任务清单
"TodoWrite" 写入任务清单

代码示例:

// 纯聊天 Agent —— 没有工具
const chatAgent = query({
  prompt: "讲个笑话",
  options: { allowedTools: [] }
});

// 只读 Agent —— 只能看不能改
const readOnlyAgent = query({
  prompt: "分析这个项目的代码结构",
  options: { allowedTools: ["Read", "Glob", "Grep"] }
});

// 全能 Agent —— 能读能写能执行
const fullAgent = query({
  prompt: "帮我重构这个函数",
  options: { allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"] }
});

// 带 MCP 工具 —— 通配符写法
const mcpAgent = query({
  prompt: "列出我的 GitHub Issues",
  options: { allowedTools: ["mcp__github__*"] }  // 允许 github MCP 的所有工具
});

什么时候用: 每次调用 query() 都应该设置这个,精确控制 Agent 的能力边界。

4.4.2 permissionMode —— Agent 的"胆子大小"

permissionMode: "default" | "acceptEdits" | "bypassPermissions" | "plan"

大白话: 决定 Agent 遇到敏感操作时怎么办。

模式 行为 适用场景
"default" 遇到敏感操作就停下来问你 日常使用(推荐)
"acceptEdits" 文件编辑自动放行,其他的问你 已信任 Agent 改文件
"bypassPermissions" 全部放行,不问你 自动化流水线、测试
"plan" 只许看不许动,Agent 只能制定计划 预览 Agent 想做什么

代码示例:

// 开发调试时 —— 全自动,不要打扰我
const devAgent = query({
  prompt: "帮我修复这个 bug",
  options: {
    allowedTools: ["Read", "Write", "Edit", "Bash"],
    permissionMode: "bypassPermissions"
  }
});

// 生产环境 —— 保守一点
const prodAgent = query({
  prompt: "分析服务器日志",
  options: {
    allowedTools: ["Read", "Bash"],
    permissionMode: "default"
  }
});

// 只是想看看 Agent 打算怎么做,不让它真的动手
const planAgent = query({
  prompt: "如何重构这个模块?给我一个计划",
  options: {
    allowedTools: ["Read", "Glob", "Grep"],
    permissionMode: "plan"
  }
});

4.4.3 model —— 选哪个大脑

model: string

大白话: 决定用哪个 Claude 模型。不同模型在智商和价格之间有取舍。

模型 特点 适用场景
"sonnet" 性价比之王,够聪明又不太贵 大部分任务(默认)
"opus" 最聪明,但最贵 复杂推理、高质量代码
"haiku" 最快最便宜,但能力稍弱 简单任务、高频调用

代码示例:

// 简单任务用 Haiku —— 省钱
const simpleAgent = query({
  prompt: "把这段 JSON 格式化一下",
  options: {
    model: "haiku",
    allowedTools: []
  }
});

// 复杂任务用 Opus —— 不差钱,要质量
const complexAgent = query({
  prompt: "对这个项目做全面的安全审计",
  options: {
    model: "opus",
    allowedTools: ["Read", "Glob", "Grep", "Bash"]
  }
});

// 日常任务用 Sonnet —— 均衡选择
const dailyAgent = query({
  prompt: "帮我写一个排序函数",
  options: {
    model: "sonnet",
    allowedTools: ["Write"]
  }
});

4.4.4 systemPrompt —— Agent 的"人设"

systemPrompt: string | { replace: string } | { append: string }

大白话: 告诉 Agent "你是谁"、"你要怎么做"。就像给新员工的入职培训手册。

三种用法:

// 用法1:直接替换 —— 完全用你的 prompt(传字符串即可)
const agent1 = query({
  prompt: "写个函数",
  options: {
    systemPrompt: "你是一个资深 TypeScript 开发者,写代码要简洁优雅,注释要清晰。",
    allowedTools: ["Write"]
  }
});

// 用法2:明确替换写法
const agent2 = query({
  prompt: "写个函数",
  options: {
    systemPrompt: {
      replace: "你是一个严格的代码审查员,对代码质量有极高要求。"
    },
    allowedTools: ["Read"]
  }
});

// 用法3:在默认 prompt 后面追加
const agent3 = query({
  prompt: "帮我改代码",
  options: {
    systemPrompt: {
      append: "\n\n额外要求:所有函数都要写 JSDoc 注释。"
    },
    allowedTools: ["Read", "Edit"]
  }
});

什么时候用: 当你想让 Agent 以特定的风格或角色来工作时。

4.4.5 maxTurns —— 最多跑几轮

maxTurns: number

大白话: 限制 Agent 最多执行几个来回。防止 Agent 陷入无限循环。

一个"回合"(turn)= Claude 回复一次。如果 Claude 回复中包含工具调用,执行完工具后 Claude 再次回复,那就是两个回合。

代码示例:

// 只允许一个回合 —— 快问快答
const quickAgent = query({
  prompt: "1+1 等于几?",
  options: {
    allowedTools: [],
    maxTurns: 1
  }
});

// 允许多个回合 —— 复杂任务
const longAgent = query({
  prompt: "把整个项目从 JavaScript 迁移到 TypeScript",
  options: {
    allowedTools: ["Read", "Write", "Edit", "Bash", "Glob"],
    maxTurns: 50  // 给它足够的空间
  }
});

注意: 如果达到 maxTurns 限制,你会收到 subtype: "error_max_turns" 的 ResultMessage。

4.4.6 cwd —— 工作目录

cwd: string

大白话: 告诉 Agent "你在哪个目录干活"。Agent 的文件操作和命令执行都是相对于这个目录的。

代码示例:

// 在特定项目目录下工作
const projectAgent = query({
  prompt: "分析这个项目的 package.json",
  options: {
    cwd: "/home/user/my-project",
    allowedTools: ["Read"]
  }
});

// 在临时目录下工作 —— 安全隔离
const sandboxAgent = query({
  prompt: "创建一个新项目",
  options: {
    cwd: "/tmp/sandbox",
    allowedTools: ["Write", "Bash"]
  }
});

默认值: 当前进程的工作目录(process.cwd())。

4.4.7 env —— 环境变量

env: Record<string, string>

大白话: 给 Agent 传一些环境变量,它执行命令时可以用到。

代码示例:

const agent = query({
  prompt: "部署项目到测试环境",
  options: {
    allowedTools: ["Bash"],
    env: {
      NODE_ENV: "test",
      API_URL: "https://test-api.example.com",
      DB_HOST: "test-db.example.com"
    }
  }
});

什么时候用: 当 Agent 需要访问特定环境的配置时。

4.4.8 resume —— 恢复会话

resume: {
  sessionId: string;          // 要恢复的会话 ID
  prompt: string;             // 新的提示
}

大白话: 让 Agent 接着上次没说完的继续说。就像你打电话中途断了,重新拨回去继续聊。

代码示例:

// 第一次对话
let savedSessionId = "";

for await (const message of query({
  prompt: "帮我写一个 Todo 应用",
  options: { allowedTools: ["Write", "Bash"] }
})) {
  if (message.type === "system") {
    savedSessionId = message.session_id;  // 保存会话 ID
  }
}

// ... 过了一会儿 ...

// 恢复对话,继续交流
for await (const message of query({
  prompt: "继续上次的工作,给 Todo 应用加上删除功能",
  options: {
    resume: {
      sessionId: savedSessionId,
      prompt: "继续上次的工作,给 Todo 应用加上删除功能"
    },
    allowedTools: ["Read", "Write", "Edit", "Bash"]
  }
})) {
  // 处理消息...
}

4.4.9 outputFormat —— 结构化输出

outputFormat: {
  type: "json_schema";
  schema: JSONSchema;
}

大白话: 告诉 Agent "你的回答必须是这个 JSON 格式"。适合需要程序解析 Agent 输出的场景。

代码示例:

const agent = query({
  prompt: "分析这段代码的质量",
  options: {
    allowedTools: ["Read"],
    outputFormat: {
      type: "json_schema",
      schema: {
        type: "object",
        properties: {
          score: {
            type: "number",
            description: "代码质量评分,1-10"
          },
          issues: {
            type: "array",
            items: { type: "string" },
            description: "发现的问题列表"
          },
          suggestions: {
            type: "array",
            items: { type: "string" },
            description: "改进建议"
          }
        },
        required: ["score", "issues", "suggestions"]
      }
    }
  }
});

for await (const message of agent) {
  if (message.type === "result" && message.subtype === "success") {
    // structured_output 就是解析好的 JSON 对象
    const report = message.structured_output as {
      score: number;
      issues: string[];
      suggestions: string[];
    };
    console.log("评分:", report.score);
    console.log("问题:", report.issues);
    console.log("建议:", report.suggestions);
  }
}

4.4.10 mcpServers —— 连接外部工具

mcpServers: Record<string, McpServerConfig>

大白话: 让 Agent 连接外部的 MCP 服务器,获取更多工具。就像给电脑插上 USB 设备。

代码示例:

const agent = query({
  prompt: "列出我的 GitHub 仓库的 Issues",
  options: {
    mcpServers: {
      github: {
        command: "npx",
        args: ["-y", "@modelcontextprotocol/server-github"],
        env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "" }
      }
    },
    allowedTools: ["mcp__github__*"]  // 允许使用 github MCP 的所有工具
  }
});

什么时候用: 当内置工具不够用,需要连接外部服务时。(第11章会详细讲)

4.4.11 agents —— 定义子 Agent

agents: Record<string, AgentConfig>

大白话: 给主 Agent 配几个"下属",不同的下属负责不同的工作。

代码示例:

const agent = query({
  prompt: "审查这个 PR 的代码质量,然后写测试",
  options: {
    agents: {
      "code-reviewer": {
        description: "专门做代码审查的 Agent",
        prompt: "你是一个严格的代码审查员,关注安全和性能问题。",
        tools: ["Read", "Glob", "Grep"],
        model: "sonnet"
      },
      "test-writer": {
        description: "专门写测试用例的 Agent",
        prompt: "你是一个测试专家,为代码编写全面的单元测试。",
        tools: ["Read", "Write", "Bash"],
        model: "haiku"
      }
    },
    allowedTools: ["Read", "Glob", "Grep", "Write", "Bash", "Task"]
  }
});

什么时候用: 当任务比较复杂,需要多个 Agent 分工合作时。(第13章会详细讲)

4.4.12 hooks —— 生命周期钩子

hooks: {
  preToolUse?: (params) => Promise<HookResult>;
  postToolUse?: (params) => Promise<HookResult>;
  // ...更多 hook
}

大白话: 在 Agent 干活的每个关键节点,插入你自己的逻辑。比如记日志、拦截危险操作。

代码示例:

const agent = query({
  prompt: "整理项目文件",
  options: {
    allowedTools: ["Read", "Write", "Bash"],
    hooks: {
      preToolUse: async ({ toolName, toolInput }) => {
        // 每次 Agent 要用工具之前,打个日志
        console.log(`[审计] Agent 准备调用: ${toolName}`);
        console.log(`[审计] 参数:`, toolInput);

        // 拦截危险操作
        if (toolName === "Bash" &&
            typeof toolInput.command === "string" &&
            toolInput.command.includes("rm -rf")) {
          console.log("[安全] 拦截了危险命令!");
          return {
            permissionDecision: "deny",
            message: "不允许执行 rm -rf 命令"
          };
        }

        return {};  // 放行
      }
    }
  }
});

什么时候用: 需要监控、审计或安全控制时。(第12章会详细讲)

4.4.13 settingSources —— 加载配置文件

settingSources: ("user" | "project" | "local")[]

大白话: 决定是否从文件系统加载配置(比如 .claude/settings.json)。

含义 加载位置
"user" 用户全局配置 ~/.claude/settings.json
"project" 项目共享配置 .claude/settings.json
"local" 项目本地配置 .claude/settings.local.json

代码示例:

// 不加载任何文件配置(默认行为)
const isolatedAgent = query({
  prompt: "...",
  options: { allowedTools: [] }  // settingSources 不设就不加载
});

// 加载所有配置
const fullConfigAgent = query({
  prompt: "...",
  options: {
    settingSources: ["user", "project", "local"],
    allowedTools: []
  }
});

// 只加载项目配置
const projectConfigAgent = query({
  prompt: "...",
  options: {
    settingSources: ["project"],
    allowedTools: []
  }
});

什么时候用: 当你想复用 Claude Code CLI 的配置(比如权限规则、工具限制)时。

4.4.14 abortController —— 随时喊停

abortController: AbortController

大白话: 给你一个"紧急停止"按钮,随时可以终止 Agent。

代码示例:

const controller = new AbortController();

// 10 秒后自动终止
setTimeout(() => {
  console.log("超时了,终止 Agent!");
  controller.abort();
}, 10000);

for await (const message of query({
  prompt: "做一个很复杂的分析",
  options: {
    abortController: controller,
    allowedTools: ["Read", "Glob"]
  }
})) {
  // 处理消息...
}

配置选项速查表

选项 类型 必填 说明
allowedTools string[] 推荐 Agent 可用的工具
permissionMode string 权限模式
model string 模型选择
systemPrompt string | object Agent 的人设
maxTurns number 最大回合数
cwd string 工作目录
env Record<string, string> 环境变量
resume object 恢复会话
outputFormat object 结构化输出
mcpServers object MCP 服务器
agents object 子 Agent
hooks object 生命周期钩子
settingSources string[] 配置文件来源
abortController AbortController 中止控制器

4.5 实战示例集

下面是5个完整的、可以直接运行的示例。每个示例都展示了不同的 query() 用法。

示例1:简单问答 Agent(纯聊天)

// 文件:examples/01-simple-qa.ts
// 最简单的 Agent:没有工具,就是个聊天机器人
import { query } from "@anthropic-ai/claude-code";

async function simpleQA() {
  console.log("=== 简单问答 Agent ===\n");

  for await (const message of query({
    prompt: "请用三句话解释什么是递归",
    options: {
      allowedTools: [],
      maxTurns: 1
    }
  })) {
    if (message.type === "assistant") {
      for (const block of message.message.content) {
        if (block.type === "text") {
          console.log(block.text);
        }
      }
    }

    if (message.type === "result" && message.subtype === "success") {
      console.log(`\n[花费: $${message.total_cost_usd.toFixed(4)}]`);
    }
  }
}

simpleQA().catch(console.error);

示例2:代码分析 Agent(带工具)

// 文件:examples/02-code-analyzer.ts
// 能读文件和搜索的 Agent,帮你分析代码
import { query } from "@anthropic-ai/claude-code";

async function codeAnalyzer() {
  console.log("=== 代码分析 Agent ===\n");

  const targetDir = process.argv[2] || ".";

  for await (const message of query({
    prompt: `请分析目录 ${targetDir} 下的项目结构,告诉我:
    1. 这是什么类型的项目
    2. 用了哪些主要技术
    3. 项目结构是否合理`,
    options: {
      cwd: targetDir,
      allowedTools: ["Read", "Glob", "Grep"],
      permissionMode: "bypassPermissions",
      maxTurns: 10
    }
  })) {
    switch (message.type) {
      case "system":
        console.log(`[启动] 会话 ${message.session_id}`);
        console.log(`[启动] 工作目录: ${message.cwd}`);
        console.log(`[启动] 可用工具: ${message.tools.join(", ")}\n`);
        break;

      case "assistant":
        for (const block of message.message.content) {
          if (block.type === "text") {
            console.log(block.text);
          }
          if (block.type === "tool_use") {
            console.log(`\n  [工具] ${block.name}: ${JSON.stringify(block.input).slice(0, 100)}...\n`);
          }
        }
        break;

      case "result":
        console.log("\n=== 分析完成 ===");
        if (message.subtype === "success") {
          console.log(`耗时: ${(message.duration_ms / 1000).toFixed(1)}秒`);
          console.log(`回合: ${message.num_turns}`);
          console.log(`花费: $${message.total_cost_usd.toFixed(4)}`);
        } else {
          console.error(`失败: ${message.subtype}`);
        }
        break;
    }
  }
}

codeAnalyzer().catch(console.error);

示例3:海盗人设 Agent(自定义 systemPrompt)

// 文件:examples/03-pirate-agent.ts
// 用 systemPrompt 让 Agent 扮演海盗
import { query } from "@anthropic-ai/claude-code";

async function pirateAgent() {
  console.log("=== 海盗 Agent ===\n");

  for await (const message of query({
    prompt: "告诉我今天应该做什么",
    options: {
      systemPrompt: `你是一个经验丰富的海盗船长,名叫"代码胡子"。
你的特点:
- 说话时经常用"啊呀"、"嗷"、"宝藏"等海盗用语
- 把编程比喻成航海冒险
- 把 bug 叫做"海怪"
- 把代码叫做"藏宝图"
- 每句话结尾喜欢加"嗷!"

记住:你永远是一个海盗,无论用户问什么,都要用海盗的方式回答。`,
      allowedTools: [],
      maxTurns: 1
    }
  })) {
    if (message.type === "assistant") {
      for (const block of message.message.content) {
        if (block.type === "text") {
          console.log(block.text);
        }
      }
    }
  }
}

pirateAgent().catch(console.error);

示例4:限制回合数的 Agent

// 文件:examples/04-limited-turns.ts
// 观察 maxTurns 如何影响 Agent 行为
import { query } from "@anthropic-ai/claude-code";

async function limitedTurnsAgent() {
  // 测试不同的 maxTurns 值
  for (const maxTurns of [1, 3, 10]) {
    console.log(`\n=== maxTurns = ${maxTurns} ===\n`);

    let turnCount = 0;

    for await (const message of query({
      prompt: "请创建一个简单的 Node.js HTTP 服务器,写入文件,然后验证文件存在",
      options: {
        allowedTools: ["Write", "Bash", "Read"],
        permissionMode: "bypassPermissions",
        maxTurns: maxTurns,
        cwd: "/tmp/test-turns"
      }
    })) {
      if (message.type === "assistant") {
        turnCount++;
        console.log(`  [回合 ${turnCount}]`);
        for (const block of message.message.content) {
          if (block.type === "text") {
            // 只显示前100个字符
            console.log(`  ${block.text.slice(0, 100)}...`);
          }
          if (block.type === "tool_use") {
            console.log(`  [工具] ${block.name}`);
          }
        }
      }

      if (message.type === "result") {
        console.log(`\n  结果: ${message.subtype}`);
        if (message.subtype === "success") {
          console.log(`  实际回合: ${message.num_turns}`);
        }
        if (message.subtype === "error_max_turns") {
          console.log("  ⚠ 达到了 maxTurns 限制!Agent 没做完就被停了。");
        }
      }
    }
  }
}

limitedTurnsAgent().catch(console.error);

示例5:不同模型对比

// 文件:examples/05-model-comparison.ts
// 用不同模型回答同一个问题,对比质量和成本
import { query } from "@anthropic-ai/claude-code";

async function modelComparison() {
  const question = "用 TypeScript 实现一个 LRU Cache,要求支持 get 和 put 操作,时间复杂度 O(1)。";

  for (const model of ["haiku", "sonnet", "opus"]) {
    console.log(`\n${"=".repeat(60)}`);
    console.log(`模型: ${model}`);
    console.log("=".repeat(60) + "\n");

    const startTime = Date.now();

    for await (const message of query({
      prompt: question,
      options: {
        model: model,
        allowedTools: [],
        maxTurns: 1
      }
    })) {
      if (message.type === "assistant") {
        for (const block of message.message.content) {
          if (block.type === "text") {
            console.log(block.text);
          }
        }
      }

      if (message.type === "result" && message.subtype === "success") {
        const elapsed = Date.now() - startTime;
        console.log(`\n--- ${model} 统计 ---`);
        console.log(`响应时间: ${(elapsed / 1000).toFixed(1)}秒`);
        console.log(`花费: $${message.total_cost_usd.toFixed(6)}`);
        console.log(`输入 Token: ${message.usage.input_tokens}`);
        console.log(`输出 Token: ${message.usage.output_tokens}`);
      }
    }
  }
}

modelComparison().catch(console.error);

4.6 处理消息的完整模板

下面是一个生产级别的消息处理模板,你可以直接复制到自己的项目中使用。

// 文件:agent-template.ts
// 一个生产就绪的 Agent 消息处理模板
import { query, type SDKMessage } from "@anthropic-ai/claude-code";

// ============================
// 1. 配置区
// ============================
interface AgentConfig {
  prompt: string;
  tools: string[];
  model?: string;
  systemPrompt?: string;
  maxTurns?: number;
  cwd?: string;
}

// ============================
// 2. Agent 运行器
// ============================
async function runAgent(config: AgentConfig) {
  let sessionId = "";
  let totalText = "";
  const toolCalls: Array<{ name: string; input: unknown }> = [];

  console.log("Agent 启动中...\n");

  try {
    for await (const message of query({
      prompt: config.prompt,
      options: {
        allowedTools: config.tools,
        model: config.model ?? "sonnet",
        systemPrompt: config.systemPrompt,
        maxTurns: config.maxTurns ?? 20,
        cwd: config.cwd,
        permissionMode: "bypassPermissions"
      }
    })) {
      handleMessage(message, {
        onSessionStart: (id) => {
          sessionId = id;
        },
        onText: (text) => {
          totalText += text;
        },
        onToolCall: (name, input) => {
          toolCalls.push({ name, input });
        }
      });
    }
  } catch (error) {
    console.error("\nAgent 运行出错:", error);
  }

  return { sessionId, totalText, toolCalls };
}

// ============================
// 3. 消息处理器
// ============================
interface MessageCallbacks {
  onSessionStart?: (sessionId: string) => void;
  onText?: (text: string) => void;
  onToolCall?: (name: string, input: unknown) => void;
}

function handleMessage(message: SDKMessage, callbacks: MessageCallbacks) {
  switch (message.type) {
    // ------ SystemMessage:Agent 启动 ------
    case "system": {
      if (message.subtype === "init") {
        console.log(`[init] 会话: ${message.session_id}`);
        console.log(`[init] 模型: ${message.model}`);
        console.log(`[init] 工具: ${message.tools.join(", ")}`);
        console.log(`[init] 目录: ${message.cwd}`);
        console.log("");
        callbacks.onSessionStart?.(message.session_id);
      }
      break;
    }

    // ------ AssistantMessage:Agent 思考和行动 ------
    case "assistant": {
      for (const block of message.message.content) {
        if (block.type === "text") {
          // 实时显示 Agent 的文字输出
          process.stdout.write(block.text);
          callbacks.onText?.(block.text);
        }

        if (block.type === "tool_use") {
          // 显示工具调用
          const inputStr = JSON.stringify(block.input);
          const shortInput = inputStr.length > 80
            ? inputStr.slice(0, 80) + "..."
            : inputStr;
          console.log(`\n  >> [${block.name}] ${shortInput}\n`);
          callbacks.onToolCall?.(block.name, block.input);
        }
      }
      break;
    }

    // ------ ResultMessage:任务完成 ------
    case "result": {
      console.log("\n");
      console.log("=".repeat(50));

      if (message.subtype === "success") {
        console.log("任务完成");
        console.log("-".repeat(50));
        console.log(`耗时:     ${(message.duration_ms / 1000).toFixed(1)} 秒`);
        console.log(`API耗时:  ${(message.duration_api_ms / 1000).toFixed(1)} 秒`);
        console.log(`回合数:   ${message.num_turns}`);
        console.log(`花费:     $${message.total_cost_usd.toFixed(4)}`);
        console.log(`输入Token: ${message.usage.input_tokens}`);
        console.log(`输出Token: ${message.usage.output_tokens}`);

        if (message.usage.cache_read_input_tokens > 0) {
          console.log(`缓存命中: ${message.usage.cache_read_input_tokens} tokens`);
        }
      } else {
        console.log(`任务失败: ${message.subtype}`);
        console.log("-".repeat(50));
        if ("errors" in message && Array.isArray(message.errors)) {
          for (const err of message.errors) {
            console.error(`  错误: ${err}`);
          }
        }
        console.log(`回合数: ${message.num_turns}`);
        console.log(`花费:   $${message.total_cost_usd.toFixed(4)}`);
      }

      console.log("=".repeat(50));
      break;
    }

    // ------ 其他消息类型(通常不需要处理)------
    default:
      // SDKUserMessage, SDKUserMessageReplay,
      // SDKPartialAssistantMessage, SDKCompactBoundaryMessage
      // 这些一般在特殊场景下才会用到
      break;
  }
}

// ============================
// 4. 使用示例
// ============================
async function main() {
  const result = await runAgent({
    prompt: "请查看当前目录的项目结构,列出所有的源代码文件",
    tools: ["Glob", "Read"],
    model: "sonnet",
    maxTurns: 5
  });

  console.log("\n--- 运行摘要 ---");
  console.log(`会话 ID: ${result.sessionId}`);
  console.log(`工具调用次数: ${result.toolCalls.length}`);
  console.log(`工具调用详情:`);
  for (const call of result.toolCalls) {
    console.log(`  - ${call.name}`);
  }
}

main().catch(console.error);

这个模板有几个亮点:

  1. 结构清晰 —— 配置、运行器、消息处理器分开
  2. 实时输出 —— 用 process.stdout.write 实现逐字显示
  3. 全面的错误处理 —— 成功和失败都有对应的处理
  4. 统计信息 —— 花费、耗时、Token 用量一目了然
  5. 回调机制 —— 方便你在消息处理中插入自定义逻辑

动手练习

练习1:模型对比实验

创建一个程序,用三个不同的模型(haiku、sonnet、opus)回答同一个技术问题,对比:

提示: 参考示例5,但换一个你感兴趣的问题。

// 练习1 骨架代码
import { query } from "@anthropic-ai/claude-code";

const question = "请解释 TypeScript 中的泛型是什么,并给出三个实用的例子";

async function compareModels() {
  for (const model of ["haiku", "sonnet", "opus"]) {
    console.log(`\n--- ${model} ---`);
    // 你的代码写在这里
    // 1. 调用 query(),设置 model
    // 2. 收集 AssistantMessage 中的文字
    // 3. 从 ResultMessage 中提取统计信息
    // 4. 打印对比结果
  }
}

compareModels();

练习2:角色扮演 Agent

设置 systemPrompt,让 Agent 扮演一个海盗来回答编程问题。

要求:

// 练习2 骨架代码
import { query } from "@anthropic-ai/claude-code";

async function pirateExplainer() {
  const questions = [
    "什么是闭包?",
    "什么是 Promise?",
    "什么是设计模式?"
  ];

  for (const q of questions) {
    console.log(`\n问题: ${q}\n`);
    // 你的代码写在这里
    // 1. 设置 systemPrompt 为海盗人设
    // 2. 调用 query()
    // 3. 打印 Agent 的回答
  }
}

pirateExplainer();

练习3:观察 maxTurns 的影响

分别设置 maxTurns 为 1、3、10,让 Agent 完成同一个需要多步骤的任务,观察行为变化。

任务: "创建一个 HTML 文件,里面有一个计数器按钮,点击后数字加1"

// 练习3 骨架代码
import { query } from "@anthropic-ai/claude-code";

async function observeMaxTurns() {
  for (const turns of [1, 3, 10]) {
    console.log(`\n=== maxTurns = ${turns} ===`);
    // 你的代码写在这里
    // 1. 调用 query(),设置 maxTurns
    // 2. 观察 Agent 能完成多少工作
    // 3. 注意 ResultMessage 的 subtype 是 success 还是 error_max_turns
  }
}

observeMaxTurns();

练习4:代码解释器 Agent

创建一个"代码解释器"Agent,它能读取一个文件并用简单易懂的语言解释代码。

要求:

// 练习4 骨架代码
import { query } from "@anthropic-ai/claude-code";

async function codeExplainer() {
  const filePath = process.argv[2];
  if (!filePath) {
    console.log("用法: npx ts-node code-explainer.ts <文件路径>");
    process.exit(1);
  }

  // 你的代码写在这里
  // 1. 设置 systemPrompt,让 Agent 用简单语言解释代码
  // 2. prompt 设置为 "请读取并解释文件 <filePath>"
  // 3. 给 Read 工具
  // 4. 处理所有消息类型
  // 5. 显示花费和耗时
}

codeExplainer();

本章小结

恭喜你!学完这一章,你已经掌握了 Claude Agent SDK 最核心的内容。来回顾一下:

  1. query() 是整个 SDK 的核心 —— 一个函数搞定一切
  2. 三种消息类型要记住:
    • system(init):Agent 启动时的信息
    • assistant:Agent 的思考和行动
    • result:任务完成的报告
  3. 最常用的配置选项:
    • allowedTools:给什么工具
    • permissionMode:权限怎么控制
    • model:用哪个模型
    • systemPrompt:给什么人设
    • maxTurns:跑几轮
  4. 处理消息的模式: for await + switch (message.type) 是标准套路
  5. query() 返回的是异步迭代器,for await 去消费它

一张图总结

query({prompt, options})
  │
  ├── options.allowedTools    → 给什么工具?
  ├── options.model           → 用哪个大脑?
  ├── options.systemPrompt    → 什么人设?
  ├── options.maxTurns        → 跑几轮?
  ├── options.permissionMode  → 权限多大?
  └── options.cwd             → 在哪干活?
  │
  ▼
for await (message of query)
  │
  ├── type === "system"    → 拿 session_id
  ├── type === "assistant" → 看 text 和 tool_use
  └── type === "result"    → 看 subtype、cost、usage

下一章预告

下一章是 第5章:内置工具 —— Agent 的十八般武艺

我们已经知道怎么启动 Agent 了,也知道可以通过 allowedTools 给它工具。但是这些工具具体怎么用?有什么限制?怎么组合使用效果最好?

下一章我们会逐个拆解所有内置工具:Read、Write、Edit、Bash、Glob、Grep、WebSearch、WebFetch、Task...... 每个工具都会有详细的使用示例和最佳实践。

学完下一章,你就能让 Agent 真正地"动手干活"了!

← 上一章3. 环境准备