3.2 前后端通吃!用 Streamlit + FastAPI 丝滑集成你的 AI Agent

1 阅读1分钟

导语:蓝图已经绘就,是时候砌上第一块砖了。一个生产级的 AI 应用,绝不仅仅是后台的智能逻辑,更需要一个稳定可靠的服务端和一个与用户流畅交互的前端界面。在本章中,我们将进行一次“全栈开发”的冲刺,亲手搭建“旅小智”项目的骨架。你将学会如何使用 FastAPI 快速构建一个高性能的后端服务,并为我们的 LangGraph Agent 暴露标准的 API 接口。紧接着,我们将使用 Streamlit,在不写一行 JavaScript 的情况下,构建一个漂亮的、支持流式对话的聊天机器人前端。学完本章,你将拥有连接“智能大脑”与“用户界面”的關鍵技能,实现从后端到前端的“丝滑”集成。

目录

  1. 项目结构搭建:建立一个清晰、可扩展的工程
    • 创建 trip-genius/ 根目录
    • 子目录划分:app/ (后端), ui/ (前端), agents/ (智能体)
  2. 后端开发(FastAPI):构建 Agent 的“神经中枢”
    • 环境准备:创建 app/requirements.txt 并安装依赖
    • API 服务器:在 app/main.py 中创建 FastAPI 实例
    • 接口实现
      • 实现 POST /invoke 流式端点,用于处理用户请求
      • 使用 StreamingResponse 和 Server-Sent Events (SSE)
      • (模拟)调用 LangGraph App 并将中间结果实时返回
    • CORS 配置:允许前端应用跨域访问
  3. 前端开发(Streamlit):打造 Agent 的“脸面”
    • 环境准备:创建 ui/requirements.txt 并安装依赖
    • UI 布局:在 ui/chat_app.py 中设计聊天界面
      • 使用 st.chat_messagest.chat_input 构建对话流
      • 使用 st.session_state 维持多轮对话的状态
    • API 调用
      • 使用 requests 库向 FastAPI 后端发起请求
      • 处理流式响应,并实时将 Agent 的“思考过程”打印在界面上
  4. 联调与运行:让前后端“对话”起来
    • 第一步:在终端 A 中启动后端 FastAPI 服务
    • 第二步:在终端 B 中启动前端 Streamlit 服务
    • 打开浏览器,与你的第一个全栈 AI 应用进行交互
  5. 代码复盘与核心知识点
    • 流式响应(Streaming):为什么它对 AI 应用的用户体验至关重要?
    • 会话管理(Session Management):FastAPI 如何通过 thread_id 追踪用户,Streamlit 如何通过 session_state 保存前端状态?
    • 跨域资源共享(CORS):解决前后端分离应用通信问题的关键配置。
  6. 总结:你已成为一名 AI 全栈工程师!

1. 项目结构搭建:建立一个清晰、可扩展的工程

一个好的项目结构是软件工程的最佳实践。我们将创建一个主目录 trip-genius,并在其中建立分离的子目录。

trip-genius/
├── app/                  # 后端 FastAPI 应用
│   ├── __init__.py
│   ├── main.py           # FastAPI 服务器主文件
│   └── requirements.txt  # 后端依赖
│
├── agents/               # 智能体核心逻辑 (我们将在下一章填充)
│   └── __init__.py
│
└── ui/                   # 前端 Streamlit 应用
    ├── __init__.py
    ├── chat_app.py       # Streamlit 聊天界面主文件
    └── requirements.txt  # 前端依赖

这种结构实现了关注点分离,使得前后端可以独立开发和维护。

2. 后端开发(FastAPI):构建 Agent 的“神经中枢”

环境准备

首先,为后端应用创建依赖文件 app/requirements.txt

# app/requirements.txt
fastapi
uvicorn
langchain-openai # 假设我们的 agent 需要
langgraph
# ... 其他 agent 依赖
python-dotenv # 用于管理环境变量
sse-starlette # 用于实现流式响应

安装依赖:pip install -r app/requirements.txt

API 服务器与接口实现

现在,我们来编写核心的后端代码 app/main.py

# app/main.py

import asyncio
import uuid
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from sse_starlette.sse import EventSourceResponse
from typing import Dict, Any

# --- FastAPI 应用实例 ---
app = FastAPI()

# --- CORS 中间件配置 ---
# 允许所有来源的跨域请求,方便本地开发
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# --- 数据模型定义 ---
class InvokeRequest(BaseModel):
    input: str
    thread_id: str | None = None

