Files
AIclinicalresearch/对话系统实现方案对比.md
2025-10-10 15:14:54 +08:00

24 KiB
Raw Blame History

对话系统实现方案详细对比

🎯 核心问题

如果不使用LobeChat整体框架基于大模型的对话聊天该怎么实现

让我为您详细对比3种方案


方案A使用Dify的对话功能

Dify的对话能力

Dify确实提供了完整的对话功能包括

  • LLM对话管理
  • 流式输出
  • 对话历史管理
  • RAG知识库集成
  • 提供WebApp界面

架构图

用户浏览器
    ↓
Dify自带的WebApp界面
    ↓
Dify后端对话+RAG
    ↓
LLM API (DeepSeek/Qwen)

实现方式

方式1直接使用Dify的WebApp

# 部署Dify后直接访问其Web界面
docker-compose up -d
# 访问 http://localhost:3000
# 创建12个应用对应12个智能体

方式2通过Dify API集成

// 调用Dify的对话API
const response = await fetch('https://api.dify.ai/v1/chat-messages', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${DIFY_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    inputs: {},
    query: "请帮我构建PICOS",
    response_mode: "streaming",  // 流式输出
    conversation_id: "conv-xxx",  // 对话ID
    user: "user-123"
  })
});

// 接收流式响应
const reader = response.body.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  // 处理流式数据
}

优点

  1. 开发成本极低

    • 不需要自己实现对话逻辑
    • 不需要自己实现流式输出
    • 不需要自己管理对话历史
    • RAG功能开箱即用
  2. 功能完整

    • 对话管理
    • Prompt管理
    • 流式输出
    • 知识库集成
    • 文件上传
  3. 快速上线

    • 1-2周即可搭建MVP
    • 无需深入理解LLM API细节

缺点

  1. 12个智能体管理复杂

    问题需要在Dify中创建12个独立的应用App
    
    Dify界面
    ├── 应用1: 选题评价
    ├── 应用2: PICOS构建
    ├── 应用3: 论文润色
    ├── ... (共12个)
    └── 应用12: 期刊稿约评审
    
    影响:
    - 用户需要在12个应用间切换体验差
    - 无法统一管理对话历史
    - 无法共享项目上下文
    
  2. 项目管理功能缺失

    我们的需求:
    - 用户创建"项目/课题"
    - 在项目内使用多个智能体
    - 项目背景信息自动注入
    - 对话可"固定"到项目背景
    
    Dify的能力
    - ❌ 没有"项目"概念
    - ❌ 无法跨应用共享上下文
    - ❌ 每个对话是独立的
    
  3. 前端定制困难

    我们的原型图:
    - 左侧:项目列表 + 智能体列表
    - 中间:智能体卡片选择
    - 右侧:对话界面
    
    Dify的界面
    - 固定的单应用界面
    - 定制需要修改Dify源码复杂
    
  4. 业务逻辑受限

    我们的特殊需求:
    - 全局快速问答 vs 项目内深度研究
    - 动态背景信息管理(固定功能)
    - 对话溯源(引用知识库)
    - 多模型即时切换
    
    Dify的支持
    - 🟡 部分支持,但需要复杂配置
    - 🟡 或需要修改源码
    

适用场景

适合: 如果您的需求是:

  • 简单的知识库问答
  • 单一的对话场景
  • 快速验证想法MVP

不适合: 我们的项目

  • 12个智能体需要统一管理
  • 项目管理是核心功能
  • 需要高度定制的UI

