Cogito-V1-Preview-Llama-3B开发实战:微信小程序AI对话前端集成

最近在折腾一个有意思的项目,想把一个轻量级的AI模型塞进微信小程序里,做个能随时聊天的AI助手。选来选去,看中了Cogito-V1-Preview-Llama-3B这个模型,它体积小、推理快,特别适合在资源有限的小程序环境里跑。模型本身已经部署好了,接下来最大的挑战就是怎么让小程序的前端页面和这个模型的API顺畅地“对话”。

这活儿听起来简单,不就是前端调个接口嘛。但真做起来,你会发现一堆坑:怎么设计聊天界面才自然?用WebSocket还是HTTP?用户说了一大段话,模型回复得慢怎么办?AI偶尔“胡说八道”的内容怎么过滤?这些都是实打实要解决的问题。今天我就把自己趟过的路、踩过的坑,还有最终跑通的代码,都整理出来分享给你。如果你也想在小程序里加个AI对话功能,这篇应该能帮你省不少时间。

1. 项目准备与环境搭建

在动手写代码之前,我们得先把“舞台”搭好。这里主要分两步:一是确保你的模型API服务已经就绪且可访问;二是在微信开发者工具里把小程序项目的基础框架建起来。

1.1 确认后端API服务

首先,最关键的是你的Cogito-V1-Preview-Llama-3B模型API得是活的、能用的。假设你已经通过类似星图GPU平台这样的服务把模型部署好了,拿到了一个API访问地址,比如 wss://your-model-api.com/chat(WebSocket)或者 https://your-model-api.com/v1/chat/completions(HTTP)。

你需要确认以下几点:

  • 接口类型:模型服务提供的是WebSocket流式接口,还是普通的HTTP POST接口?流式接口能实现打字机效果,体验更好,我们后面会优先采用这种方式。
  • 请求格式:API需要什么样的请求体(Body)?通常需要包含 messages 对话历史数组,每个消息对象有 roleuserassistant)和 content 字段。
  • 响应格式:API返回的数据结构是什么?流式接口通常是一段一段返回文本或特定的数据块。
  • 鉴权方式:需要API Key吗?如果需要,通常是在请求头(Header)里加一个 Authorization: Bearer your-api-key
  • 域名备案:微信小程序要求所有网络请求的域名都必须在小程序管理后台的“开发设置”-“服务器域名”中配置。你需要将你的API服务域名(如 your-model-api.com)添加到 request 合法域名或 socket 合法域名列表中。

把这些信息记下来,等会儿写代码的时候要用。

1.2 初始化微信小程序项目

打开微信开发者工具,新建一个项目。这里我选择不使用云开发,创建一个标准的、纯净的小程序项目模板。

项目创建好后,我们先规划一下主要的页面和文件结构。对于一个简单的AI对话应用,通常只需要一个核心的聊天页面。但为了结构清晰,我们可以这样组织:

miniprogram/
├── pages/
│   └── chat/               # 聊天主页面
│       ├── chat.js         # 页面逻辑
│       ├── chat.json       # 页面配置
│       ├── chat.wxml       # 页面结构
│       └── chat.wxss       # 页面样式
├── utils/
│   └── api.js             # 封装网络请求
│   └── safetyFilter.js    # 安全过滤工具
├── app.js                 # 小程序逻辑
├── app.json               # 小程序配置
└── app.wxss               # 全局样式

app.json 中,我们把 chat 页面设置为首页,并配置一下窗口样式。

