# 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 context,24h 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 │ │
│ │ - 调 GetWelcomeMessage(InitSession) │ │
│ │ - 调 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
- 分段 overlap:50-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 }}`
- TopK:3
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 能力
// 复合实现:LLM(Workflow 内)+ RAG(Workflow 内)+ Conversation(Dify 管)
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 §10(V2 不重复)。
| Stage | 触发条件 | 关键改动 |
|-------|---------|---------|
| **Stage 2** | 用户量 > 1000 OR 加第 2 个星 | 1. 多星 Dataset 切换
2. WebSocket 端 InitSession 欢迎语动态化
3. ai_conversations 加 UNIQUE |
| **Stage 3** | Dify 偶发故障 OR SLA 要求 | 1. MiniMax fallback
2. Dify retry 循环 |
| **Stage 4** | 用户量 > 10万 OR 接 2+ AI 平台 | 1. AIProvider 抽象
2. ProviderFactory 策略模式
3. CozeProvider / OpenAIProvider 实现 |
| **Stage 5** | 用户量 > 100万 OR 业务复杂 | 1. ChatEngine Pipeline 化
2. AIProfile 配置化
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 API:https://docs.dify.ai/api-reference/workflow/run-workflow
- Knowledge Retrieval 节点:https://docs.dify.ai/guides/workflow/node/knowledge-retrieval
---
## 十三、后续优化(不在 V2 范围)
> 这些是 P0 修复笔记和架构评审笔记的具体化,**详见 MVP §10**:
> - ★ P0 修复:并发 race(RedisLock)、审计后保存、组合敏感词(滑动窗口)
> - ★ 架构评审: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 是**互补**关系,不是替代关系。