通义千问1.5-1.8B-Chat-GPTQ-Int4微信小程序开发实战:集成AI对话功能

你是不是也想过,给自己的微信小程序加上一个能说会道的AI助手?比如,做一个能陪你聊天的解忧杂货铺,或者一个能回答产品问题的智能客服。以前这听起来很复杂,需要自己搞服务器、部署大模型,光是技术门槛就能劝退不少人。

但现在,事情变得简单多了。我们可以利用现成的AI模型服务,通过一个标准的API接口,就能在小程序里轻松调用强大的对话能力。今天,我就带你走一遍完整的流程,从后端模型服务的快速搭建,到前端小程序的代码集成,手把手教你打造一个属于你自己的、能流畅对话的AI小程序。

整个过程就像搭积木:后端我们用一个高性能的量化模型提供服务,前端用小程序原生的网络请求去调用它。我会提供每一步的详细代码和说明,确保你跟着做就能跑起来。

1. 项目整体思路与准备工作

在开始写代码之前,我们先理清整个项目的脉络。我们的目标是在微信小程序里实现一个类似ChatGPT的对话界面,用户输入问题,AI模型返回回答。

为了实现这个目标,我们需要两个部分协同工作:

  1. 后端(AI大脑):需要一个持续运行、能够处理对话请求的AI模型服务。我们将它部署在云端,并对外提供一个HTTP API接口。
  2. 前端(交互界面):就是我们的微信小程序。它负责收集用户输入,将问题发送给后端的API,接收AI的回复,并漂亮地展示在聊天界面上。

听起来是不是清晰多了?接下来,我们分别看看这两部分具体怎么做。

1.1 后端方案选择:为什么是通义千问与GPTQ-Int4?

为小程序提供AI服务,对后端有几点核心要求:响应要快、成本要低、要容易部署。基于这几点,我选择了“通义千问1.5-1.8B-Chat”模型的“GPTQ-Int4”量化版本。

  • 通义千问1.5-1.8B-Chat:这是一个参数规模为18亿的对话模型。对于小程序场景来说,它“个头”适中,能力足够应对常见的聊天、问答、文案生成等任务,同时又不至于对计算资源要求过高。
  • GPTQ-Int4量化:这是关键的一步。“量化”可以简单理解为给模型“瘦身”。原始的模型参数通常是32位浮点数(FP32),比较“胖”,运行起来慢且占内存。GPTQ-Int4技术能把模型压缩到4位整数(INT4)存储和计算。带来的好处非常直接:模型体积大幅减小,运行速度显著提升,并且所需的内存也少了很多。这意味着我们能用更低的成本、更普通的服务器来部署它,并且用户的每次请求都能得到更快的回复。

我们将在一个云服务平台(例如CSDN星图镜像广场提供的环境)上,找到这个模型的预置镜像,一键部署。部署好后,它会自动启动一个Web服务,我们只需要知道它的API地址(一个URL)和调用方式即可。

1.2 前端核心:微信小程序的能力与限制

微信小程序前端,我们的任务很明确:

  1. 发送请求:使用 wx.requestwx.requestTask 调用后端API。
  2. 管理对话:在本地(小程序的 Storage 或内存中)维护一个对话历史列表,记录用户和AI的每一轮问答。
  3. 展示交互:实现一个聊天界面,包括输入框、发送按钮、聊天消息列表。这里有个体验上的小挑战:如何优雅地展示AI逐字输出的效果(流式响应)?

同时,我们也要留意小程序的限制

  • 域名要求:请求的API地址(URL)必须在小程序管理后台的“开发设置”->“服务器域名”中配置,加入到 request 合法域名列表中。这是上线前必须做的一步,开发阶段可以在开发者工具中临时开启“不校验合法域名”选项来调试。
  • 网络状态:需要处理好网络异常情况,给用户友好的提示。

思路理清了,工具选好了,接下来我们就从后端开始,一步步搭建起来。

2. 后端部署:快速搭建AI模型API服务

这一部分,我们在云平台上完成。以找到预置的“通义千问1.5-1.8B-Chat-GPTQ-Int4”镜像为例,部署过程可以非常快捷。

2.1 部署模型服务

  1. 选择镜像:在云服务平台的镜像市场或应用中心,搜索“通义千问”、“Qwen1.5-1.8B-Chat”或“GPTQ”等关键词,找到对应的预置镜像。这些镜像通常已经配置好了所有依赖环境和启动脚本。
  2. 启动实例:选择这个镜像,创建一个新的计算实例。根据模型大小,选择合适配置的服务器(对于这个1.8B的Int4模型,中等配置的CPU服务器或带少量GPU的服务器通常就足够了)。
  3. 获取访问信息:实例创建并启动后,平台通常会提供该服务的访问方式。最常见的是提供一个 HTTP端点(Endpoint) 和一个端口号。例如,你可能会得到一个类似 http://your-instance-ip:8000 的地址。记下这个地址,这是我们小程序的“对话接口”。
  4. 验证服务:在浏览器中访问 http://your-instance-ip:8000/docs (很多AI服务框架如FastAPI会自带API文档页),或者使用 curl 命令测试一下接口是否正常。如果能看到API文档或收到响应,说明服务已经成功运行。

部署完成后,你的模型服务就已经在云端7x24小时待命了。它提供了一个标准的HTTP API,等待我们的小程序来调用。

2.2 了解API接口格式

大多数标准的模型服务API都大同小异。我们需要知道如何“告诉”模型我们的问题,以及它会“回答”什么。通常,对话接口是一个 POST 请求。

请求体(我们发送的数据) 通常是一个JSON对象,包含:

{
  "model": "Qwen1.5-1.8B-Chat-GPTQ-Int4", // 模型名称,有时可省略
  "messages": [
    {"role": "system", "content": "你是一个乐于助人的AI助手。"}, // 系统指令,设定AI角色(可选)
    {"role": "user", "content": "你好,请介绍一下你自己。"} // 用户当前的问题
  ],
  "stream": true // 是否启用流式输出。为true时,回复会像打字一样逐个词返回。
}
  • messages 数组非常重要,它记录了完整的对话上下文。如果你想实现多轮对话,就需要把之前用户和AI的对话历史也按顺序放在这个数组里,再附上新的用户问题。
  • stream: true 是我们实现“逐字输出”效果的关键。

响应(我们接收的数据)

  • stream: false 时,接口会一次性返回完整的回答。
  • stream: true 时,接口会返回一个“数据流”(Server-Sent Events, SSE)。你会收到多个数据块(chunk),每个块包含AI生成的下一个词或几个词,直到生成结束。每个数据块通常是一个JSON字符串,格式如 data: {"choices":[{"delta":{"content":"你"}}]}\n\n

好了,后端已经准备就绪,API也清楚了。现在我们把目光转回微信小程序,开始打造用户看得见、摸得着的部分。

3. 前端实战:构建微信小程序对话界面

让我们创建一个新的微信小程序项目,开始编写前端代码。我会把核心逻辑拆解成几个部分,并附上完整的 demo 代码。

3.1 项目结构与页面布局

首先,我们规划一个简单的页面:一个聊天消息列表,一个底部的输入框和发送按钮。

WXML结构 (index.wxml):

<!-- index.wxml -->
<view class="container">
  <!-- 聊天消息区域 -->
  <scroll-view class="chat-list" scroll-y scroll-into-view="{{scrollToView}}" scroll-with-animation>
    <block wx:for="{{chatHistory}}" wx:key="index">
      <!-- 用户消息 -->
      <view wx:if="{{item.role === 'user'}}" class="message user-message">
        <view class="avatar">你</view>
        <view class="bubble">{{item.content}}</view>
      </view>
      <!-- AI消息 -->
      <view wx:elif="{{item.role === 'assistant'}}" class="message assistant-message">
        <view class="avatar">AI</view>
        <view class="bubble">
          <!-- 如果是正在流式输出的消息,显示loading和动态文本 -->
          <block wx:if="{{item.isStreaming}}">
            <text>{{item.content}}</text>
            <text class="cursor">▋</text>
          </block>
          <block wx:else>
            <text>{{item.content}}</text>
          </block>
        </view>
      </view>
    </block>
  </scroll-view>

  <!-- 底部输入区域 -->
  <view class="input-area">
    <input 
      class="input" 
      value="{{inputValue}}" 
      bindinput="onInput" 
      placeholder="请输入您的问题..." 
      confirm-type="send"
      bindconfirm="sendMessage"
      focus="{{autoFocus}}"
    />
    <button class="send-btn" bindtap="sendMessage" disabled="{{isLoading}}">发送</button>
  </view>
