跳到正文

流式输出与工具渲染

agent.generate()封锁,直到整个回复完成。一旦Agent超过一两步,等待就变得不舒服。你盯着一个看起来死机的终端,完全不知道它到底是正常工作还是卡住了。

agent.stream()是升级。他们到达时会收到短信delta,开火时会调用工具,回来时工具结果。用户看到的是运动。你会看到Agent实际上在做什么。

本课将generate替换为CLI中的stream,并决定每个区块的渲染方式。

学习成果

CLI使用agent.stream()。文本delta是实时写入stdout的。工具调用及其结果会渲染成stderr,避免与Agent的实际响应混淆。

快速路径

  1. agent.generate({ prompt })替换agent.stream({ prompt })
  2. 迭代result.fullStream并开启chunk.type
  3. text-delta发给stdout,tool-calltool-resultstderr

动手练习 10.2

切换到流式输出,并适当渲染区块类型。

要求:

  1. agent.generate(...)替换agent.stream(...)
  2. for await环绕result.fullStream
  3. text-delta 上,将 delta 写入 stdout(无换行)
  4. tool-call时,把工具名和 args 记录到stderr
  5. tool-result,将截断预览记录为stderr

实现提示:

  • result.fullStream 是一个异步迭代。for await (const chunk of result.fullStream) 是自然环
  • 工具结果可能会很长。切片到大约100字符以预览
  • 不要把tool-result块渲染到stdout。他们是元者。它们是与反应并行,而不是在其中

流式输出循环

ts
const result = await agent.stream({ prompt });

for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case "text-delta":
      process.stdout.write(chunk.textDelta);
      break;
    case "tool-call":
      console.error(
        `\n[tool] ${chunk.toolName}(${JSON.stringify(chunk.args)})`,
      );
      break;
    case "tool-result": {
      const preview =
        typeof chunk.result === "string"
          ? chunk.result.slice(0, 100)
          : JSON.stringify(chunk.result).slice(0, 100);
      console.error(`  -> ${preview}`);
      break;
    }
  }
}

console.log();

这就是全部的改变。Agent、工具、沙箱和提示词保持不变。CLI的工作从“等待答案”转变为“渲染直播”。

实际操作中的样子

和之前一样提示词,但多了动作:

[tool] grep({"pattern":"TODO","glob":"*.ts"})
  -> src/auth.ts:42: // TODO: add rate limiting
  -> src/routes.ts:15: // TODO: validate input

Based on my search, there are 2 TODO comments left in this project...

工具调用在模型决定制作的那一刻就会出现。结果在工具返回的那一刻就显现出来。文本答案在模型写入时会源源不断地输入。用户可以边读边看,不用等待。

各工具渲染选择

不同的工具需要不同的摘要。这里有一个起点:

工具渲染为
read文件路径与行数
grep比赛数与前三场比赛
bash命令与退出码
write文件路径与字节数
edit文件路径与“1次替换”
task子 Agent类型和步数
askUser完整问题和选项列表

把桌子当作提示,而不是契约。简单来说,CLI上简单的 tool-calltool-result 切换已经涵盖了大部分需求。当某个工具的输出持续有噪声时,按工具格式化才会有其用。

**注意:当你流式输出时,在线审批会更难**

模块1的bash在命令未被审批时返回一个块串。这和generate一起用得很好。使用流式输出时,你可能需要暂停直播,询问用户,然后继续。那是一个交互循环,不是区块处理程序。完整的模式存在于下一个模块(可扩展性和事件)。目前,封锁举报的行为仍然是正确的做法。

动手试试

多步运行任何项目,看看CLI如何活起来:

bash
bun run index.ts . "Find all TODO comments, then read the files that contain them"

你应该能看到工具调用在stderr中出现,模型的最终响应会流式输出stdout token。

作为对比,重定向stderr,只看到Agent的文本回复:

bash
bun run index.ts . "Find all TODO comments, then read the files that contain them" 2>/dev/null

你会得到回应,没有工具噪音。这就是stdout/stderr分成给你的。

bash
npx tsc --noEmit

提交

bash
git add index.ts
git commit -m "feat(cli): stream agent output with chunk-level rendering"

完成标准

  • [ ] agent.stream({ prompt })取代了agent.generate(...)
  • [ ] for await反复强调result.fullStream
  • [ ] text-delta给stdout写信
  • [ ] tool-calltool-result写信给stderr
  • [ ] 重新定向stderr只剩下Agent的回应stdout
  • [ ] npx tsc --noEmit

**注意:每节课都算工具渲染**

用一个小型renderTool(chunk)函数替代通用tool-calltool-result日志,该函数会启动chunk.toolName并为每个函数生成特定的摘要。read显示文件路径和行数。grep显示比赛数和首场比赛。bash显示命令和退出码。观察新工具加入时会发生什么。每个工具的格式定义在哪里是合适的存放点?

非官方简体中文翻译 · 原课程来自 Vercel Academy