Skip to content

feat(ai): 完善可插拔 AI Provider 并优先使用 Qwen 模型 - #19

Merged
yydounai1234 merged 12 commits into
mainfrom
feature/ai-provider
Jul 29, 2026
Merged

feat(ai): 完善可插拔 AI Provider 并优先使用 Qwen 模型#19
yydounai1234 merged 12 commits into
mainfrom
feature/ai-provider

Conversation

@suerzzh

@suerzzh suerzzh commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
## 修改内容

本次完成 UniSpeaking AI Provider 模块的统一接入与可插拔模型路由改造。

主要修改如下:

1. 完善统一的 `AiProvider` 接口,支持:
   - Realtime SDP 交换
   - LLM 文本生成
   - ASR 语音识别
   - TTS 语音合成
   - 发音评分

2. 完善模型实现:
   - Qwen Realtime
   - Qwen LLM
   - Qwen ASR
   - Qwen3-TTS
   - DeepSeek LLM
   - 豆包 ASR
   - CosyVoice TTS
   - MiniMax TTS
   - 科大讯飞发音评分

3. 完善 `AiProviderRegistry` 可插拔路由,当前默认顺序为:
   - Realtime:Qwen
   - LLM:Qwen → DeepSeek
   - ASR:Qwen → 豆包
   - TTS:Qwen3-TTS → CosyVoice → MiniMax
   - 发音评分:科大讯飞

4. 统一凭证使用方式:
   - 只有 Realtime 使用短生命周期临时 Key
   - LLM、ASR、TTS 和发音评分使用服务端永久凭证
   - 非 Realtime 接口保留 `token` 参数,但模型实现不会使用该参数

5. 统一模型返回结果:
   - LLM 返回模型生成的 `content`
   - ASR 返回识别文本
   - TTS 返回 WAV 音频字节
   - 发音评分返回科大讯飞最终原始响应
   - Realtime 返回 Answer SDP

6. 删除不再使用的 `domain/dto/ai` DTO 层,业务调用统一通过 `AiProviderRegistry` 的基础类型接口完成。

7. 更新 `.env.example`,补充模型路由、Qwen3-TTS、备用模型、超时和响应大小等配置。

## 修改原因

原有 AI Provider 代码存在以下问题:

- 部分厂商实现不完整
- 模型选择与厂商枚举耦合
- Registry 路由不便于动态替换模型
- DTO 与统一 `AiProvider` 接口重复定义调用契约
- 部分模型凭证和临时 Token 的职责不够明确
- TTS 缺少真正的 Qwen 主模型实现
- 环境变量示例不完整

本次修改将不同 AI 能力统一接入 Registry,使业务层无需感知具体厂商。

后续替换模型时,只需:

1. 实现对应能力的 Provider
2. 注册模型 ID
3. 修改 `AI_PROVIDER_ROUTE_*` 路由配置

无需修改业务调用逻辑。

## 涉及范围

- [ ] 项目文档
- [ ] GitHub 配置
- [ ] CI/CD
- [ ] 前端
- [x] 后端
- [x] WebRTC
- [ ] 数据库
- [x] 测试
- [x] 其他:AI Provider、模型路由及环境变量示例

## 本地验证

执行完整后端测试:

```bash
cd backend/unispeaking-server
./mvnw test

执行结果:

Tests run: 44
Failures: 0
Errors: 0
Skipped: 0
BUILD SUCCESS

使用新的 .env.example 验证 Spring 配置加载:

UNISPEAKING_ENV_FILE=/path/to/UniSpeaking/deploy/env/.env.example \
./mvnw -Dtest=UniSpeakingApplicationTests test

执行结果:

Tests run: 1
Failures: 0
Errors: 0
Skipped: 0
BUILD SUCCESS

同时执行:

git diff --check

执行结果:未发现空白字符或补丁格式问题。

GitHub Actions

  • GitHub Actions 已通过
  • 本次修改暂时不涉及自动化测试

已完成本地测试,GitHub Actions 结果需要在 PR 提交后确认。

环境变量

  • 未新增环境变量
  • 已更新 .env.example
  • 未提交任何真实密钥或密码

新增或补充的主要环境变量包括:

AI_PROVIDER_ROUTE_REALTIME
AI_PROVIDER_ROUTE_LLM
AI_PROVIDER_ROUTE_TRANSCRIPTION
AI_PROVIDER_ROUTE_TTS
AI_PROVIDER_ROUTE_SCORING

QWEN_TTS_ENDPOINT
QWEN_TTS_MODEL
QWEN_TTS_VOICE
QWEN_TTS_LANGUAGE_TYPE
QWEN_TTS_CONNECT_TIMEOUT_SECONDS
QWEN_TTS_READ_TIMEOUT_SECONDS
QWEN_TTS_MAX_RESPONSE_BYTES
QWEN_TTS_MAX_AUDIO_BYTES

备用模型所需的 DeepSeek、豆包和 MiniMax 凭证也已在 .env.example 中预留。

风险说明

用户登录

不受影响。

本次未修改认证、用户信息、Token 登录校验及权限相关代码。

实时语音会话

存在较低影响风险。

Realtime Provider 调用接口进行了统一,但临时 Key 获取流程、有效期管理和 SDP 交换流程保持不变,并已通过相关测试。

WebRTC 连接

存在较低影响风险。

Realtime SDP 返回值由旧 DTO 调整为直接返回 String answerSdp。业务层对外返回结构未改变,相关连接和主备路由测试已通过。

RequestID

不受影响。

本次未修改业务 RequestID 生成与传递逻辑。原 Realtime Provider 的 aiCallIdnull,调整后仍保持相同行为。

用量统计

不受影响。

本次未修改用量采集、会话计量及统计逻辑。

计费链路

不受影响。

本次未修改配额预留、扣减、结算或计费相关代码。

其他风险

  • 如果主模型调用失败,Registry 会按照配置顺序尝试备用模型。
  • 如果备用模型未配置密钥,对应备用调用将失败。
  • 修改模型 ID 后,需要同步修改对应的 AI_PROVIDER_ROUTE_* 配置。
  • Qwen3-TTS 当前按照 WAV 输出契约校验音频,模型配置应保持兼容的输出格式。

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found two correctness/security issues in the provider routing and realtime credential configuration. The backend test suite passes (mvn test).

Findings without inline locations

  • backend/unispeaking-server/src/main/java/com/unispeaking/infrastructure/ai/qwen/RealtimeProperties.java:39: This accepts http for the temporary-key endpoint even though RealtimeCredentialServiceImpl sends the permanent DASHSCOPE_API_KEY in the Authorization header to this URL. A misconfigured or intercepted endpoint can therefore expose the permanent credential; require HTTPS (and ideally validate the expected DashScope host) before allowing startup.

List<String> models = model.isBlank()
? route(AiCapability.REALTIME)
: List.of(model);
if (!model.isBlank()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When requestedModel is blank, this branch builds the route from configuration but never validates requestedProvider. A caller that supplies (for example) ProviderType.OPENAI with no model is silently routed to the configured Qwen model, whereas the previous implementation rejected a provider/model mismatch for both explicit and default models. Either reject a non-null provider hint that does not own the selected route, or select the requested provider's default model.

@yydounai1234 yydounai1234 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@yydounai1234
yydounai1234 merged commit 4239a8b into main Jul 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants