Web 界面
Agent是无界面。CLI是单一表面。网络聊天界面也是一个。有趣的是,Agent代码在它们之间不会改变。区别都出在包装上。
本课是一个概念攻略。CLI是工作演示;Web 界面是建筑草图。关键是要让分隔足够明显,这样你就能在周五下午构建Web 界面而不用动Agent。
学习成果
你可以描述Web 界面如何重用无界面 Agent,表面在上面添加了什么(持久性、HTTP流式输出、工具结果分量),以及Agent与表面边界的分布位置。
两个表面并排
| CLI | Web | |
|---|---|---|
| 输出 | 终端文本 | 聊天气泡 |
| 工具调用 | stderr 文本行 | 工具结果组件 |
| 审批 | stdin 提示 | 按钮组 |
| 生命周期 | 进程退出 | 会话持久化 |
| 流式输出 | process.stdout.write | 服务器发送事件 |
| 输入 | 通过 argv 一次性输入 | 通过文本框持续输入 |
Agent代码是同一列。地面代码是其右侧的列。
Web 界面新增内容
持久性
终端关闭时,CLI会话会中断。网页会话不应该有。表面上延续着对话:
await db.saveMessages(sessionId, messages);
const messages = await db.loadMessages(sessionId);
const result = await agent.stream({ prompt, messages });Agent不知道数据库的存在。它接受messages(或没有)并继续前进。表面决定是否加载并保存。
流式输出HTTP
CLI流向TTY。Web 界面会流向浏览器。服务器发送的事件是最简单的机制:
export async function POST(req: Request) {
const { prompt, sessionId } = await req.json();
const messages = await loadMessages(sessionId);
const stream = new ReadableStream({
async start(controller) {
const result = await agent.stream({ prompt, messages });
for await (const chunk of result.fullStream) {
controller.enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "text/event-stream" },
});
}CLI消耗的块状和HTTP一样,只是超过而不是stdout。客户端会在每个块到达时渲染。
工具结果作为组成部分
CLI将工具结果呈现为文本。Web 界面可以将它们渲染为React组件:
| 工具 | CLI 渲染 | Web 渲染 |
|---|---|---|
read | 文件路径与行数 | 带语法高亮的代码块 |
grep | 匹配数和前几项结果 | 带文件链接的搜索结果 |
bash | 命令与退出码 | 带退出码标识的终端输出 |
write | 路径与字节数 | Diff 视图 |
edit | 路径与“一处替换” | 行内 diff |
askUser | 问题文本 | 选项按钮组 |
Agent会发出相同的块状物质。表面决定如何展示它们。
可恢复流
用户在回复中途关闭了浏览器标签页。五分钟后他们重新打开了它。Web 界面可以从中断处恢复流(如果Agent仍在运行),或者继续使用持久状态(如果不运行)。CLI做不到,因为没有地面可以回来。
这里是坚持与流式输出交汇的地方。会议是工作单元。流是该会话的一个渲染。
**注意:Agent不知道有一张网**
Web 界面代码都不进入Agent。Agent有可选项prompt且可选messages。它返回的是一连串区块。其他所有东西(认证、持久性、布局、组件)都存在于表面层。如果你发现自己在Agent上加上“这是网吗?”的分支,那就是缝线滑动。把特殊箱拉回表面。
随行教学缺少什么
这个模块的工作代码是CLI。课程仓库里没有任何Web 界面。这个决定是有意为之。构建一个可运行的网页前端需要一个独立模块(或多个模块),教学重点是分离,而不是 React。
如果你想尝试:先用一个包裹agent.stream()的Next.js路由,像 SSE 一样对区块进行管道处理,然后写一个小客户端来处理这些事件。如果你在模块1到模块9中写的Agent代码运行无变,你就知道分离干净了。
动手试试
这是一个概念课。自我检视:
- 不要回头,列出CLI和网面之间的变化
- 在网页案例中勾勒出单一用户的数据流提示词。持久性发生在哪里?该流在网络中穿过哪里?
- 从你的Harness上选一个工具。写下CLI渲染,然后草图网页渲染的 React 组件。它们读到的是相同的块状吗?
提交
这节课没有代码。下一个模块(可扩展性)是基于事件的挂钩,为工具结果组件和在线审批打开了大门,以便能够干净利落地插入。
完成标准
- [ ] 你可以解释为什么Agent代码不会在不同表面之间变化
- [ ] 你可以草图持久化、流式输出和组件渲染,作为Web 界面
- [ ] 你可以区分哪些留在Agent里,哪些会移动到表面
**注意:建立一个流式输出 Web 界面**
设置一条Next.js路线绕过agent.stream({ prompt, messages })。把每个区块都作为服务器发送的事件进行管道处理。在客户端,使用带有EventSource或fetch加ReadableStream的事件。把text-delta块渲染到聊天气泡里。用工具名渲染tool-call块作为旋转器。tool-result到时用一个小的结果卡替换旋转器。添加一个取消按钮,可以终止流并存储部分结果。 有趣的是文本和工具调用之间的视觉过渡,没有布局的卡顿。你怎么会句柄这样?