feat: 优化 语音速度

This commit is contained in:
liqupan
2025-12-06 22:41:44 +08:00
parent c20aca3da0
commit c82d24ddae
20 changed files with 3942 additions and 33 deletions

View File

@@ -0,0 +1,311 @@
# 🔍 Grok API 调试指南
## 数据格式说明
### WebClient 处理后的数据格式
Spring WebFlux 的 `WebClient.bodyToFlux(String.class)` 会自动处理 SSE 流,每个元素直接就是一个完整的 JSON 字符串:
```json
{
"id": "8670de6a-75b3-97e2-fa5a-c2e3d7f1d0f2",
"object": "chat.completion.chunk",
"created": 1764858564,
"model": "grok-4-1-fast-non-reasoning",
"choices": [{
"index": 0,
"delta": {
"content": "你",
"role": "assistant"
},
"finish_reason": null
}],
"system_fingerprint": "fp_174298dd8e"
}
```
### 字段说明
| 字段 | 说明 | 示例值 |
|------|------|--------|
| `id` | 请求唯一ID | `"8670de6a-75b3-97e2-fa5a-c2e3d7f1d0f2"` |
| `object` | 对象类型 | `"chat.completion.chunk"` |
| `created` | 创建时间戳 | `1764858564` |
| `model` | 使用的模型 | `"grok-4-1-fast-non-reasoning"` |
| `choices[0].index` | 选择索引 | `0` |
| `choices[0].delta.content` | 当前token内容 | `"你"` |
| `choices[0].delta.role` | 角色(首次出现) | `"assistant"` |
| `choices[0].finish_reason` | 结束原因 | `null`, `"stop"`, `"length"`, `"content_filter"` |
| `system_fingerprint` | 系统指纹 | `"fp_174298dd8e"` |
## finish_reason 说明
| 值 | 含义 | 处理建议 |
|----|------|----------|
| `null` | 流还在继续 | 继续接收token |
| `"stop"` | 正常结束 | ✅ 回复完整 |
| `"length"` | 达到最大token限制 | ⚠️ 回复可能被截断,建议增加 `max_tokens` |
| `"content_filter"` | 内容被过滤 | ⚠️ 回复包含敏感内容 |
## 解析流程
```
收到数据 → 跳过空行 → 检查 [DONE] → 去除 "data: " 前缀(如果有)
解析 JSON → 验证 choices 字段 → 获取 choices[0]
检查 delta 字段 → 提取 content → 回调 onToken(content)
检查 finish_reason → 记录日志 → 完成
```
## 日志级别配置
`application.yml` 中配置:
```yaml
logging:
level:
com.xiaozhi.dialogue.llm.GrokStreamService: DEBUG # 或 TRACE
```
### 各级别输出内容
#### TRACE最详细
```
TRACE - 收到原始SSE数据: {"id":"8670de6a...
TRACE - 提取到token: 你
TRACE - 提取到token: 好
```
#### DEBUG
```
DEBUG - 请求体: {"model":"grok-4-1-fast-non-reasoning",...}
DEBUG - 收到角色信息: assistant
DEBUG - JSON中缺少choices字段如果有问题
```
#### INFO
```
INFO - 开始调用Grok API - Model: grok-4-1-fast-non-reasoning, UserMessage: 你好...
INFO - 流结束原因: length
INFO - Grok API流式调用完成
```
#### ERROR
```
ERROR - JSON解析失败 - 原始数据: xxx
ERROR - LLM调用失败: Connection refused
```
## 常见问题排查
### 问题1: 没有收到任何token
**可能原因**
- API Key 错误
- 网络连接问题
- API URL 配置错误
**检查步骤**
1. 查看日志是否有 `INFO - 开始调用Grok API`
2. 检查是否有错误日志
3. 验证 `application.yml` 中的配置:
```yaml
xiaozhi:
voice-stream:
grok:
api-key: ${GROK_API_KEY}
api-url: https://api.x.ai/v1/chat/completions
```
### 问题2: token乱码或格式错误
**检查点**
1. 启用 TRACE 日志查看原始数据
2. 确认 JSON 格式是否正确
3. 查看是否有 `ERROR - JSON解析失败` 日志
### 问题3: 回复被截断
**日志特征**
```
INFO - 流结束原因: length
```
**解决方案**
增加 `DEFAULT_MAX_TOKENS` 常量:
```java
private static final int DEFAULT_MAX_TOKENS = 4000; // 或更大
```
### 问题4: WebClient 连接超时
**错误日志**
```
ERROR - LLM调用失败: Connection timeout
```
**解决方案**
在 `initWebClient()` 中增加超时配置:
```java
this.webClient = WebClient.builder()
.baseUrl(apiUrl)
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
.defaultHeader(HttpHeaders.ACCEPT, "text/event-stream")
.build();
```
## 测试建议
### 1. 使用 curl 测试 API
```bash
curl -X POST https://api.x.ai/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "grok-4-1-fast-non-reasoning",
"messages": [{"role": "user", "content": "你好"}],
"stream": true
}'
```
预期输出:
```
data: {"id":"xxx","object":"chat.completion.chunk",...}
data: {"id":"xxx","object":"chat.completion.chunk",...}
data: [DONE]
```
### 2. 检查配置
```java
// 在 GrokStreamService 中添加测试方法
@PostConstruct
public void testConfig() {
logger.info("Grok配置:");
logger.info(" API URL: {}", voiceStreamConfig.getGrok().getApiUrl());
logger.info(" Model: {}", voiceStreamConfig.getGrok().getModel());
logger.info(" API Key配置: {}", voiceStreamConfig.getGrok().getApiKey() != null ? "已配置" : "未配置");
}
```
### 3. 单元测试
```java
@Test
public void testParseSSE() {
String testJson = """
{
"id": "test",
"choices": [{
"delta": {
"content": "测试"
}
}]
}
""";
// 测试解析逻辑
}
```
## 性能监控
### 关键指标
1. **首 token 延迟**:从发送请求到收到第一个 token 的时间
2. **token 吞吐率**:每秒收到的 token 数量
3. **总响应时间**:从开始到收到 [DONE] 的时间
### 添加监控日志
```java
private long startTime;
private int tokenCount = 0;
public void streamChat(...) {
startTime = System.currentTimeMillis();
// ...
.doOnNext(token -> {
tokenCount++;
if (tokenCount == 1) {
long firstTokenLatency = System.currentTimeMillis() - startTime;
logger.info("首token延迟: {}ms", firstTokenLatency);
}
})
.doOnComplete(() -> {
long totalTime = System.currentTimeMillis() - startTime;
double tps = tokenCount / (totalTime / 1000.0);
logger.info("总计: {}个token, 耗时: {}ms, 吞吐率: {:.2f} tokens/s",
tokenCount, totalTime, tps);
})
}
```
## 调试技巧
### 1. 保存原始响应
```java
// 在parseSSE中
logger.trace("原始响应: {}", line);
```
### 2. 使用断点调试
在以下位置设置断点:
- `parseSSE()` 方法入口
- `sink.next(content)` 之前
- `callback.onToken()` 调用处
### 3. 模拟测试
创建测试类:
```java
@Test
public void testGrokStreamService() {
GrokStreamService service = new GrokStreamService();
service.streamChat("你好", null, new TokenCallback() {
@Override
public void onToken(String token) {
System.out.println("Token: " + token);
}
@Override
public void onComplete() {
System.out.println("完成");
}
@Override
public void onError(String error) {
System.err.println("错误: " + error);
}
});
// 等待完成
Thread.sleep(10000);
}
```
## 总结
正常工作流程:
1. ✅ 看到 `INFO - 开始调用Grok API`
2. ✅ 看到多个 `TRACE - 提取到token: xxx`
3. ✅ 看到 `INFO - 流结束原因: stop`(或 length
4. ✅ 看到 `INFO - Grok API流式调用完成`
如果出现问题:
1. 检查日志级别是否足够详细
2. 查找 ERROR 级别的日志
3. 验证配置是否正确
4. 使用 curl 直接测试 API
5. 检查网络连接
---
**需要更多帮助?** 提供完整的错误日志和配置信息。