导语:蓝图已经绘就,是时候砌上第一块砖了。一个生产级的 AI 应用,绝不仅仅是后台的智能逻辑,更需要一个稳定可靠的服务端和一个与用户流畅交互的前端界面。在本章中,我们将进行一次“全栈开发”的冲刺,亲手搭建“旅小智”项目的骨架。你将学会如何使用 FastAPI 快速构建一个高性能的后端服务,并为我们的 LangGraph Agent 暴露标准的 API 接口。紧接着,我们将使用 Streamlit,在不写一行 JavaScript 的情况下,构建一个漂亮的、支持流式对话的聊天机器人前端。学完本章,你将拥有连接“智能大脑”与“用户界面”的關鍵技能,实现从后端到前端的“丝滑”集成。
目录
- 项目结构搭建:建立一个清晰、可扩展的工程
- 创建
trip-genius/根目录 - 子目录划分:
app/(后端),ui/(前端),agents/(智能体)
- 创建
- 后端开发(FastAPI):构建 Agent 的“神经中枢”
- 环境准备:创建
app/requirements.txt并安装依赖 - API 服务器:在
app/main.py中创建 FastAPI 实例 - 接口实现:
- 实现
POST /invoke流式端点,用于处理用户请求 - 使用
StreamingResponse和 Server-Sent Events (SSE) - (模拟)调用 LangGraph App 并将中间结果实时返回
- 实现
- CORS 配置:允许前端应用跨域访问
- 环境准备:创建
- 前端开发(Streamlit):打造 Agent 的“脸面”
- 环境准备:创建
ui/requirements.txt并安装依赖 - UI 布局:在
ui/chat_app.py中设计聊天界面- 使用
st.chat_message和st.chat_input构建对话流 - 使用
st.session_state维持多轮对话的状态
- 使用
- API 调用:
- 使用
requests库向 FastAPI 后端发起请求 - 处理流式响应,并实时将 Agent 的“思考过程”打印在界面上
- 使用
- 环境准备:创建
- 联调与运行:让前后端“对话”起来
- 第一步:在终端 A 中启动后端 FastAPI 服务
- 第二步:在终端 B 中启动前端 Streamlit 服务
- 打开浏览器,与你的第一个全栈 AI 应用进行交互
- 代码复盘与核心知识点
- 流式响应(Streaming):为什么它对 AI 应用的用户体验至关重要?
- 会话管理(Session Management):FastAPI 如何通过
thread_id追踪用户,Streamlit 如何通过session_state保存前端状态? - 跨域资源共享(CORS):解决前后端分离应用通信问题的关键配置。
- 总结:你已成为一名 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!"}
代码解读:
- CORS:
CORSMiddleware是关键。由于我们的前端(Streamlit,运行在比如 8501 端口)和后端(FastAPI,运行在 8000 端口)是两个不同的服务(不同的源origin),浏览器默认会阻止前端的 JavaScript 代码向后端发起请求。CORS 配置告诉浏览器:“放行!我允许来自任何地方的请求。” InvokeRequest: 使用 Pydantic 定义了请求体的结构,FastAPI 会自动进行数据校验。mock_agent_stream: 这是一个异步生成器。我们用asyncio.sleep来模拟耗时操作。yield关键字是实现流式响应的核心,每次yield一个值,这个值就会被立刻发送给客户端。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})
代码解读:
st.session_state: 这是 Streamlit 的“魔法”之一。它是一个类似字典的对象,在用户每次与界面交互(如点击按钮、输入文本)导致页面重新运行时,st.session_state中存储的数据会保持不变。我们用它来存储messages聊天历史和后再返回的thread_id,从而实现了跨越多次交互的会话记忆。- UI 渲染: 我们遍历
st.session_state.messages来显示全部的聊天记录。st.chat_input负责接收用户的最新输入。 requests.post(..., stream=True): 这是调用流式 API 的关键。设置stream=True后,requests不会一次性下载所有响应内容,而是允许我们通过r.iter_content()迭代地接收服务器发送的数据块(chunk)。- SSE 解析: 我们收到的
chunk是原始的 SSE 格式字节。我们需要将其解码,并解析出data:字段后面的 JSON 内容,才能拿到真正的 Agent 输出。 - 实时 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)。
现在,你可以在这个精美的聊天界面中输入你的问题了。当你点击发送时,你会看到:
- 前端界面发起一个网络请求(你可以在浏览器的开发者工具中看到)。
- 后端的 FastAPI 终端会打印出接收到请求的日志。
- 前端界面的助手消息框中,会像打字机一样,逐字显示我们
mock_agent_stream中yield的内容。
我们成功地用纯 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 内核。