方案B参考LobeChat + 自研后端(推荐

核心思路

前端参考LobeChat的UI组件不用整体框架
后端自己实现对话逻辑调用LLM API
RAG使用Dify专注知识库检索

架构图

┌─────────────────────────────────────────┐
│    前端React + Vite                  │
│  ┌──────────────────────────────────┐   │
│  │ 聊天UI参考LobeChat实现        │   │
│  │ - ChatMessage 组件               │   │
│  │ - ChatInput 组件                 │   │
│  │ - StreamRenderer 组件            │   │
│  │ - MarkdownContent 组件           │   │
│  └──────────────────────────────────┘   │
│  ┌──────────────────────────────────┐   │
│  │ 业务界面(自己实现)              │   │
│  │ - 项目管理                       │   │
│  │ - 智能体选择                     │   │
│  │ - 历史记录                       │   │
│  └──────────────────────────────────┘   │
└─────────────────────────────────────────┘
                    ↓ REST API
┌─────────────────────────────────────────┐
│    后端Node.js + TypeScript         │
│  ┌──────────────────────────────────┐   │
│  │ 对话服务(自己实现)              │   │
│  │ - 接收用户消息                   │   │
│  │ - 组装上下文(项目背景+历史)     │   │
│  │ - 调用LLM API                    │   │
│  │ - 流式返回结果                   │   │
│  └──────────────────────────────────┘   │
│  ┌──────────────────────────────────┐   │
│  │ 业务服务(自己实现)              │   │
│  │ - 项目管理                       │   │
│  │ - 智能体路由                     │   │
│  │ - 权限控制                       │   │
│  └──────────────────────────────────┘   │
└─────────────────────────────────────────┘
          ↓                    ↓
┌──────────────────┐  ┌─────────────────┐
│   Dify (RAG)     │  │  LLM API        │
│  - 知识库检索     │  │  - DeepSeek-V3  │
│  仅RAG        │  │  - Qwen3        │
└──────────────────┘  └─────────────────┘

详细实现

1. 前端聊天UI参考LobeChat

提取LobeChat的核心组件

// src/components/chat/ChatMessage.tsx
// 从LobeChat移植做少量修改

import React from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';

interface Message {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  timestamp: number;
}

export function ChatMessage({ message, onCopy, onPin }: Props) {
  const isUser = message.role === 'user';
  
  return (
    <div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : ''}`}>
      {/* 头像 */}
      <Avatar type={isUser ? 'user' : 'ai'} />
      
      {/* 消息内容 */}
      <div className={`flex-1 ${isUser ? 'items-end' : 'items-start'}`}>
        <div className={`
          max-w-3xl p-4 rounded-lg
          ${isUser ? 'bg-blue-600 text-white' : 'bg-white border'}
        `}>
          {isUser ? (
            <p className="whitespace-pre-wrap">{message.content}</p>
          ) : (
            <ReactMarkdown
              components={{
                code({ node, inline, className, children, ...props }) {
                  const match = /language-(\w+)/.exec(className || '');
                  return !inline && match ? (
                    <SyntaxHighlighter
                      language={match[1]}
                      PreTag="div"
                      {...props}
                    >
                      {String(children).replace(/\n$/, '')}
                    </SyntaxHighlighter>
                  ) : (
                    <code className={className} {...props}>
                      {children}
                    </code>
                  );
                }
              }}
            >
              {message.content}
            </ReactMarkdown>
          )}
        </div>
        
        {/* 操作按钮 */}
        {!isUser && (
          <div className="flex gap-2 mt-2">
            <button onClick={onCopy}>复制</button>
            <button onClick={onPin}>固定到项目</button>
            <button>重新生成</button>
          </div>
        )}
      </div>
    </div>
  );
}
// src/components/chat/ChatInput.tsx
// 从LobeChat移植

export function ChatInput({ onSend, onUpload, onKbSelect }: Props) {
  const [message, setMessage] = useState('');
  
  const handleSend = () => {
    if (!message.trim()) return;
    onSend(message);
    setMessage('');
  };
  
  return (
    <div className="border rounded-lg p-2 bg-white">
      <textarea
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            handleSend();
          }
        }}
        placeholder="输入您的问题..."
        className="w-full resize-none"
        rows={3}
      />
      
      <div className="flex justify-between items-center mt-2">
        <div className="flex gap-2">
          <button onClick={onUpload}>📎 上传文件</button>
          <button onClick={onKbSelect}>@ 引用知识库</button>
        </div>
        
        <button 
          onClick={handleSend}
          className="px-4 py-2 bg-blue-600 text-white rounded-lg"
        >
          发送
        </button>
      </div>
    </div>
  );
}

2. 后端对话逻辑(自己实现)

核心代码:

// backend/src/services/chat.service.ts

import { agentConfig } from '../config/agent-loader';
import { LLMFactory } from '../adapters/llm-factory';
import { difyClient } from '../clients/dify';

export class ChatService {
  
  /**
   * 处理对话请求(流式输出)
   */
  async handleChatStream(req: Request, res: Response) {
    const { 
      message, 
      projectId, 
      agentId, 
      conversationId,
      modelName = 'deepseek-v3'
    } = req.body;
    
    // 1. 获取智能体配置
    const agent = agentConfig.getAgent(agentId);
    if (!agent) {
      return res.status(404).json({ error: 'Agent not found' });
    }
    
    // 2. 组装上下文
    const context = await this.buildContext({
      projectId,
      agentId,
      conversationId,
      message,
      agent
    });
    
    // 3. 设置流式响应头
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    
    try {
      // 4. 调用LLM流式
      const llm = LLMFactory.create(modelName);
      const stream = await llm.chatStream({
        messages: context,
        temperature: agent.models[modelName]?.temperature || 0.7,
        max_tokens: agent.models[modelName]?.max_tokens || 2000
      });
      
      // 5. 转发流式数据
      let fullResponse = '';
      for await (const chunk of stream) {
        fullResponse += chunk;
        res.write(`data: ${JSON.stringify({ 
          type: 'token', 
          content: chunk 
        })}\n\n`);
      }
      
      // 6. 保存对话记录
      await this.saveMessage({
        conversationId,
        projectId,
        agentId,
        userMessage: message,
        aiMessage: fullResponse
      });
      
      // 7. 结束流
      res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
      res.end();
      
    } catch (error) {
      res.write(`data: ${JSON.stringify({ 
        type: 'error', 
        message: error.message 
      })}\n\n`);
      res.end();
    }
  }
  
  /**
   * 组装对话上下文
   */
  private async buildContext(params: BuildContextParams) {
    const { projectId, agentId, conversationId, message, agent } = params;
    
    const messages = [];
    
    // 1. 项目背景(如果有)
    if (projectId) {
      const project = await db.projects.findOne({ id: projectId });
      messages.push({
        role: 'system',
        content: `# 项目背景\n${project.description}`
      });
    }
    
    // 2. 智能体系统提示词
    const systemPrompt = agentConfig.getPrompt(agent.system_prompt_file);
    messages.push({
      role: 'system',
      content: systemPrompt
    });
    
    // 3. 历史对话最近10轮
    const history = await db.messages.find({
      conversation_id: conversationId
    }).limit(20).sort({ created_at: -1 });
    
    for (const msg of history.reverse()) {
      messages.push(
        { role: 'user', content: msg.user_message },
        { role: 'assistant', content: msg.ai_message }
      );
    }
    
    // 4. 知识库检索(如果需要)
    if (agent.rag_enabled && message.includes('@')) {
      const kbName = this.extractKbReference(message);
      if (kbName) {
        const ragResults = await difyClient.queryKnowledgeBase({
          datasetId: this.getKbId(kbName),
          query: message,
          topK: 5
        });
        
        messages.push({
          role: 'system',
          content: `# 相关知识库内容\n${
            ragResults.map(r => `[${r.metadata.filename}]\n${r.content}`).join('\n\n')
          }`
        });
      }
    }
    
    // 5. 当前用户问题
    messages.push({
      role: 'user',
      content: message
    });
    
    return messages;
  }
  
  /**
   * 保存对话记录
   */
  private async saveMessage(params: SaveMessageParams) {
    await db.messages.create({
      conversation_id: params.conversationId,
      project_id: params.projectId,
      agent_id: params.agentId,
      user_message: params.userMessage,
      ai_message: params.aiMessage,
      created_at: new Date()
    });
  }
}

