使用 Async Generator 实现 Agent 流式输出与流程控制

随着自动 AI Chat 的兴起,流式输出已成为默认的交互方式。尽管如今 AI Agent 逐渐取代传统聊天模式成为主流,但流式输出凭借其"打字机"般的实时反馈效果,依然是提升用户体验的核心环节。

而像 Cline 这类 Code Agent,在实际运行中不仅需要处理流式响应,还频繁面临工具调用、用户确认、流程暂停、失败重试等复杂控制场景。如何优雅地实现这些功能?答案正是:Async Generator ------ 这或许是 JavaScript 中最被低估的高级特性之一。

什么是 Generator

在深入了解 Async Generator 之前,先回顾一下它的"前身"------Generator。

Generator 是一种特殊的函数,可以在执行过程中暂停和恢复,通过 yield 关键字逐个返回值。

js 复制代码
function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}
const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

Generator 返回一个迭代器(Iterator) ,每次调用 .next() 返回都会返回一个形如 { value, done } 的对象:

  • value:当前产出的值。
  • done:布尔值,表示迭代是否完成。false 表示还有值可产出,true 表示结束。

由于返回的是迭代器,因此可以直接使用 for...of 循环遍历,无需手动调用 .next()。实际上,for...of 的底层机制就是自动调用对象的 Symbol.iterator 方法获取迭代器,并持续调用 .next(),直到 done: true。

Symbol.iterator 是一个特殊的 Symbol,JavaScript 引擎用它来识别对象是否"可被迭代"。

例如,数组之所以能被 for...of 遍历,正是因为内部实现了 Symbol.iterator:

js 复制代码
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }

只要在自定义对象上实现 Symbol.iterator,就能享受 for...of 带来的便利。

Async Generator:异步世界的流式引擎

Async Generator(异步生成器)是 ES2018 引入的强大特性,它融合了 Generator 的"暂停/恢复"能力与 async/await 的异步处理优势。你可以在一个函数中既 yield 值,又 await 异步操作。

一个简单例子:

js 复制代码
async function* asyncGenerator() {
  yield 1;
  yield 2;
  // 可以 await 异步操作
  const data = await fetch('/api/data');
  yield data.json();
}
js 复制代码
const asyncGen = asyncGenerator();

asyncGen.next().then(console.log); // { value: 1, done: false }
asyncGen.next().then(console.log); // { value: 2, done: false }
asyncGen.next().then(result => console.log(result.value)); // 解析后的 JSON 数据

调用该函数后返回一个 Async Iterator(异步迭代器),其 .next() 方法返回的是一个 Promise,解析结果为 { value, done }:

异步迭代器使用 for await...of 进行遍历:

js 复制代码
async function consumeAsyncGenerator() {
  for await (const value of asyncGenerator()) {
    console.log(value); // 依次输出 1, 2, 解析后的数据
  }
}

这种"按需产出 + 异步等待"的机制,使得 Async Generator 成为处理流式数据和复杂流程控制的理想选择。接下来,我们结合 AI 场景,实战演示它的强大之处。

代码实战

实现流式输出

我们可以直接使用 fetch 读取服务端的流式响应:

ts 复制代码
export async function* chatWithLLM(prompt: string) {
  const stream = await fetch('/api/chat', {
    method: 'POST',
    body: JSON.stringify({
      model: 'qwen3-coder-plus',
      messages: [
        {
          role: 'system',
          content: prompt
        }
      ]
    })
  });

  if (!stream.ok) {
    throw new Error(`HTTP ${stream.status}`);
  }

  const reader = stream.body?.getReader();
  const decoder = new TextDecoder();
  while (true) {
    // @ts-ignore
    const { done, value } = await reader?.read();
    if (done) {
      break;
    }
    const chunk = decoder.decode(value);
    yield { type: 'text', content: chunk };
  } 
}

当然,也可以借助 OpenAI SDK 等封装好的工具。以下是 Cline 的真实实现:(src/core/api/providers/openai.ts):

ts 复制代码
import OpenAI, { AzureOpenAI } from "openai"

const stream = await client.chat.completions.create({
  model: modelId,
  messages: openAiMessages,
  temperature,
  max_tokens: maxTokens,
  reasoning_effort: reasoningEffort,
  stream: true,
  stream_options: { include_usage: true },
})
for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta
  if (delta?.content) {
    yield {
      type: "text",
      text: delta.content,
    }
  }

  if (delta && "reasoning_content" in delta && delta.reasoning_content) {
    yield {
      type: "reasoning",
      reasoning: (delta.reasoning_content as string | undefined) || "",
    }
  }

  if (chunk.usage) {
    yield {
      type: "usage",
      inputTokens: chunk.usage.prompt_tokens || 0,
      outputTokens: chunk.usage.completion_tokens || 0,
      // @ts-ignore-next-line
      cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
      // @ts-ignore-next-line
      cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
    }
  }
}

通过 yield 逐步输出不同类型的消息(文本、推理过程、Token 使用量),前端即可实时渲染内容,实现流畅的"打字机"效果。

实现暂停和恢复