# --- Agent 的模拟 ---
# 在我们真正实现 Agent 之前,先用一个模拟函数来代替
# 它会模拟流式地返回思考过程
async def mock_agent_stream(user_input: str, thread_id: str):
    yield "Thinking about your request... "
    await asyncio.sleep(1)
    yield f"Looking up information for '{user_input[:20]}...'. "
    await asyncio.sleep(1)
    yield "Found some initial results. "
    await asyncio.sleep(1)
    yield f"Final Answer for thread {thread_id}: The best place to visit is the beach!"
    await asyncio.sleep(1)

# --- API 端点实现 ---
@app.post("/invoke")
async def invoke_agent(request: InvokeRequest):
    """
    流式调用 Agent,并实时返回中间结果
    """
    thread_id = request.thread_id or str(uuid.uuid4())
    
    async def stream_generator():
        # 这是一个异步生成器,用于产生 SSE 事件
        async for chunk in mock_agent_stream(request.input, thread_id):
            # SSE 要求数据格式为 "data: ...\n\n"
            yield {
                "event": "message", 
                "data": chunk,
                "id": thread_id # 可以用 id 来标识事件流
            }

    return EventSourceResponse(stream_generator())

@app.get("/")
def read_root():
    return {"message": "Welcome to the TripGenius Backend!"}

代码解读

  1. CORS: CORSMiddleware 是关键。由于我们的前端(Streamlit,运行在比如 8501 端口)和后端(FastAPI,运行在 8000 端口)是两个不同的服务(不同的源 origin),浏览器默认会阻止前端的 JavaScript 代码向后端发起请求。CORS 配置告诉浏览器:“放行!我允许来自任何地方的请求。”
  2. InvokeRequest: 使用 Pydantic 定义了请求体的结构,FastAPI 会自动进行数据校验。
  3. mock_agent_stream: 这是一个异步生成器。我们用 asyncio.sleep 来模拟耗时操作。yield 关键字是实现流式响应的核心,每次 yield 一个值,这个值就会被立刻发送给客户端。
  4. EventSourceResponse: 这是 sse-starlette 库提供的、专门用于实现 Server-Sent Events (SSE) 的响应类。SSE 是一种简单、高效的、实现服务器向客户端单向推送数据的 Web 技术,非常适合 AI Agent 的流式输出场景。它将我们的异步生成器包装成一个标准的 HTTP 流式响应。

3. 前端开发(Streamlit):打造 Agent 的“脸面”

环境准备

为前端应用创建依赖文件 ui/requirements.txt

# ui/requirements.txt
streamlit
requests

安装依赖:pip install -r ui/requirements.txt

UI 布局与 API 调用

现在,我们来编写前端界面 ui/chat_app.py

# ui/chat_app.py

import streamlit as st
import requests
import json

# --- 页面配置 ---
st.set_page_config(
    page_title="旅小智 - 你的 AI 旅行规划师",
    page_icon="✈️",
    layout="wide"
)

# --- 应用标题 ---
st.title("✈️ 旅小智 - TripGenius")
st.caption("一个由多智能体协作驱动的 AI 旅行规划应用")

# --- 后端 API 地址 ---
BACKEND_URL = "http://localhost:8000/invoke"

# --- 会话状态管理 ---
# 初始化聊天历史
if "messages" not in st.session_state:
    st.session_state.messages = []
# 初始化 thread_id
if "thread_id" not in st.session_state:
    st.session_state.thread_id = None

# --- UI 渲染 ---
# 显示历史消息
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# 接收用户输入
if prompt := st.chat_input("你想去哪里?"):
    # 将用户输入显示在聊天界面
    st.chat_message("user").markdown(prompt)
    # 将用户输入存入会话状态
    st.session_state.messages.append({"role": "user", "content": prompt})
    
    # --- 调用后端 API ---
    with st.chat_message("assistant"):
        message_placeholder = st.empty()
        full_response = ""
        
        try:
            # 使用 stream=True 来接收流式响应
            with requests.post(BACKEND_URL, json={"input": prompt, "thread_id": st.session_state.thread_id}, stream=True) as r:
                r.raise_for_status() # 如果请求失败,则抛出异常
                
                for chunk in r.iter_content(chunk_size=None):
                    if chunk:
                        # 解析 SSE 数据
                        # SSE 数据通常以 "data: " 开头
                        chunk_str = chunk.decode('utf-8')
                        if chunk_str.startswith("data:"):
                            data_str = chunk_str[len("data:"):].strip()
                            try:
                                data_json = json.loads(data_str)
                                # 更新 thread_id
                                if 'id' in data_json and st.session_state.thread_id is None:
                                    st.session_state.thread_id = data_json['id']
                                
                                # 拼接流式返回的文本
                                full_response += data_json['data']
                                message_placeholder.markdown(full_response + "▌")
                            except json.JSONDecodeError:
                                pass # 忽略无法解析的行
            
            message_placeholder.markdown(full_response)
        except requests.exceptions.RequestException as e:
            st.error(f"请求后端服务失败: {e}")
            full_response = "抱歉,服务暂时不可用。"

    # 将助手的完整回复存入会话状态
    st.session_state.messages.append({"role": "assistant", "content": full_response})