3. LLM Adapter统一接口

// backend/src/adapters/llm-factory.ts

// DeepSeek Adapter
export class DeepSeekAdapter {
  constructor(private apiKey: string) {}
  
  async chatStream(params: ChatParams): AsyncGenerator<string> {
    const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'deepseek-chat',
        messages: params.messages,
        temperature: params.temperature,
        max_tokens: params.max_tokens,
        stream: true
      })
    });
    
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      const chunk = decoder.decode(value);
      const lines = chunk.split('\n').filter(line => line.trim());
      
      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = line.slice(6);
          if (data === '[DONE]') continue;
          
          try {
            const json = JSON.parse(data);
            const content = json.choices[0]?.delta?.content;
            if (content) {
              yield content;
            }
          } catch (e) {
            // 忽略解析错误
          }
        }
      }
    }
  }
}

// Qwen Adapter
export class QwenAdapter {
  constructor(private apiKey: string) {}
  
  async chatStream(params: ChatParams): AsyncGenerator<string> {
    // 类似实现调用阿里云DashScope API
    // ...
  }
}

// 工厂
export class LLMFactory {
  static create(modelName: string) {
    switch (modelName) {
      case 'deepseek-v3':
        return new DeepSeekAdapter(process.env.DEEPSEEK_API_KEY);
      case 'qwen3-72b':
        return new QwenAdapter(process.env.DASHSCOPE_API_KEY);
      default:
        throw new Error(`Unsupported model: ${modelName}`);
    }
  }
}

