🐎 第8章 器以载道:Agent SDK 智能体开发套件
📄 一句话概括

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 秒实验

  1. 把休眠从二十毫秒改成二百毫秒,重跑一遍。第一条到手的时间变了吗? 这说明「握手」和「干活」是两段时间。
  2. 在循环里读到第二条就 break,重跑一遍。总耗时变成多少?这说明「不读完」是合法的。
  3. 把 async for 改成普通 for,看会发生什么。它报错了吗?这种错误为什么难发现?

常见误区

  1. 用普通 for 遍历。不报错,只拿到一个迭代器对象,后面全是空的。
  2. 在循环里做重活(比如写大文件)。它跑在事件循环上,会把整个流拖慢。
  3. 以为必须读完。想中途放弃直接 break,剩下的不读是合法的。
  4. 把「再调一次 query」当成接着聊。一次 query 是一轮,跨轮要靠会话标识。
  5. 用回调去接。控制流问题(结束、异常)会全推到你这边。
自测三问(点开对答案) 1. query 返回的是什么? —— 一个异步迭代器,用 async for 逐条读消息。它不是一次性返回的结果对象。 2. 为什么用异步迭代器而不是回调? —— 循环结束就是结束,异常直接抛在循环里能 try 住。回调把这两个控制流问题推给调用方。 3. 一次 query 等于一个会话吗? —— 不等于。一次 query 是一轮交互,跨轮维持上下文要靠会话标识。

接口会调了,下一节看它吐出来的东西:四种消息,各带什么。

相关笔记