这是我的第477篇原创文章。
『数据杂坛』以Python语言为核心,垂直于数据科学领域,专注于(可戳👉)Python程序设计|数据分析|特征工程|机器学习分类|机器学习回归|深度学习分类|深度学习回归|单变量时序预测|多变量时序预测|语音识别|图像识别|自然语音处理|大语言模型|软件设计开发等技术栈交流学习,涵盖数据挖掘、计算机视觉、自然语言处理等应用领域。(文末有惊喜福利)

一、引言
本项目展示了使用 FastAPI 构建 AI 聊天应用的完整流程,核心技术包括:
这个架构具有良好的扩展性,可以轻松添加用户认证、多模态交互等高级功能。FastAPI 的高性能和完善的类型系统使其成为构建现代 AI 应用的理想选择。
核心功能
连续多轮对话:AI 能记住对话历史,就像和真人聊天一样自然
多角色切换:可以选择不同的 AI 角色(智能助手、AI 老师、编程专家)
流式响应:AI 回复时有打字机效果,体验更流畅
会话管理:支持多个对话会话,可以随时切换
Web 界面:简洁美观的聊天界面,操作简单
技术栈
后端框架:FastAPI(Python 的现代 Web 框架)
数据存储:Redis(高性能内存数据库)
AI 模型:支持 OpenAI 接口请求调用
前端:HTML + CSS + JavaScript
服务器:Uvicorn(高性能 ASGI 服务器)
fastapi-ai-chat-demo/├── main.py # 主应用文件├── config.py # 配置管理├── start_server.py # 启动脚本├── requirements.txt # 依赖包列表├── .env.example # 环境变量模板├── ai_providers/ # AI提供商模块│ ├── __init__.py│ ├── base.py # 基础类定义│ ├── factory.py # 提供商工厂│ ├── openai_provider.py│ ├── deepseek_provider.py│ ├── doubao_provider.py│ ├── kimi_provider.py│ ├── qianwen_provider.py│ └── openai_compatible_provider.py├── static/ # 静态文件│ ├── index.html # 聊天界面│ ├── css/│ └── js/├── docs/ # 项目文档└── logs/ # 日志文件目录
这个项目是"逻辑上分离,工程上未分离"。
逻辑层面(API 通信):是分离的。前端通过 fetch 调 HTTP API 获取数据,后端只返回 JSON 数据,不返回 HTML 页面,前端独立渲染。这就是标准的"前后端分离"通信模式:
工程/部署层面:没分离。同一进程提供:前端文件由 FastAPI 直接托管 —— app.mount("/static", StaticFiles(...)) (main.py:61),访问 / 重定向到 /static/index.html (main.py:394)。前端不需要独立服务器。同一目录、同一仓库:static/ 就放在后端项目里。无前端构建工具链:没有 Vite/Webpack/npm,没有 React/Vue,没有独立构建产物。就是一个原生 HTML + 内联 <script> 单文件。同端口部署:前后端都跑在 0.0.0.0:8000,不存在跨域问题,所以接口连 CORS 配置都没有(只有响应头手写了个 Access-Control-Allow-Origin,main.py:428)。

前端文件和后端 API 由同一个进程提供。但是:前端代码的执行不在这个进程 。关键点——FastAPI 只是把 index.html 的字节发给浏览器,它自己不会执行 JS。 下载到浏览器后,<script> 里的代码是在你电脑上的浏览器进程里跑的。

