Files
chatgpt-dingtalk/STREAM_MODE.md
二丫讲梵 f7326b6797 增加卡片交互流式输出的能力 (#315)
* 将ai交互切换为go-openai

* add stream

*  feat(stream): 优化流式响应机制,实现实时卡片更新

- 将固定1.5秒更新改为基于300ms最小间隔的实时更新策略
- 新增内容缓冲区机制,避免过于频繁的API调用
- 改进流式中断处理,保护已接收的内容不丢失

🔧 chore(llm): 优化HTTP客户端配置

- 增加连接池设置(MaxIdleConns: 100, MaxIdleConnsPerHost: 10)
- 设置空闲连接超时时间为90秒
- 添加HTTP/2禁用选项注释,用于解决流式错误问题

📝 docs(stream): 更新流式更新策略文档

- 详细说明实时流式更新机制和缓冲策略
- 新增HTTP/2流式错误的故障排除指南
- 更新配置参数说明和建议范围

🐛 fix(stream): 修复流式中断时的内容丢失问题

- 在流式接收中断时,确保已接收的内容不会丢失
- 改进错误处理逻辑,区分有内容和无内容的情况

* modify ai
2025-12-11 18:22:35 +08:00

338 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 流式输出功能使用指南
## 功能概述
项目已支持钉钉机器人的流式输出功能,可以让 AI 回答像打字一样逐字显示,提供更好的用户体验。
## 两种流式模式
### 1. 简化流式模式(推荐快速开始)
**特点**:
- 无需配置钉钉卡片模板
- 直接使用累积内容一次性回复
- 配置简单,开箱即用
**配置方式**:
`config.yml` 中添加:
```yaml
# 启用流式输出
stream_mode: true
```
### 2. 高级流式卡片模式
**特点**:
- 使用钉钉互动卡片实现真正的流式更新
- 内容逐步显示,类似 ChatGPT 网页版效果
- 需要在钉钉开放平台创建卡片模板
**配置方式**:
`config.yml` 中添加:
```yaml
# 启用流式输出
stream_mode: true
# 钉钉卡片模板ID (可选,用于高级流式卡片模式)
card_template_id: "your-card-template-id"
```
## 配置钉钉卡片模板(高级模式)
### 步骤 1: 创建卡片模板
1. 登录 [钉钉开放平台](https://open.dingtalk.com/)
2. 进入你的应用 -> 互动卡片 -> 卡片模板管理
3. 创建新模板,使用以下 JSON Schema:
```json
{
"type": "object",
"properties": {
"content": {
"type": "string",
"title": "内容"
}
}
}
```
### 步骤 2: 设计卡片样式
在卡片编辑器中,添加一个 Markdown 组件来显示 `content` 字段:
```json
{
"type": "markdown",
"text": "{{content}}"
}
```
### 步骤 3: 发布并获取模板ID
1. 保存并发布卡片模板
2. 复制模板ID类似: `4d18414c-aabc-4ec8-9e67-4ceefeada72a.schema`
3. 将模板ID填入 `config.yml``card_template_id` 字段
## 完整配置示例
```yaml
# 日志级别
log_level: "info"
# OpenAI 配置
api_key: "sk-..."
model: "gpt-4o"
base_url: "" # 可选,用于 API 中转
# 流式输出配置
stream_mode: true # 启用流式输出
card_template_id: "" # 可选钉钉卡片模板ID
# 其他配置...
session_timeout: 600s
max_question_len: 2048
max_answer_len: 2048
max_text: 4096
default_mode: "单聊"
```
## 实现原理
### 简化模式流程
```
用户提问 → OpenAI 流式响应 → 累积完整内容 → 一次性回复
```
### 高级卡片模式流程
```
用户提问
创建钉钉卡片(空内容)
发送初始状态 "稍等,让我想一想..."
OpenAI 流式响应
接收到内容 → 立即累积到缓冲区
距离上次更新超过300ms? → 是 → 更新卡片
↓ 否 ↓
继续接收 ←-----------┘
流式结束,发送最终内容(标记为完成)
```
## 技术架构
### 新增文件
1. **[pkg/llm/stream.go](pkg/llm/stream.go)**
- 实现 OpenAI 流式响应
- 提供 `SingleQaStream()``ContextQaStream()` API
- 支持 ChatCompletion 流式调用
2. **[pkg/dingbot/stream.go](pkg/dingbot/stream.go)**
- 实现钉钉流式卡片更新
- 封装钉钉 Streaming Update API
- 提供 `UpdateAIStreamCard()` 方法
3. **[pkg/process/stream.go](pkg/process/stream.go)**
- 实现流式处理逻辑
- `DoStream()` - 简化流式模式
- `DoStreamWithCard()` - 高级卡片模式
- 包含定时更新和错误处理
### 改动文件
1. **[config/config.go](config/config.go)**
- 添加 `StreamMode` 配置项
- 添加 `CardTemplateID` 配置项
2. **[pkg/process/process_request.go](pkg/process/process_request.go)**
- 根据配置自动选择流式或普通模式
- 支持流式卡片和流式普通两种方式
## API 使用示例
### 在代码中使用流式 API
```go
import "github.com/eryajf/chatgpt-dingtalk/pkg/llm"
// 单聊流式
contentCh, cleanup, err := llm.SingleQaStream("你好", "user123")
if err != nil {
log.Fatal(err)
}
defer cleanup()
for content := range contentCh {
fmt.Print(content) // 逐块输出
}
// 串聊流式
client, contentCh, err := llm.ContextQaStream("继续", "user123")
if err != nil {
log.Fatal(err)
}
defer client.Close()
fullAnswer := ""
for content := range contentCh {
fullAnswer += content
fmt.Print(content)
}
// 保存对话上下文
client.ChatContext.SaveConversation("user123")
```
## 性能优化
### 流式更新策略
高级卡片模式采用**实时流式更新**策略:
- 从大模型接收到内容后立即更新卡片
- 使用缓冲机制避免更新过于频繁(默认最小间隔 **300ms**)
- 这样可以实现真正的实时流式体验,类似 ChatGPT 网页版
可以在 [pkg/process/stream.go](pkg/process/stream.go) 中修改最小更新间隔:
```go
minUpdateInterval := 300 * time.Millisecond // 修改这里
```
建议范围200ms - 500ms
- 更小的间隔:更实时,但 API 调用更频繁
- 更大的间隔:API 调用较少,但流式感觉不明显
### 流式模式选择建议
| 场景 | 推荐模式 | 原因 |
|------|---------|------|
| 快速部署 | 简化模式 | 无需额外配置 |
| 追求体验 | 高级卡片模式 | 真正的流式显示 |
| 高频使用 | 简化模式 | 减少 API 调用 |
| 演示展示 | 高级卡片模式 | 视觉效果更好 |
## 兼容性
- ✅ 保持原有非流式模式完全兼容
- ✅ 支持单聊和串聊两种对话模式
- ✅ 支持所有 OpenAI 兼容的模型
- ✅ 支持 Azure OpenAI
- ✅ 保留敏感词过滤、请求限制等功能
## 故障排查
### 问题 1: 流式模式不生效
**检查项**:
1. 确认 `config.yml``stream_mode: true`
2. 重启应用以加载新配置
3. 查看日志是否有错误信息
### 问题 2: 卡片模式无法显示 / 日志显示 "robot code is empty"
**原因**: 这是正常的降级行为
**说明**:
- 高级卡片模式需要通过 `credentials` 配置才能工作
- 如果没有配置 `credentials`,系统会自动降级为简化流式模式
- 简化流式模式不需要卡片,依然可以正常工作
**解决方案**:
1. 如果想使用高级卡片模式,需要在 `config.yml` 中配置 `credentials`:
```yaml
credentials:
- client_id: "your-app-key"
client_secret: "your-app-secret"
```
2. 如果不需要卡片模式,可以忽略这个警告,或者将 `card_template_id` 留空
### 问题 3: 卡片模板配置正确但不显示
**检查项**:
1. 确认卡片模板ID正确
2. 确认卡片模板已发布
3. 确认钉钉应用有卡片权限
4. 确认 `credentials` 配置正确
5. 查看日志是否有降级提示
### 问题 4: 流式响应中断
**可能原因**:
1. OpenAI API 超时 - 检查网络连接
2. 钉钉 Access Token 过期 - 会自动刷新
3. 上下文超过限制 - 减少 `max_text` 配置
### 问题 5: HTTP/2 流式错误 "stream error: INTERNAL_ERROR"
**错误信息**:
```
Post "https://api.xxx.com/v1/completions": stream error: stream ID 5; INTERNAL_ERROR; received from peer
```
**可能原因**:
1. 上游 API 服务器内部错误或资源不足
2. 网络不稳定,长连接中断
3. HTTP/2 连接管理问题
**解决方案**:
1. **已优化**: 代码已优化 HTTP 客户端配置,增加连接池和超时设置
2. **部分内容保护**: 如果已接收到部分内容,不会因错误而丢失
3. **禁用 HTTP/2** (如果问题频繁): 在 [pkg/llm/client.go](pkg/llm/client.go) 中取消注释:
```go
Transport: &http.Transport{
// ...
ForceAttemptHTTP2: false, // 取消注释这行
}
```
注意:禁用 HTTP/2 会降低性能,但可能更稳定
4. **检查 API 服务器**: 如果使用中转服务,检查中转服务器的健康状况和资源
## 智能降级机制
系统实现了智能降级机制,确保即使高级功能无法使用,基础功能依然可用:
```
尝试高级卡片模式
检查 RobotCode 是否存在
↓ (否)
降级为简化流式模式
检查 credentials 配置
↓ (无配置)
降级为简化流式模式
尝试创建卡片
↓ (失败)
降级为简化流式模式
正常流式输出
```
这意味着:
- ✅ 即使配置不完整,流式功能依然可用
- ✅ 不会因为卡片失败而导致整个功能不可用
- ✅ 日志会清楚地告诉你当前使用的是哪种模式
## 参考资料
- [钉钉流式消息更新 API](https://open.dingtalk.com/document/development/api-streamingupdate)
- [OpenAI Streaming API](https://platform.openai.com/docs/api-reference/streaming)
- [PandaWiki 项目参考实现](tmp/PandaWiki)
## 贡献
欢迎提交 Issue 和 Pull Request 来改进流式功能!