微信小程序连接大模型:开发一个集成通义千问的智能聊天小程序

最近在捣鼓一些有意思的小项目,想着能不能把现在很火的大模型能力,塞进我们天天都在用的微信小程序里。比如做个智能客服助手、个人知识库问答,或者就是个能聊天的AI伙伴,应该挺有意思的。

说干就干,我花了点时间,把通义千问的模型能力接入了微信小程序,做了一个能实时对话的智能聊天应用。整个过程下来,感觉并没有想象中那么复杂,核心就是小程序端做好界面和交互,后端处理好模型调用和API转发。这篇文章,我就把从零开始搭建这个“小程序+大模型”应用的完整过程,以及其中遇到的一些坑和解决方案,跟大家详细分享一下。如果你也想给自己的小程序加点AI智能,希望这篇内容能给你带来一些实用的参考。

1. 整体思路与准备工作

在动手写代码之前,我们先理清楚整个项目是怎么跑起来的。简单来说,就是“前后端分离”的模式:微信小程序作为前端,负责展示界面和收集用户输入;我们还需要一个后端服务器,它充当中间人的角色,接收小程序的请求,再去调用通义千问的官方API,拿到AI的回复后,再返回给小程序。

为什么不能小程序直接调大模型API呢?主要是两个原因:安全和密钥管理。大模型服务的API Key是非常敏感的信息,绝对不能放在小程序的前端代码里,否则很容易被别人扒走。所以,我们必须通过自己的后端服务器来中转,把密钥安全地保存在服务器端。

为此,我们需要准备以下几样东西:

  1. 一个通义千问的API密钥:去对应的云服务平台申请,这是调用模型能力的通行证。
  2. 一台后端服务器:用来部署我们的中转API服务。可以选择常见的云服务器,或者一些Serverless服务,能运行Python或Node.js环境就行。
  3. 注册一个微信小程序:在微信公众平台注册,拿到小程序的AppID,这是开发的必备条件。
  4. 本地开发环境:安装微信开发者工具,以及后端的开发环境(比如Python的Flask/Django框架,或者Node.js的Express/Koa框架)。

我这次选择的技术栈是:小程序端用原生的JavaScript/微信小程序语法,后端用Python的FastAPI框架,因为它轻量、异步支持好,适合处理这种聊天请求。数据库暂时没用到,因为是个简单的对话demo,如果你需要记录历史对话,可以加上MySQL或MongoDB。

2. 后端API服务搭建

后端是我们的核心中转站,它的任务很明确:提供一个安全的接口给小程序调用,然后它自己再去请求通义千问,最后把结果整理好返回。

2.1 项目初始化与依赖安装

首先,我们在服务器上创建一个新的项目目录,然后初始化Python环境。我强烈建议使用虚拟环境来管理依赖,避免包冲突。

# 创建项目目录并进入
mkdir wechat-ai-assistant
cd wechat-ai-assistant

# 创建虚拟环境(以venv为例)
python -m venv venv

# 激活虚拟环境
# 在Windows上:
venv\Scripts\activate
# 在Mac/Linux上:
source venv/bin/activate

# 安装必要的包
pip install fastapi uvicorn httpx python-dotenv

这里简单说明一下这几个包的作用:

  • fastapiuvicorn:用来快速创建高性能的Web API服务。
  • httpx:一个现代化的HTTP客户端,我们将用它来请求通义千问的API,它比传统的requests库对异步支持更好。
  • python-dotenv:用来管理环境变量,比如我们的API密钥就不会硬编码在代码里。

2.2 核心API接口实现

接下来,我们创建主要的应用文件,比如叫 main.py。在这个文件里,我们要做几件事:创建FastAPI应用、设置跨域(因为小程序要访问)、实现一个处理对话的接口。

# main.py
import os
from typing import Optional
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import httpx
from pydantic import BaseModel
from dotenv import load_dotenv

# 加载环境变量,从同目录下的 .env 文件读取
load_dotenv()

app = FastAPI(title="微信小程序AI助手后端API")

# 配置跨域,允许微信小程序的域名访问
# 注意:上线前需要根据小程序的实际域名进行精确配置
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 开发阶段可以用*,生产环境务必替换为具体域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 从环境变量获取通义千问的API密钥和基础URL
API_KEY = os.getenv("QWEN_API_KEY")
BASE_URL = os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1") # 示例URL,请替换为实际

if not API_KEY:
    raise ValueError("请在 .env 文件中设置 QWEN_API_KEY 环境变量")

# 定义请求数据模型
class ChatRequest(BaseModel):
    message: str  # 用户发送的消息
    conversation_id: Optional[str] = None  # 可选,用于多轮对话会话管理

