topfans/docs/specs/2026-06-29-ai-chat-dify-integration-v2-design.md
Lenticular Studio Agent 65ce6bba12 feat: Dify 部署脚本修复 + AI 搭子 MVP 接入
主要改动:

fix(docker/dify-deploy): 修复脚本核心功能
- heredoc 单引号 bug: 'ENVEOF' 改为 ENVEOF,变量正确展开
- 端口默认值 8083/8084/8085 对齐 .env.prod 生产配置
- 加 dc_cmd() 兼容 docker-compose v1/v2 plugin
- openssl rand 生成强随机密码与 SECRET_KEY(42 字符)
- install 跳过已存在 .env,保护用户配置(管理员密码/SECRET_KEY)
- read -p < /dev/tty 兼容非 tty 环境(CI/CD)
- show-config 改用 DIFY_NGINX_PORT(nginx 入口)而非 APP_WEB_PORT

docs(mvp-design): 修正 §3.2 workflow inputs 描述
- 实际只有 query,删除错误的 user_id input 声明
- 节点序列图同步更新

feat(aiChatService): 新增 Dify 客户端与适配器
- service/dify_client.go: Dify Workflow 调用 + SSE 解析
- service/dify_adapter.go: 与现有 chat_service 桥接
- provider/ai_chat_provider.go: Dubbo 入口简化
- main.go: 装配 ConversationRepository + DifyClient

feat(migrations): 新增 AI 搭子会话表 ai_chat.sql
- ai_conversations / ai_messages 表 + 索引

docs: 新增 Dify 集成设计文档
- 2026-06-29-ai-chat-dify-mvp-design.md (MVP 实施级)
- 2026-06-29-ai-chat-dify-integration-v2-design.md (V2 演进路线图)
- docs/dify/角角.yml (Workflow DSL 导出)

config: 更新 env 模板与 docker 配置
- backend/.env.example: DIFY_* 环境变量声明
- docker/.env.prod: DIFY_API_BASE 对齐 8083
- docker/build.sh: 微调
- CLAUDE.md: 项目规范补充

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-02 12:32:34 +08:00

