前言
你有没有过这种感觉:看了无数教程,收藏了上百个"必学路线图",但真正动手时却不知从何下手?
我就是这样。直到一周前,我给自己定了一个小目标:不管结果如何,先亲手做出一个能用的AI应用。
于是有了这篇周报。它不仅记录了我的学习轨迹,更是一个普通开发者从0到1的真实写照。
项目概况
项目名称: AI流式对话助手
技术栈:
纯文本
后端:Python 3.10 + FastAPI + SQLAlchemy + SQLite + DeepSeek API
前端:Vue 3 + TypeScript + Vite + Marked(Markdown渲染)
工具:Git + GitHub + Postman + NVM + pnpm
源码:[[week1](https://github.com/qishuixian/ai-fullstack-journey/tree/main/week1)]
已完成功能:
- ✅ 流式输出(打字机效果)
- ✅ 停止生成 / 清空对话
- ✅ 消息时间显示(年月日时分)
- ✅ localStorage本地持久化
- ✅ Markdown渲染(代码块、列表等)
- ✅ SQLite数据库永久存储
Day1-3:环境配置与API调用(痛苦但必要)
虚拟环境:第一个拦路虎
第一天就给了我一个下马威。Python虚拟环境这个概念对我来说完全是陌生的。
踩坑记录:
bash
# 错误做法:直接在全局环境安装
pip install fastapi uvicorn openai
# 正确做法:先创建虚拟环境
python -m venv venv
.\venv\Scripts\Activate # Windows
pip install fastapi uvicorn openai python-dotenv
为什么需要虚拟环境?
- 每个项目可以有独立的依赖版本
- 不会污染全局Python环境
- 便于项目迁移和部署
API调用:模型名称的陷阱
当我终于写好了第一个接口,满心欢喜地测试时,迎接我的是冰冷的500错误。
json
{
"detail": "Error code: 400 - {'error': {'message': 'The supported API model names are deepseek-v4-pro or deepseek-v4-flash, but you passed deepseek-chat.'}}"
}
教训: AI模型更新太快,教程里的deepseek-chat早已废弃。从此我养成了一个习惯:遇到API报错,先去查官方文档确认最新参数。
环境变量管理
使用.env文件管理API Key是个好习惯,但初学者容易犯的错误:
- 等号两边不能有空格:
KEY=value✅,KEY = value❌ - 变量名要跟代码里
os.getenv()的参数完全一致 .env文件要添加到.gitignore,避免泄露密钥
Day4:前端项目搭建(从零到有)
选择Vite的理由
相比Webpack,Vite的开发服务器启动速度快了不止一个量级。对于频繁调试的前端开发来说,这简直是福音。
bash
npm create vite@latest frontend --template vue-ts
cd frontend
npm install
npm run dev
Node版本兼容问题
一开始我用Node 18创建项目,结果create-vite报错。后来通过NVM安装了Node 20 LTS才解决问题。
bash
nvm install 20
nvm use 20
nvm alias default 20
小贴士: 推荐使用Node 20 LTS,兼容性最好,生态最成熟。
Day5:流式输出(高光时刻)
什么是SSE?
SSE(Server-Sent Events)是一种服务器向客户端推送数据的技术。与WebSocket不同,它是单向的(服务器→客户端),非常适合AI流式回复的场景。
后端核心代码:
python
async def generate():
for chunk in stream:
if chunk.choices[0].delta.content is not None:
content = chunk.choices[0].delta.content
yield f"data: {json.dumps({'content': content})}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
前端核心代码:
javascript
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
// 解析SSE数据,更新UI
}
跨域问题(CORS)
前后端分离架构下,跨域是绕不开的坎。我的解决方案是Vite代理:
typescript
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, '')
}
}
}
})
这样前端请求/api/chat/stream,Vite会自动转发到后端8000端口,浏览器以为是同源请求,完美绕过CORS。
异步生成器的坑
最初我天真地写了async def generate(),结果报错连连。原因是openai库的流式响应是同步迭代器,不能在异步生成器里直接用for循环。
解决方案: 改为同步生成器def generate()。
Day6:体验优化(打磨细节)
消息时间显示
给每条消息加上时间戳,让对话更有真实感。
typescript
function nowTime(): string {
const d = new Date()
const h = d.getHours().toString().padStart(2, '0')
const m = d.getMinutes().toString().padStart(2, '0')
return `${h}:${m}`
}
localStorage持久化
用watch监听messages的变化,自动保存到本地存储。
typescript
watch(messages, (newVal) => {
localStorage.setItem('chat', JSON.stringify(newVal))
}, { deep: true })
Markdown渲染
安装marked库,让AI回复中的代码块、列表能正确显示。
bash
pnpm add marked
typescript
import { marked } from 'marked'
const html = marked.parse(fullReply)
停止生成的实现
用AbortController控制fetch请求的中断,这个设计模式在很多场景下都适用。
typescript
const controller = new AbortController()
// 发送请求时绑定signal
fetch(url, { signal: controller.signal })
// 点击停止时中断
controller.abort()
注意: 中断后要在catch中处理AbortError,保留已输出的内容。
Day7:数据库持久化(质的飞跃)
为什么需要数据库?
之前的数据存在localStorage里,换个设备或清缓存就没了。引入数据库后,数据真正做到了"永久存储"。
SQLAlchemy ORM入门
ORM(对象关系映射)让你用Python对象操作数据库,不用写SQL语句。
python
class ChatMessage(Base):
__tablename__ = "chat_messages"
id = Column(Integer, primary_key=True)
role = Column(String(10))
content = Column(Text)
session_id = Column(String(36))
created_at = Column(DateTime, default=datetime.now)
安装依赖的教训
纯文本
ModuleNotFoundError: No module named 'sqlalchemy'
这个错误困扰了我很久。原因是包安装到了全局环境,而不是虚拟环境。
正确姿势:
bash
# 先激活虚拟环境
.\venv\Scripts\Activate
# 再安装
pip install sqlalchemy aiosqlite
数据库写入时机
流式接口中,数据库写入要放在流结束后、发送[DONE]之前。
python
async def generate():
full_reply = ""
for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_reply += content
yield f"data: {json.dumps({'content': content})}\n\n"
# 流结束后写入数据库
async with async_session() as session:
session.add(ChatMessage(role="user", content=request.message))
session.add(ChatMessage(role="assistant", content=full_reply))
await session.commit()
yield "data: [DONE]\n\n"
遇到的坑 & 解决方案(完整版)
| 问题 | 原因 | 解决方式 | 花费时间 |
|---|---|---|---|
| 500 Internal Server Error | 模型名称错误 | 改为deepseek-v4-flash |
2小时 |
| CORS跨域报错 | 前后端端口不同 | 配置Vite代理 | 1小时 |
| 流式输出不逐字 | 异步生成器与同步迭代器混用 | 改为同步生成器 | 3小时 |
| 点击停止后出现空白AI气泡 | AbortError未正确处理 |
在catch中保存已收到的内容 | 1小时 |
| ModuleNotFoundError: sqlalchemy | 包安装到了全局环境 | 在虚拟环境中重新安装 | 30分钟 |
| 点击发送出现两个AI消息 | 事件冒泡导致重复调用 | 添加.stop修饰符 |
30分钟 |
| 清空对话后残留空白消息 | 状态未完全重置 | 同时重置controller和isLoading | 20分钟 |
技术成长与认知升级
1. 从"复制代码"到"理解原理"
初期我习惯于直接复制AI生成的代码,结果出了问题完全不知道从哪里排查。后来我强迫自己:
- 每段代码至少读三遍
- 不理解的地方先查文档再问AI
- 亲手敲一遍,而不是复制粘贴
2. 调试能力的提升
以前看到报错就慌,现在学会了:
- 先看错误类型和堆栈信息
- 定位到具体的文件和行号
- 用
console.log或print输出中间变量 - 查阅官方文档或搜索引擎
3. 工程化思维的建立
从一个简单的脚本到一个完整的项目,需要考虑:
- 目录结构如何组织
- 环境变量如何管理
- 错误如何处理
- 代码如何提交和版本控制
下周计划
- Day8-9:深入学习FastAPI高级特性(依赖注入、中间件、WebSocket)
- Day10-11:前端UI框架集成(Element Plus或Naive UI)
- Day12-13:多会话支持(用户可以创建多个对话)
- Day14:部署上线(Docker + 云服务器)
写在最后
这一周,我从一个只会写简单脚本的初学者,成长为能独立搭建前后端分离AI应用的开发者。
虽然过程中充满了挫折和困惑,但每当解决一个问题,那种成就感是无法言喻的。
送给正在学习的你:
- 不要害怕犯错,每个错误都是进步的阶梯
- 不要追求完美,先完成再优化
- 不要闭门造车,多写博客多交流
如果你也在学习AI全栈开发,欢迎在评论区留言交流。让我们一起成长,共同进步!
如果你也在学习AI全栈开发,欢迎在评论区留言交流。让我们一起成长,共同进步!
如果你对我的项目感兴趣,欢迎访问GitHub仓库:[ai-fullstack-journey](https://github.com/qishuixian/ai-fullstack-journey)