query 的签名很短:给一段提示,给你一串消息。 关键是「一串」——它不是返回结果对象,是可逐条读的流。 实测六条消息:第一条几乎立刻到,后面每条隔二十毫秒。如果是一次性返回,第一条也要等到最后。
上一节定了坐标,这一节看第一个、也是最常用的接口。 这一节真跑了一个异步迭代器:逐条读,并记下每条到手的时间。
一、调用的形状
两个参数:提示词,和选项。提示词是这一轮要做的事,选项是这一轮怎么跑。 返回的不是结果,是异步迭代器——用 async for 一条一条读。 「异步」这个词是关键:每读一条都可能要等(它还在跑),所以循环本身是异步的。
这种形状在流式接口里很常见,理由见下面第三节。
二、真跑:逐条读,记下每条的时间
演示里六条消息:一条 init、四条 assistant、一条 result。 第一条几乎立刻到(握手),后面每条隔二十毫秒——那是它「干活」的时间。 这就是「流」的手感:你能看见它在推进,而不是等一个黑盒。
如果是一次性返回,第一条也要等到最后,那就退化成 7.6 节那个「等完再读」。
三、为什么是异步迭代器而不是回调
回调要处理「什么时候算结束」,异步迭代器不用:循环结束就是结束。 回调还要处理「抛异常了怎么通知」,异步迭代器直接抛在循环里,能 try 住。 这两个理由足够解释为什么新一代的流式接口几乎都是异步迭代器。
回调不是不能用,是它把这些控制流问题推给了调用方。
四、三个使用要点
必须 async for,不能普通 for。写错了不会报错,只会拿到一个迭代器对象。 循环里别做重活。它跑在事件循环上,卡住会拖慢整个流。 提前 break 是合法的。想中途放弃就直接跳出循环,剩下的不读。
第三条很多人不知道:以为必须读完,于是硬等一个已经跑偏的任务跑完。
五、Python 与 TypeScript 两个版本
两个版本的形状是一样的:都给提示、都返回一个异步可迭代的流、都是逐条读。 差别在语法:Python 用 async for 和 await,TypeScript 用 for await...of。 所以理解了「流」这件事,换语言只是换写法;不理解的话,两种都写不对。
这本书的例子以 Python 为主,但概念是通用的。
六、动手 Demo:query 是异步迭代器:一条一条来,不是一次给完
query 的签名很短:给它一段提示,它给你一串消息。 这里真跑一个异步迭代器,逐条读并记下每条到手的时间。
async def _mock_query(prompt, n=4):
yield {"type": u"system", "subtype": u"init"}
for i in range(n):
await asyncio.sleep(0.02)
yield {"type": u"assistant", "i": i}
实跑输出:
--------------------------------------------------------------------
query 是异步迭代器:一条一条来,不是一次给完
--------------------------------------------------------------------
query 的签名很短:给它一段提示,它给你一串消息。
关键是「一串」——它不是返回一个结果对象,是一个可以逐条读的流。
① 调用的形状
--------------------------------------------------------------------
[教材原样 · 本沙箱不执行]
| async for message in query(prompt=u"分析这个目录"):
| print(message)
async for 这个词是关键:每读一条都可能要等(它还在跑),所以循环本身是异步的。
② 真跑:逐条读,记下每条到手的时间
--------------------------------------------------------------------
| 第几条 | 类型 | 到手时间 |
|:---:|---|:---:|
| 1 | system | 0.000 秒 |
| 2 | assistant | 0.023 秒 |
| 3 | assistant | 0.055 秒 |
| 4 | assistant | 0.087 秒 |
| 5 | assistant | 0.119 秒 |
| 6 | result | 0.119 秒 |
第一条几乎立刻到(握手),后面每条隔二十毫秒。这就是「流」的手感。
如果是一次性返回,第一条也要等到最后——那就退化成 7.6 节那个「等完再读」。
③ 为什么是异步而不是回调
--------------------------------------------------------------------
回调要处理「什么时候算结束」,异步迭代器不用:循环结束就是结束。
回调还要处理「抛异常了怎么通知」,异步迭代器直接抛在 for 里,能 try 住。
这两个理由足够解释为什么新一代的流式接口几乎都是异步迭代器。
④ 三个使用要点
--------------------------------------------------------------------
- 必须 async for,不能普通 for。写错了不会报错,只会拿到一个迭代器对象。
- 循环里别做重活。它跑在事件循环上,卡住会拖慢整个流。
- 提前 break 是合法的。想中途放弃就直接跳出循环,剩下的不读。
⑤ 一次 query 不等于一个会话
--------------------------------------------------------------------
一次 query 是一轮交互。要跨轮维持上下文,靠的是会话标识,那是 8.5 节的事。
分清这两件事很重要:很多人以为「再调一次 query」就是接着聊,其实是从头开始。
! 这里的 query 是替身,形状与真实接口一致;真实调用需要认证和网络。
①调用的形状:两个参数(提示、选项),返回异步迭代器,用 async for 一条一条读。 「异步」是因为每读一条都可能要等。 ②第一条几乎立刻到(握手),后面每条隔二十毫秒。这就是流的手感:能看见它在推进,而不是等一个黑盒。 ③为什么用异步迭代器不用回调:循环结束就是结束,异常直接抛在循环里能 try 住。 回调把这两个控制流问题推给了调用方。 ④三个要点:必须 async for(写错不报错,只拿到迭代器)、循环里别做重活(它跑在事件循环上)、 提前 break 是合法的。
30 秒实验
- 把休眠从二十毫秒改成二百毫秒,重跑一遍。第一条到手的时间变了吗? 这说明「握手」和「干活」是两段时间。
- 在循环里读到第二条就 break,重跑一遍。总耗时变成多少?这说明「不读完」是合法的。
- 把 async for 改成普通 for,看会发生什么。它报错了吗?这种错误为什么难发现?
常见误区
- 用普通 for 遍历。不报错,只拿到一个迭代器对象,后面全是空的。
- 在循环里做重活(比如写大文件)。它跑在事件循环上,会把整个流拖慢。
- 以为必须读完。想中途放弃直接 break,剩下的不读是合法的。
- 把「再调一次 query」当成接着聊。一次 query 是一轮,跨轮要靠会话标识。
- 用回调去接。控制流问题(结束、异常)会全推到你这边。
自测三问(点开对答案)
1. query 返回的是什么? —— 一个异步迭代器,用 async for 逐条读消息。它不是一次性返回的结果对象。 2. 为什么用异步迭代器而不是回调? —— 循环结束就是结束,异常直接抛在循环里能 try 住。回调把这两个控制流问题推给调用方。 3. 一次 query 等于一个会话吗? —— 不等于。一次 query 是一轮交互,跨轮维持上下文要靠会话标识。接口会调了,下一节看它吐出来的东西:四种消息,各带什么。
- ☐ (本篇完,下一篇 8.3 消息类型:解读 Claude 的输出流)
相关笔记
- 流式输出:CLI 的流式输出
- 消息类型:四种消息类型
- Claude Code 橙皮书 · 索引
- 🐎 Claude Code 实战 · 索引