111 lines
3.9 KiB
Markdown
111 lines
3.9 KiB
Markdown
|
|
采用**异步并行生成**(Asynchronous Parallel Generation)是目前平衡“高响应速度”与“AI 生成质量”最立竿见影的方案。
|
|||
|
|
|
|||
|
|
其核心思路是将原来“一个请求等五个名字”的线性过程,拆分为“多个并发请求同时生成名字”。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 后端重构:`MiniMaxService.java`
|
|||
|
|
|
|||
|
|
我们需要利用 Java 的 `CompletableFuture` 来并发调用 AI 接口。
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
public class MiniMaxService {
|
|||
|
|
|
|||
|
|
// 模拟原有的单次生成逻辑
|
|||
|
|
private NameCard callAiToGenerateSingleName(String keyword, String mode, String surname) {
|
|||
|
|
// 这里是原有的 MiniMax API 调用逻辑,但只要求它返回 1 个名字
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public List<NameCard> generateNamesAsync(String keyword, String mode, String surname) {
|
|||
|
|
int count = 5; // 我们需要生成 5 个名字
|
|||
|
|
List<CompletableFuture<NameCard>> futures = new ArrayList<>();
|
|||
|
|
|
|||
|
|
for (int i = 0; i < count; i++) {
|
|||
|
|
// 为每个名字开启一个异步线程
|
|||
|
|
CompletableFuture<NameCard> future = CompletableFuture.supplyAsync(() -> {
|
|||
|
|
return callAiToGenerateSingleName(keyword, mode, surname);
|
|||
|
|
});
|
|||
|
|
futures.add(future);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 等待所有线程完成(或部分完成即可返回,提升容错)
|
|||
|
|
return futures.stream()
|
|||
|
|
.map(CompletableFuture::join) // 合并结果
|
|||
|
|
.collect(Collectors.toList());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 为什么这样做更快?
|
|||
|
|
* **线性模式**:AI 生成 5 个名字的时间 = $T1 + T2 + T3 + T4 + T5$(通常需要 8-12 秒)。
|
|||
|
|
* **并行模式**:总耗时 = $max(T1, T2, T3, T4, T5)$(通常只需 2-3 秒)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 前端感知优化:伪流式展示
|
|||
|
|
|
|||
|
|
虽然后端并行生成了结果,但如果我们等 5 个全部完成再返回,前端仍有空窗期。我们可以配合前端做一个“分批次渲染”。
|
|||
|
|
|
|||
|
|
### 修改 `index.js` 的 `fetchNames` 逻辑:
|
|||
|
|
我们可以将请求拆分为两拨:一拨追求“极速响应”(先拿 2 个名字),二拨追求“完整体验”(再拿 3 个)。
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
fetchNames(keyword, mode, surname) {
|
|||
|
|
// 1. 发起第一波请求:先取 2 个名字,让用户立刻有卡片可以翻
|
|||
|
|
this.requestNamingApi(keyword, mode, surname, 2).then(firstBatch => {
|
|||
|
|
this.setData({
|
|||
|
|
nameList: firstBatch,
|
|||
|
|
currentIndex: 0
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 2. 第一波成功后,静默发起第二波请求:补齐剩下的名字
|
|||
|
|
this.requestNamingApi(keyword, mode, surname, 3).then(secondBatch => {
|
|||
|
|
this.setData({
|
|||
|
|
nameList: [...this.data.nameList, ...secondBatch]
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 仪式感补偿:Loading 文案池
|
|||
|
|
|
|||
|
|
为了让异步加载的 1-2 秒不尴尬,在 `home.js` 跳转到 `index.js` 时,利用一个动态的文案池来分散用户注意力。
|
|||
|
|
|
|||
|
|
### 在 `index.wxml` 中添加:
|
|||
|
|
```html
|
|||
|
|
<view wx:if="{{nameList.length === 0}}" class="loading-container">
|
|||
|
|
<view class="ink-blob"></view>
|
|||
|
|
<text class="loading-text">{{loadingQuote}}</text>
|
|||
|
|
</view>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 在 `index.js` 中添加文案轮换:
|
|||
|
|
```javascript
|
|||
|
|
const quotes = [
|
|||
|
|
"正在翻阅《古今集成》...",
|
|||
|
|
"字里行间,寻觅归处...",
|
|||
|
|
"笔墨将就,意境自生...",
|
|||
|
|
"正在感悟山川意向..."
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 在 onLoad 中开启定时器
|
|||
|
|
this.loadingTimer = setInterval(() => {
|
|||
|
|
this.setData({
|
|||
|
|
loadingQuote: quotes[Math.floor(Math.random() * quotes.length)]
|
|||
|
|
});
|
|||
|
|
}, 2000);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🛠️ 实施建议
|
|||
|
|
|
|||
|
|
1. **后端线程池**:在使用 `CompletableFuture` 时,务必自定义一个 `Executor`(线程池),不要使用默认的 `ForkJoinPool`,防止在高并发下把后端拖垮。
|
|||
|
|
2. **音律分析延迟**:如果 `ToneAnalysisService` 依然很慢,可以先返回名字和出处,音律评分字段标记为“计算中”,待卡片翻转时再动态获取。
|
|||
|
|
|
|||
|
|
**这套“并行生成 + 分批返回”的方案,能将用户的体感等待时间从 10 秒以上直接压缩到 2 秒左右。**
|