</view>

WXSS样式 (index.wxss):

/* index.wxss */
.container {
  height: 100vh;
  display: flex;
  flex-direction: column;
  background-color: #f5f5f5;
}

.chat-list {
  flex: 1;
  padding: 20rpx;
  box-sizing: border-box;
}

.message {
  display: flex;
  margin-bottom: 30rpx;
  align-items: flex-start;
}

.user-message {
  flex-direction: row-reverse;
}

.avatar {
  width: 80rpx;
  height: 80rpx;
  border-radius: 50%;
  background-color: #07c160;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28rpx;
  flex-shrink: 0;
}

.assistant-message .avatar {
  background-color: #007aff;
}

.bubble {
  max-width: 70%;
  padding: 20rpx;
  border-radius: 12rpx;
  font-size: 32rpx;
  line-height: 1.5;
  word-break: break-word;
}

.user-message .bubble {
  background-color: #95ec69;
  margin-left: 20rpx;
  margin-right: 0;
}

.assistant-message .bubble {
  background-color: white;
  margin-left: 0;
  margin-right: 20rpx;
  border: 1rpx solid #e5e5e5;
}

.cursor {
  display: inline-block;
  font-weight: bold;
  animation: blink 1s step-end infinite;
}

@keyframes blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

.input-area {
  display: flex;
  padding: 20rpx;
  background-color: white;
  border-top: 1rpx solid #e5e5e5;
  align-items: center;
}

.input {
  flex: 1;
  border: 1rpx solid #ccc;
  border-radius: 10rpx;
  padding: 20rpx;
  font-size: 32rpx;
  margin-right: 20rpx;
  min-height: 80rpx;
  box-sizing: border-box;
}

.send-btn {
  background-color: #07c160;
  color: white;
  border-radius: 10rpx;
  padding: 0 40rpx;
  height: 80rpx;
  line-height: 80rpx;
  font-size: 32rpx;
}

.send-btn[disabled] {
  background-color: #cccccc;
}

3.2 核心逻辑:状态管理与API调用

接下来是JavaScript部分,这是小程序的大脑。我们在 index.js 中处理所有交互和逻辑。

初始化数据与事件处理 (index.js):

