312 lines
7.3 KiB
Markdown
312 lines
7.3 KiB
Markdown
# 🔍 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. 检查网络连接
|
||
|
||
---
|
||
|
||
**需要更多帮助?** 提供完整的错误日志和配置信息。
|
||
|
||
|