二、实现过程
API接口
聊天相关
POST /chat/start- 开始新的聊天会话
POST /chat/stream- 流式聊天接口
GET /chat/history- 获取聊天历史
GET /chat/sessions- 获取用户会话列表
DELETE /chat/session/{session_id}- 删除聊天会话
DELETE /chat/history/{session_id}- 清除对话历史
配置相关
GET /roles- 获取可用的AI角色列表
GET /providers- 获取可用的AI提供商列表
文件上传
POST /upload/image- 图片上传接口
其他
GET /- 重定向到聊天界面
GET /api- API信息
详细的API文档可访问:http://localhost:8000/docs
我们的聊天应用提供了 5 个核心 API 接口,就像一个完整的"聊天服务台":
1. 开始新对话:为每个用户创建一个新的"聊天房间",返回房间号(会话 ID)。
@app.post("/chat/start")async def start_chat(user_id: str):session_id = generate_session_id()return {"session_id": session_id, "welcome_message": "你好!我是你的AI助手"}
2. 流式聊天:这是核心接口!处理用户消息,调用 AI 生成回复,并实时返回。
@app.get("/chat/stream")async def chat_stream(user_id: str, session_id: str, message: str, role: str = "assistant"):return StreamingResponse(generate_streaming_response(user_id, session_id, message, role))
3. 获取聊天历史:查看之前的聊天记录,就像翻看聊天记录本
@app.get("/chat/history")async def get_chat_history(user_id: str, session_id: str):history = await get_conversation_history(user_id, session_id)return {"messages": history, "total": len(history)}
4. 清除对话历史:清空聊天记录,重新开始对话。
@app.delete("/chat/history/{session_id}")async def clear_conversation_history(session_id: str, user_id: str):redis_client.delete(get_conversation_key(user_id, session_id))return {"message": "对话历史已清除"}
5. 获取 AI 角色列表:获取所有可用的 AI 角色(助手、老师、程序员等)。
@app.get("/roles")async def get_roles():return {"roles": AI_ROLES, "default_role": "assistant"}
安全特性
前端就是用户看到和操作的界面,我们用 HTML、CSS 和 JavaScript 构建了一个现代化的聊天界面。
界面结构:我们的聊天界面包含几个主要部分:
<divclass="chat-container"><!-- 1. 头部:显示标题和角色选择 --><divclass="chat-header"><h1>🤖 AI智能助手</h1><selectid="roleSelect"><optionvalue="assistant">💬 智能助手</option><optionvalue="teacher">👨🏫 AI老师</option><optionvalue="programmer">👨💻 编程专家</option></select></div><!-- 2. 消息区域:显示对话内容 --><divclass="messages-container"id="messagesContainer"><!-- 消息会动态添加到这里 --></div><!-- 3. 输入区域:用户输入消息 --><divclass="input-container"><inputtype="text"id="messageInput"placeholder="输入你的消息..."><buttononclick="sendMessage()">📤 发送</button></div><!-- 4. 工具栏:常用功能按钮 --><divclass="toolbar"><buttononclick="clearHistory()">🗑️ 清除历史</button><buttononclick="newChat()">🆕 新对话</button></div></div>
效果展示

