01 · 为什么 agent 需要实时反馈
没有中间输出的 agent 像黑箱——用户只能等它结束。加上事件流,agent 从脚本升级成交互工具。
黑箱问题
到第 4 章结束时,agent 已经具备了不少能力:能搜索代码、读写文件、执行命令,还有权限检查。但从用户的视角看,使用体验是这样的:
- 输入任务
- 等待……
- 继续等待……
- 最终答案出现
如果任务简单(一次工具调用就搞定),几秒钟就能出结果。但如果任务复杂——模型要搜索、读文件、创建计划、执行多个步骤——用户可能要等上一两分钟,期间终端什么都看不到。
这不是小问题。黑箱等待会让用户焦虑:
- 不知道在不在工作——agent 是在思考还是卡死了?
- 不知道进度——它做了几步?还有几步?
- 不知道方向对不对——如果模型跑偏了,要等它全部跑完才能看到
对比一下 Claude Code、Codex CLI 这些产品的体验:它们都会在执行过程中实时显示"正在搜索"、"正在读文件"、"正在执行命令"。用户能随时看到 agent 在做什么。
一个观察:agent 内部已经知道自己在做什么
回头看 agent 循环的代码,每一步其实都有明确的动作:
for (let i = 0; i < maxIterations; i++) {
/** 模型思考 */
let response = await model.chat(allMessages, allTools);
/** 执行工具 */
for (const toolCall of response.toolCalls) {
const result = await tool.execute(toolCall.arguments, state);
/** ... */
}
}模型在"思考"、工具在"执行"、计划在"更新"——这些信息都存在于 agent 的执行过程中,只是没有暴露出来。
问题不是"没有信息",而是"信息没有传递出去"。
解决方案:事件流(Event Stream)
思路很简单:在 agent 的关键节点,把正在发生的事情告诉外部。
就像浏览器的事件模型一样——用户点击按钮,按钮发出一个 click 事件,谁关心谁监听。agent 也可以做一样的事:
- 开始处理任务时 → 发出
agent_start事件 - 调用模型时 → 发出
model_call事件 - 执行工具时 → 发出
tool_call_start事件 - 工具返回结果时 → 发出
tool_call_result事件 - 计划更新时 → 发出
todo_update事件 - 任务完成时 → 发出
agent_done事件
这些事件构成了一条时间线,完整地描述了 agent 的执行过程。外部只需要监听这条时间线,就能实时展示 agent 的状态。
角色分离
事件流的一个重要好处是角色分离——agent 负责做事,渲染器负责展示,两者互不干扰。
agent(做事)→ 发出事件 → emitter(传递)→ renderer(展示)- agent 不关心事件怎么展示。它只在关键节点发出事件,不管谁在监听。
- emitter 是事件总线。有人注册监听器,有人发出事件,它负责传递。
- renderer 不关心 agent 的内部逻辑。它只关心"收到什么事件,怎么渲染"。
这种分离带来两个好处:
- 可替换。 今天用简单的
console.log渲染,明天可以换成 ink(React 终端框架)做漂亮的 TUI,agent 代码不需要改。 - 可测试。 测试时注册一个监听器,检查发出的事件是否正确,不需要真的在终端里看输出。
和第 4 章权限系统的关系
事件流和权限系统是正交的——它们解决不同的问题,但可以协同工作。
权限系统解决的是安全问题:"这个操作能不能执行?" 事件流解决的是可见性问题:"agent 正在做什么?"
当权限系统弹出确认请求时,这也是一个事件(permission_ask)。渲染器可以用特殊的方式显示它——比如高亮、暂停、等待用户输入。这样用户能同时看到"agent 想做什么"和"系统要求确认"两个信息。
本章目标
这一章要实现三件事:
1. 事件类型系统。 定义 agent 在执行过程中会发出哪些事件,每个事件携带什么数据。
2. 事件发射器。 一个简单的 AgentEmitter 类,支持注册监听器和发出事件。
3. 终端渲染器。 一个 TerminalRenderer 类,监听事件并把状态实时输出到终端。
完成后,agent 从"黑箱脚本"升级为"可观察的交互工具"——用户能实时看到每一步在做什么。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。