# 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 是**互补**关系,不是替代关系。