// app.json
{
  "pages": [
    "pages/chat/chat"
  ],
  "window": {
    "navigationBarTitleText": "AI对话助手",
    "navigationBarBackgroundColor": "#07c160",
    "navigationBarTextStyle": "white"
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json"
}

这样,基础环境就准备好了。接下来,我们开始构建聊天的核心——页面交互。

2. 构建聊天界面与交互逻辑

用户第一眼看到和直接操作的就是聊天界面。我们的目标是做一个看起来舒服、用起来顺手的对话窗口。

2.1 设计聊天页面布局

chat.wxml 里,我们搭建一个经典的三段式布局:顶部的标题栏(小程序自带)、中间滚动的聊天消息区域、底部固定的输入框和发送按钮。

<!-- pages/chat/chat.wxml -->
<view class="chat-container">
  <!-- 消息列表区域 -->
  <scroll-view
    scroll-y
    class="message-list"
    scroll-into-view="{{scrollToId}}"
    scroll-with-animation
    enhanced
    show-scrollbar="{{false}}"
  >
    <block wx:for="{{messageList}}" wx:key="id">
      <view id="msg-{{item.id}}">
        <!-- 用户消息 -->
        <view wx:if="{{item.role === 'user'}}" class="message-item user-message">
          <view class="avatar user-avatar">我</view>
          <view class="bubble user-bubble">{{item.content}}</view>
        </view>
        <!-- AI消息 -->
        <view wx:elif="{{item.role === 'assistant'}}" class="message-item assistant-message">
          <view class="avatar assistant-avatar">AI</view>
          <view class="bubble assistant-bubble">
            <text>{{item.content}}</text>
            <!-- 加载动画,仅在AI思考时显示 -->
            <view wx:if="{{item.isThinking}}" class="thinking">
              <text class="dot">.</text><text class="dot">.</text><text class="dot">.</text>
            </view>
          </view>
        </view>
      </view>
    </block>
  </scroll-view>

  <!-- 底部输入区域 -->
  <view class="input-area">
    <textarea
      class="input-box"
      value="{{inputText}}"
      placeholder="请输入您的问题..."
      bindinput="onInput"
      bindconfirm="sendMessage"
      auto-height
      maxlength="500"
      cursor-spacing="20"
      adjust-position="{{false}}"
    />
    <button
      class="send-btn"
      bindtap="sendMessage"
      disabled="{{isLoading || !inputText.trim()}}"
    >
      {{isLoading ? '思考中' : '发送'}}
    </button>
  </view>
</view>

对应的样式写在 chat.wxss 里,主要调整气泡、头像、滚动区域和输入框的样式,让它们看起来更接近常见的聊天软件。这里的关键是让消息列表能自动滚动到底部,以及处理好输入框被键盘顶起时的布局。

2.2 实现核心交互与状态管理

页面布局好了,接下来在 chat.js 里让它们动起来。核心状态就几个:消息列表、输入框内容、是否正在加载。

// pages/chat/chat.js
Page({
  data: {
    messageList: [], // 消息数组,包含 {id, role, content, isThinking}
    inputText: '',   // 输入框内容
    isLoading: false, // 是否正在请求AI
    scrollToId: '',   // 控制滚动到哪条消息
    socketTask: null, // WebSocket连接任务
  },

  onInput(e) {
    this.setData({ inputText: e.detail.value });
  },

  // 发送消息的核心函数
  async sendMessage() {
    const text = this.data.inputText.trim();
    if (!text || this.data.isLoading) return;

    // 1. 清空输入框,并禁用发送按钮
    this.setData({ inputText: '', isLoading: true });

    // 2. 将用户消息添加到列表并滚动到底部
    const userMsgId = 'user_' + Date.now();
    const newMessageList = [...this.data.messageList, {
      id: userMsgId,
      role: 'user',
      content: text
    }, {
      id: 'thinking_' + Date.now(),
      role: 'assistant',
      content: '',
      isThinking: true // 标记为“思考中”状态
    }];
    this.setData({ messageList: newMessageList }, () => {
      this.scrollToBottom(userMsgId);
    });

    // 3. 准备请求数据,包含对话历史
    const messagesForAPI = this.prepareMessagesForAPI(text);

    try {
      // 4. 调用工具函数,发起AI请求(这里先留空,下一节实现)
      await this.requestAIResponse(messagesForAPI);
    } catch (error) {
      console.error('请求AI失败:', error);
      // 5. 出错时,更新最后一条AI消息为错误提示
      this.updateLastAIMessage('抱歉,我好像出了点问题,请稍后再试。', false);
    } finally {
      // 6. 无论成功失败,都解除加载状态
      this.setData({ isLoading: false });
    }
  },

  // 准备发送给API的对话历史
  prepareMessagesForAPI(newUserText) {
    // 简单策略:只保留最近N轮对话,避免上下文过长。
    // 这里我们拼接所有历史消息(包括刚新增的用户消息)和系统提示。
    const history = this.data.messageList
      .filter(msg => msg.role !== 'system' && !msg.isThinking) // 过滤掉系统消息和思考中占位符
      .map(msg => ({ role: msg.role, content: msg.content }));
    // 加上最新的用户消息
    history.push({ role: 'user', content: newUserText });

    // 可以在开头加一个系统指令,引导AI行为
    const systemPrompt = { role: 'system', content: '你是一个乐于助人的AI助手。请用中文简洁、友好地回答问题。' };
    return [systemPrompt, ...history.slice(-10)]; // 限制最近10轮对话
  },

  // 滚动到底部
  scrollToBottom(targetId) {
    setTimeout(() => {
      this.setData({ scrollToId: targetId });
    }, 100);
  },

  // 更新最后一条AI消息的内容(用于流式接收)
  updateLastAIMessage(content, isThinking = false) {
    const list = this.data.messageList;
    const lastIndex = list.length - 1;
    if (lastIndex >= 0 && list[lastIndex].role === 'assistant') {
      const key = `messageList[${lastIndex}]`;
      this.setData({
        [`${key}.content`]: content,
        [`${key}.isThinking`]: isThinking
      });
    }
  },

  onUnload() {
    // 页面卸载时,关闭WebSocket连接
    if (this.data.socketTask) {
      this.data.socketTask.close();
    }
  }
})

到这里,一个静态的、能发送用户消息的聊天界面就完成了。但AI还没接进来。接下来就是最关键的环节:连接模型API。

3. 集成模型API:WebSocket流式对话

为了让AI的回复有“一个字一个字蹦出来”的打字机效果,我们优先使用WebSocket连接模型的流式接口。这比HTTP接口一次性返回所有内容体验好得多。

3.1 封装WebSocket连接与通信

我们在 utils/api.js 里创建一个专门处理WebSocket连接的函数。它需要处理连接建立、消息发送、流式数据接收、连接关闭和错误处理。

// utils/api.js
/**
 * 创建并管理一个与AI模型服务的WebSocket连接
 * @param {string} url - WebSocket服务器地址 (e.g., wss://your-api.com/chat)
 * @param {Array} messages - 对话历史消息数组
 * @param {Function} onMessage - 接收到数据片段的回调函数 (content)
 * @param {Function} onError - 错误回调函数 (error)
 * @param {Function} onClose - 连接关闭回调函数
 * @returns {Promise<WechatMiniprogram.SocketTask>} - 返回SocketTask对象
 */
export function connectAIChatStream(url, messages, onMessage, onError, onClose) {
  return new Promise((resolve, reject) => {
    // 构建请求数据,具体格式需参照你的模型API文档
    const requestData = {
      messages: messages,
      stream: true, // 明确要求流式输出
      // 其他可能的参数,如 max_tokens, temperature 等
    };

    const socketTask = wx.connectSocket({
      url: url,
      header: {
        // 如果需要鉴权,在这里添加Authorization头
        // 'Authorization': 'Bearer your-api-key'
        'Content-Type': 'application/json'
      },
      protocols: ['json'], // 如果服务端支持子协议
      success: () => {
        console.log('WebSocket连接创建成功');
      },
      fail: (err) => {
        reject(new Error(`连接失败: ${err.errMsg}`));
      }
    });

    socketTask.onOpen(() => {
      console.log('WebSocket连接已打开');
      // 连接成功后,发送对话请求
      socketTask.send({
        data: JSON.stringify(requestData),
        success: () => {
          console.log('消息发送成功');
          resolve(socketTask); // 连接和发送都成功,返回task
        },
        fail: (sendErr) => {
          socketTask.close();
          reject(new Error(`发送消息失败: ${sendErr.errMsg}`));
        }
      });
    });

    socketTask.onMessage((res) => {
      try {
        // 解析服务端返回的数据,这里假设是JSON格式的流式数据块
        // 实际格式需根据你的API调整,常见的是 { choices: [{ delta: { content: “...” } }] }
        const data = JSON.parse(res.data);
        if (data.choices && data.choices[0].delta && data.choices[0].delta.content) {
          const textChunk = data.choices[0].delta.content;
          onMessage(textChunk); // 将文本片段传给回调函数
        }
        // 处理流结束的标志,例如 data.choices[0].finish_reason 为 'stop'
        if (data.choices && data.choices[0].finish_reason === 'stop') {
          console.log('流式响应结束');
          // 可以在这里触发完成回调,或者由外部根据情况关闭
        }
      } catch (e) {
        console.warn('解析消息失败:', e, res.data);
        // 有些API可能直接返回文本流,而非JSON,这里需要灵活处理
        if (typeof res.data === 'string') {
          onMessage(res.data);
        }
      }
    });

    socketTask.onError((err) => {
      console.error('WebSocket错误:', err);
      onError(err);
    });

    socketTask.onClose((res) => {
      console.log('WebSocket连接关闭:', res);
      onClose(res);
    });
  });
}

3.2 在页面中调用流式接口

现在,我们回到 chat.js,实现之前留空的 requestAIResponse 方法。

// pages/chat/chat.js
import { connectAIChatStream } from '../../utils/api.js';

Page({
  // ... 其他 data 和函数 ...

  async requestAIResponse(messages) {
    // 你的模型WebSocket API地址
    const wsUrl = 'wss://your-model-api.com/chat';
    let fullResponse = '';

    try {
      const socketTask = await connectAIChatStream(
        wsUrl,
        messages,
        // onMessage 回调:接收到一个文本片段
        (chunk) => {
          fullResponse += chunk;
          // 实时更新UI,显示已接收到的所有内容
          this.updateLastAIMessage(fullResponse, true); // 思考中状态
        },
        // onError 回调
        (err) => {
          throw new Error(`通信错误: ${err.errMsg}`);
        },
        // onClose 回调
        (closeRes) => {
          console.log('连接关闭', closeRes);
          // 连接正常关闭后,将AI消息状态改为非思考中
          if (closeRes.code === 1000) {
            this.updateLastAIMessage(fullResponse, false);
            this.setData({ socketTask: null });
          }
        }
      );
      // 保存socketTask引用,以便在页面卸载时关闭
      this.setData({ socketTask });
      // 注意:这里不关闭连接,等待服务端流结束或onClose回调处理

    } catch (error) {
      console.error('AI请求过程出错:', error);
      this.updateLastAIMessage(`请求失败: ${error.message}`, false);
      throw error; // 将错误抛给上层函数处理
    }
  },
})

这样,一个完整的、带有流式打字机效果的AI对话功能就实现了。用户发送消息后,AI的回答会逐字显示,体验非常流畅。

4. 内容安全与体验优化

功能跑通了,但我们还得考虑两个现实问题:一是AI生成的内容可能不合规,二是网络不好时用户体验会变差。我们必须处理这些问题。

4.1 实现响应内容的安全过滤

我们不能直接把AI生成的所有内容都展示给用户。需要一个简单的过滤层。我们在 utils/safetyFilter.js 里实现一个基础过滤器。

// utils/safetyFilter.js
/**
 * 简单的文本安全过滤函数
 * @param {string} text - 待过滤的文本
 * @returns {string} - 过滤后的文本,或替换后的提示
 */
export function filterText(text) {
  if (!text || typeof text !== 'string') return text;

  let filteredText = text;
  // 1. 定义一组需要过滤的敏感词或正则模式(示例,请根据实际情况扩充)
  const sensitivePatterns = [
    // 这里添加需要过滤的词汇或正则表达式,例如:
    // /(badword1|badword2)/gi,
    // /不当政治言论示例/gi,
  ];

  sensitivePatterns.forEach(pattern => {
    filteredText = filteredText.replace(pattern, '***');
  });

  // 2. 检查是否包含过多无意义字符或疑似乱码(简单示例)
  const gibberishRatio = (filteredText.match(/[^a-zA-Z0-9\u4e00-\u9fa5\s.,!?;:]/g) || []).length / filteredText.length;
  if (gibberishRatio > 0.5) { // 如果超过50%是非正常字符
    console.warn('检测到可能无意义的内容:', text.substring(0, 50));
    return '[内容已过滤]';
  }

  // 3. 可以添加更多规则,如长度限制、重复度检查等

  return filteredText;
}

/**
 * 在渲染前过滤消息内容
 * @param {Array} messageList - 原始消息列表
 * @returns {Array} - 过滤后的消息列表
 */
export function filterMessageList(messageList) {
  return messageList.map(msg => ({
    ...msg,
    content: msg.role === 'assistant' ? filterText(msg.content) : msg.content // 通常只过滤AI回复
  }));
}

然后,在 chat.js 中更新消息时应用过滤。可以在 updateLastAIMessage 函数中调用 filterText

// pages/chat/chat.js
import { filterText } from '../../utils/safetyFilter.js';

// ... 在 updateLastAIMessage 函数内 ...
updateLastAIMessage(content, isThinking = false) {
  const list = this.data.messageList;
  const lastIndex = list.length - 1;
  if (lastIndex >= 0 && list[lastIndex].role === 'assistant') {
    const filteredContent = isThinking ? content : filterText(content); // 思考中的内容不过滤
    const key = `messageList[${lastIndex}]`;
    this.setData({
      [`${key}.content`]: filteredContent,
      [`${key}.isThinking`]: isThinking
    });
  }
},

4.2 优化网络与用户体验

1. 超时与重试机制: 网络请求可能失败。我们需要为WebSocket连接或HTTP请求添加超时控制,并在适当时机进行重试。

// utils/api.js (补充)
export function connectAIChatStream(url, messages, onMessage, onError, onClose, timeout = 30000) {
  return new Promise((resolve, reject) => {
    let isResolvedOrRejected = false;
    const timeoutId = setTimeout(() => {
      if (!isResolvedOrRejected) {
        isResolvedOrRejected = true;
        reject(new Error(`连接超时 (${timeout}ms)`));
        if (socketTask) socketTask.close();
      }
    }, timeout);

    // ... 原有的连接逻辑 ...

    // 在 onOpen 的成功和失败回调中,以及 onError 中,清除定时器并标记状态
    const finalize = (error = null) => {
      clearTimeout(timeoutId);
      if (!isResolvedOrRejected) {
        isResolvedOrRejected = true;
        if (error) reject(error);
      }
    };

    socketTask.onOpen(() => {
      // ... 发送消息 ...
      // 在发送成功的回调里 finalize(),在发送失败里 finalize(error)
    });

    socketTask.onError((err) => {
      finalize(new Error(`WebSocket错误: ${err.errMsg}`));
      onError(err);
    });

    // 注意:onClose 也可能需要处理
  });
}

2. 加载状态与用户反馈: 我们已经用 isLoading 控制了按钮状态。还可以在消息列表顶部或底部添加一个“AI正在思考”的加载动画,提升反馈感。

3. 历史记录与上下文管理: 目前的 prepareMessagesForAPI 函数只是简单截取最近10轮对话。对于更复杂的场景,你可能需要:

  • Token计数:模型有上下文长度限制,需要估算消息的token数量并截断。
  • 本地缓存:使用 wx.setStorage 将对话历史缓存到本地,下次打开小程序可以恢复。
  • 会话管理:允许用户创建新的对话会话。

获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