# 定义响应数据模型
class ChatResponse(BaseModel):
    reply: str  # AI的回复
    conversation_id: Optional[str] = None  # 返回会话ID,便于后续延续对话

@app.post("/api/chat", response_model=ChatResponse)
async def chat_with_ai(request: ChatRequest):
    """
    处理小程序发来的聊天请求,转发至通义千问API并返回结果。
    """
    if not request.message or request.message.strip() == "":
        raise HTTPException(status_code=400, detail="消息内容不能为空")

    # 准备请求通义千问API的头部和数据
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }
    
    # 这里需要根据通义千问API的实际要求构造请求体
    # 以下是一个示例结构,具体字段请查阅官方文档
    payload = {
        "model": "qwen-max",  # 指定模型,例如 qwen-max, qwen-plus等
        "input": {
            "messages": [
                {"role": "user", "content": request.message}
            ]
        },
        "parameters": {
            "result_format": "message"  # 指定返回格式
        }
    }

    async with httpx.AsyncClient(timeout=30.0) as client:  # 设置一个较长的超时时间
        try:
            # 发送请求到通义千问API
            resp = await client.post(
                f"{BASE_URL}/chat/completions",  # 具体的API端点路径
                headers=headers,
                json=payload
            )
            resp.raise_for_status()  # 如果响应状态码不是2xx,抛出异常
            result = resp.json()

            # 解析通义千问API的返回结果,提取AI回复文本
            # 这里的解析逻辑需要根据API返回的实际JSON结构进行调整
            ai_reply = result.get("output", {}).get("choices", [{}])[0].get("message", {}).get("content", "抱歉,我暂时无法理解。")
            
            # 简单处理,如果返回内容为空,给个默认回复
            if not ai_reply or ai_reply.strip() == "":
                ai_reply = "我已收到你的消息,但回复内容为空。"

            return ChatResponse(reply=ai_reply, conversation_id=request.conversation_id)

        except httpx.RequestError as e:
            # 处理网络请求错误
            raise HTTPException(status_code=500, detail=f"请求模型服务时发生网络错误: {str(e)}")
        except httpx.HTTPStatusError as e:
            # 处理API返回的错误状态码(如鉴权失败、额度不足等)
            raise HTTPException(status_code=e.response.status_code, detail=f"模型服务返回错误: {e.response.text}")
        except (KeyError, IndexError) as e:
            # 处理响应数据解析错误
            raise HTTPException(status_code=502, detail=f"解析模型服务响应时出错: {str(e)}")

if __name__ == "__main__":
    # 使用uvicorn启动服务,指定主机和端口
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

代码看起来有点长,但逻辑是清晰的。我们创建了一个 /api/chat 的接口,它接收用户消息,然后带着我们的API密钥去问通义千问,拿到回答后再原路返回。这里特别要注意错误处理,网络请求、API错误、数据解析错误都要考虑到,给小程序一个明确的错误信息。

2.3 环境配置与运行

我们还需要一个 .env 文件来保存密钥(切记不要把这个文件提交到Git等代码仓库):

# .env
QWEN_API_KEY=你的通义千问API密钥在这里
QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1

现在,在项目根目录下,运行我们的后端服务:

python main.py

如果一切正常,你会看到服务启动在 http://0.0.0.0:8000。你可以用浏览器访问 http://localhost:8000/docs,这是FastAPI自动生成的交互式API文档,可以在这里测试 /api/chat 接口是否工作正常。

3. 微信小程序前端开发

后端跑起来了,接下来就是打造小程序的界面了。打开微信开发者工具,新建一个项目,填入你的AppID。

3.1 项目结构与页面布局

小程序的基本结构我们就不赘述了,主要关注聊天页面。假设我们有一个主要的聊天页面 pages/chat/chat

首先设计 chat.wxml 文件,这是我们的页面结构:

<!-- pages/chat/chat.wxml -->
<view class="container">
  <!-- 聊天消息列表区域 -->
  <scroll-view class="message-list" scroll-y scroll-into-view="{{scrollToView}}" scroll-with-animation>
    <block wx:for="{{messages}}" wx:key="index">
      <view class="message-item {{item.role}}">
        <view class="avatar">
          <image wx:if="{{item.role === 'user'}}" src="/images/user-avatar.png"></image>
          <image wx:if="{{item.role === 'assistant'}}" src="/images/ai-avatar.png"></image>
        </view>
        <view class="bubble">
          <text class="content">{{item.content}}</text>
        </view>
      </view>
    </block>
    <!-- 加载指示器 -->
    <view wx:if="{{isLoading}}" class="message-item assistant">
      <view class="avatar">
        <image src="/images/ai-avatar.png"></image>
      </view>
      <view class="bubble loading">
        <text>正在思考...</text>
      </view>
    </view>
  </scroll-view>

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