1432 lines
55 KiB
Markdown
Raw 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 搭子对接 Dify 知识库 - V2 完整架构设计
> ⚠️ **本文档状态2026-06-29 重建)**
> - V2 是"100 明星 + 多 AI 平台"的**完整架构设计**
> - **MVP 阶段不实施**——MVP 阶段只做 1 个明星(肖战)+ 1 个 Dify Workflow
> - **MVP 实施级方案**[2026-06-29-ai-chat-dify-mvp-design.md](2026-06-29-ai-chat-dify-mvp-design.md)**先读这个**
> - V2 文档保留作为 Stage 2+ 的**演进路线图**和**详细代码参考**
> ★ **MVP 优先原则**MVP 阶段不实施本文档中"为未来 100 明星 + 多 AI 平台"准备的设计。请先按 MVP 实施(约 1 周),跑通后再按本文档的 §10 演进。
---
## 〇、文档说明
- **V2 适用范围**:所有 star_id所有 AI 平台对接Dify 先行)
- **V2 工作量**:约 4-5 周(含 Dify 端 + 后端 + 测试)—— **不要分配实施时间,按业务节奏推进**
- **目标状态**长期可维护、可扩展30+ 明星 + 多 AI 平台)
- **V1 → V2 演进**V1 评审发现"一星一 App / API Key 散落 / Prompt 边界混乱 / 上下文重复 / 会话仅 Redis / Fallback 副作用 / 抽象太浅 / if-else 工厂" 8 大问题V2 全部解决(详见 §1.2 和 §14
### .1 方案概述(★ 必读)
#### .1.1 我们要解决的问题
**业务问题**:追星 App 用户与"角角"AI 搭子聊天时AI 没有**专属知识库**,无法针对所追明星(默认肖战)给出有针对性的回答。
**技术问题**4 大类):
1. **缺乏 RAG 能力**:现有 AI 只能用通用 LLM 知识,无明星专属数据
2. **会话无持久化**:当前仅 Redis 缓存 24h用户次日回来会失忆
3. **AI 平台耦合**:未来要接 Coze/FastGPT/OpenAI 等
4. **运营成本失控**:扩到 100 明星时,配置/版本/审计会爆炸
#### .1.2 整体实现路径
```
数据迁移 Dify 端准备 后端代码 联调测试
├ 建 2 张表 ├ 准备知识库 ├ model ├ 内部账号测试
├ ├ 创 Workflow ├ repository ├ WebSocket 联调
└ └ 不需要 Code 节点 └ service └ 错误注入
└ provider
└ main.go 装配
```
**不要分配实施时间**——按业务节奏推进。
#### .1.3 关键决策
| 决策项 | V2 选择 | 理由 |
|--------|---------|------|
| Dify 架构 | **Workflow**(不是 Chat App | Workflow 支持 Knowledge Retrieval Node 动态选 dataset_id |
| 多星隔离 | Backend 维护 `star_id → dataset_id` 映射 | Dify 端 Workflow 内部用变量查 |
| API Key | **全局 1 个** | App 重建/删除不影响 Backend |
| Prompt 责任 | **Dify 路径Dify 负责MiniMax 路径Backend 拼 system prompt** | Dify 端用 Workflow 模板组装MiniMax 端无 Dify 替代 |
| 会话存储 | PostgreSQL 主Redis 1h 缓存 | 永久保留,跨设备同步 |
| Backend context 维护 | **删除** SaveContext 给 LLM 用 | Dify 内部管 LLM 上下文 |
| Fallback 策略 | **仅无 conversation 时**允许 | 已开 conversation 失败则报错,不切 LLM |
| Provider 抽象 | `AIProvider` 复合接口 | LLM + RAG + Conversation 一起实现 |
#### .1.4 核心架构图TL;DR
```
Mobile App
│ WebSocket
Gateway (不感知 Provider)
│ Dubbo Triple
AIChatService
├─ ChatEngine (核心编排)
│ ├─ AuditService (前置 + 后置 + 滑动窗口)
│ ├─ MemoryStore (长期记忆)
│ ├─ RedisLock (防并发 race)
│ └─ ProviderFactory (按 star_id 选 Provider)
├─ DifyProvider (协调整合)
│ ├─ DatasetResolver (star_id → dataset_id)
│ ├─ WorkflowClient (Dify HTTP + SSE)
│ ├─ HistoryClient (Dify /v1/messages)
│ └─ ConversationStore (缓存 + 持久化)
└─ MiniMaxProvider (直接 LLM + 本地 Conversation)
外部Dify Workflow (开始 → 知识检索 → LLM → 内容审核 → 结束)
dataset_id 由 Backend 传入Workflow 不再维护 mapping
```
#### .1.5 演进路径(参见 [MVP §10](2026-06-29-ai-chat-dify-mvp-design.md#十stage-2-演进路径)
> ★ **Stage 2+ 必踩的 3 个坑**P0 修复)和 **3 个架构评审笔记**已迁移到 MVP §10。
> 实施 V2 任何章节前,**先读 MVP §10** 的"演进时必踩的坑"。
---
## 一、背景与目标
### 1.1 现状
| 维度 | 现状 |
|------|------|
| AI 角色 | 所有人共用"角角"默认人设,与"星"无关 |
| LLM | MiniMax `M2-her`[llm_service.go](backend/services/aiChatService/service/llm_service.go) |
| star_id 传递 | JWT → middleware → Dubbo Attachments → AIChat 解析,链路完整 |
| 知识维度 | **缺失**。当前 AI 无法获取所追明星的事实性资料 |
| Dify 现状 | Gateway 侧 [dify_client.go](backend/gateway/service/dify_client.go) 已存在,但仅服务"镭射卡"链路 |
| 会话存储 | Redis context24h TTL**无持久化** |
### 1.2 V1 方案已识别的设计缺陷(架构评审结论)
> V1 评审打分:工程完整度 9/10可维护性 6.5/10长期扩展性 5.5/10与 Dify 结合方式 6/10
| 缺陷 | V1 做法 | 后果 |
|------|---------|------|
| **一星一 App** | 每个明星独立 Chat App + Dataset + API Key | 100 星 = 100 个 App运营改 prompt 要改 100 次 |
| **API Key 数量爆炸** | `star_app_mapping: {87: app-xxx, 88: app-yyy}` | Key 跟业务身份绑定App 重建/删除会全失效 |
| **Prompt 边界混乱** | Dify system prompt + Backend persona 双写 | 两边都在管"人设",冲突不可避免 |
| **上下文重复维护** | Dify Conversation + Backend Redis Context 双份 | 必然漂移 |
| **会话仅在 Redis** | conversation_id 24h TTL | 用户次日重连会失忆 |
| **Fallback 有副作用** | 失败即切 MiniMax | 中途切会丢上下文 |
| **Provider 抽象太浅** | 只抽 LLMProvider | 接 Coze/FastGPT 时发现 Knowledge/Conversation 硬编码了 Dify |
| **if-else 工厂** | Provider 内部 `if useDify { ... } else { ... }` | 扩展性差 |
### 1.3 V2 目标
1.**运营成本 O(1)**:改 prompt 只改 1 处
2.**会话永久保留**:用户 1 周后重连,能继续上次对话
3.**职责清晰**Dify 路径 Backend 不参与 prompt 组装
4.**可扩展**:未来接 Coze/FastGPT只需实现 `AIProvider` 接口
5.**风险可控**:渐进式上线,配置回滚
### 1.4 V2 关键决策
| 决策项 | V2 选择 | 理由 |
|--------|---------|------|
| Dify 架构 | **Workflow**(不是 Chat App | Workflow 支持 Knowledge Retrieval Node 动态选 dataset_id |
| 多星隔离 | Backend 维护 `star_id → dataset_id` 映射 | Dify 端 Workflow 内部用变量查 |
| API Key | **全局 1 个** | App 重建/删除不影响 Backend |
| Prompt 责任 | **Dify 路径Dify 负责MiniMax 路径Backend 拼 system prompt** | Dify 端用 Workflow 模板组装MiniMax 端无 Dify 替代Backend 沿用 [prompt_builder.go](backend/services/aiChatService/service/prompt_builder.go) |
| 会话存储 | PostgreSQL 主Redis 1h 缓存 | 永久保留,跨设备同步 |
| Backend context 维护 | **删除** SaveContext 给 LLM 用 | Dify 内部管 LLM 上下文 |
| Fallback 策略 | **仅无 conversation 时**允许 | 已开 conversation 失败则报错,不切 LLM |
| Provider 抽象 | `AIProvider` 复合接口 | LLM + RAG + Conversation 一起实现 |
---
## 二、整体架构
### 2.1 V2 分层架构
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Mobile App (UniApp) │
│ WebSocket /ai-chat?token=Bearer_xxx │
│ 协议:保持不变(仅服务端内部切换 Provider
└─────────────────────────────────┬───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Gateway (:8080) │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ AIChatWebSocketHandler (Hub) │ │
│ │ 透传到 AIChatService不感知 AI Provider │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ Dubbo Triple │
└──────────────────────────────┼──────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AIChatService (:20008) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Chat Engine (核心编排) │ │
│ │ 职责: │ │
│ │ - 鉴权 / 审计 / 限流(横切关注点) │ │
│ │ - 选 AIProvider按 star_id │ │
│ │ - 调 MemoryStore 召回长期记忆 │ │
│ │ - 调 AIProvider.GetConversation / StreamChat │ │
│ │ - 流式审核 + 推 WebSocket │ │
│ │ - 调 AIProvider.AppendMessage 保存 │ │
│ │ - 调 AIProvider.UpdateExternalID 持久化 Dify conv_id │ │
│ │ - 调 GetWelcomeMessageInitSession │ │
│ │ - 调 MemoryStore.ExtractMemory每 N 轮) │ │
│ │ Chat Engine 不知道 Dify / MiniMax 是什么,只跟 AIProvider 打交道 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ AIProvider (复合接口) │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ DifyProvider │ │ MiniMaxProvider │ │ CozeProvider │ │ │
│ │ │ (当前实现) │ │ (当前实现) │ │ (未来) │ │ │
│ │ │ LLM ✓ │ │ LLM ✓ │ │ LLM ✓ │ │ │
│ │ │ RAG ✓ (Dify 知识)│ │ RAG ✗ (无) │ │ RAG ✓ (Coze KB) │ │ │
│ │ │ Conversation ✓ │ │ Conversation ✓ │ │ Conversation ✓ │ │ │
│ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────────────────────┘
┌──────────────────────────────┐
│ Dify Server │
│ POST /v1/workflows/run │
│ { │
│ inputs: { │
│ dataset_id, │
│ query, │
│ user_*, │
│ }, │
│ user, response_mode, │
│ conversation_id │
│ } │
│ │
│ Workflow 内部: │
│ 1. Knowledge Retrieval 节点 │
│ 2. LLM 节点 │
│ 3. 内容审核节点 │
│ 4. 输出 │
└──────────────────────────────┘
```
### 2.2 核心数据流(一次完整对话)
```
[Mobile]
│ WebSocket send {action: "send_message", session_id, message, persona_id}
[Gateway Hub]
│ 鉴权 (JWT → user_id, star_id)
[AIChatService Provider.SendMessage]
├─ 1. 前置审核 (AuditService.AuditText)
├─ 2. 选 AIProvider (ProviderFactory.Select(starID))
│ ├─ dify.star_dataset_mapping 命中 → DifyProvider
│ └─ 未命中 → MiniMaxProvider
├─ 3. 长期记忆召回 (MemoryStore.Recall)
├─ 4. 加分布式锁 (RedisLock.Acquire)
│ └─ conv_lock:{userId}:{starId} 30s TTL
├─ 5. 获取/创建会话
│ └─ DifyProvider.GetOrCreateConversation(key)
│ └─ ★ ConversationStore 内部封装
├─ 6. 构造请求 (ChatRequest)
├─ 7. 调 Provider.StreamChat
│ ├─ Dify 路径: DatasetResolver + WorkflowClient.StreamRun
│ └─ MiniMax 路径: 调 MiniMax API
├─ 8. 流式返回 + 逐 token 后置审核
├─ 9. 更新 Dify conv_id
├─ 10. 保存会话消息
└─ 11. 记忆提取 (每 5 轮)
```
### 2.3 Provider 决策点
```go
// ProviderFactory 单例,按 star_id 选 AIProvider
type ProviderFactory struct {
difyConfig model.DifyConfig
difyProvider AIProvider
miniMaxProvider AIProvider
}
func (f *ProviderFactory) Select(starID int64) AIProvider {
if !f.difyConfig.Enable {
return f.miniMaxProvider
}
if _, ok := f.difyConfig.StarDatasetMapping[starID]; ok {
return f.difyProvider
}
return f.miniMaxProvider
}
func (f *ProviderFactory) SelectFallback(currentName string) AIProvider {
if !f.difyConfig.FallbackToMiniMax {
return nil
}
if currentName == "dify" {
return f.miniMaxProvider
}
return nil
}
```
---
## 三、Dify 端准备工作
> **前置条件**:需要 1 名 Dify 管理员账号(产品/运营),负责 Datasets、Workflow 维护。
### 3.1 准备数据集Datasets
**为每个明星创建一个 Dataset**
1. 登录 Dify → "知识库" → "创建知识库"
2. 命名规范:`star-{identity_id}-kb`(如 `star-xz-kb`、`star-wyb-kb`
3. 选择索引模式:`high_quality`embedding + 向量检索)
4. 导入资料(多种方式任选):
- **文件上传**PDF / Word / Markdown / CSV
- **Notion 同步**:绑定 Notion 数据库
- **Web 同步**:抓取官网、官方微博、官方粉丝站
- **API 同步**Dify 提供 `/v1/datasets/{id}/document/create_by_file`
5. **配置分段**
- 段落长度500-1024 tokens
- 分段 overlap50-100
6. **等待所有文档向量化完成**Dify UI 每个文档显示 ✓ 后才继续)
**Dataset ID 记录**:每个 Dataset 创建后获得 UUID。
### 3.2 创建统一 Workflow
1. 进入"工作室" → "创建空白应用" → 类型选 **Workflow**
2. 命名:`star-chat-workflow`(全平台共用)
3. 配置"开始"节点的 Input 变量:
```yaml
inputs:
- name: dataset_id # string, ★ Backend 传入
type: text
- name: query # string, 用户原始消息
type: text
- name: user_nickname # string
type: text
- name: user_memory # string, optional
type: paragraph
- name: user_style # string, optional
type: text
- name: user_language # string, default "zh-CN"
type: text
```
4. 添加 **知识检索节点 (Knowledge Retrieval)**
- Knowledge选"运行时根据输入选择"
- 变量:`{{ dataset_id }}`
- Query`{{ query }}`
- TopK3
5. 添加 **LLM 节点**
```markdown
你是一个专属 AI 搭子,名字叫"角角"。
【用户称呼】请称呼用户为"{{ user_nickname }}"
【说话风格】{{ user_style }}
【语言】{{ user_language }}
# 用户原始问题
{{ query }}
# 用户长期记忆
{{ user_memory }}
# 知识库检索结果
{{#knowledge_retrieval_node.result#}}
请基于以上信息回答用户问题。
```
6. 添加"内容审核"节点:
- 类型Dify 原生"内容审核"节点
- 位置LLM 节点 → 内容审核节点 → 结束节点
- 配置:
- moderation_type: `keywords`
- keywords: 敏感词列表(每行一个)
- on_failure: `replace_with_text` = "抱歉,这个话题我无法继续,我们换个话题聊聊吧。"
7. 添加"结束"节点
8. **调试**:传 `inputs={dataset_id, query, user_*}` 验证 RAG 召回
9. **发布** → 复制 API Key
### 3.3 单一数据源原则
> **★ 关键设计原则**dataset_id 的"star → dataset"映射**只在 Backend 维护**`ai_chat_configs.dify.star_dataset_mapping`Workflow 不感知 star 概念。
```
[Backend DifyProvider] [Dify Workflow]
│ │
│ 1. SELECT dify.star_dataset_mapping │
│ WHERE star_id = 87 │
│ → dataset_id = "ds-xxxx" │
│ │
│ 2. POST /v1/workflows/run │
│ inputs: { │
│ dataset_id: "ds-xxxx", ←────────┼──→ 开始节点
│ query, user_* │
│ } │
```
**拒绝的做法**V1 错误示例):
```python
# ❌ Workflow 内部维护映射(数据冗余)
STAR_DATASET_MAP = {
"87": "ds-aaaa-xxxx",
"88": "ds-bbbb-yyyy",
}
```
**为什么错**
- Backend 改 mapping → 忘改 Workflow → 用户问"肖战"答"王一博"的资料
- 运营修改 Dataset ID → 必须同步两个地方
**Workflow 内部节点串联**
```
[开始] inputs:{dataset_id, query, user_*}
[知识检索] dataset_id 直接使用
[LLM] prompt 模板
[内容审核]
[结束]
```
### 3.4 关于"换星新建会话"
- `(user_id, star_id)` 是 ai_conversations 表的唯一键
- 用户从 star=87 切到 star=88自动建新会话
- 用户从 star=88 切回 star=87恢复原会话
- **这是预期行为**
### 3.5 知识库自动同步(未来项目)
> V2 不在 AIChat 范围,但留出接口。
- 独立 Worker 负责:拉取 RSS / 微博 / 公众号 / Notion去重调 Dify API
- AIChat 端不感知此 Worker 存在
---
## 四、配置与数据层
### 4.1 ai_chat_configs 新增数据
```sql
INSERT INTO ai_chat_configs (config_key, config_value, config_type, category, description, is_encrypted) VALUES
('dify.enable', 'false', 'boolean', 'dify', 'Dify 集成总开关', FALSE),
('dify.api_base', 'https://api.dify.ai/v1', 'string', 'dify', 'Dify API 基础 URL', FALSE),
('dify.workflow_url', '/workflows/run', 'string', 'dify', 'Workflow API 路径', FALSE),
('dify.star_dataset_mapping', '{"87":"ds-aaaa-xxxx"}', 'json', 'dify', 'star_id → Dataset ID 映射', FALSE),
('dify.api_key', 'app-xxxxxxxxxxxx', 'string', 'dify', 'Dify Workflow API Key', TRUE),
('dify.timeout_sec', '60', 'number', 'dify', 'Dify 单次请求超时(秒)', FALSE),
('dify.user_id_salt', 'topfans-default-salt-CHANGE-ME', 'string', 'dify', 'Dify user 字段哈希盐值', TRUE),
('dify.retry_count', '2', 'number', 'dify', 'Dify 单次失败重试次数', FALSE),
('dify.fallback_to_minimax', 'false', 'boolean', 'dify', 'Dify 失败后是否 fallback', FALSE);
```
### 4.2 PostgreSQL 新增表ai_conversations
```sql
CREATE TABLE IF NOT EXISTS ai_conversations (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
star_id BIGINT NOT NULL,
provider_name VARCHAR(32) NOT NULL,
external_conversation_id VARCHAR(128),
message_count INT DEFAULT 0,
last_active_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT,
updated_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT,
CONSTRAINT uk_ai_conv_user_star UNIQUE (user_id, star_id)
);
CREATE INDEX idx_ai_conversations_user_star ON ai_conversations(user_id, star_id);
CREATE INDEX idx_ai_conversations_last_active ON ai_conversations(last_active_at DESC);
COMMENT ON TABLE ai_conversations IS 'AI 会话元数据';
-- AI 消息表(仅 MiniMax 路径使用)
CREATE TABLE IF NOT EXISTS ai_messages (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES ai_conversations(id) ON DELETE CASCADE,
role VARCHAR(16) NOT NULL,
content TEXT NOT NULL,
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT
);
CREATE INDEX idx_ai_messages_conversation ON ai_messages(conversation_id, created_at);
COMMENT ON TABLE ai_messages IS 'AI 消息表';
```
### 4.3 Redis 角色
| Key 模式 | Value | TTL | 角色 |
|----------|-------|-----|------|
| `conv_meta:{userId}:{starId}` | JSON 会话元数据 | 1h | ai_conversations 读缓存 |
| `msg_cache:{conversation_id}` | JSON 消息列表 | 10min | GetHistory 响应缓存 |
| `ai_user_memories` 相关 | 不变 | 24h | 长期记忆召回缓存 |
| `conv_lock:{userId}:{starId}` | token | 30s | 分布式锁(★ P0-1 |
> **关键**`conv_meta` 缓存**只是性能优化**。Redis flush 不影响数据完整性。
---
## 五、代码改造
### 5.1 改造总览
| 层级 | 改动 | 工作量 |
|------|------|--------|
| `service/ai_provider.go` | **新增**`AIProvider` 复合接口 + 数据结构 | 中 |
| `service/dify_provider.go` | **新增**`DifyProvider` | 中 |
| `service/dify_workflow_client.go` | **新增**Dify HTTP + SSE | 中 |
| `service/dify_history_client.go` | **新增**Dify /v1/messages | 中 |
| `service/dataset_resolver.go` | **新增**star_id → dataset_id | 小 |
| `service/conversation_store.go` | **新增**ConversationStore 接口 + CachedConversationStore | 中 |
| `service/minimax_provider.go` | **新增**`MiniMaxProvider` | 中 |
| `service/chat_engine.go` | **新增**:核心编排 | 中 |
| `service/provider_factory.go` | **新建**:按 star_id 选 Provider | 中 |
| `service/redis_lock.go` | **新建**RedisLock | 中 |
| `model/ai_chat_models.go` | **新增**`DifyConfig` / `Conversation` / `AIMessage` | 小 |
| `repository/conversation_repository.go` | **新建**ai_conversations GORM 仓储 | 中 |
| `repository/message_repository.go` | **新建**ai_messages GORM 仓储 | 中 |
| `provider/ai_chat_provider.go` | **大幅简化**:只做参数解析 + 鉴权 + 调 ChatEngine | 小 |
| `main.go` | **改造**:装配 ProviderFactory + ChatEngine | 小 |
| `migrations/ai_conversations.sql` | **新增**:上述 DDL | 小 |
| **前端** | **无改动** ✅ | 0 |
**总工作量估计**:核心代码 ~1500-2000 行
### 5.2 AIProvider 复合接口定义
**新文件** `service/ai_provider.go`
```go
package service
import (
"context"
"github.com/topfans/backend/services/aiChatService/model"
)
// AIProvider 复合 AI 后端能力
// 每个实现负责LLM 调用 + RAG 检索 + 会话管理Composite Pattern
type AIProvider interface {
Name() string
// LLM 能力
StreamChat(ctx context.Context, req *ChatRequest) (StreamReader, error)
StreamChatWithBackup(ctx context.Context, req *ChatRequest) (StreamReader, error)
// RAG 能力
Retrieve(ctx context.Context, req *RetrieveRequest) (*RetrieveResult, error)
// Conversation 能力
GetOrCreateConversation(ctx context.Context, key ConversationKey) (*Conversation, error)
GetConversationMessages(ctx context.Context, conv *Conversation, limit int) ([]*ChatMessage, error)
AppendConversationMessages(ctx context.Context, conv *Conversation, msgs []*ChatMessage) error
UpdateConversationExternalID(ctx context.Context, conv *Conversation, externalID string) error
}
// 数据结构
type ChatRequest struct {
UserID int64
StarID int64
SessionID string
Message string
Conversation *Conversation
SystemInputs *SystemInputs
}
type SystemInputs struct {
UserMemory string
UserStyle string
UserNickname string
UserLanguage string
}
type ConversationKey struct {
UserID int64
StarID int64
}
type Conversation struct {
ID int64
UserID int64
StarID int64
ProviderName string
ExternalConvID string
MessageCount int
LastActiveAt int64
}
type ChatMessage struct {
Role string
Content string
}
type RetrieveRequest struct {
StarID int64
Query string
TopK int
DatasetID string
}
type RetrieveResult struct {
Documents []*Document
}
type Document struct {
ID string
Content string
Score float64
Metadata map[string]string
}
// MemoryStore 长期记忆接口
type MemoryItem struct {
ID uint
Content string
Keywords []string
Weight int
}
type MemoryStore interface {
Recall(ctx context.Context, userID int64, query string, limit int) ([]*MemoryItem, error)
Extract(ctx context.Context, userID int64, recentMessages []*ChatMessage) error
GetUserMemories(ctx context.Context, userID int64) ([]*MemoryItem, error)
}
// ConversationStore 抽象
type ConversationStore interface {
GetOrCreate(ctx context.Context, key ConversationKey, providerName string) (*Conversation, error)
GetMessages(ctx context.Context, conv *Conversation, limit int) ([]*ChatMessage, error)
AppendMessages(ctx context.Context, conv *Conversation, msgs []*ChatMessage) error
UpdateExternalID(ctx context.Context, conv *Conversation, externalID string) error
}
```
### 5.3 DifyProvider 实现
**新文件** `service/dify_provider.go`
```go
package service
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/topfans/backend/services/aiChatService/model"
)
// DifyProvider 通过 Dify Workflow 提供 AI 能力
// 复合实现LLMWorkflow 内)+ RAGWorkflow 内)+ ConversationDify 管)
type DifyProvider struct {
workflowClient WorkflowClient
historyClient HistoryClient
convStore ConversationStore
datasetResolver DatasetResolver
userIDSalt string
sensitiveWords []string
}
func NewDifyProvider(
cfg model.DifyConfig,
convStore ConversationStore,
workflowClient WorkflowClient,
historyClient HistoryClient,
datasetResolver DatasetResolver,
sensitiveWords []string,
) *DifyProvider {
return &DifyProvider{
workflowClient: workflowClient,
historyClient: historyClient,
convStore: convStore,
datasetResolver: datasetResolver,
userIDSalt: cfg.UserIDSalt,
sensitiveWords: sensitiveWords,
}
}
func (p *DifyProvider) Name() string { return "dify" }
func (p *DifyProvider) StreamChat(ctx context.Context, req *ChatRequest) (StreamReader, error) {
return p.workflowClient.StreamRun(ctx, WorkflowRunParams{
DatasetID: p.datasetResolver.Resolve(req.StarID),
Query: req.Message,
UserNickname: req.SystemInputs.UserNickname,
UserMemory: req.SystemInputs.UserMemory,
UserStyle: req.SystemInputs.UserStyle,
UserLanguage: req.SystemInputs.UserLanguage,
UserHashed: p.hashUserID(req.UserID),
ExternalConvID: req.Conversation.ExternalConvID,
SensitiveWords: p.sensitiveWords,
})
}
func (p *DifyProvider) GetOrCreateConversation(ctx context.Context, key ConversationKey) (*Conversation, error) {
return p.convStore.GetOrCreate(ctx, key, p.Name())
}
func (p *DifyProvider) GetConversationMessages(ctx context.Context, conv *Conversation, limit int) ([]*ChatMessage, error) {
if conv.ExternalConvID == "" {
return nil, nil
}
return p.historyClient.FetchMessages(ctx, HistoryFetchParams{
ExternalConvID: conv.ExternalConvID,
UserHashed: p.hashUserID(conv.UserID),
Limit: limit,
})
}
func (p *DifyProvider) AppendConversationMessages(ctx context.Context, conv *Conversation, msgs []*ChatMessage) error {
conv.MessageCount += len(msgs)
conv.LastActiveAt = time.Now().UnixMilli()
return p.convStore.AppendMessages(ctx, conv, msgs)
}
func (p *DifyProvider) UpdateConversationExternalID(ctx context.Context, conv *Conversation, externalID string) error {
if externalID == "" || externalID == conv.ExternalConvID {
return nil
}
return p.convStore.UpdateExternalID(ctx, conv, externalID)
}
func (p *DifyProvider) hashUserID(userID int64) string {
h := sha256.Sum256([]byte(fmt.Sprintf("%d:%s", userID, p.userIDSalt)))
return "aichat-" + hex.EncodeToString(h[:8])
}
```
### 5.4 DifyClient 组件
**新文件** `service/dify_workflow_client.go`
```go
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/topfans/backend/pkg/logger"
"go.uber.org/zap"
)
type DifyWorkflowClientConfig struct {
APIBase string
WorkflowURL string
APIKey string
TimeoutSec int
RetryCount int
}
type DifyWorkflowClient struct {
cfg DifyWorkflowClientConfig
httpClient *http.Client
}
func NewDifyWorkflowClient(cfg DifyWorkflowClientConfig) *DifyWorkflowClient {
return &DifyWorkflowClient{
cfg: cfg,
httpClient: &http.Client{
Timeout: time.Duration(cfg.TimeoutSec) * time.Second,
},
}
}
func (c *DifyWorkflowClient) StreamRun(ctx context.Context, params WorkflowRunParams) (StreamReader, error) {
inputs := map[string]interface{}{
"dataset_id": params.DatasetID,
"query": params.Query,
"user_nickname": params.UserNickname,
"user_memory": params.UserMemory,
"user_style": params.UserStyle,
"user_language": params.UserLanguage,
}
body := map[string]interface{}{
"inputs": inputs,
"response_mode": "streaming",
"conversation_id": params.ExternalConvID,
"user": params.UserHashed,
}
var lastErr error
attempts := c.cfg.RetryCount
if attempts < 1 {
attempts = 1
}
for i := 0; i < attempts; i++ {
jsonData, _ := json.Marshal(body)
httpReq, _ := http.NewRequestWithContext(ctx, "POST",
c.cfg.APIBase+c.cfg.WorkflowURL, bytes.NewReader(jsonData))
httpReq.Header.Set("Authorization", "Bearer "+c.cfg.APIKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
lastErr = fmt.Errorf("dify request: %w", err)
time.Sleep(500 * time.Millisecond)
continue
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
lastErr = fmt.Errorf("dify returned HTTP %d", resp.StatusCode)
time.Sleep(500 * time.Millisecond)
continue
}
return NewDifyStreamReader(resp.Body, params.SensitiveWords), nil
}
return nil, lastErr
}
```
**新文件** `service/dify_history_client.go`
```go
package service
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type DifyHistoryClientConfig struct {
APIBase string
APIKey string
TimeoutSec int
}
type DifyHistoryClient struct {
cfg DifyHistoryClientConfig
httpClient *http.Client
}
func NewDifyHistoryClient(cfg DifyHistoryClientConfig) *DifyHistoryClient {
return &DifyHistoryClient{
cfg: cfg,
httpClient: &http.Client{Timeout: time.Duration(cfg.TimeoutSec) * time.Second},
}
}
func (c *DifyHistoryClient) FetchMessages(ctx context.Context, params HistoryFetchParams) ([]*ChatMessage, error) {
url := fmt.Sprintf("%s/messages?conversation_id=%s&user=%s&limit=%d",
c.cfg.APIBase, params.ExternalConvID, params.UserHashed, params.Limit)
httpReq, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
httpReq.Header.Set("Authorization", "Bearer "+c.cfg.APIKey)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("dify fetch history: %w", err)
}
defer resp.Body.Close()
var result struct {
Data []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("dify decode history: %w", err)
}
msgs := make([]*ChatMessage, len(result.Data))
for i, m := range result.Data {
msgs[i] = &ChatMessage{Role: m.Role, Content: m.Content}
}
return msgs, nil
}
```
### 5.5 model 包新增
**修改文件** `model/ai_chat_models.go`
```go
package model
type DifyConfig struct {
Enable bool
APIBase string
WorkflowURL string
APIKey string
TimeoutSec int
UserIDSalt string
RetryCount int
FallbackToMiniMax bool
StarDatasetMapping map[int64]string
}
type Conversation struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
UserID int64 `gorm:"index;not null"`
StarID int64 `gorm:"index;not null"`
ProviderName string `gorm:"type:varchar(32);not null"`
ExternalConvID string `gorm:"type:varchar(128);default:''"`
MessageCount int `gorm:"default:0"`
LastActiveAt int64 `gorm:"autoUpdateTime:milli"`
CreatedAt int64 `gorm:"autoCreateTime:milli"`
UpdatedAt int64 `gorm:"autoUpdateTime:milli"`
}
func (Conversation) TableName() string { return "ai_conversations" }
type AIMessage struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
ConversationID int64 `gorm:"index;not null"`
Role string `gorm:"type:varchar(16);not null"`
Content string `gorm:"type:text;not null"`
CreatedAt int64 `gorm:"autoCreateTime:milli"`
}
func (AIMessage) TableName() string { return "ai_messages" }
```
### 5.6 ChatEngine 编排
**新文件** `service/chat_engine.go`
```go
package service
import (
"context"
"fmt"
"strings"
"time"
"github.com/redis/go-redis/v9"
"github.com/topfans/backend/pkg/logger"
"go.uber.org/zap"
)
type ChatEngine struct {
providers *ProviderFactory
memoryStore MemoryStore
auditService *AuditService
redisLock RedisLock
redisClient *redis.Client
triggerTurns int
}
func NewChatEngine(
providers *ProviderFactory,
memory MemoryStore,
audit *AuditService,
redisLock RedisLock,
redisClient *redis.Client,
triggerTurns int,
) *ChatEngine {
return &ChatEngine{
providers: providers,
memoryStore: memory,
auditService: audit,
redisLock: redisLock,
redisClient: redisClient,
triggerTurns: triggerTurns,
}
}
type RedisLock interface {
Acquire(ctx context.Context, key string, ttl time.Duration) (acquired bool, token string, err error)
Release(ctx context.Context, key string, token string) error
}
type redisLockImpl struct{ client *redis.Client }
func NewRedisLock(client *redis.Client) *redisLockImpl { return &redisLockImpl{client} }
func (l *redisLockImpl) Acquire(ctx context.Context, key string, ttl time.Duration) (bool, string, error) {
token := uuid.New().String()
ok, err := l.client.SetNX(ctx, key, token, ttl).Result()
return ok, token, err
}
func (l *redisLockImpl) Release(ctx context.Context, key, token string) error {
const script = `if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`
return l.client.Eval(ctx, script, []string{key}, token).Err()
}
func (e *ChatEngine) SendMessage(ctx context.Context, userID, starID int64, personaID, message string) (<-chan *StreamChunk, error) {
out := make(chan *StreamChunk, 16)
go func() {
defer close(out)
if !e.auditService.AuditText(message) {
e.sendSafeResponse(out)
return
}
provider := e.providers.Select(starID)
providerName := provider.Name()
memories, _ := e.memoryStore.Recall(ctx, userID, message, 5)
memoryText := formatMemories(memories)
lockKey := fmt.Sprintf("conv_lock:%d:%d", userID, starID)
lockAcquired, lockToken, _ := e.redisLock.Acquire(ctx, lockKey, 30*time.Second)
if !lockAcquired {
time.Sleep(200 * time.Millisecond)
lockAcquired, lockToken, _ = e.redisLock.Acquire(ctx, lockKey, 30*time.Second)
if !lockAcquired {
out <- &StreamChunk{Type: "error", Error: "系统繁忙"}
return
}
}
defer e.redisLock.Release(ctx, lockKey, lockToken)
conv, _ := provider.GetOrCreateConversation(ctx, ConversationKey{UserID: userID, StarID: starID})
if conv == nil {
out <- &StreamChunk{Type: "error", Error: "会话创建失败"}
return
}
req := &ChatRequest{
UserID: userID,
StarID: starID,
Message: message,
Conversation: conv,
SystemInputs: &SystemInputs{UserMemory: memoryText, UserLanguage: "zh-CN"},
}
streamReader, err := provider.StreamChat(ctx, req)
if err != nil {
if handled := e.handleLLMFailure(ctx, provider, req, out); !handled {
out <- &StreamChunk{Type: "error", Error: "服务暂不可用"}
}
return
}
defer streamReader.Close()
var fullResponse string
for {
content, done, err := streamReader.Next()
if err != nil {
out <- &StreamChunk{Type: "error", Error: "服务异常"}
return
}
if content != "" && !e.auditService.AuditResponse(content) {
safeMsg := e.auditService.DefaultSafeResponse()
out <- &StreamChunk{Type: "message", Content: safeMsg, IsEnd: false}
out <- &StreamChunk{Type: "message", IsEnd: true}
provider.AppendConversationMessages(ctx, conv, []*ChatMessage{
{Role: "user", Content: message},
{Role: "assistant", Content: safeMsg},
})
return
}
fullResponse += content
out <- &StreamChunk{Type: "message", Content: content, IsEnd: done}
if done { break }
}
if newConvID := streamReader.GetConversationID(); newConvID != "" && newConvID != conv.ExternalConvID {
provider.UpdateConversationExternalID(ctx, conv, newConvID)
conv.ExternalConvID = newConvID
}
provider.AppendConversationMessages(ctx, conv, []*ChatMessage{
{Role: "user", Content: message},
{Role: "assistant", Content: fullResponse},
})
newTurns := conv.MessageCount / 2
if newTurns >= e.triggerTurns && newTurns%e.triggerTurns == 0 {
extractCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
go func() {
defer cancel()
e.memoryStore.Extract(extractCtx, userID, []*ChatMessage{
{Role: "user", Content: message},
{Role: "assistant", Content: fullResponse},
})
}()
}
}()
return out, nil
}
func (e *ChatEngine) sendSafeResponse(out chan<- *StreamChunk) {
safeMsg := e.auditService.DefaultSafeResponse()
out <- &StreamChunk{Type: "message", Content: safeMsg, IsEnd: false}
out <- &StreamChunk{Type: "message", IsEnd: true}
}
func (e *ChatEngine) handleLLMFailure(ctx context.Context, provider AIProvider, req *ChatRequest, out chan<- *StreamChunk) bool {
if req.Conversation.MessageCount > 0 {
out <- &StreamChunk{Type: "error", Error: "服务暂不可用,请稍后重试"}
return true
}
fallback := e.providers.SelectFallback(provider.Name())
if fallback != nil {
streamReader, _ := fallback.StreamChat(ctx, req)
if streamReader != nil {
defer streamReader.Close()
for {
content, done, _ := streamReader.Next()
if content != "" {
out <- &StreamChunk{Type: "message", Content: content, IsEnd: done}
}
if done { break }
}
return true
}
}
return false
}
```
### 5.7 main.go 装配
```go
// 加载 Dify 配置
difyConfigs, _ := configRepo.GetByCategory(loadCtx, "dify")
difyConfig, _ := loadDifyConfig(difyConfigs)
// 创建 ConversationStore
convStore := service.NewCachedConversationStore(convRepo, msgRepo, redisClient)
// 创建 Dify 客户端组件
workflowClient := service.NewDifyWorkflowClient(service.DifyWorkflowClientConfig{
APIBase: difyConfig.APIBase,
WorkflowURL: difyConfig.WorkflowURL,
APIKey: difyConfig.APIKey,
TimeoutSec: difyConfig.TimeoutSec,
RetryCount: difyConfig.RetryCount,
})
historyClient := service.NewDifyHistoryClient(service.DifyHistoryClientConfig{
APIBase: difyConfig.APIBase,
APIKey: difyConfig.APIKey,
})
datasetResolver := service.NewDatasetResolver(difyConfig.StarDatasetMapping)
// 创建 Provider
difyProvider := service.NewDifyProvider(difyConfig, convStore, workflowClient, historyClient, datasetResolver, nil)
miniMaxProvider := service.NewMiniMaxProvider(convRepo, msgRepo)
// 创建 ProviderFactory
factory := service.NewProviderFactory(difyConfig, difyProvider, miniMaxProvider)
// 创建 Redis 锁
redisLock := service.NewRedisLock(redisClient)
// 创建 ChatEngine
chatEngine := service.NewChatEngine(factory, memoryService, auditService, redisLock, redisClient, 5)
// Provider 大幅简化
aiChatProvider := provider.NewAIChatProvider(chatEngine)
```
### 5.8 Provider 大幅简化
```go
// 极简版本:只做 Dubbo 入口 + 参数解析
func (p *AIChatProvider) SendMessage(ctx context.Context, req *pb.ChatMessageRequest, stream pb.AIChatService_SendMessageServer) error {
userID, _, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil { return err }
chunks, err := p.chatEngine.SendMessage(ctx, userID, 87, "", req.Message)
if err != nil { return err }
for chunk := range chunks {
if chunk.Type == "error" {
stream.Send(&pb.ChatMessageResponse{Content: chunk.Error, IsEnd: true})
} else {
stream.Send(&pb.ChatMessageResponse{Content: chunk.Content, IsEnd: chunk.IsEnd})
}
}
return nil
}
```
> **从此 Provider 不再有 `if useDify` 分支**。
---
## 六、消息流时序
### 6.1 首次对话Dify 路径)
```
[Mobile] → [Gateway] → [Provider] → [ChatEngine]
├─ 1. 前置审核
├─ 2. 选 DifyProvider
├─ 3. 长期记忆召回
├─ 4. 加锁 (RedisLock)
├─ 5. GetOrCreateConv
│ └─ 查 Redis 缓存 → 查 PG → 创建
├─ 6. StreamChat
│ ├─ DatasetResolver.Resolve(87) → dataset_id
│ ├─ WorkflowClient.StreamRun(params)
│ └─ POST /v1/workflows/run
│ └─ {dataset_id, query, user_*}
│ ↓ Dify 返回 SSE
│ ↓
│ audit + stream.Send
│ ↓
│ message{content, is_end}
│ ↓
├─ 7. UpdateExternalID
│ └─ convStore.UpdateExternalID
├─ 8. AppendMessages
│ └─ 更新 message_count
└─ 9. 释放锁 + 记忆提取
```
### 6.2 续接对话(次日重连)
```
1. ChatEngine.SendMessage
2. GetOrCreateConv → ConvStore.GetOrCreate → 查 Redis 缓存命中
3. StreamChat → WorkflowClient.StreamRun → POST {conversation_id: "uuid-xxx"}
4. 流式返回
5. AppendMessages
```
**关键**conversation_id 在 DB 中永久保留,跨天/跨设备都能续上。
### 6.3 Fallback 场景
```
DifyProvider.StreamChat
↓ 失败
DifyProvider.StreamChatWithBackup // 网络层重试一次
↓ 失败
├─ conv.MessageCount == 0 && dify.fallback_to_minimax=true
│ → 切到 MiniMaxProvider
└─ 其他情况
→ 返回错误给前端
```
---
## 七、错误处理与降级
### 7.1 错误分类与处理
| 错误类型 | 处理 |
|---------|------|
| **Dify 401/403** | 直接返回错误(不重试、不 fallback |
| **Dify 5xx** | 重试 N 次dify.retry_count仍失败按 fallback 规则 |
| **Dify timeout** | 同 5xx |
| **Dify 流中断** | 后置 audit 拦截 + 返回安全回复 |
| **PostgreSQL 不可用** | GetOrCreateConv 失败,返回错误 |
| **Redis 不可用** | 降级到直接读 PostgreSQL |
### 7.2 监控指标(建议)
| 指标 | 类型 | 来源 |
|------|------|------|
| `aichat_provider_request_total{provider, status}` | counter | ChatEngine |
| `aichat_provider_fallback_total{from, reason}` | counter | ChatEngine |
| `aichat_dify_stream_duration_ms` | histogram | DifyProvider |
---
## 八、安全与审核
### 8.1 审核
- **前置审核**`auditService.AuditText(message)` 拦截敏感词
- **后置审核**`auditService.AuditResponse(token)` 逐 token 拦截
- **Dify 流中 error 事件**ChatEngine 捕获 err 后发安全回复
### 8.2 知识库内容安全
- Dify Dataset 导入时由 Dify 端做内容审核
- 建议 Workflow 内部加"内容审核节点"Dify 原生能力)
### 8.3 Dify API Key 安全
- 存储:`ai_chat_configs.config_value``is_encrypted=TRUE`
- 实际加解密:依赖 PostgreSQL 访问控制
- 不记录到日志
- 不返回给前端
### 8.4 user 字段隐私
- 哈希化:`aichat-{sha256(userId + salt)[:16]}`
- 盐值:`dify.user_id_salt` 环境变量
- Dify 端运营无法反推 user_id
---
## 九、配置文件清单
### 9.1 环境变量
| 变量 | 用途 | 必填 |
|------|------|------|
| `DIFY_USER_ID_SALT` | 哈希 user_id 的盐 | 是 |
### 9.2 ai_chat_configs 数据
9 个 `dify.*` 配置项(详见 §4.1
### 9.3 不需要的环境变量
- `DIFY_API_KEY`:走 ai_chat_configs
- `DIFY_API_BASE`:走 ai_chat_configs
---
## 十、Stage 2+ 演进路径
> **MVP 优先**MVP 阶段不实施本文档的复杂设计。先读 [MVP 文档](2026-06-29-ai-chat-dify-mvp-design.md),按 MVP 实施。
**Stage 2+ 演进时必踩的 3 个坑**P0 修复笔记)和 **3 个架构评审笔记**,已迁到 MVP §10V2 不重复)。
| Stage | 触发条件 | 关键改动 |
|-------|---------|---------|
| **Stage 2** | 用户量 > 1000 OR 加第 2 个星 | 1. 多星 Dataset 切换<br>2. WebSocket 端 InitSession 欢迎语动态化<br>3. ai_conversations 加 UNIQUE |
| **Stage 3** | Dify 偶发故障 OR SLA 要求 | 1. MiniMax fallback<br>2. Dify retry 循环 |
| **Stage 4** | 用户量 > 10万 OR 接 2+ AI 平台 | 1. AIProvider 抽象<br>2. ProviderFactory 策略模式<br>3. CozeProvider / OpenAIProvider 实现 |
| **Stage 5** | 用户量 > 100万 OR 业务复杂 | 1. ChatEngine Pipeline 化<br>2. AIProfile 配置化<br>3. 长期记忆提取 |
---
## 十一、目录与文件改动
```
backend/services/aiChatService/
├── main.go ← 改
├── model/
│ ├── ai_chat_models.go ← 改(新增 DifyConfig / Conversation / AIMessage
│ └── ai_chat_errors.go
├── provider/
│ ├── ai_chat_provider.go ← 大幅简化
├── service/
│ ├── ai_provider.go ← 新建AIProvider interface
│ ├── dify_provider.go ← 新建DifyProvider
│ ├── dify_workflow_client.go ← 新建
│ ├── dify_history_client.go ← 新建
│ ├── dataset_resolver.go ← 新建
│ ├── conversation_store.go ← 新建
│ ├── minimax_provider.go ← 新建
│ ├── chat_engine.go ← 新建
│ ├── provider_factory.go ← 新建
│ ├── redis_lock.go ← 新建
│ ├── chat_service.go ← 删除
│ ├── llm_service.go ← 保留
│ ├── prompt_builder.go ← 保留
│ ├── memory_service.go ← 改
│ ├── audit_service.go ← 改(加 GetSensitiveWords
│ ├── persona_service.go ← 不改
└── repository/
├── conversation_repository.go ← 新建
├── message_repository.go ← 新建
├── memory_repository.go ← 不改
├── persona_repository.go ← 不改
└── config_repository.go ← 不改
migrations/
└── ai_conversations.sql ← 新建
```
**新建文件**11 个
**修改文件**5 个
**删除文件**1 个
---
## 十二、关键文件参考索引
### 12.1 相关文档
- **MVP 实施级方案**[2026-06-29-ai-chat-dify-mvp-design.md](2026-06-29-ai-chat-dify-mvp-design.md)**先读**
- AI Chat 整体设计:[backend/docs/AI-Chat-Service设计方案.md](backend/docs/AI-Chat-Service设计方案.md)
### 12.2 后端代码
- AIChat Provider[backend/services/aiChatService/provider/ai_chat_provider.go](backend/services/aiChatService/provider/ai_chat_provider.go)
- AIChat LLM Service[backend/services/aiChatService/service/llm_service.go](backend/services/aiChatService/service/llm_service.go)
- AIChat Memory Service[backend/services/aiChatService/service/memory_service.go](backend/services/aiChatService/service/memory_service.go)
- AIChat 装配:[backend/services/aiChatService/main.go](backend/services/aiChatService/main.go)
- 模型定义:[backend/services/aiChatService/model/ai_chat_models.go](backend/services/aiChatService/model/ai_chat_models.go)
- Star 模型:[backend/pkg/models/user.go](backend/pkg/models/user.go)
- Gateway WebSocket[backend/gateway/socket/ai_chat_socket.go](backend/gateway/socket/ai_chat_socket.go)
- Dify 现有(镭射卡):[backend/gateway/service/dify_client.go](backend/gateway/service/dify_client.go)
### 12.3 前端
- 页面:[frontend/pages/ai-dazi/index.vue](frontend/pages/ai-dazi/index.vue)
- WebSocket 封装:[frontend/utils/socket/AiChatSocket.js](frontend/utils/socket/AiChatSocket.js)
- 全局管理:[frontend/utils/socket/GlobalSocketManager.js](frontend/utils/socket/GlobalSocketManager.js)
### 12.4 Dify 官方
- Workflow APIhttps://docs.dify.ai/api-reference/workflow/run-workflow
- Knowledge Retrieval 节点https://docs.dify.ai/guides/workflow/node/knowledge-retrieval
---
## 十三、后续优化(不在 V2 范围)
> 这些是 P0 修复笔记和架构评审笔记的具体化,**详见 MVP §10**
> - ★ P0 修复:并发 raceRedisLock、审计后保存、组合敏感词滑动窗口
> - ★ 架构评审Provider God Class、Provider 依赖存储、Workflow 重复 mapping
其他:
- Coze / FastGPT 接入Stage 4
- 知识库自动同步 Worker独立项目
- ConfigWatcher 热加载
- 用户 persona 完全自定义
- 对话质量评估
- A/B 测试框架
- 多轮摘要压缩
- 跨星记忆
---
## 十四、总结
V2 是"100 明星 + 多 AI 平台"的完整架构设计。
**V2 vs MVP**
- V2 完整(约 1500-2000 行代码、5 周工作量)
- MVP 精简(约 300-500 行代码、1 周可落地)
**实施路径**
1. **MVP 阶段**:按 [MVP 文档](2026-06-29-ai-chat-dify-mvp-design.md) 实施(**不要看 V2**
2. **Stage 2+**MVP 跑通后,参考 V2 的具体代码示例 + MVP §10 的关键坑笔记
3. **Stage 4 抽象**:从 V2 复制 `AIProvider` interface 代码,按需调整
V2 与 MVP 是**互补**关系,不是替代关系。