4. 前端调用(流式接收)

// frontend/src/services/chat.ts

export async function sendMessage(params: SendMessageParams) {
  const { message, projectId, agentId, conversationId, onToken, onDone } = params;
  
  const response = await fetch('/api/chat/stream', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message,
      project_id: projectId,
      agent_id: agentId,
      conversation_id: conversationId,
      model_name: 'deepseek-v3'
    })
  });
  
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const chunk = decoder.decode(value);
    const lines = chunk.split('\n');
    
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = JSON.parse(line.slice(6));
        
        if (data.type === 'token') {
          onToken(data.content);  // 实时显示
        } else if (data.type === 'done') {
          onDone();
        } else if (data.type === 'error') {
          throw new Error(data.message);
        }
      }
    }
  }
}

// React组件中使用
function ChatView() {
  const [messages, setMessages] = useState([]);
  const [currentResponse, setCurrentResponse] = useState('');
  
  const handleSend = async (message: string) => {
    // 添加用户消息
    setMessages(prev => [...prev, { role: 'user', content: message }]);
    
    // 添加空的AI消息准备接收流式内容
    const aiMessageId = Date.now();
    setMessages(prev => [...prev, { 
      id: aiMessageId,
      role: 'assistant', 
      content: '' 
    }]);
    
    // 发送请求
    await sendMessage({
      message,
      projectId: currentProject.id,
      agentId: currentAgent.id,
      conversationId: currentConversation.id,
      
      // 接收每个token
      onToken: (token) => {
        setMessages(prev => prev.map(msg => 
          msg.id === aiMessageId 
            ? { ...msg, content: msg.content + token }
            : msg
        ));
      },
      
      // 完成
      onDone: () => {
        console.log('Stream completed');
      }
    });
  };
  
  return (
    <div className="chat-container">
      <div className="messages">
        {messages.map(msg => (
          <ChatMessage key={msg.id} message={msg} />
        ))}
      </div>
      <ChatInput onSend={handleSend} />
    </div>
  );
}

优点

  1. 完全可控

    • 业务逻辑完全自主
    • UI 100%匹配原型图
    • 可以实现任何定制需求
  2. 功能完整

    • 项目管理(核心功能)
    • 12个智能体统一管理
    • 上下文动态管理
    • 对话固定到项目背景
  3. 聊天体验好

    • 参考LobeChat的成熟实现
    • 流式输出流畅
    • Markdown渲染完美
  4. 成本可控

    • 只需实现业务逻辑
    • 聊天UI复用LobeChat节省9.5天)
    • RAG交给Dify节省40天