代码解读

  1. st.session_state: 这是 Streamlit 的“魔法”之一。它是一个类似字典的对象,在用户每次与界面交互(如点击按钮、输入文本)导致页面重新运行时,st.session_state 中存储的数据会保持不变。我们用它来存储 messages 聊天历史和后再返回的 thread_id,从而实现了跨越多次交互的会话记忆。
  2. UI 渲染: 我们遍历 st.session_state.messages 来显示全部的聊天记录。st.chat_input 负责接收用户的最新输入。
  3. requests.post(..., stream=True): 这是调用流式 API 的关键。设置 stream=True 后,requests 不会一次性下载所有响应内容,而是允许我们通过 r.iter_content() 迭代地接收服务器发送的数据块(chunk)。
  4. SSE 解析: 我们收到的 chunk 是原始的 SSE 格式字节。我们需要将其解码,并解析出 data: 字段后面的 JSON 内容,才能拿到真正的 Agent 输出。
  5. 实时 UI 更新: st.empty() 创建了一个占位符。在接收流式响应的循环中,我们不断地用最新的 full_response 更新这个占位符的内容,从而实现了打字机一样的动态显示效果。

4. 联调与运行:让前后端“对话”起来

现在,激动人心的时刻到了。我们需要打开两个终端窗口。

  • 终端 A:启动后端
    cd trip-genius/app
    uvicorn main:app --host 0.0.0.0 --port 8000
    
  • 终端 B:启动前端
    cd trip-genius/ui
    streamlit run chat_app.py
    

Streamlit 会自动在你的浏览器中打开一个新的页面(通常是 http://localhost:8501)。

现在,你可以在这个精美的聊天界面中输入你的问题了。当你点击发送时,你会看到:

  1. 前端界面发起一个网络请求(你可以在浏览器的开发者工具中看到)。
  2. 后端的 FastAPI 终端会打印出接收到请求的日志。
  3. 前端界面的助手消息框中,会像打字机一样,逐字显示我们 mock_agent_streamyield 的内容。

我们成功地用纯 Python 构建了一个全栈 AI 应用的骨架!

5. 代码复盘与核心知识点

  • 流式响应的重要性:为什么不直接等待 Agent 完成所有工作再返回最终结果?因为复杂的 Agent 任务可能耗时数十秒甚至数分钟。如果界面在这期间没有任何反应,用户会以为程序卡死了,体验极差。流式响应将 Agent 的“思考过程”实时地反馈给用户,让用户有“参与感”和“掌控感”,极大地提升了用户体验。
  • 会话管理:我们通过一个 thread_id 实现了跨越多轮请求的后端会话(LangGraph Checkpointer 将依赖它),通过 st.session_state 实现了前端会话。理解并区分这两种会话状态是构建复杂对话应用的关键。
  • CORS:是所有前后端分离应用开发者的“必修课”。理解其原理,能让你在遇到“请求被浏览器阻止”的错误时,从容应对。

6. 总结:你已成为一名 AI 全栈工程师!

恭喜!通过本章的学习,你已经不再仅仅是一个“算法工程师”或“Python 开发者”,你已经掌握了构建一个端到端 AI 应用所需的全栈技能。

你学会了:

  • 使用 FastAPI 构建稳定、高性能的后端 API。
  • 使用 Streamlit 快速构建美观、可交互的前端用户界面。
  • 使用流式响应(SSE)技术,打造极致的 AI 对话体验。
  • 处理了前后端分离架构中必然会遇到的 CORS 和会话管理问题。

现在,我们的“旅小智”已经有了健壮的“骨骼”(前后端框架)和通畅的“神经”(API 通信)。在下一章,我们将为它注入真正的“灵魂”——实现我们精心设计的、由多个专家 Agent 组成的 LangGraph 内核。