// index.js
Page({
  data: {
    chatHistory: [], // 完整的对话历史
    inputValue: '', // 输入框内容
    isLoading: false, // 是否正在加载(等待AI回复)
    scrollToView: '', // 用于控制滚动到底部
    autoFocus: true, // 自动聚焦输入框
    apiBaseUrl: 'http://your-instance-ip:8000', // TODO: 替换为你的后端API地址
  },

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

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

    // 1. 清空输入框,更新状态
    this.setData({
      inputValue: '',
      isLoading: true
    });

    // 2. 将用户消息添加到历史记录并立即显示
    const userMessage = { role: 'user', content: userInput };
    const updatedHistory = [...this.data.chatHistory, userMessage];
    this.setData({ chatHistory: updatedHistory });
    this.scrollToBottom();

    // 3. 准备请求数据
    // 构建messages数组,包含系统指令和完整的对话历史
    const messages = [
      { role: 'system', content: '你是一个乐于助人的AI助手,请用简洁友好的语气回答。' },
      ...updatedHistory.map(msg => ({ role: msg.role, content: msg.content }))
    ];

    const requestData = {
      model: 'Qwen1.5-1.8B-Chat-GPTQ-Int4', // 根据你的后端模型名称调整
      messages: messages,
      stream: true // 启用流式输出
    };

    // 4. 调用AI接口
    try {
      await this.callAIStreaming(requestData);
    } catch (error) {
      console.error('API调用失败:', error);
      // 在历史记录中添加一条错误消息
      const errorMessage = { role: 'assistant', content: `抱歉,请求出错: ${error.errMsg || '网络或服务异常'}` };
      this.setData({
        chatHistory: [...this.data.chatHistory, errorMessage]
      });
    } finally {
      // 5. 无论成功失败,都重置加载状态
      this.setData({ isLoading: false });
      this.scrollToBottom();
    }
  },

  // 流式调用API的核心函数
  async callAIStreaming(requestData) {
    return new Promise((resolve, reject) => {
      // 首先,在历史记录中添加一个“正在输入”的AI消息占位符
      const streamingMessageIndex = this.data.chatHistory.length;
      const initialAssistantMessage = { role: 'assistant', content: '', isStreaming: true };
      this.setData({
        chatHistory: [...this.data.chatHistory, initialAssistantMessage]
      });

      const requestTask = wx.request({
        url: `${this.data.apiBaseUrl}/v1/chat/completions`, // TODO: 确认你的后端API路径
        method: 'POST',
        data: requestData,
        responseType: 'text', // 重要!对于流式响应,需要以文本形式接收
        enableChunked: true, // 启用分块传输,用于流式响应
        success: (res) => {
          // 注意:对于流式响应,success回调可能只包含状态码,实际数据在onChunkReceived中处理
          if (res.statusCode === 200) {
            resolve();
          } else {
            reject(new Error(`HTTP ${res.statusCode}`));
          }
        },
        fail: reject,
      });

      let accumulatedText = '';
      // 监听数据流块
      requestTask.onChunkReceived((res) => {
        // res.data 是字符串,可能包含多个以 "data: " 开头的SSE事件
        const rawData = res.data;
        const lines = rawData.split('\n').filter(line => line.trim());

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const eventData = line.substring(6); // 去掉 "data: "
            if (eventData === '[DONE]') {
              // 流式传输结束
              console.log('流式传输结束');
              // 更新最后一条消息,移除“正在流式”状态
              const finalHistory = [...this.data.chatHistory];
              finalHistory[streamingMessageIndex] = {
                ...finalHistory[streamingMessageIndex],
                content: accumulatedText,
                isStreaming: false
              };
              this.setData({ chatHistory: finalHistory });
              this.scrollToBottom();
              return;
            }

            try {
              const parsed = JSON.parse(eventData);
              const contentDelta = parsed.choices?.[0]?.delta?.content || '';
              if (contentDelta) {
                accumulatedText += contentDelta;
                // 关键步骤:实时更新UI,显示已接收到的文本
                const updatedHistory = [...this.data.chatHistory];
                updatedHistory[streamingMessageIndex] = {
                  ...updatedHistory[streamingMessageIndex],
                  content: accumulatedText
                };
                this.setData({ chatHistory: updatedHistory });
                this.scrollToBottom();
              }
            } catch (e) {
              console.warn('解析数据块失败:', e, '原始数据:', eventData);
            }
          }
        }
      });
    });
  },

  // 工具函数:滚动到底部
  scrollToBottom() {
    // 简单延时确保视图更新后再滚动
    setTimeout(() => {
      if (this.data.chatHistory.length > 0) {
        const lastIndex = this.data.chatHistory.length - 1;
        this.setData({
          scrollToView: `msg-${lastIndex}`
        });
      }
    }, 100);
  },

  onLoad() {
    // 页面加载时,可以尝试从本地存储读取历史记录
    try {
      const history = wx.getStorageSync('chatHistory');
      if (history) {
        this.setData({ chatHistory: history });
      }
    } catch (e) {
      console.error('读取历史记录失败:', e);
    }
  },

  onUnload() {
    // 页面卸载时,可以选择保存历史记录到本地
    try {
      wx.setStorageSync('chatHistory', this.data.chatHistory);
    } catch (e) {
      console.error('保存历史记录失败:', e);
    }
  }
});

