Cogito-V1-Preview-Llama-3B开发实战:微信小程序AI对话前端集成
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对话历史数组,每个消息对象有role(user或assistant)和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星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐




所有评论(0)