然后,在 chat.wxss 中加上样式,让界面看起来像个聊天软件:

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

.message-list {
  flex: 1;
  padding: 20rpx;
  box-sizing: border-box;
  overflow: auto;
}

.message-item {
  display: flex;
  margin-bottom: 30rpx;
}
.message-item.user {
  flex-direction: row-reverse;
}
.message-item.assistant {
  flex-direction: row;
}

.avatar image {
  width: 80rpx;
  height: 80rpx;
  border-radius: 50%;
}

.bubble {
  max-width: 500rpx;
  padding: 20rpx;
  border-radius: 10rpx;
  margin: 0 20rpx;
  word-break: break-word;
}
.user .bubble {
  background-color: #95ec69;
  color: #000;
}
.assistant .bubble {
  background-color: #fff;
  color: #333;
  box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
}
.bubble.loading text {
  color: #999;
  font-style: italic;
}

.input-area {
  display: flex;
  align-items: center;
  padding: 20rpx;
  background-color: #fff;
  border-top: 1rpx solid #eee;
}
.input-box {
  flex: 1;
  height: 80rpx;
  padding: 0 20rpx;
  border: 1rpx solid #ddd;
  border-radius: 40rpx;
  margin-right: 20rpx;
}
.send-btn {
  width: 140rpx;
  height: 80rpx;
  line-height: 80rpx;
  border-radius: 40rpx;
  background-color: #07c160;
  color: #fff;
  font-size: 28rpx;
}
.send-btn[disabled] {
  background-color: #ccc;
  color: #999;
}

3.2 页面逻辑与API调用

界面有了,灵魂在于逻辑。我们来看 chat.js

// pages/chat/chat.js
// 这里填写你刚刚启动的后端API地址,本地测试时可能是局域网IP,上线后需改为正式域名
const API_BASE_URL = 'http://192.168.1.100:8000'; // 请替换为你的实际后端地址

Page({
  data: {
    messages: [], // 聊天记录数组,格式如 [{role: 'user', content: '你好'}, {role: 'assistant', content: '你好!'}]
    inputValue: '', // 输入框内容
    isLoading: false, // 是否正在加载AI回复
    scrollToView: '', // 用于控制滚动到底部的视图ID
    autoFocus: true // 自动聚焦输入框
  },

  onLoad: function() {
    // 页面加载时,可以尝试从本地缓存读取历史记录
    const history = wx.getStorageSync('chatHistory');
    if (history && Array.isArray(history)) {
      this.setData({ messages: history });
      this.scrollToBottom();
    }
  },

  // 输入框内容变化
  onInput: function(e) {
    this.setData({
      inputValue: e.detail.value
    });
  },

  // 发送消息
  sendMessage: function() {
    const userMessage = this.data.inputValue.trim();
    if (!userMessage || this.data.isLoading) {
      return;
    }

    // 1. 清空输入框,并将用户消息添加到界面
    this.setData({
      inputValue: '',
      messages: [...this.data.messages, { role: 'user', content: userMessage }],
      isLoading: true
    }, () => {
      this.scrollToBottom(); // 确保滚动到最新消息
    });

    // 2. 调用后端API获取AI回复
    wx.request({
      url: `${API_BASE_URL}/api/chat`,
      method: 'POST',
      header: {
        'content-type': 'application/json'
      },
      data: {
        message: userMessage
        // 如果需要多轮对话,可以在这里传入 conversation_id
      },
      success: (res) => {
        if (res.statusCode === 200 && res.data.reply) {
          // 成功收到回复,添加到消息列表
          this.setData({
            messages: [...this.data.messages, { role: 'assistant', content: res.data.reply }],
            isLoading: false
          }, () => {
            this.scrollToBottom();
            this.saveHistory(); // 保存到本地历史
          });
        } else {
          // 处理API返回的业务错误
          this.showError('获取回复失败:' + (res.data.detail || '未知错误'));
        }
      },
      fail: (err) => {
        // 处理网络请求失败
        console.error('请求失败:', err);
        this.showError('网络请求失败,请检查后端服务是否运行及地址是否正确。');
      }
    });
  },

  // 滚动到底部
  scrollToBottom: function() {
    // 通过设置scroll-into-view到最后一个消息的id来实现滚动
    // 这里简单使用消息索引作为id
    const lastIndex = this.data.messages.length - 1;
    if (lastIndex >= 0) {
      // 给最后一条消息设置一个id,这里用索引模拟
      // 注意:在实际的wxml中,需要动态设置id,这里是一种简化实现思路。
      // 更优做法是在渲染消息时,使用 `id="msg{{index}}"`
      this.setData({
        scrollToView: `msg${lastIndex}`
      });
    }
  },

  // 显示错误提示
  showError: function(msg) {
    wx.showToast({
      title: msg,
      icon: 'none',
      duration: 3000
    });
    this.setData({ isLoading: false });
  },

  // 保存聊天记录到本地缓存
  saveHistory: function() {
    // 为了防止缓存过大,可以只保存最近N条记录
    const historyToSave = this.data.messages.slice(-50); // 保存最近50条
    wx.setStorageSync('chatHistory', historyToSave);
  },

  // 清空聊天记录
  clearHistory: function() {
    wx.showModal({
      title: '提示',
      content: '确定要清空聊天记录吗?',
      success: (res) => {
        if (res.confirm) {
          this.setData({ messages: [] });
          wx.removeStorageSync('chatHistory');
          wx.showToast({ title: '已清空', icon: 'success' });
        }
      }
    });
  }
})