3.3 关键点解析与优化建议

上面的代码已经是一个可工作的Demo了。这里有几个关键点值得深入说一下:

  1. 流式响应 (stream: true):这是实现“打字机效果”的核心。我们设置 enableChunked: true 并监听 onChunkReceived 事件,服务器每生成一个词或一段词就推送过来,我们立即更新UI,视觉上就是逐字出现的效果。
  2. 对话历史管理:我们用一个数组 chatHistory 在内存中维护所有消息。每次发送新消息时,我们把整个历史(包括新问题)传给API,这样模型就能理解上下文,实现连续对话。退出小程序时,可以将其存入 wx.setStorageSync 以便下次恢复。
  3. 错误处理:网络请求总有失败的可能。我们用 try...catch 包裹API调用,并在失败时给用户一个友好的提示,而不是让程序崩溃。
  4. 体验优化
    • 滚动到底部:每次新增消息或更新流式文本后,都调用 scrollToBottom,让最新内容始终可见。
    • 加载状态:发送请求时禁用按钮并显示加载状态,防止用户重复提交。
    • 本地存储:虽然示例中用了同步存储,但对于历史记录较长的情况,建议使用异步存储 wx.setStorage 或考虑存储大小限制。

4. 运行测试与上线前准备

代码写完了,让我们来跑一下看看效果。

  1. 替换API地址:在 index.jsdata 中,将 apiBaseUrl 的值替换成你实际部署的后端服务的IP和端口。
  2. 开发环境调试:在微信开发者工具中,勾选“详情”->“本地设置”->“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,这样可以先绕过域名校验进行开发测试。
  3. 真机预览:点击开发者工具的“预览”,生成二维码,在手机微信上扫描测试。真机上需要确保手机和你的后端服务器网络互通(通常需要服务器有公网IP)。
  4. 上线部署
    • 后端:确保你的模型服务稳定运行,API地址固定。
    • 前端
      • apiBaseUrl 替换为正式的、带域名(最好HTTPS)的地址。
      • 登录微信公众平台,进入你的小程序管理后台。
      • 在“开发”->“开发设置”->“服务器域名”中,将你的后端API域名(不含路径)添加到 request 合法域名列表中。
      • 然后提交代码审核,通过后即可发布。

5. 总结与扩展思路

跟着上面的步骤走一遍,一个具备基本AI对话功能的微信小程序就诞生了。我们利用了量化后轻量高效的“通义千问1.5-1.8B-Chat-GPTQ-Int4”模型作为后端大脑,通过标准的HTTP接口与前端小程序通信,前端则通过处理流式响应实现了流畅的对话体验。

这个Demo是一个坚实的起点。在此基础上,你可以根据实际需求进行大量扩展,让这个小助手变得更聪明、更贴心:

  • 上下文长度管理:模型对输入的上下文长度有限制。当对话轮数很多时,chatHistory 数组会变得很长。你可以实现一个策略,比如只保留最近10轮对话,或者当长度超过阈值时,智能地总结或丢弃最早的对话。
  • 消息类型扩展:除了文本,可以尝试支持图片上传,让AI“看图说话”。这需要后端模型支持多模态,并且前端使用 wx.chooseImage 和文件上传接口。
  • UI与交互增强:增加消息复制、重新生成、编辑再发送、语音输入(利用 wx.startRecord)等功能,提升用户体验。
  • 性能与稳定性:加入请求超时设置、失败重试机制,对于长回复可以考虑分页加载历史消息。
  • 业务逻辑集成:这不仅仅是聊天。你可以把它变成智能客服(接入知识库)、创作助手(指定文案风格)、学习工具(扮演老师)等等,关键在于你如何设计系统提示词(system message)和交互流程。

希望这篇实战指南能帮你顺利跨出第一步。从“想”到“做”,最难的部分往往就是开始。现在,代码就在你手里,后端服务也可以一键获取,剩下的就是发挥你的创意,去打造那个独一无二的AI小程序了。


获取更多AI镜像

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

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