Async Generator 天然支持"暂停并恢复"的语义。我们只需控制执行流程的开关即可:

ts 复制代码
let iterator: AsyncGenerator<any>;
let isRunning = true;

async function start() {
  if (!iterator) iterator = chatWithLLM('使用 js 实现 debounce 函数');

  if (!isRunning) return;

  while (isRunning) {
    const { value, done } = await iterator.next();
    if (done) break;
    console.log(value)
  }
}

function pause() { isRunning = false; }
function resume() { isRunning = true; start(); } // 继续从上次 yield 开始

Generator 会自动保存执行上下文,无需手动维护状态。Cline 在处理用户确认时,也采用了类似思路。例如在 src/core/task/index.ts 中的 ask 方法:

ts 复制代码
await pWaitFor(() => this.taskState.askResponse !== undefined || this.taskState.lastMessageTs !== askTs, {
  interval: 100,
})

这段代码会持续轮询,直到用户完成确认或消息时间戳更新,期间整个生成器暂停,等待响应到来后再恢复执行。

(p-wait-for:github.com/sindresorhu...)

错误重试

网络请求可能失败,我们需要在出错时自动重试。借助 Async Generator,可以非常优雅地实现递归重试逻辑:

js 复制代码
async function *attempt () {
  const iterator = chatWithLLM('使用 js 实现 debounce 函数');

  try {
    console.log('attempt')
    const firstChunk = await iterator.next();
    yield firstChunk.value
  } catch(error) {
    console.log('error', error);
    yield* attempt();
    return;
  }

  yield* iterator;
}
js 复制代码
let isRunning = true;

async function start() {
  const stream = attempt();
  const iterator = stream[Symbol.asyncIterator]();
  while (isRunning) {
    const { value, done } = await iterator.next();
    if (done) break;
    console.log(value)
  }
}

function pause() { isRunning = false; }
function resume() { isRunning = true; start(); } // 继续从上次 yield 开始

这里关键使用了 yield* 语法:

yield* iterator 的作用是"把 iterator 中的所有 value 依次 yield 出去",直到 done: true。等价于:

js 复制代码
for await (const value of iterator) {
  yield value;
}

yield* 更简洁、性能更好,且能正确处理 return 值和异常传播。

这里我们其实是参考了 Cline 的实现,下面是简化后的代码:

ts 复制代码
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
  const stream = this.api.createMessage(systemPrompt, contextManagementMetadata.truncatedConversationHistory)
  const iterator = stream[Symbol.asyncIterator]();
  
  try {
      // awaiting first chunk to see if it will throw an error
      this.taskState.isWaitingForFirstChunk = true
      const firstChunk = await iterator.next()
      yield firstChunk.value
      this.taskState.isWaitingForFirstChunk = false
	} catch (error) {
      yield* this.attemptApiRequest(previousApiReqIndex)
      return;
    }
    yield* iterator
}

通过递归调用 attemptApiRequest 并使用 yield* 代理输出,实现了无感重试,同时保持了流式输出的连续性。

总结

Async Generator 是处理异步流式数据与复杂流程控制的利器。它结合了 Generator 的"暂停/恢复"能力和 async/await 的异步表达力,使得我们能够以同步的写法处理异步流,极大提升了代码的可读性与可维护性。

在 Cline 这类 AI Agent 中,Async Generator 被广泛用于:

  • 流式输出:实时返回模型生成的文本片段;
  • 流程暂停:等待用户确认后再继续执行;
  • 错误重试:请求失败时自动重试,不影响整体流程。

通过 yield、for await...of、yield* 等语法,我们能以极简的方式构建出健壮、灵活的异步控制流。可以说,Async Generator 是构建现代 AI 应用底层逻辑的基石之一。

如果你正在开发一个需要处理流式响应或复杂状态流转的系统,不妨试试 Async Generator ------ 它可能正是你一直在寻找的那个"优雅解"。

相关推荐
用户47949283569156 小时前
ESLint支持多线程Linting啦
前端·javascript·面试
猪哥帅过吴彦祖6 小时前
JavaScript Symbol:那个被忽视的"隐形"数据类型
前端·javascript·面试
Jooolin7 小时前
【C++】C++11出来之后,到目前为止官方都做了些什么更新?
数据结构·c++·ai编程
薛定谔的算法7 小时前
面试官问你知道哪些es6新特性?赶紧收好,猜这里一定有你不知道的?
前端·javascript·面试
用户47949283569157 小时前
🚀 打包工具文件名哈希深度解析:为什么bundle.js变成了bundle.abc123.js
前端·javascript·面试
晴空雨7 小时前
遇到第三方库 bug 怎么办?5 种修改外部依赖的方法帮你搞定
前端·javascript·架构
Jooolin7 小时前
【C++】C++11都有什么新特性?
c++·ai编程·编程语言
得帆云低代码9 小时前
2025企业数智化转型基建标配:没有AI网关的企业正在掉队
openai·ai编程
Jacob02349 小时前
JavaScript 的进化之旅 Part 2:现代特性与算法优化实战
前端·javascript·性能优化