为了让滚动到底部更精确,我们稍微修改一下 chat.wxml 中消息的渲染部分,给每条消息加上动态ID:

<!-- 修改消息渲染部分 -->
<block wx:for="{{messages}}" wx:key="index">
  <view id="msg{{index}}" class="message-item {{item.role}}">
    <!-- ... 头像和气泡内容保持不变 ... -->
  </view>
</block>

3.3 配置网络请求域名

微信小程序对网络请求有严格的安全限制,只能访问事先在后台配置过的域名。所以,在开发阶段,我们可以在微信开发者工具的“详情”->“本地设置”中,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,以便用本地IP地址进行调试。

但是,小程序正式上线前,必须完成以下配置:

  1. 将后端API服务部署到拥有HTTPS域名(https://)的服务器上。
  2. 登录微信公众平台,进入你的小程序管理后台。
  3. 在“开发”->“开发管理”->“开发设置”->“服务器域名”中,将你的后端API域名(如 https://api.yourdomain.com)添加到 request合法域名 列表中。

4. 功能扩展与优化建议

一个最基础的聊天功能已经完成了。但要让这个小程序更实用、更健壮,我们还可以做很多事。

4.1 实现连续对话(会话管理)

上面的例子是“一问一答”,没有上下文关联。要让AI记住之前的对话,我们需要在后端维护一个“会话”。简单的方法是,小程序在第一次请求时生成一个唯一的 conversation_id 并保存,之后每次请求都带上这个ID。后端根据这个ID,在请求大模型API时,将之前的历史消息也一并发送过去。

这需要后端引入一个临时存储(比如Redis)来保存会话历史,或者将历史直接传递给模型API(如果API支持上下文传递)。

4.2 增加流式输出(打字机效果)

目前我们是等AI完全生成完所有文本后,一次性返回并显示。这样如果回答很长,用户需要等待较长时间。更好的体验是“流式输出”,即AI生成一个字就返回一个字,像打字机一样实时显示在小程序上。

这需要后端和模型API都支持Server-Sent Events (SSE) 或 WebSocket。后端以流的方式从模型API接收数据,并同样以流的形式推送给小程序。小程序端则需要使用 wx.connectSocket 来建立WebSocket连接,或者处理分块传输的HTTP响应。

4.3 完善错误处理与用户体验

  • 网络状态提示:检测网络断开时,给出友好提示。
  • 消息重发机制:发送失败的消息,允许用户点击重发。
  • 加载动画:发送中和接收中可以有更丰富的动画效果。
  • 消息复制与分享:长按消息气泡,可以复制AI回复的内容。

4.4 后端安全与性能加固

  • 接口鉴权:为你的后端API增加简单的鉴权(比如小程序AppSecret验证),防止被他人滥用。
  • 请求限流:防止单个用户恶意刷接口,保护你的模型API额度。
  • 敏感词过滤:在后端对用户输入和AI输出进行基本的合规性检查。
  • 服务监控与日志:记录请求日志,便于排查问题和分析使用情况。

5. 总结

走完这一趟,你会发现,把大模型能力集成到微信小程序里,技术路径是清晰的。核心就是构建一个安全可靠的后端桥梁,然后在小程序端做好交互。这个过程里,调试和配置可能会花些时间,尤其是网络域名、跨域这些细节问题。

我把自己这个demo跑起来后,感觉还是挺有成就感的。它不仅仅是一个玩具,你可以基于这个框架,扩展出很多实用的功能。比如,加上语音输入输出,就变成了语音助手;对接特定的知识库,就能做成垂直领域的智能问答工具;结合小程序的拍照功能,还能玩出“拍照问AI”的花样。

开发过程中,多查阅微信小程序官方文档和通义千问的API文档,大部分问题都能找到答案。希望这个从零开始的案例,能帮你打开思路。动手试试吧,从最简单的对话开始,一步步添加你想要的功能,这个过程本身就是一个很好的学习体验。


获取更多AI镜像

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

Logo

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

更多推荐