AI Content Automation Platform · 开发日志 Day2

19 阅读7分钟

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.0
      • greenlet>=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 时:
    • 宿主机访问 Docker 中的 PostgreSQL 需使用 localhost:5432,不能使用服务名 db
    • 更新 .env.example 文档,明确说明:
      • 本机连 Docker 的 db 时应配置 DB_HOST=localhost,用户/密码/库名与 compose 保持一致(user/pass/content_platform)。

二、后端: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 文本片段。
  • 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/.envbackend/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: 行;
      • 根据事件类型调用回调:
        • chunkonChunk(payload)
        • doneonDone(payload)
        • erroronError(payload)
    • 支持传入 AbortController.signal,实现前端中断。

3.2 UI 集成:ArticleContent 流式生成

  • 文件:frontend/src/components/ArticleContent.vue
  • 新增状态:
    • streaming:当前是否处于流式生成中;
    • abortController:用于中途取消请求。
  • 新增操作:
    • 「流式生成」按钮:
      • 校验 topic 非空;
      • 清空当前 contenttitle
      • 调用 generateArticleSSE
        • onChunkcontent += delta,并尝试从首行 # 标题 中解析标题;
        • onDone
          • 使用返回的 titlesaved_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。
  • 异常策略
    • 只对“临时性错误”(网络抖动、超时、429/5xx)进行重试;
    • 对参数错误、鉴权错误、配额用尽等“永久错误”直接失败,避免无意义兜圈。
  • 与 SSE 的关系
    • 对于 SSE 流式接口,更推荐“快失败 + event: error”的方式;
    • 是否发起新一轮流式请求,交由前端和用户决定,而不是在同一条 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 / donedata 中增加 session_idsegment_id,标记是第几段会话(初始生成/第 N 次续写)。

5.2 后端续写策略

  • 初次生成:与当前实现一致,仅基于 topic 构造 messages。
  • 续写模式:
    • partial_content 作为草稿包装进 prompt:
      • 明确要求模型“从草稿结尾继续写后续小节/总结”;
      • 明确禁止重复已有段落和标题。
    • 每次续写生成的新内容通过 SSE 推送,前后端约定是“增量文本”,由前端拼接为完整正文。

5.3 前端配合与多次续写

  • 前端维护:
    • fullContent:当前页面展示的完整 Markdown;
    • sessionId / segmentId:追踪本次生成任务下的多段续写。
  • 流程:
    1. 初次生成:正常消费 SSE,累加到 fullContent
    2. 流中断或用户主动中止:保留当前 fullContent
    3. 用户点击“继续写”:携带相同 topicfullContent 作为 partial_content,发起新一轮 SSE;
    4. 新来的 delta 继续 append 到 fullContent,视觉上实现“从断点续写”的效果。

当前仅输出设计文档,尚未在代码中实现断点续传逻辑,为后续迭代预留清晰的接口与任务清单。


六、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/generatePOST /api/generate-sse 均可用;
    • Docker 内部 backend 能正确连上 db 和 redis。

七、今日小结 & 明日计划

今日小结:

  • 打通了文章生成从“同步 JSON 返回”到“SSE 流式 + 本地下载”的完整链路,用户可以边看边等,生成完成后自动拿到 .md 文件; +- 修复了数据库与 SQLAlchemy 异步依赖问题,明确了本机 vs Docker 场景下的配置差异;
  • 理清了 Tenacity 在 AI 调用中的定位与局限,避免误用重试作为“续流”手段;
  • 完成了断点续传的设计草案,为后续提高长文生成的抗抖动能力打下基础。

明日计划:

  • 落地断点续传设计中的一部分:先实现“前端携带 partial_content 续写”的端到端最小闭环;
  • 在 SSE 协议中补充 session_id / segment_id 字段,完善日志与排错能力;
  • 根据实际生成体验,优化 Prompt 模板与流式渲染中的小细节(如段落间距、标题样式等)。