问题解构

用户的需求是开发一款整合市面上各大 AI 的 App,核心功能是在一个界面同时展示两个不同 AI 的回答,以便用户进行横向对比。为了实现这一目标,我们需要从以下几个维度对问题进行解构:

  1. 数据接入层:如何同时调用两个不同 AI(例如 ChatGPT 和 Claude,或者文心一言和通义千问)的接口?
  2. 数据传输层:如何处理并发请求,确保两个回答能够尽快返回,而不是串行等待?
  3. 前端展示层:如何在 UI 布局上左右(或上下)分屏显示两个回答?如何处理流式输出的打字机效果?
  4. 交互逻辑层:如何实现“停止生成”、“查看历史记录”以及“自动滚动”等辅助功能?

方案推演

1. 技术选型与架构设计

为了满足跨平台(Android/iOS)及高效开发的需求,推荐使用 UniApp 作为前端框架。它基于 Vue.js,能够很好地兼容现有的 AI 对话组件逻辑,且参考资料中已有基于 UniApp 开发类似对话功能的成熟案例 。

后端方面,由于需要直接调用各大 AI 的 API,建议搭建一个简单的中间层(Node.js 或 Python),用于统一管理 API Key、处理并发请求以及转发流式数据。

2. 核心功能实现逻辑

  • 并发请求处理:当用户输入问题后,前端应向后端发送一次请求。后端接收到请求后,利用 Promise.all 或异步并发机制,同时向两个不同的 AI 模型接口发起请求。这样可以确保两个 AI 几乎同时开始思考,减少用户的等待时间。
  • 流式响应(打字机效果):为了提升用户体验,回答不应等待全部生成完再显示,而应采用流式传输。前端可以使用 fetchEventSourceWebSocket 来接收数据流,并逐字渲染到界面上 。Vue 3 的 Composition API 非常适合封装这种状态管理 。
  • 双栏布局与同步:在 UI 设计上,采用双栏布局(左屏 AI A,右屏 AI B)。需要特别注意滚动逻辑,当用户在某一侧滚动时,另一侧是否需要跟随滚动(通常建议独立滚动,以便用户对比不同长度的内容)。

具体实现方案

以下是基于 UniApp + Vue 3 的具体实现思路和代码示例。

1. 前端页面布局(双栏对比)

我们需要构建一个包含两个独立对话容器的页面。每个容器负责展示一个 AI 的流式回答。

<template>
  <view class="container">
    <!-- 左侧 AI A 的回答区域 -->
    <view class="ai-panel">
      <view class="ai-header">AI Model A (e.g., GPT-4)</view>
      <scroll-view scroll-y class="chat-box" :scroll-into-view="scrollIntoViewLeft">
        <view v-for="(item, index) in chatListA" :key="index" class="message-item">
          <text>{{ item.content }}</text>
        </view>
        <!-- 流式输出的临时占位 -->
        <view v-if="isStreamingA" class="message-item streaming">
          <text>{{ tempContentA }}</text><text class="cursor">|</text>
        </view>
      </scroll-view>
    </view>

    <!-- 右侧 AI B 的回答区域 -->
    <view class="ai-panel">
      <view class="ai-header">AI Model B (e.g., Claude 3)</view>
      <scroll-view scroll-y class="chat-box" :scroll-into-view="scrollIntoViewRight">
        <view v-for="(item, index) in chatListB" :key="index" class="message-item">
          <text>{{ item.content }}</text>
        </view>
        <!-- 流式输出的临时占位 -->
        <view v-if="isStreamingB" class="message-item streaming">
          <text>{{ tempContentB }}</text><text class="cursor">|</text>
        </view>
      </scroll-view>
    </view>

    <!-- 底部输入框 -->
    <view class="input-area">
      <input v-model="userInput" placeholder="输入你想对比的问题..." />
      <button @click="handleSend" :disabled="isLoading">发送对比</button>
      <button v-if="isLoading" @click="handleStop">停止</button>
    </view>
  </view>
</template>

2. 逻辑处理与流式数据接收

script setup 中,我们需要处理并发请求和流式数据的接收。这里模拟了流式数据的处理过程,实际项目中应替换为真实的 API 调用。

<script setup>
import { ref } from 'vue';

// 状态定义
const userInput = ref('');
const isLoading = ref(false);
const chatListA = ref([]);
const chatListB = ref([]);
const tempContentA = ref('');
const tempContentB = ref('');
const isStreamingA = ref(false);
const isStreamingB = ref(false);
const scrollIntoViewLeft = ref('');
const scrollIntoViewRight = ref('');