样式设计特点
JavaScript 核心逻辑:JavaScript 负责处理用户交互和与后端的通信,就像聊天应用的"大脑"。
1. 开始新对话
async function startNewChat() {// 调用后端API创建新会话const response = await fetch('/api/chat/start', { method: 'POST' });const data = await response.json();currentSessionId = data.session_id;// 显示欢迎消息addMessage('assistant', '你好!我是你的AI助手,有什么可以帮助你的吗?');}
2. 发送消息
async function sendMessage() {const message = document.getElementById('messageInput').value;// 显示用户消息addMessage('user', message);// 使用EventSource接收流式响应const eventSource = new EventSource(`/api/chat/stream?session_id=${currentSessionId}&message=${message}`);eventSource.onmessage = function(event) {const data = JSON.parse(event.data);if (data.content) {// 实时显示AI回复updateAIMessage(data.content);}};}
3. 添加消息到界面
function addMessage(role, content) {const messageDiv = document.createElement('div');messageDiv.className = `message ${role}`;// 用户消息显示在右边,AI消息显示在左边const icon = role === 'user' ? '👤' : '🤖';messageDiv.innerHTML = `${icon}${content}`;document.getElementById('messagesContainer').appendChild(messageDiv);// 自动滚动到最新消息messageDiv.scrollIntoView({ behavior: 'smooth' });}
4. 清除历史记录
async function clearHistory() {if (confirm('确定要清除所有对话历史吗?')) {await fetch(`/api/chat/history/${currentSessionId}`, { method: 'DELETE' });document.getElementById('messagesContainer').innerHTML = '';addMessage('system', '对话历史已清除');}}
技术亮点
在聊天应用中,我们需要一个标准的"消息格式"来确保数据的一致性。就像寄信需要标准的信封格式一样:
class ChatMessage(BaseModel):role: str # 谁说的话:"user"(用户) 或 "assistant"(AI)content: str # 说了什么:具体的对话内容timestamp: float # 什么时候说的:消息时间戳
为什么需要这个格式?
这种标准化的数据格式让我们的应用更加稳定可靠,也方便后续的功能扩展。
会话管理就像给每个用户分配一个"聊天房间",让 AI 能够记住每个用户的对话历史。
1. 生成会话 ID
def generate_session_id() -> str:return str(uuid.uuid4())
每个用户开始聊天时,系统会生成一个唯一的"房间号"(会话 ID),就像酒店给客人分配房间一样。
2. 保存对话消息
def save_message(user_id: str, session_id: str, message: ChatMessage):conversation_key = get_conversation_key(user_id, session_id)redis_client.lpush(conversation_key, json.dumps(message_data))redis_client.ltrim(conversation_key, 0, 19) # 只保留最近20条消息
通过 userid + 会话 id 生成 key,将消息保存到 Redis 队列中
3. 获取对话历史
def get_conversation_history(user_id: str, session_id: str):conversation_key = get_conversation_key(user_id, session_id)messages = redis_client.lrange(conversation_key, 0, -1)return [json.loads(msg) for msg in messages]
从 userid + 会话 id 生成 key,从 Redis 中读取用户该会话的历史消息,让 AI 了解之前聊了什么
为什么这样设计?
唯一性:每个会话都有独特的 ID,避免混淆
持久化:消息存储在 Redis 中,重启应用也不会丢失
性能优化:只保留最近的消息,避免内存占用过大
自动清理:每次只保留最近 20 条消息,自动清理旧数据
流式响应就像 AI 在"实时打字",让用户看到回复逐字出现,而不是等待很久后一次性显示全部内容。
1. 保存用户消息
user_msg = ChatMessage(role="user", content=user_message)save_message(session_id, user_msg)
首先将用户的问题保存到"聊天记录本"中。
2. 获取对话历史
history = get_conversation_history(session_id, limit=10)读取最近 10 条对话记录,让 AI 了解聊天的上下文。
3. 构建完整对话
messages = [{"role": "system", "content": AI_ROLES[role]}, # AI角色设定*history, # 历史对话{"role": "user", "content": user_message} # 当前问题]
将角色设定、历史对话和当前问题组合成完整的对话上下文。
4. 调用 AI 服务
response = client.chat.completions.create(model="gpt-4o",messages=messages,stream=True # 关键:启用流式响应)
在 openAi 接口请求格式中,stream=True 表示启用流式响应。
5. 实时返回回复
for chunk in response:if chunk.choices[0].delta.content:content = chunk.choices[0].delta.contentyield f"data: {json.dumps({'content': content})}\n\n"
AI 每生成一小段文字,就立即发送给前端显示。
技术亮点
Server-Sent Events (SSE) :使用 SSE 协议实现服务器向浏览器的实时推送
异步处理:不阻塞其他用户的请求
错误恢复:网络中断时能够优雅处理
上下文保持:每次对话都能"记住"之前聊过的内容
① 前端准备(浏览器)
<head> 和 <body> 是 HTML 文档的两大组成部分:
<head> — 元信息区(不可见)
- 浏览器渲染前读取,不直接显示在页面上
- 内容:<title>、<meta>(charset、viewport、description)、<link>(CSS、图标)、<style>、 <script>(通常放这里预加载)
- 本文件中的例子:index.html:3-15 里的 charset、viewport、marked.js/highlight.js 引用
<body> — 内容区(可见)
- 用户实际看到的页面内容
- 内容:文本、图片、按钮、div 等结构元素,以及通常放在末尾的 <script>(此时 DOM 已加载完)
- 本文件中的例子:index.html:1686 起的 .app-container 布局、聊天界面、模态框
步骤:
1、打开页面 GET /static/index.html(main.py:398 根路径重定向)2、getUserId()从 localStorage取或生成 user_id(demo_user_xxx,index.html:1845)3、启动时 loadProviders()调 GET /providers(index.html:1864),拿可选提供商/模型填充下拉框4、会话:前端本地 currentSessionId(无后端建会话接口,前端自行维护/复用)
② 发送消息(前端 → 后端)
前端通过 #messageInput textarea 获取用户输入,两条触发路径:
输入框事件:textarea 绑定 onkeypress="handleKeyPress(event)" (index.html:1736),按 Enter(非 Shift)时 handleKeyPress (index.html:2318) 调用 sendMessage()。
点击发送按钮:onclick="sendMessage()" (index.html:1763)。
核心取值逻辑在 sendMessage() (index.html:2340):
const message = messageInput.value.trim(); // 读取并去空格...messageInput.value = ''; // 发送后清空输入框
图片输入是另一条链:handleImageUpload(event) (index.html:2944) 从 <input type="file" id="imageUpload" onchange="handleImageUpload(event)"> (index.html:1755) 读取文件,经 /upload/image 转 base64 存入 currentImageData,随消息一起发。
1、sendMessage() 构建请求体(index.html:2390):{ user_id, session_id, message, provider, model, image_data, image_type }2、fetch POST /chat/stream,Content-Type: application/json(index.html:2401)
③ 后端入口 chat_stream(main.py:408)
1、读取 ChatRequest,role被硬编码为 "assistant"(request.role被忽略)2、校验角色、直接返回 StreamingResponse(media_type="text/event-stream")(SSE)3、实际工作在生成器 generate_streaming_response中执行
④ 后端 generate_streaming_response(main.py:307)
1、存用户消息:把用户消息封装 AIMessage→ save_message_to_redis(main.py:177)- Redis 可用:LPUSH conversation:{user_id}:{session_id},设 7 天过期,并更新 user_sessionshash- Redis 不可用:写入进程内 MEMORY_STORAGE2、取历史:get_conversation_history(main.py:234)LRANGE后 reverse(Redis 倒序存)3、裁剪:只取最近 MAX_HISTORY_MESSAGES(默认20)条4、构造消息:每轮转换回 AIMessage(带 image_data/image_type)5、调模型:ai_manager.generate_streaming_response(messages, provider, model, system_prompt)
⑤ MultiProviderManager 路由(factory.py:332)
1、选定提供商:前端传的 provider(存在则用),否则默认提供商2、model非空时放进 kwargs 透传3、调对应实例的 generate_streaming_response
⑥ OpenAICompatibleProvider 调 OpenAI SDK(openai_compatible_provider.py:117)
1、format_messages(:165):系统提示放最前;带图的 user 消息转成多模态数组:[{type:"image_url", image_url:{url:"data:image/jpeg;base64,..."}}, {type:"text", text:"..."}]2、_build_request_params(:216):model(优先 kwargs → 配置)、max_tokens、temperature、extra_body={"enable_thinking": True}、stream=True3、client.chat.completions.create(**params)返回流迭代器4、逐 chunk 输出(:147):- 有 delta.reasoning_content → data: {"type":"reasoning","content":...}- 有 delta.content → data: {"type":"content","content":...}
⑦ 后端透传与落库(回到 main.py:350)
1、每个 chunk:累计 full_response;解析 JSON,只把 type=='content' 累进 content_only_response(深度思考不进历史)2、yield chunk原样透传前端3、流结束后:content_only_response封装 assistant ChatMessage → save_message_to_redis入库4、最后 yield data: {"type":"end","session_id":...}(main.py:387)
⑧ 前端渲染流(index.html:2424 processStream)
1、reader.read()逐块解码,按 data: 前缀解析 JSON2、type==='reasoning'→ 渲染进思考区(.reasoning-content,折叠展示,实时 markdown)3、type==='content' → 渲染正文消息(renderMarkdownContent)4、收到 type==='end' → 恢复输入框、更新会话列表
⑨ 异常路径
1、后端生成器内:抛错则 yield data: {"type":"error",...}(main.py:392)2、provider 层:SDK 异常被 provider 内 catch,yield 的是裸错误字符串(无 data: 前缀,openai_compatible_provider.py:163),前端按 data:过滤会直接忽略——错误对用户不可见(配合日志里 403 过期 key 的现象,前端表现为"没回复")
一次请求的完整调用栈
index.html sendMessage→ POST /chat/stream (main.py:408)→ generate_streaming_response (main.py:307)→ save_message_to_redis (用户消息) [main.py:177]→ get_conversation_history [main.py:234]→ MultiProviderManager.generate_streaming_response (factory.py:332)→ OpenAICompatibleProvider.generate_streaming_response (openai_compatible_provider.py:117)→ OpenAI SDK chat.completions.create(stream=True)→ 逐 chunk 解析 + 透传 SSE→ save_message_to_redis (assistant 回复,仅 content) [main.py:384]→ 前端 processStream 按类型渲染

三、小结
基本聊天
访问应用首页
点击"开始新对话"创建会话
输入消息开始聊天
支持实时流式响应
切换AI提供商
在聊天界面可以选择不同的AI提供商
支持在对话中动态切换模型
每个提供商支持多个模型选择
角色切换
智能助手:通用AI助手
AI老师:教学和解释专家
编程专家:编程和技术问题专家
图片理解
点击图片上传按钮
选择图片文件(支持jpg、png等格式)
发送消息,AI将分析图片内容
会话管理
查看历史会话列表
删除不需要的会话
清除会话对话历史
环境要求
Python 3.8+
Redis (可选,不配置时使用内存存储)
安装依赖
pip install -r requirements.txt配置环境变量
复制环境变量模板:
cp .env.example .env编辑 .env 文件,配置你的AI提供商API密钥:
# OpenAI配置OPENAI_API_KEY=your_openai_api_keyOPENAI_BASE_URL=https://api.openai.com/v1# DeepSeek配置DEEPSEEK_API_KEY=your_deepseek_api_key# 豆包配置DOUBAO_API_KEY=your_doubao_api_key# Kimi配置KIMI_API_KEY=your_kimi_api_key# 通义千问配置QIANWEN_API_KEY=your_qianwen_api_key# Redis配置(可选)REDIS_HOST=localhostREDIS_PORT=6379REDIS_PASSWORD=REDIS_DB=0# 应用配置DEFAULT_AI_PROVIDER=openaiDEBUG=trueLOG_LEVEL=INFO
启动应用
# 方式1:直接启动python main.py# 方式2:使用启动脚本python start_server.py# 方式3:使用uvicornuvicorn main:app --host 0.0.0.0 --port 8000 --reload
访问 http://localhost:8000 开始使用聊天应用。
三、小结
服务启动后效果展示:

应用提供完整的日志记录功能:
logs/ 目录下日志内容包括:
作者简介:
读研期间发表6篇SCI数据算法相关论文,目前在某研究院从事数据算法相关研究工作,结合自身科研实践经历不定期持续分享关于Python、数据分析、特征工程、机器学习、深度学习、人工智能系列基础知识与案例。
致力于只做原创,以最简单的方式理解和学习,关注我一起交流成长。
1、关注下方公众号,点击“领资料”即可免费领取电子资料书籍。
2、文章底部点击喜欢作者即可联系作者获取相关数据集和源码。
3、数据算法方向论文指导或就业指导,点击“联系我”添加作者微信直接交流。
4、有商务合作相关意向,点击“联系我”添加作者微信直接交流。

