Typography

学习笔记

Claude Agent SDK学习04 流式输入

发布于 # Claude Agent SDK

当前 Agent 的问题

第三章之后,你的 agent 已经能跨调用记住上下文,但它还有一个非常明显的交互缺口:所有信息都必须在启动时一次性给完。

这和真实使用场景并不匹配。用户通常会这样工作:

如果你的 agent 只能在开始时接受一条静态 prompt,它就更像批处理脚本,而不像一个可协作系统。

本章功能的作用

这一章会把输入模式从“单条字符串”升级为“持续消息流”。

你要掌握的是:

本章最核心的变化,是把输入模型从“启动前一次性交付”改成“执行过程中持续补充”。这使得 agent 更像一个正在协作的对象,而不是一个拿到完整任务包才开始工作的离线脚本。

官方文档把这种模式直接定义为推荐模式,因为它天然支持图片、排队消息、插入式补充说明以及更自然的中断处理。也就是说,流式输入并不是一个少数高级场景的技巧,而是更接近真正产品形态的默认交互模式。

具体使用方式

第一步:把 prompt 从字符串改成异步生成器

当你希望同一轮任务中连续追加信息时,把 prompt 改成 async function*。你每次 yield 一条消息,SDK 就会把它注入当前会话,而不是开启新 query。

从编程接口角度看,这一步相当于把“请求体”升级成“输入流”。一旦接受这个模型,后面你接图片、接用户中途补充说明、接审批结果,就都会变得顺理成章。

这也是为什么它和“单字符串 prompt + 多次 resume”不完全一样。两者都能把信息分段送给 Claude,但流式输入发生在同一次 query 内部,Claude 不需要重新建立一个新的调用边界。

第二步:每次只追加一个明确的新信息

流式输入最适合承载“增量补充”,例如补约束、补文件、补重点。不要把它写成多个重复 prompt,而应该让每一条消息只承担一个新增意图。

这是因为 Claude 会把后续消息当成对当前任务的修正。如果你每次都把前面说过的话全部重复一遍,真正新增的限制反而会被旧信息稀释,阅读和推理成本都会更高。

第三步:必要时在消息之间等待外部事件

示例里的 setTimeout 只是最简单的模拟。实际系统里,这里可以替换成用户输入、上传完成、人工审批结果,或者别的服务返回的新上下文。

换句话说,流式输入不是为了“让例子看起来更高级”,而是为了贴近真实产品交互。真实世界里的上下文几乎从来都不是一次性齐备的,而是随着任务推进不断到达。

第四步:仍然只维护一个 query 实例

流式输入的重点是“同一次 query 内追加消息”。如果你为了补一句话就重新调用一次 query(),那就退回到了多轮 session,而不是单次流式输入。

关键概念

流式输入和 session 的区别

二者常常配合使用,但不是一回事。

为什么流式输入重要

它是这些体验的基础:

prompt 变成消息生成器后,SDK 在做什么

SDK 会把你 yield 出来的每条消息加入同一个 agent 会话。Claude 会把它们理解成同一任务上下文中的连续输入,而不是多个相互独立的调用。

可运行示例

把下面代码保存为 chapter-04-streaming-input.ts

import { mkdtemp, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { query } from "@anthropic-ai/claude-agent-sdk";

async function main() {
  const workspace = await mkdtemp(join(tmpdir(), "agent-sdk-ch04-"));

  try {
    await writeFile(
      join(workspace, "README.md"),
      "# Demo App\n\nThis app has an authentication layer and a billing layer.\n",
      "utf8"
    );
    await writeFile(join(workspace, "auth.ts"), "export function login() { return true; }\n", "utf8");
    await writeFile(join(workspace, "billing.ts"), "export function charge() { return 42; }\n", "utf8");

    async function* promptStream() {
      yield {
        type: "user" as const,
        message: {
          role: "user" as const,
          content: "Inspect this workspace and tell me what the project seems to do."
        }
      };

      await new Promise((resolve) => setTimeout(resolve, 500));

      yield {
        type: "user" as const,
        message: {
          role: "user" as const,
          content: "Focus more on the authentication part than the billing part."
        }
      };
    }

    for await (const message of query({
      prompt: promptStream(),
      options: {
        cwd: workspace,
        allowedTools: ["Read", "Glob", "Grep"],
        permissionMode: "dontAsk"
      }
    })) {
      if (message.type === "result") {
        console.log(message.result);
      }
    }
  } finally {
    await rm(workspace, { recursive: true, force: true });
  }
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

运行:

npx tsx chapter-04-streaming-input.ts

示例拆解

第一步:先准备一个带多个模块的工作区

示例同时写入 README.mdauth.tsbilling.ts,目的是让 Claude 初始时有多个可能关注点,便于第二条输入把重点重新拉回认证模块。

第二步:定义 promptStream()

promptStream() 是整章最关键的结构。它先发第一条“整体总结”消息,再延迟发送第二条“聚焦认证”消息,从而模拟真实协作中的补充说明。

这里的延迟不是重点,重点是顺序。第一条消息先让 Claude 建立全局认识,第二条消息再把注意力重新压到认证模块,这正好复现了人和 agent 协作时最常见的工作节奏。

第三步:让 query() 消费这个生成器

prompt 传入的是 promptStream() 时,SDK 会把两条消息都视为同一任务中的连续输入。Claude 不会把第二条当成新会话,而会把它当成当前任务的最新约束。

第四步:只在最后读取结果,验证关注点是否被改变

示例最后只打印 result,你应该观察到最终输出不只是总结项目,还明显更偏向 auth.ts。这说明后续追加输入已经成功改变了会话方向。

运行时你应该观察什么

易错点

本章结束后你应该掌握

本章小结

到这里,你的 agent 已经从“单次调用任务”变成了“可持续接受上下文补充的协作对象”。这一步是实现真实交互产品的关键分水岭。