系列简介:从零搭建一个多 Agent AI 助手,覆盖原理、实现、部署全链路。不讲空话,每篇都有可运行的代码。
项目地址:github.com/CodeMomentY…
本篇目标:把 Agent 从命令行变成一个能用的产品,用 SSE + Vue3 落地一个可交互的对话界面。
前言
大家好,我是一名前端工程师。都说前端“已死”,那与其担心被 AI 替代,不如打入敌人内部,于是我开始折腾 Agent 开发。
折腾下来发现,Agent 的核心不是算法,而是“工程能力”(怎么设计架构、怎么串联服务、怎么把 LLM 的能力落地成产品)。这些恰好是我们擅长的事。
这个系列记录我从零搭建多 Agent 系统的完整过程。只聊技术知识和设计思路,代码交给 AI 写。如果你也想从应用层切入 AI,希望这个系列对你有帮助。
读完本篇你将学到:
- 用 FastAPI 搭建 Agent 的 HTTP 服务;
- SSE 流式推送的原理和实现(让用户看到 Agent 的思考过程);
- Vue3 前端对接 SSE,实现思考过程可视化;
- 从“能跑”到“能用”中间差了哪些工程细节;
背景与动机
前三篇我们搭好了多 Agent 架构——Dispatcher 分流、专业 Agent 各司其职、串行并行都支持。但有个问题:只能在终端用 curl 测试。对于一个“产品”来说,这远远不够。用户不会打开终端敲命令,他们需要:
- 一个对话界面(输入框 + 消息气泡);
- 实时反馈(不是干等 15 秒突然蹦出一大段文字);
- 能看到 Agent 在做什么(正在分析意图... & 正在调用工具...);
核心概念
项目整体技术栈
| 层 | 技术 | 职责 |
|---|---|---|
| 前端 | Vue3 + Vite + TS + element-ai-vue | 对话界面、思考过程展示 |
| 通信 | SSE(Server-Sent Events) | 流式推送 Agent 执行过程 |
| 后端 | FastAPI + Python | HTTP 服务、调用 LangGraph |
| Agent | LangGraph | 多 Agent 协作 |
SSE 是什么?
SSE(Server-Sent Events)是 HTML5 的一个标准特性,本质上就是“服务器主动给浏览器推消息”,技术细节如下:
- 基于普通 HTTP 协议,请求头声明
Accept: text/event-stream; - 服务端返回
Content-Type: text/event-stream,连接保持不断开; - 数据格式固定:
data: 内容\n\n(每条消息以两个换行结尾); - 浏览器原生支持
EventSourceAPI,自动重连;
举一个最简单的例子:
# 服务端:循环往里塞数据
@app.get("/stream")
def stream():
def gen():
yield "data: hello\n\n"
yield "data: world\n\n"
return StreamingResponse(gen(), media_type="text/event-stream")
// 浏览器端:一行 API 搞定
const es = new EventSource('/stream');
es.onmessage = (e) => console.log(e.data); // hello → world
类比理解一下:
- 普通 HTTP:你问一句,我答一句,答完就挂电话;
- SSE:你问一句,我边想边说,全部说完才挂电话;
- WebSocket:开个对讲机,两头都能随时说话;
为什么用 SSE 而不是 WebSocket ?
看一下对比:
| SSE | WebSocket | |
|---|---|---|
| 方向 | 单向(服务器 → 客户端) | 双向 |
| 协议 | 基于 HTTP | 独立协议(ws://) |
| 复杂度 | 简单(几行代码) | 复杂(握手、心跳、重连) |
| 适合场景 | 进度推送、流式输出 | 实时聊天、协作编辑 |
我们的场景是“服务器单向推送 Agent 执行进度”,不需要客户端实时发消息给服务器(发消息用普通 POST 就行)。所以 SSE 是最合适的选择——简单、够用。
数据流全貌
动手实现
Step 1:FastAPI 后端搭建
后端的核心职责:接收用户消息 → 调用 LangGraph → 流式返回结果。
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json
app = FastAPI()
@app.post("/api/chat/stream")
async def chat_stream(request: ChatRequest):
"""SSE 流式接口:逐步推送 Agent 执行过程"""
return StreamingResponse(
_stream_events(request),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
)
关键点:media_type="text/event-stream" 告诉浏览器这是 SSE 流,不要等全部数据到了再处理。
Step 2:SSE 事件生成
利用 LangGraph 的 stream(mode="updates"),每个节点执行完都会输出一次,我们把它转成 SSE 事件:
def _stream_events(request):
"""生成 SSE 事件流"""
initial_state = {
"messages": [HumanMessage(content=request.message)],
"intents": [],
"mode": "sequential",
"current_step": 0,
}
# 推送开始
yield f"data: {json.dumps({'type': 'start'})}\n\n"
# 逐节点执行并推送
for event in agent_app.stream(initial_state, stream_mode="updates"):
for node_name, node_output in event.items():
if node_output is None:
continue
# 推送节点状态
status = NODE_STATUS_MAP.get(node_name, "")
if status:
yield f"data: {json.dumps({'type': 'status', 'text': status})}\n\n"
# 推送意图识别结果
if node_name == "dispatcher" and "intents" in node_output:
yield f"data: {json.dumps({'type': 'intents', 'intents': node_output['intents']})}\n\n"
# 推送工具调用
if "messages" in node_output:
for msg in node_output["messages"]:
if hasattr(msg, "tool_calls") and msg.tool_calls:
yield f"data: {json.dumps({'type': 'tool_call', 'tools': [tc['name'] for tc in msg.tool_calls]})}\n\n"
# 推送最终回复
yield f"data: {json.dumps({'type': 'reply', 'content': final_reply})}\n\n"
yield f"data: {json.dumps({'type': 'done'})}\n\n"
每条 SSE 消息的格式是 data: {JSON}\n\n——这是 SSE 协议的规定。浏览器收到后会触发 onmessage 事件。
Step 3:前端实现
前端这块作为前端工程师的主场,技术选型:Vue3 + Vite + TS + UI组件库(对话)。
我们这里不展开讲代码细节,只说三件事:组件架构、SSE 对接、组件库选型。
组件架构:聊天页面拆成 4 个组件 + 1 个 composable,职责分离:
src/
├── App.vue # 布局编排
├── types/chat.ts # 类型定义
├── composables/useChat.ts # 聊天逻辑(SSE 处理)
├── api/chat.ts # HTTP 请求封装
└── components/
├── Sidebar.vue # 会话列表
├── MessageList.vue # 消息渲染
├── ChatInput.vue # 输入框
└── EmptyState.vue # 空状态
SSE 对接:用 fetch + ReadableStream 接收(不用 EventSource,因为我们需要 POST 携带消息体)。核心逻辑:从流里读数据 → 按 \n\n 分割 → 解析 JSON → 回调更新 UI:
const response = await fetch("/api/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() || "";
for (const part of parts) {
if (part.startsWith("data: ")) {
onEvent(JSON.parse(part.slice(6))); // 每条事件都回调,实时更新 UI
}
}
}
组件库:用了朋友开源的对话组件库 element-ai-vue,开箱即用,省了很多样式工作。本项目主要用到:
ElAThinking:思考过程展示,配合 SSE 事件实时追加,回复完自动折叠;ElAMarkdown:Markdown 打字机渲染 + 代码高亮,自动跟随主题切换;ElAConfigProvider:全局主题,配合@vueuse/core的useDark一行代码搞定日夜切换;
前端主界面展示:
思考过程 & 最终输出:
刨根问底
| 序号 | 问题 |
|---|---|
| 1️⃣ | Q:为什么不用 WebSocket? |
| A:我们的场景是“服务器单向推送进度”,不需要双向通信。SSE 基于 HTTP,实现简单,浏览器原生支持自动重连。WebSocket 更适合实时聊天、协作编辑这种需要双向通信的场景。 | |
| 2️⃣ | Q:SSE 和 ChatGPT 的流式输出是一样的吗? |
| A:原理一样。ChatGPT 的逐字输出就是 SSE——每生成一个 token 就推送一次。我们的区别是推送的不是 token,而是 Agent 的执行步骤(意图识别、工具调用、最终回复)。 | |
| 3️⃣ | Q:前端为什么用 Vue 不用 React? |
| A:个人偏好,而且 element-ai-vue 组件库只有 Vue 版本。React 也完全能做,核心逻辑(fetch + ReadableStream)和框架无关。 |
本篇小结
- Agent 从“能跑”到“能用”,需要 API 服务 + 流式输出 + 前端界面三件套;
- SSE 是流式推送的最佳选择——简单、单向、基于 HTTP;
- LangGraph 的
stream(mode="updates")天然支持逐节点输出,和 SSE 完美配合; - 思考过程可视化让用户知道 Agent 在做什么,不再干等;
写在最后
很多人觉得 AI 应用开发就是“调 API”,但真正把它做成产品,80% 的工作在工程侧,像:怎么设计接口、怎么处理流式数据、怎么让 UI 跟上数据的节奏、怎么处理各种边界情况(超时、中断、错误)、交互体验等等。
这些事情,恰好是前端工程师每天都在做的。相信未来随着 AI 的不断发展,人们对于应用交互要求会越来越高。
下一篇预告:Agent 现在能对话了,但它“没有记忆”——换个会话就忘了之前聊过什么。下一篇我们给它加上 Memory 和 RAG 能力,让它拥有上下文记忆能力。