第 4 篇:看见 Agent 思考过程,落地可交互对话

221 阅读7分钟

系列简介:从零搭建一个多 Agent AI 助手,覆盖原理、实现、部署全链路。不讲空话,每篇都有可运行的代码。
项目地址github.com/CodeMomentY…
本篇目标:把 Agent 从命令行变成一个能用的产品,用 SSE + Vue3 落地一个可交互的对话界面。

IMG_7498.JPG

前言

  大家好,我是一名前端工程师。都说前端“已死”,那与其担心被 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 + PythonHTTP 服务、调用 LangGraph
AgentLangGraph多 Agent 协作

SSE 是什么?

  SSE(Server-Sent Events)是 HTML5 的一个标准特性,本质上就是“服务器主动给浏览器推消息”,技术细节如下:

  • 基于普通 HTTP 协议,请求头声明 Accept: text/event-stream
  • 服务端返回 Content-Type: text/event-stream,连接保持不断开;
  • 数据格式固定:data: 内容\n\n(每条消息以两个换行结尾);
  • 浏览器原生支持 EventSource API,自动重连;

举一个最简单的例子:

# 服务端:循环往里塞数据
@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 ?

  看一下对比:

SSEWebSocket
方向单向(服务器 → 客户端)双向
协议基于 HTTP独立协议(ws://)
复杂度简单(几行代码)复杂(握手、心跳、重连)
适合场景进度推送、流式输出实时聊天、协作编辑

  我们的场景是“服务器单向推送 Agent 执行进度”,不需要客户端实时发消息给服务器(发消息用普通 POST 就行)。所以 SSE 是最合适的选择——简单、够用。

数据流全貌

Snipaste_2026-05-26_15-22-04.png

动手实现

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 事件。

Snipaste_2026-05-26_10-14-13.png

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/coreuseDark 一行代码搞定日夜切换;

前端主界面展示

Snipaste_2026-05-26_10-14-31.png

Snipaste_2026-05-26_10-02-48.png

思考过程 & 最终输出

Snipaste_2026-05-26_10-24-04.png

刨根问底

序号问题
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 能力,让它拥有上下文记忆能力。