900 lines
37 KiB
Markdown
900 lines
37 KiB
Markdown
|
|
# AIGC 架构深度分析与改造方案
|
|||
|
|
|
|||
|
|
> 文档版本: 1.0.0
|
|||
|
|
> 更新日期: 2024-12-09
|
|||
|
|
> 状态: 待实施
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 当前架构概览
|
|||
|
|
|
|||
|
|
### 1.1 系统组成
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 前端 (Vue/React) │
|
|||
|
|
│ - 用户交互界面 │
|
|||
|
|
│ - WebSocket 接收进度推送 │
|
|||
|
|
│ - HTTP API 调用 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Java 后端 (Spring Boot) │
|
|||
|
|
│ - 用户认证与权限 │
|
|||
|
|
│ - 数据库 CRUD (MySQL) │
|
|||
|
|
│ - 消息队列调度 (RabbitMQ) │
|
|||
|
|
│ - WebSocket 推送 │
|
|||
|
|
│ - API 次数管理 │
|
|||
|
|
│ - 任务状态管理 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Python 后端 (FastAPI) │
|
|||
|
|
│ - AI/LLM 调用 │
|
|||
|
|
│ - Prompt 管理与渲染 │
|
|||
|
|
│ - 海报图片生成 │
|
|||
|
|
│ - 内容生成逻辑 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 1.2 核心业务流程
|
|||
|
|
|
|||
|
|
#### NoteCreator (笔记生成) 完整流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户点击"生成笔记"
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 1. NoteCreatorController (Java) │
|
|||
|
|
│ - 接收请求: userId, packageIds, styleIds, audienceIds, dates, numTopics │
|
|||
|
|
│ - 创建 NoteCreatorTask 记录 (状态: PENDING) │
|
|||
|
|
│ - 发送消息到 RabbitMQ (NOTE_CREATOR_QUEUE_HIGH/NORMAL) │
|
|||
|
|
│ - 立即返回 taskId 给前端 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
│ RabbitMQ 异步消费
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 2. NoteCreatorQueueConsumer (Java) │
|
|||
|
|
│ - 更新任务状态: PROCESSING │
|
|||
|
|
│ - 推送 WebSocket: 任务开始 (1%) │
|
|||
|
|
│ - 调用 NoteCreatorGenerateService.executeFullNoteCreator() │
|
|||
|
|
│ - 接收进度回调,推送 WebSocket (5% → 20% → 80% → 90% → 100%) │
|
|||
|
|
│ - 更新任务状态: SUCCESS/FAILED │
|
|||
|
|
│ - ACK 消息 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 3. NoteCreatorGenerateServiceImpl (Java) │
|
|||
|
|
│ │
|
|||
|
|
│ 步骤 1: 生成选题 (5% → 20%) │
|
|||
|
|
│ ├── 将 packageIds 转换为 productIds + scenicSpotIds │
|
|||
|
|
│ ├── 调用 TopicGenerateService.generateAndSaveTopic() │
|
|||
|
|
│ └── 推送进度: 20% │
|
|||
|
|
│ │
|
|||
|
|
│ 步骤 2: 预扣除 API 次数 │
|
|||
|
|
│ │
|
|||
|
|
│ 步骤 3: 生成内容 (20% → 90%) │
|
|||
|
|
│ ├── 遍历每个选题 │
|
|||
|
|
│ ├── 调用 ContentGenerateService.generateAndSaveContents() │
|
|||
|
|
│ ├── 每完成一篇,推送进度 + 新结果 (流式推送) │
|
|||
|
|
│ └── 保存到数据库 │
|
|||
|
|
│ │
|
|||
|
|
│ 步骤 4: 处理失败退款 │
|
|||
|
|
│ │
|
|||
|
|
│ 步骤 5: 返回结果 (90% → 100%) │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 4. TopicGenerateServiceImpl (Java) │
|
|||
|
|
│ │
|
|||
|
|
│ 当前实现 (有问题): │
|
|||
|
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ Map<String, Object> requestBody = new HashMap<>(); │ │
|
|||
|
|
│ │ requestBody.put("dates", datesStr); │ │
|
|||
|
|
│ │ requestBody.put("numTopics", request.getNumTopics()); │ │
|
|||
|
|
│ │ requestBody.put("styleIds", request.getStyleIds()); // ⚠️ ID │ │
|
|||
|
|
│ │ requestBody.put("audienceIds", request.getAudienceIds()); // ⚠️ ID │ │
|
|||
|
|
│ │ requestBody.put("scenicSpotIds", ...); // ⚠️ ID │ │
|
|||
|
|
│ │ requestBody.put("productIds", ...); // ⚠️ ID │ │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │ // 调用旧接口 │ │
|
|||
|
|
│ │ externalServiceClient.post("content-generate", "topics", ...); │ │
|
|||
|
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
│ HTTP 同步调用
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 5. Python FastAPI │
|
|||
|
|
│ │
|
|||
|
|
│ 旧接口 (已废弃): │
|
|||
|
|
│ POST /api/v1/tweet/topics │
|
|||
|
|
│ - 接收 ID 列表 │
|
|||
|
|
│ - 查询数据库获取完整信息 ← ⚠️ Python 访问数据库 │
|
|||
|
|
│ - 构建 Prompt │
|
|||
|
|
│ - 调用 LLM │
|
|||
|
|
│ - 返回选题列表 │
|
|||
|
|
│ │
|
|||
|
|
│ 新接口 (V2): │
|
|||
|
|
│ POST /api/v2/aigc/execute │
|
|||
|
|
│ - 接收完整对象 │
|
|||
|
|
│ - 直接使用 PromptRegistry 渲染 │
|
|||
|
|
│ - 调用 LLM │
|
|||
|
|
│ - 返回结果 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 问题分析
|
|||
|
|
|
|||
|
|
### 2.1 数据库双端访问问题
|
|||
|
|
|
|||
|
|
**现状**:
|
|||
|
|
```
|
|||
|
|
Java 端:
|
|||
|
|
- 存储 景区(ScenicSpot)、产品(Product)、风格(Style)、人群(Audience)
|
|||
|
|
- 存储 选题(Topic)、内容(Content)、任务(Task)
|
|||
|
|
|
|||
|
|
Python 端:
|
|||
|
|
- 也需要查询 景区、产品、风格、人群 信息来构建 Prompt
|
|||
|
|
- 导致两端都需要数据库连接
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题**:
|
|||
|
|
1. 数据一致性风险
|
|||
|
|
2. 连接池资源浪费
|
|||
|
|
3. 部署复杂度增加
|
|||
|
|
4. Python 端需要维护 ORM 模型
|
|||
|
|
|
|||
|
|
### 2.2 ID vs 完整对象
|
|||
|
|
|
|||
|
|
**现状**:
|
|||
|
|
```java
|
|||
|
|
// Java 端传递 ID
|
|||
|
|
requestBody.put("scenicSpotIds", Arrays.asList("1", "2", "3"));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# Python 端需要查数据库
|
|||
|
|
scenic_spots = db.query(ScenicSpot).filter(ScenicSpot.id.in_(ids)).all()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题**:
|
|||
|
|
1. 额外的数据库查询
|
|||
|
|
2. ID 可能无效或已删除
|
|||
|
|
3. 数据版本不一致
|
|||
|
|
|
|||
|
|
### 2.3 Prompt 管理分散
|
|||
|
|
|
|||
|
|
**现状**:
|
|||
|
|
```
|
|||
|
|
Python 端:
|
|||
|
|
prompts/
|
|||
|
|
├── topic_generate/v1.0.0.yaml
|
|||
|
|
├── content_generate/v1.0.0.yaml
|
|||
|
|
├── style/gonglue/v1.0.0.yaml
|
|||
|
|
└── audience/qinzi/v1.0.0.yaml
|
|||
|
|
|
|||
|
|
Java 端:
|
|||
|
|
- 需要知道有哪些风格/人群可选
|
|||
|
|
- 需要展示风格/人群的名称和描述
|
|||
|
|
- 但这些信息在 Python 端的 YAML 文件中
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题**:
|
|||
|
|
1. Java 端无法动态获取可用的风格/人群列表
|
|||
|
|
2. 前端下拉框的选项需要硬编码或单独维护
|
|||
|
|
3. 新增风格/人群需要两端同步修改
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 目标架构 (方案 A: Python 作为纯计算服务)
|
|||
|
|
|
|||
|
|
### 3.1 职责划分
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Java 后端 │
|
|||
|
|
│ │
|
|||
|
|
│ 职责: │
|
|||
|
|
│ ✅ 用户认证与权限管理 │
|
|||
|
|
│ ✅ 所有数据库 CRUD 操作 │
|
|||
|
|
│ ✅ 消息队列调度 (RabbitMQ) │
|
|||
|
|
│ ✅ WebSocket 进度推送 │
|
|||
|
|
│ ✅ 任务状态管理 │
|
|||
|
|
│ ✅ API 次数管理 │
|
|||
|
|
│ ✅ 查询并组装完整数据对象 │
|
|||
|
|
│ ✅ 提供风格/人群配置 API (从 Python 获取或本地缓存) │
|
|||
|
|
│ │
|
|||
|
|
│ 不做: │
|
|||
|
|
│ ❌ Prompt 模板管理 │
|
|||
|
|
│ ❌ LLM 调用 │
|
|||
|
|
│ ❌ 图片处理 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
│ HTTP (同步调用,传完整对象)
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Python 后端 │
|
|||
|
|
│ │
|
|||
|
|
│ 职责: │
|
|||
|
|
│ ✅ Prompt 模板管理 (PromptRegistry) │
|
|||
|
|
│ ✅ Prompt 渲染 (Jinja2) │
|
|||
|
|
│ ✅ LLM 调用 │
|
|||
|
|
│ ✅ 海报图片生成 │
|
|||
|
|
│ ✅ 提供风格/人群配置查询 API │
|
|||
|
|
│ │
|
|||
|
|
│ 不做: │
|
|||
|
|
│ ❌ 数据库访问 │
|
|||
|
|
│ ❌ 用户认证 │
|
|||
|
|
│ ❌ 任务状态管理 │
|
|||
|
|
│ ❌ 消息队列 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 新的调用流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Java: TopicGenerateServiceImpl │
|
|||
|
|
│ │
|
|||
|
|
│ // 1. 从数据库查询完整对象 │
|
|||
|
|
│ ScenicSpot scenicSpot = scenicSpotService.getById(scenicSpotId); │
|
|||
|
|
│ Product product = productService.getById(productId); │
|
|||
|
|
│ Style style = styleService.getById(styleId); │
|
|||
|
|
│ Audience audience = audienceService.getById(audienceId); │
|
|||
|
|
│ │
|
|||
|
|
│ // 2. 构建请求体 (完整对象) │
|
|||
|
|
│ Map<String, Object> request = Map.of( │
|
|||
|
|
│ "engine", "topic_generate", │
|
|||
|
|
│ "params", Map.of( │
|
|||
|
|
│ "month", "2025-01", │
|
|||
|
|
│ "num_topics", 5, │
|
|||
|
|
│ "scenic_spot", Map.of( │
|
|||
|
|
│ "id", scenicSpot.getId(), │
|
|||
|
|
│ "name", scenicSpot.getName(), │
|
|||
|
|
│ "description", scenicSpot.getDescription(), │
|
|||
|
|
│ "address", scenicSpot.getAddress(), │
|
|||
|
|
│ "highlights", scenicSpot.getHighlights() │
|
|||
|
|
│ ), │
|
|||
|
|
│ "product", Map.of( │
|
|||
|
|
│ "id", product.getId(), │
|
|||
|
|
│ "name", product.getName(), │
|
|||
|
|
│ "price", product.getPrice(), │
|
|||
|
|
│ "description", product.getDescription() │
|
|||
|
|
│ ), │
|
|||
|
|
│ "style", Map.of( │
|
|||
|
|
│ "id", style.getId(), │
|
|||
|
|
│ "name", style.getName() │
|
|||
|
|
│ ), │
|
|||
|
|
│ "audience", Map.of( │
|
|||
|
|
│ "id", audience.getId(), │
|
|||
|
|
│ "name", audience.getName() │
|
|||
|
|
│ ) │
|
|||
|
|
│ ) │
|
|||
|
|
│ ); │
|
|||
|
|
│ │
|
|||
|
|
│ // 3. 调用 Python V2 接口 │
|
|||
|
|
│ Response response = externalServiceClient.post( │
|
|||
|
|
│ "content-generate", │
|
|||
|
|
│ "/api/v2/aigc/execute", │
|
|||
|
|
│ request, │
|
|||
|
|
│ TopicGenerateResponse.class │
|
|||
|
|
│ ); │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Python: /api/v2/aigc/execute │
|
|||
|
|
│ │
|
|||
|
|
│ // 1. 解析请求 │
|
|||
|
|
│ engine_id = request.engine # "topic_generate" │
|
|||
|
|
│ params = request.params # 包含完整对象 │
|
|||
|
|
│ │
|
|||
|
|
│ // 2. 获取引擎并执行 │
|
|||
|
|
│ engine = registry.get(engine_id) │
|
|||
|
|
│ result = await engine.execute(params) │
|
|||
|
|
│ │
|
|||
|
|
│ // 3. 引擎内部 │
|
|||
|
|
│ // - 使用 PromptRegistry 渲染 Prompt │
|
|||
|
|
│ // - 调用 LLM │
|
|||
|
|
│ // - 返回结果 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Prompt 与配置管理方案
|
|||
|
|
|
|||
|
|
### 4.1 风格/人群配置的双端访问
|
|||
|
|
|
|||
|
|
**问题**:Java 端需要展示风格/人群列表供用户选择,但配置在 Python 端。
|
|||
|
|
|
|||
|
|
**方案 1: Python 提供查询 API (推荐)**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Python 端提供 API:
|
|||
|
|
|
|||
|
|
GET /api/v2/aigc/prompts/list
|
|||
|
|
返回:
|
|||
|
|
{
|
|||
|
|
"prompts": [
|
|||
|
|
"topic_generate",
|
|||
|
|
"content_generate",
|
|||
|
|
"style/gonglue",
|
|||
|
|
"style/tuijian",
|
|||
|
|
"audience/qinzi",
|
|||
|
|
"audience/zhoubianyou",
|
|||
|
|
"audience/gaoshe"
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GET /api/v2/aigc/prompts/styles
|
|||
|
|
返回:
|
|||
|
|
{
|
|||
|
|
"styles": [
|
|||
|
|
{
|
|||
|
|
"id": "gonglue",
|
|||
|
|
"name": "攻略风",
|
|||
|
|
"description": "以实用信息为主,包含详细的游玩路线..."
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "tuijian",
|
|||
|
|
"name": "极力推荐风",
|
|||
|
|
"description": "热情推荐,强调产品亮点..."
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GET /api/v2/aigc/prompts/audiences
|
|||
|
|
返回:
|
|||
|
|
{
|
|||
|
|
"audiences": [
|
|||
|
|
{
|
|||
|
|
"id": "qinzi",
|
|||
|
|
"name": "亲子向",
|
|||
|
|
"description": "家庭出游,有小孩同行..."
|
|||
|
|
},
|
|||
|
|
...
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Java 端使用**:
|
|||
|
|
```java
|
|||
|
|
// 启动时或定时刷新,缓存到本地
|
|||
|
|
@Scheduled(fixedRate = 3600000) // 每小时刷新
|
|||
|
|
public void refreshStylesAndAudiences() {
|
|||
|
|
StylesResponse styles = pythonClient.get("/api/v2/aigc/prompts/styles");
|
|||
|
|
AudiencesResponse audiences = pythonClient.get("/api/v2/aigc/prompts/audiences");
|
|||
|
|
|
|||
|
|
// 缓存到 Redis 或内存
|
|||
|
|
cache.put("styles", styles.getStyles());
|
|||
|
|
cache.put("audiences", audiences.getAudiences());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 前端请求时返回缓存
|
|||
|
|
@GetMapping("/styles")
|
|||
|
|
public List<StyleVO> getStyles() {
|
|||
|
|
return cache.get("styles");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**方案 2: 配置同步到数据库**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Python 端:
|
|||
|
|
- 仍然是 Prompt 的权威来源
|
|||
|
|
- 提供导出 API
|
|||
|
|
|
|||
|
|
Java 端:
|
|||
|
|
- 定时从 Python 同步配置到数据库
|
|||
|
|
- 前端从 Java 数据库读取
|
|||
|
|
|
|||
|
|
问题:
|
|||
|
|
- 数据可能不一致
|
|||
|
|
- 需要同步机制
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**推荐方案 1**,因为:
|
|||
|
|
- Python 是 Prompt 的唯一权威来源
|
|||
|
|
- 无数据一致性问题
|
|||
|
|
- Java 只做缓存,不存储
|
|||
|
|
|
|||
|
|
### 4.2 Prompt YAML 结构规范
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# prompts/style/gonglue/v1.0.0.yaml
|
|||
|
|
|
|||
|
|
meta:
|
|||
|
|
name: style_gonglue
|
|||
|
|
version: "1.0.0"
|
|||
|
|
description: "攻略风文案风格"
|
|||
|
|
author: "team"
|
|||
|
|
created_at: "2024-12-08"
|
|||
|
|
|
|||
|
|
# 用于 Java 端展示的元信息
|
|||
|
|
style_id: "gonglue" # 唯一标识
|
|||
|
|
style_name: "攻略风" # 显示名称
|
|||
|
|
style_description: "以实用信息为主,包含详细的游玩路线、时间安排、费用预算等"
|
|||
|
|
style_icon: "📝" # 可选,前端图标
|
|||
|
|
style_order: 1 # 排序权重
|
|||
|
|
|
|||
|
|
# 风格提示词内容 (用于 Prompt 渲染)
|
|||
|
|
content: |
|
|||
|
|
你是景区小红书爆款文案策划...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# prompts/audience/qinzi/v1.0.0.yaml
|
|||
|
|
|
|||
|
|
meta:
|
|||
|
|
name: audience_qinzi
|
|||
|
|
version: "1.0.0"
|
|||
|
|
description: "亲子向用户画像"
|
|||
|
|
|
|||
|
|
# 用于 Java 端展示的元信息
|
|||
|
|
audience_id: "qinzi"
|
|||
|
|
audience_name: "亲子向"
|
|||
|
|
audience_description: "家庭出游,有小孩同行,关注安全性和趣味性"
|
|||
|
|
audience_icon: "👨👩👧"
|
|||
|
|
audience_order: 1
|
|||
|
|
|
|||
|
|
# 人群提示词内容
|
|||
|
|
content: |
|
|||
|
|
用户画像:亲子家庭
|
|||
|
|
- 年龄构成:父母25-45岁,孩子3-12岁
|
|||
|
|
- 出游特点:注重安全、趣味、教育意义
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 接口规范
|
|||
|
|
|
|||
|
|
### 5.1 Python V2 API
|
|||
|
|
|
|||
|
|
#### 5.1.1 执行引擎
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
POST /api/v2/aigc/execute
|
|||
|
|
|
|||
|
|
请求体:
|
|||
|
|
{
|
|||
|
|
"engine": "topic_generate",
|
|||
|
|
"params": {
|
|||
|
|
"month": "2025-01",
|
|||
|
|
"num_topics": 5,
|
|||
|
|
"scenic_spot": {
|
|||
|
|
"id": 1,
|
|||
|
|
"name": "天津冒险湾",
|
|||
|
|
"description": "天津最大的水上乐园...",
|
|||
|
|
"address": "天津市滨海新区...",
|
|||
|
|
"highlights": ["水上过山车", "儿童戏水区"]
|
|||
|
|
},
|
|||
|
|
"product": {
|
|||
|
|
"id": 10,
|
|||
|
|
"name": "家庭套票",
|
|||
|
|
"price": 299,
|
|||
|
|
"original_price": 399,
|
|||
|
|
"description": "含2大1小门票..."
|
|||
|
|
},
|
|||
|
|
"style": {
|
|||
|
|
"id": "gonglue",
|
|||
|
|
"name": "攻略风"
|
|||
|
|
},
|
|||
|
|
"audience": {
|
|||
|
|
"id": "qinzi",
|
|||
|
|
"name": "亲子向"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
响应体:
|
|||
|
|
{
|
|||
|
|
"success": true,
|
|||
|
|
"data": {
|
|||
|
|
"topics": [
|
|||
|
|
{
|
|||
|
|
"index": 1,
|
|||
|
|
"date": "2025-01-15",
|
|||
|
|
"title": "寒假遛娃好去处",
|
|||
|
|
"logic": "寒假期间家庭出游需求旺盛..."
|
|||
|
|
},
|
|||
|
|
...
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"execution_time": 12.5,
|
|||
|
|
"metadata": {
|
|||
|
|
"model": "gpt-4",
|
|||
|
|
"prompt_version": "v1.0.0",
|
|||
|
|
"tokens_used": 1500
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.1.2 查询风格列表
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET /api/v2/aigc/config/styles
|
|||
|
|
|
|||
|
|
响应体:
|
|||
|
|
{
|
|||
|
|
"styles": [
|
|||
|
|
{
|
|||
|
|
"id": "gonglue",
|
|||
|
|
"name": "攻略风",
|
|||
|
|
"description": "以实用信息为主...",
|
|||
|
|
"icon": "📝",
|
|||
|
|
"order": 1
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "tuijian",
|
|||
|
|
"name": "极力推荐风",
|
|||
|
|
"description": "热情推荐,强调亮点...",
|
|||
|
|
"icon": "🔥",
|
|||
|
|
"order": 2
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"count": 2
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.1.3 查询人群列表
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET /api/v2/aigc/config/audiences
|
|||
|
|
|
|||
|
|
响应体:
|
|||
|
|
{
|
|||
|
|
"audiences": [
|
|||
|
|
{
|
|||
|
|
"id": "qinzi",
|
|||
|
|
"name": "亲子向",
|
|||
|
|
"description": "家庭出游...",
|
|||
|
|
"icon": "👨👩👧",
|
|||
|
|
"order": 1
|
|||
|
|
},
|
|||
|
|
...
|
|||
|
|
],
|
|||
|
|
"count": 3
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 Java 端改造清单
|
|||
|
|
|
|||
|
|
#### 5.2.1 TopicGenerateServiceImpl 改造
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 改造前
|
|||
|
|
private TopicGenerateResponse callAIService(TopicGenerateRequest request) {
|
|||
|
|
Map<String, Object> requestBody = new HashMap<>();
|
|||
|
|
requestBody.put("dates", datesStr);
|
|||
|
|
requestBody.put("styleIds", request.getStyleIds()); // ❌ 传 ID
|
|||
|
|
requestBody.put("audienceIds", request.getAudienceIds()); // ❌ 传 ID
|
|||
|
|
requestBody.put("scenicSpotIds", request.getScenicSpotIds());
|
|||
|
|
requestBody.put("productIds", request.getProductIds());
|
|||
|
|
|
|||
|
|
return externalServiceClient.post(SERVICE_NAME, "topics", requestBody, ...);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 改造后
|
|||
|
|
private TopicGenerateResponse callAIService(TopicGenerateRequest request) {
|
|||
|
|
// 1. 查询完整对象
|
|||
|
|
List<ScenicSpot> scenicSpots = scenicSpotService.listByIds(request.getScenicSpotIds());
|
|||
|
|
List<Product> products = productService.listByIds(request.getProductIds());
|
|||
|
|
Style style = styleConfigService.getById(request.getStyleIds().get(0));
|
|||
|
|
Audience audience = audienceConfigService.getById(request.getAudienceIds().get(0));
|
|||
|
|
|
|||
|
|
// 2. 构建 V2 请求
|
|||
|
|
Map<String, Object> params = new HashMap<>();
|
|||
|
|
params.put("month", datesStr);
|
|||
|
|
params.put("num_topics", request.getNumTopics());
|
|||
|
|
params.put("scenic_spot", convertToMap(scenicSpots.get(0)));
|
|||
|
|
params.put("product", convertToMap(products.get(0)));
|
|||
|
|
params.put("style", Map.of("id", style.getId(), "name", style.getName()));
|
|||
|
|
params.put("audience", Map.of("id", audience.getId(), "name", audience.getName()));
|
|||
|
|
|
|||
|
|
Map<String, Object> requestBody = Map.of(
|
|||
|
|
"engine", "topic_generate",
|
|||
|
|
"params", params
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 3. 调用 V2 接口
|
|||
|
|
return externalServiceClient.post(SERVICE_NAME, "/api/v2/aigc/execute", requestBody, ...);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private Map<String, Object> convertToMap(ScenicSpot spot) {
|
|||
|
|
Map<String, Object> map = new HashMap<>();
|
|||
|
|
map.put("id", spot.getId());
|
|||
|
|
map.put("name", spot.getName());
|
|||
|
|
map.put("description", spot.getDescription());
|
|||
|
|
map.put("address", spot.getAddress());
|
|||
|
|
map.put("traffic_info", spot.getTrafficInfo());
|
|||
|
|
map.put("highlights", spot.getHighlights());
|
|||
|
|
map.put("opening_hours", spot.getOpeningHours());
|
|||
|
|
return map;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.2.2 ContentGenerateServiceImpl 改造
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 改造后
|
|||
|
|
private ContentGenerateResponse callAIService(ContentGenerateRequest.ContentsInfo request) {
|
|||
|
|
// 1. 查询完整对象
|
|||
|
|
ScenicSpot scenicSpot = scenicSpotService.getById(request.getScenicSpotIds().get(0));
|
|||
|
|
Product product = productService.getById(request.getProductIds().get(0));
|
|||
|
|
Style style = styleConfigService.getById(request.getStyleIds().get(0));
|
|||
|
|
Audience audience = audienceConfigService.getById(request.getAudienceIds().get(0));
|
|||
|
|
|
|||
|
|
// 2. 构建 V2 请求
|
|||
|
|
Map<String, Object> params = new HashMap<>();
|
|||
|
|
params.put("topic", request.getTopic());
|
|||
|
|
params.put("scenic_spot", convertToMap(scenicSpot));
|
|||
|
|
params.put("product", convertToMap(product));
|
|||
|
|
params.put("style", Map.of("id", style.getId(), "name", style.getName()));
|
|||
|
|
params.put("audience", Map.of("id", audience.getId(), "name", audience.getName()));
|
|||
|
|
params.put("enable_judge", true);
|
|||
|
|
|
|||
|
|
Map<String, Object> requestBody = Map.of(
|
|||
|
|
"engine", "content_generate",
|
|||
|
|
"params", params
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return externalServiceClient.post(SERVICE_NAME, "/api/v2/aigc/execute", requestBody, ...);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.2.3 ExternalServicesConfig 改造
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# application.yml
|
|||
|
|
|
|||
|
|
external-services:
|
|||
|
|
content-generate:
|
|||
|
|
enabled: true
|
|||
|
|
base-url: http://localhost:8000
|
|||
|
|
connect-timeout: 30
|
|||
|
|
read-timeout: 120
|
|||
|
|
endpoints:
|
|||
|
|
# 旧接口 (待废弃)
|
|||
|
|
topics: /api/v1/tweet/topics
|
|||
|
|
content: /api/v1/tweet/content
|
|||
|
|
|
|||
|
|
# 新接口 (V2)
|
|||
|
|
execute: /api/v2/aigc/execute
|
|||
|
|
styles: /api/v2/aigc/config/styles
|
|||
|
|
audiences: /api/v2/aigc/config/audiences
|
|||
|
|
engines: /api/v2/aigc/engines
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.2.4 新增 StyleConfigService
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
public class StyleConfigService {
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private ExternalServiceClient pythonClient;
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private RedisTemplate<String, Object> redisTemplate;
|
|||
|
|
|
|||
|
|
private static final String CACHE_KEY_STYLES = "aigc:config:styles";
|
|||
|
|
private static final String CACHE_KEY_AUDIENCES = "aigc:config:audiences";
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有风格配置
|
|||
|
|
*/
|
|||
|
|
public List<StyleConfig> getAllStyles() {
|
|||
|
|
// 1. 先查缓存
|
|||
|
|
List<StyleConfig> cached = (List<StyleConfig>) redisTemplate.opsForValue().get(CACHE_KEY_STYLES);
|
|||
|
|
if (cached != null) {
|
|||
|
|
return cached;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 从 Python 获取
|
|||
|
|
StylesResponse response = pythonClient.get("content-generate", "styles", StylesResponse.class);
|
|||
|
|
List<StyleConfig> styles = response.getStyles();
|
|||
|
|
|
|||
|
|
// 3. 缓存 1 小时
|
|||
|
|
redisTemplate.opsForValue().set(CACHE_KEY_STYLES, styles, 1, TimeUnit.HOURS);
|
|||
|
|
|
|||
|
|
return styles;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据 ID 获取风格
|
|||
|
|
*/
|
|||
|
|
public StyleConfig getById(String styleId) {
|
|||
|
|
return getAllStyles().stream()
|
|||
|
|
.filter(s -> s.getId().equals(styleId))
|
|||
|
|
.findFirst()
|
|||
|
|
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "风格不存在: " + styleId));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 刷新缓存
|
|||
|
|
*/
|
|||
|
|
@Scheduled(fixedRate = 3600000)
|
|||
|
|
public void refreshCache() {
|
|||
|
|
try {
|
|||
|
|
redisTemplate.delete(CACHE_KEY_STYLES);
|
|||
|
|
redisTemplate.delete(CACHE_KEY_AUDIENCES);
|
|||
|
|
getAllStyles();
|
|||
|
|
getAllAudiences();
|
|||
|
|
log.info("风格/人群配置缓存已刷新");
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
log.error("刷新配置缓存失败", e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 迁移计划
|
|||
|
|
|
|||
|
|
### 6.1 阶段划分
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
阶段 1: Python 端准备 (已完成 ✅)
|
|||
|
|
├── [x] 实现 PromptRegistry
|
|||
|
|
├── [x] 迁移所有 Prompt 到 YAML
|
|||
|
|
├── [x] 实现 V2 引擎 (topic_generate, content_generate, poster_generate)
|
|||
|
|
├── [x] 实现 /api/v2/aigc/execute 接口
|
|||
|
|
└── [x] 删除旧的数据库访问代码
|
|||
|
|
|
|||
|
|
阶段 2: Python 端补充 (待实施)
|
|||
|
|
├── [ ] 实现 /api/v2/aigc/config/styles 接口
|
|||
|
|
├── [ ] 实现 /api/v2/aigc/config/audiences 接口
|
|||
|
|
├── [ ] 完善 Prompt YAML 的 meta 信息
|
|||
|
|
└── [ ] 添加接口文档
|
|||
|
|
|
|||
|
|
阶段 3: Java 端改造 (待实施)
|
|||
|
|
├── [ ] 新增 StyleConfigService
|
|||
|
|
├── [ ] 新增 AudienceConfigService
|
|||
|
|
├── [ ] 改造 TopicGenerateServiceImpl
|
|||
|
|
├── [ ] 改造 ContentGenerateServiceImpl
|
|||
|
|
├── [ ] 改造 PosterGenerateServiceImpl
|
|||
|
|
├── [ ] 更新 ExternalServicesConfig
|
|||
|
|
└── [ ] 添加配置缓存机制
|
|||
|
|
|
|||
|
|
阶段 4: 联调测试 (待实施)
|
|||
|
|
├── [ ] 单元测试
|
|||
|
|
├── [ ] 集成测试
|
|||
|
|
├── [ ] 性能测试
|
|||
|
|
└── [ ] 回归测试
|
|||
|
|
|
|||
|
|
阶段 5: 上线切换 (待实施)
|
|||
|
|
├── [ ] 灰度发布
|
|||
|
|
├── [ ] 监控告警
|
|||
|
|
├── [ ] 旧接口废弃
|
|||
|
|
└── [ ] 文档更新
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 兼容性策略
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
过渡期间:
|
|||
|
|
1. Python 端同时保留 V1 和 V2 接口
|
|||
|
|
2. Java 端逐步切换到 V2 接口
|
|||
|
|
3. 通过配置开关控制使用哪个版本
|
|||
|
|
4. 完全切换后删除 V1 接口
|
|||
|
|
|
|||
|
|
配置示例:
|
|||
|
|
external-services:
|
|||
|
|
content-generate:
|
|||
|
|
use-v2-api: true # 开关
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 风险评估
|
|||
|
|
|
|||
|
|
| 风险 | 等级 | 影响 | 缓解措施 |
|
|||
|
|
|-----|------|------|---------|
|
|||
|
|
| Python 服务不可用 | 🔴 高 | 所有 AIGC 功能不可用 | 健康检查、自动重启、降级提示 |
|
|||
|
|
| 配置缓存过期 | 🟡 中 | 风格/人群列表可能不一致 | 定时刷新、手动刷新接口 |
|
|||
|
|
| 接口响应超时 | 🟡 中 | 用户体验差 | 合理超时设置、进度推送 |
|
|||
|
|
| Prompt 渲染错误 | 🟡 中 | 生成内容质量差 | 变量验证、默认值、错误提示 |
|
|||
|
|
| 数据对象字段缺失 | 🟢 低 | Prompt 渲染不完整 | 字段校验、默认值填充 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 附录
|
|||
|
|
|
|||
|
|
### 8.1 数据对象字段规范
|
|||
|
|
|
|||
|
|
#### ScenicSpot (景区)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": 1,
|
|||
|
|
"name": "天津冒险湾",
|
|||
|
|
"description": "天津最大的水上乐园,拥有多种刺激水上项目和亲子设施",
|
|||
|
|
"address": "天津市滨海新区海滨大道",
|
|||
|
|
"location": "天津市",
|
|||
|
|
"traffic_info": "地铁9号线直达,自驾可走津滨高速",
|
|||
|
|
"highlights": ["水上过山车", "儿童戏水区", "漂流河"],
|
|||
|
|
"opening_hours": "09:00-18:00",
|
|||
|
|
"ticket_info": "成人票 199 元,儿童票 99 元",
|
|||
|
|
"tips": "建议自带泳衣,园区内也有售卖"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Product (产品)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": 10,
|
|||
|
|
"name": "家庭套票",
|
|||
|
|
"price": 299,
|
|||
|
|
"original_price": 399,
|
|||
|
|
"description": "含2大1小门票,赠送储物柜",
|
|||
|
|
"includes": ["2张成人票", "1张儿童票", "储物柜1个"],
|
|||
|
|
"valid_period": "2025-01-01 至 2025-03-31",
|
|||
|
|
"usage_rules": "需提前1天预约,入园当日有效"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Style (风格)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": "gonglue",
|
|||
|
|
"name": "攻略风"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Audience (人群)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": "qinzi",
|
|||
|
|
"name": "亲子向"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Topic (选题)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"index": 1,
|
|||
|
|
"date": "2025-01-15",
|
|||
|
|
"title": "寒假遛娃好去处",
|
|||
|
|
"object": "天津冒险湾",
|
|||
|
|
"product": "家庭套票",
|
|||
|
|
"style": "攻略风",
|
|||
|
|
"targetAudience": "亲子向",
|
|||
|
|
"logic": "寒假期间家庭出游需求旺盛,水上乐园是热门选择"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.2 错误码规范
|
|||
|
|
|
|||
|
|
| 错误码 | 说明 | HTTP 状态码 |
|
|||
|
|
|-------|------|------------|
|
|||
|
|
| ENGINE_NOT_FOUND | 引擎不存在 | 404 |
|
|||
|
|
| INVALID_PARAMS | 参数校验失败 | 400 |
|
|||
|
|
| PROMPT_NOT_FOUND | Prompt 不存在 | 404 |
|
|||
|
|
| PROMPT_RENDER_ERROR | Prompt 渲染失败 | 500 |
|
|||
|
|
| LLM_ERROR | LLM 调用失败 | 500 |
|
|||
|
|
| TIMEOUT | 执行超时 | 504 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. 变更记录
|
|||
|
|
|
|||
|
|
| 版本 | 日期 | 作者 | 变更内容 |
|
|||
|
|
|-----|------|------|---------|
|
|||
|
|
| 1.0.0 | 2024-12-09 | Team | 初始版本 |
|