缺点

  1. 需要自己实现对话逻辑

    • 但其实不复杂(上面的代码已展示)
    • 核心就是:接收消息 → 组装上下文 → 调用LLM → 返回流式结果
  2. 需要处理流式输出

    • 但有成熟的实现可参考LobeChat、OpenAI SDK
    • Server-Sent Events (SSE) 是标准协议

开发工作量

模块 工作量 说明
前端聊天UI 1天 复用LobeChat组件
流式输出前端 0.5天 参考LobeChat实现
Markdown渲染 0.5天 使用react-markdown
后端对话服务 2天 上下文组装 + LLM调用
流式输出后端 1天 SSE实现
LLM Adapter 1天 DeepSeek + Qwen接入
总计 6天 vs 纯手写13天

方案C完全自研不推荐

架构

前端完全自己实现聊天UI
后端:完全自己实现对话逻辑
RAG完全自己实现向量检索

缺点

  • 开发周期长4-6个月
  • 技术难度高
  • RAG系统复杂度被严重低估
  • 不推荐!

📊 三种方案对比总结

维度 方案A: 用Dify对话 方案B: 自研对话+Dify RAG 方案C: 完全自研
开发周期 1-2周 2.5个月 4-6个月
开发成本 ¥10k-20k ¥49k ¥80k-180k
12个智能体管理 🔴 复杂 🟢 简单 🟢 简单
项目管理 🔴 不支持 🟢 完全支持 🟢 完全支持
UI定制 🔴 困难 🟢 完全可控 🟢 完全可控
业务逻辑灵活性 🟡 受限 🟢 完全灵活 🟢 完全灵活
聊天体验 🟢 🟢 🟡 需大量调试
RAG能力 🟢 🟢 🟡 需自己实现
技术难度
维护成本 🟢 🟡 🔴
功能匹配度 🔴 40% 🟢 95% 🟢 100%

🎯 最终推荐方案B

核心理由

Dify能帮我们什么

  • RAG知识库功能文档解析、向量检索、答案溯源
  • 对话功能不适合我们12个智能体管理困难

我们自己做什么?

  • 对话逻辑(接收消息 → 组装上下文 → 调用LLM
  • 项目管理(核心功能)
  • 智能体编排12个智能体统一管理
  • 前端UI100%匹配原型图)

LobeChat帮我们什么

  • 聊天UI组件开源可复用
  • 流式输出参考实现
  • Markdown渲染方案

技术可行性

对话功能其实不复杂! 核心只有4步

// 1. 接收消息
const { message, projectId, agentId } = req.body;

// 2. 组装上下文
const context = [
  { role: 'system', content: '项目背景...' },
  { role: 'system', content: '智能体提示词...' },
  { role: 'user', content: '历史对话...' },
  { role: 'assistant', content: '历史回复...' },
  { role: 'user', content: message }
];

// 3. 调用LLM
const stream = await deepseek.chatStream({ messages: context });

// 4. 返回流式结果
for await (const chunk of stream) {
  res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
}

就这么简单! 不需要Dify的对话功能。

最佳实践

Dify擅长的RAG知识库检索
  → 让Dify做它擅长的

我们需要的:复杂业务逻辑(项目管理、智能体编排)
  → 自己实现,完全可控

聊天UI成熟方案LobeChat
  → 参考实现,节省时间

📝 总结

问题如果不使用LobeChat对话聊天该怎么实现

答案:

  1. 前端聊天UI参考LobeChat开源组件复用不是不用
  2. 后端对话逻辑自己实现调用DeepSeek/Qwen API
  3. RAG知识库使用Dify专注检索

Dify的角色

  • 用于RAG知识库功能
  • 不用其对话功能(不适合我们的复杂业务)

这个方案的优势:

  • 开发周期2.5个月(可接受)
  • 开发成本¥49,300可控
  • 功能匹配度95%(满足需求)
  • 聊天体验优秀参考LobeChat
  • 灵活性:高(业务逻辑完全自主)

关键点:对话功能不复杂,核心是业务逻辑!


文档版本v1.0
更新时间2025-10-10