// 模拟流式接收数据的辅助函数
const simulateStream = (targetAI, content, callback) => {
  let index = 0;
  const interval = setInterval(() => {
    if (!isLoading.value) {
      clearInterval(interval); // 响应停止指令
      return;
    }
    if (index < content.length) {
      // 每次追加一个字符
      callback(content.charAt(index));
      index++;
    } else {
      clearInterval(interval);
      // 流式结束,将临时内容存入历史记录
      if (targetAI === 'A') {
        chatListA.value.push({ content: tempContentA.value });
        tempContentA.value = '';
        isStreamingA.value = false;
      } else {
        chatListB.value.push({ content: tempContentB.value });
        tempContentB.value = '';
        isStreamingB.value = false;
      }
    }
  }, 50); // 模拟打字速度
};

// 发送请求
const handleSend = async () => {
  if (!userInput.value.trim()) return;

  isLoading.value = true;
  isStreamingA.value = true;
  isStreamingB.value = true;
  
  // 将用户问题添加到两侧历史(可选)
  chatListA.value.push({ content: 'User: ' + userInput.value, isUser: true });
  chatListB.value.push.push({ content: 'User: ' + userInput.value, isUser: true });

  // 模拟两个 AI 的不同回答
  const answerA = "这是 AI A 的回答。它通常逻辑严密,擅长代码生成。";
  const answerB = "这是 AI B 的回答。它通常文采飞扬,擅长创意写作。";

  // 并发启动两个流式输出
  simulateStream('A', answerA, (char) => {
    tempContentA.value += char;
    scrollToBottom('Left');
  });
  
  simulateStream('B', answerB, (char) => {
    tempContentB.value += char;
    scrollToBottom('Right');
  });

  // 清空输入框
  userInput.value = '';
};

// 停止生成
const handleStop = () => {
  isLoading.value = false;
  // 逻辑中会自动检测 isLoading 并停止 interval
  // 将已生成的部分保存
  if (tempContentA.value) chatListA.value.push({ content: tempContentA.value });
  if (tempContentB.value) chatListB.value.push({ content: tempContentB.value });
  tempContentA.value = '';
  tempContentB.value = '';
  isStreamingA.value = false;
  isStreamingB.value = false;
};

// 滚动到底部逻辑
const scrollToBottom = (side) => {
  // 在实际 UniApp 中,可以通过计算 scroll-view 的高度或使用 uni.createSelectorQuery 来实现自动滚动
  // 这里简化处理
  const timestamp = new Date().getTime();
  if (side === 'Left') scrollIntoViewLeft.value = `msg-${timestamp}`;
  if (side === 'Right') scrollIntoViewRight.value = `msg-${timestamp}`;
};
</script>

<style>
.container {
  display: flex;
  flex-direction: column;
  height: 100vh;
}
.ai-panel {
  flex: 1;
  border: 1px solid #eee;
  padding: 10px;
  display: flex;
  flex-direction: column;
}
.ai-header {
  font-weight: bold;
  margin-bottom: 10px;
  color: #333;
}
.chat-box {
  flex: 1;
  overflow-y: auto;
}
.message-item {
  margin-bottom: 10px;
  padding: 8px;
  background-color: #f9f9f9;
  border-radius: 4px;
}
.cursor {
  display: inline-block;
  width: 2px;
  height: 16px;
  background-color: #333;
  animation: blink 1s infinite;
}
.input-area {
  height: 50px;
  border-top: 1px solid #ccc;
  display: flex;
  align-items: center;
  padding: 0 10px;
}
@keyframes blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}
</style>

方案总结与优化建议

  1. 流式技术选择:在真实对接后端 API 时,推荐使用 fetchEventSource 技术。它比传统的 WebSocket 更适合处理 Server-Sent Events (SSE) 场景,能够更高效地实现 AI 的流式回答 。Vue 3 组件封装时,应将流式数据的解析逻辑与 UI 渲染逻辑分离,以提高代码的可维护性 。
  2. 性能优化:由于同时渲染两个流式文本,DOM 操作会比较频繁。建议使用虚拟滚动列表(如 uni-list 或第三方虚拟 Scroll 组件)来优化长对话的渲染性能,避免页面卡顿。
  3. 历史记录管理:为了方便用户回看,应将对比过的问答对(User Question + AI A Answer + AI B Answer)存储在本地数据库或 Vuex/Pinia 状态管理中。参考资料中提到的“查看历史”功能可以通过本地存储实现 。
  4. 体验细节:增加“一键复制”功能,允许用户快速复制某一侧的回答;增加“重新生成”功能,针对某一侧不满意的结果单独重试。

通过以上方案,您可以构建一个功能完善、体验流畅的 AI 回答对比 App。核心在于利用 Vue 3 的响应式特性处理流式数据,以及合理的 UI 布局来呈现对比效果。


参考来源

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