AI Content Automation Platform · 开发日志 Day2
日期:2026-03-16
项目:AI Content Automation Platform(基于 AI 的内容自动化 SaaS)
今天围绕「文章生成体验」和「稳定性」做了多轮迭代:打通后端 SSE 流式生成链路、前端流式消费与本地文件保存,引入 Tenacity 重试与数据库连接修正,并输出了一套断点续传的设计草案。
一、后端:DB & 运行时依赖修正
1.1 数据库 URL 规范化
- 问题:本机运行 backend 时,如果
.env中配置的是DATABASE_URL=postgresql://...,SQLAlchemy 会尝试加载同步驱动psycopg2,导致ModuleNotFoundError: No module named 'psycopg2'。 - 调整:
- 在
app.config.get_database_url()中,对从环境变量读取的DATABASE_URL做规范化:- 若以
postgresql://开头且未显式指定+驱动,自动替换为postgresql+asyncpg://...。
- 若以
- 保持拆分字段(
DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME)的默认行为不变,避免破坏已有配置。
- 在
1.2 async SQLAlchemy 所需依赖补齐
- 问题:启动 FastAPI 时出现
ValueError: the greenlet library is required to use this function。 - 原因:启用 SQLAlchemy AsyncEngine 时需要
greenlet支持。 - 调整:
- 在
backend/requirements.txt中引入:sqlalchemy[asyncio]>=2.0.0greenlet>=3.0.0
- 重新安装依赖并在 Docker 构建过程中自动安装,保证本地与容器环境一致。
- 在
1.3 本机 vs Docker 的 DB 主机配置
- Docker 场景(
docker/docker-compose.yml):- 容器内 backend 通过
DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/content_platform访问 db 服务,主机名为db。
- 容器内 backend 通过
- 本地直接跑 backend 时:
- 宿主机访问 Docker 中的 PostgreSQL 需使用
localhost:5432,不能使用服务名db。 - 更新
.env.example文档,明确说明:- 本机连 Docker 的 db 时应配置
DB_HOST=localhost,用户/密码/库名与 compose 保持一致(user/pass/content_platform)。
- 本机连 Docker 的 db 时应配置
- 宿主机访问 Docker 中的 PostgreSQL 需使用
二、后端:SSE 流式文章生成接口
2.1 新增 POST /api/generate-sse
- 协议:
- 请求:
Content-Type: application/json,{ "topic": "..." } - 响应:
text/event-stream
- 请求:
- 事件定义:
event: chunk
data: {"delta": "..."}event: done
data: {"title": "...", "saved_path": "...", "content": "全文" }event: error
data: {"message": "错误信息" }
- 实现位置:
- 路由:
backend/app/routes/article.py中新增generate_sse; - 存储:沿用文章生成接口的
save_article_markdown,在done末尾落盘。
- 路由:
2.2 流式迭代器抽象
- 抽出
generate_article_stream_iter(topic: str) -> Iterator[str]:- 使用 DeepSeek
chat.completions.create(..., stream=True); - 同步迭代获得
chunk.choices[0].delta.content,逐个yield文本片段。
- 使用 DeepSeek
- SSE 路由中使用
iterate_in_threadpool将同步迭代器包装为异步可消费的生成器:- 修正错误用法
for delta in await iterate_in_threadpool(iterator),改为:async for delta in iterate_in_threadpool(iterator): ...
- 支持在协程内安全消费同步迭代器,避免阻塞事件循环。
- 修正错误用法
- 在 SSE 循环中:
- 维护
full字符串,累加所有 delta; - 每个 delta 即刻通过
event: chunk推送给前端; - 最后计算标题并调用
save_article_markdown后以event: done返回完整结果。
- 维护
2.3 本地调试脚本
- 新增
backend/scripts/test_article_stream.py:- 在终端中直接调用
generate_article_stream,打印实时输出; - 支持从命令行传入 topic,便于快速调试 Prompt 与模型表现;
- 在脚本中主动加载
backend/.env与backend/app/.env,确保从任意工作目录执行都能拿到 API Key 等配置。
- 在终端中直接调用
三、前端:流式消费与 Markdown 文件保存
3.1 SSE 客户端封装
- 文件:
frontend/src/api/article.js - 新增函数:
generateArticleSSE(topic, { onChunk, onDone, onError, signal })- 使用原生
fetch(而非 axios)发起POST /api/generate-sse请求; - 调用
res.body.getReader()+TextDecoder手动解析 SSE:- 以空行
\n\n作为事件 block 分隔; - 解析
event:与data:行; - 根据事件类型调用回调:
chunk→onChunk(payload)done→onDone(payload)error→onError(payload)
- 以空行
- 支持传入
AbortController.signal,实现前端中断。
- 使用原生
3.2 UI 集成:ArticleContent 流式生成
- 文件:
frontend/src/components/ArticleContent.vue - 新增状态:
streaming:当前是否处于流式生成中;abortController:用于中途取消请求。
- 新增操作:
- 「流式生成」按钮:
- 校验 topic 非空;
- 清空当前
content和title; - 调用
generateArticleSSE:onChunk:content += delta,并尝试从首行# 标题中解析标题;onDone:- 使用返回的
title与saved_path更新视图; - 提示“已落盘:saved_path”;
- 通过
Blob+ 隐形<a download>自动下载.md文件。
- 使用返回的
- 「取消」按钮:
- 调用
abortController.abort(); - 提示“已取消”。
- 调用
- 「流式生成」按钮:
- 保持原有「生成文章(一次性返回)」按钮逻辑不变,两种生成方式并存,方便对比体验。
四、Tenacity 重试语义与文档
4.1 引入 Tenacity 依赖
- 在
backend/requirements.txt中增加:tenacity>=8.0.0
- 为后续在 LLM 调用路径(如文章生成、对话)上添加可控的重试机制做准备。
4.2 文档:docs/ai-tenacity-retry.md
主要内容:
- 重试语义:
- 重试 = 重新发起一次请求,而不是“接上断掉的流”;
- 对生成类场景(无副作用)可以接受多次生成,对有副作用操作需慎重。
- async vs sync 差异:
- 装饰 async 函数时,Tenacity 使用
await asyncio.sleep()等方式等待,不阻塞事件循环; - 装饰同步函数时,等待是阻塞式的(例如
time.sleep),要注意其所在线程/worker。
- 装饰 async 函数时,Tenacity 使用
- 异常策略:
- 只对“临时性错误”(网络抖动、超时、429/5xx)进行重试;
- 对参数错误、鉴权错误、配额用尽等“永久错误”直接失败,避免无意义兜圈。
- 与 SSE 的关系:
- 对于 SSE 流式接口,更推荐“快失败 +
event: error”的方式; - 是否发起新一轮流式请求,交由前端和用户决定,而不是在同一条 SSE 连接内悄悄重试。
- 对于 SSE 流式接口,更推荐“快失败 +
五、断点续传设计草案
5.1 文档:docs/ai-stream-resume-design.md
提出一套应用层的断点续传方案(不依赖模型的 token offset 能力):
- 请求扩展:
- 在现有
POST /api/generate-sse的请求体中新增可选resume字段:resume.mode="append":表示从已有内容末尾继续写;resume.partial_content:前端已拿到的 Markdown 草稿;- 可选
session_id:用于日志与多段续写的关联。
- 在现有
- SSE 事件元信息:
- 在
chunk/done的data中增加session_id与segment_id,标记是第几段会话(初始生成/第 N 次续写)。
- 在
5.2 后端续写策略
- 初次生成:与当前实现一致,仅基于
topic构造 messages。 - 续写模式:
- 将
partial_content作为草稿包装进 prompt:- 明确要求模型“从草稿结尾继续写后续小节/总结”;
- 明确禁止重复已有段落和标题。
- 每次续写生成的新内容通过 SSE 推送,前后端约定是“增量文本”,由前端拼接为完整正文。
- 将
5.3 前端配合与多次续写
- 前端维护:
fullContent:当前页面展示的完整 Markdown;sessionId/segmentId:追踪本次生成任务下的多段续写。
- 流程:
- 初次生成:正常消费 SSE,累加到
fullContent; - 流中断或用户主动中止:保留当前
fullContent; - 用户点击“继续写”:携带相同
topic与fullContent作为partial_content,发起新一轮 SSE; - 新来的 delta 继续 append 到
fullContent,视觉上实现“从断点续写”的效果。
- 初次生成:正常消费 SSE,累加到
当前仅输出设计文档,尚未在代码中实现断点续传逻辑,为后续迭代预留清晰的接口与任务清单。
六、Docker 构建与验证
- 本地通过
npm run build验证前端打包无误; - 使用
docker compose -f docker/docker-compose.yml build --no-cache重新构建 backend / frontend 镜像:- 确认
backend.Dockerfile中新的 Python 依赖链条正常安装; - 确认
frontend.Dockerfile能在容器内部成功执行npm run build。
- 确认
- 使用
docker compose -f docker/docker-compose.yml up -d --build启动全栈,验证:- 前端访问
/能正常进入文章生成页面; POST /api/generate与POST /api/generate-sse均可用;- Docker 内部 backend 能正确连上 db 和 redis。
- 前端访问
七、今日小结 & 明日计划
今日小结:
- 打通了文章生成从“同步 JSON 返回”到“SSE 流式 + 本地下载”的完整链路,用户可以边看边等,生成完成后自动拿到
.md文件; +- 修复了数据库与 SQLAlchemy 异步依赖问题,明确了本机 vs Docker 场景下的配置差异; - 理清了 Tenacity 在 AI 调用中的定位与局限,避免误用重试作为“续流”手段;
- 完成了断点续传的设计草案,为后续提高长文生成的抗抖动能力打下基础。
明日计划:
- 落地断点续传设计中的一部分:先实现“前端携带 partial_content 续写”的端到端最小闭环;
- 在 SSE 协议中补充
session_id/segment_id字段,完善日志与排错能力; - 根据实际生成体验,优化 Prompt 模板与流式渲染中的小细节(如段落间距、标题样式等)。