流式输出与工具渲染
agent.generate()封锁,直到整个回复完成。一旦Agent超过一两步,等待就变得不舒服。你盯着一个看起来死机的终端,完全不知道它到底是正常工作还是卡住了。
agent.stream()是升级。他们到达时会收到短信delta,开火时会调用工具,回来时工具结果。用户看到的是运动。你会看到Agent实际上在做什么。
本课将generate替换为CLI中的stream,并决定每个区块的渲染方式。
学习成果
CLI使用agent.stream()。文本delta是实时写入stdout的。工具调用及其结果会渲染成stderr,避免与Agent的实际响应混淆。
快速路径
- 用
agent.generate({ prompt })替换agent.stream({ prompt }) - 迭代
result.fullStream并开启chunk.type - 把
text-delta发给stdout,tool-call,tool-resultstderr
动手练习 10.2
切换到流式输出,并适当渲染区块类型。
要求:
- 用
agent.generate(...)替换agent.stream(...) - 用
for await环绕result.fullStream - 在
text-delta上,将 delta 写入 stdout(无换行) - 在
tool-call时,把工具名和 args 记录到stderr - 在
tool-result,将截断预览记录为stderr
实现提示:
result.fullStream是一个异步迭代。for await (const chunk of result.fullStream)是自然环- 工具结果可能会很长。切片到大约100字符以预览
- 不要把
tool-result块渲染到stdout。他们是元者。它们是与反应并行,而不是在其中
流式输出循环
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-call 和 tool-result 切换已经涵盖了大部分需求。当某个工具的输出持续有噪声时,按工具格式化才会有其用。
**注意:当你流式输出时,在线审批会更难**
模块1的bash在命令未被审批时返回一个块串。这和generate一起用得很好。使用流式输出时,你可能需要暂停直播,询问用户,然后继续。那是一个交互循环,不是区块处理程序。完整的模式存在于下一个模块(可扩展性和事件)。目前,封锁举报的行为仍然是正确的做法。
动手试试
多步运行任何项目,看看CLI如何活起来:
bun run index.ts . "Find all TODO comments, then read the files that contain them"你应该能看到工具调用在stderr中出现,模型的最终响应会流式输出stdout token。
作为对比,重定向stderr,只看到Agent的文本回复:
bun run index.ts . "Find all TODO comments, then read the files that contain them" 2>/dev/null你会得到回应,没有工具噪音。这就是stdout/stderr分成给你的。
npx tsc --noEmit提交
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-call和tool-result写信给stderr - [ ] 重新定向stderr只剩下Agent的回应stdout
- [ ]
npx tsc --noEmit
**注意:每节课都算工具渲染**
用一个小型renderTool(chunk)函数替代通用tool-call和tool-result日志,该函数会启动chunk.toolName并为每个函数生成特定的摘要。read显示文件路径和行数。grep显示比赛数和首场比赛。bash显示命令和退出码。观察新工具加入时会发生什么。每个工具的格式定义在哪里是合适的存放点?