文章目录
聊天功能
实现功能
要实现一个聊天机器人,它能够解答用户疑问,并且能够识别到用户聊天的主题,涉及到饮食方面时,会自动决定是否要去数据库中读取用户的相关喜好信息,以实现自动化。同时,还要支持重新生成、复制回答、同时生成两条文本让用户选择以优化模型回复。同时还要对每次的会话进行管理。
实现思路
主要思路就是要将用户输入传给模型,模型予以解答。
主要实现的功能和解决的问题:
- 需要维护一个history来存储聊天记录。
- 实现打字机效果。
- 消息正在生成时,发送按钮变成灰色图片并禁止点击。
- 解决输入法弹起后输入框自动调整高度以避免被遮挡问题。
- 解决输入法弹起后,顶部导航栏不会上移。
- 实现文本的复制功能。
- 实现重新生成功能,如果不是最后一条消息,则要删掉该会话中要重新生成的消息后面的所有消息。
- 实现同时生成两种回答,让用户选择更喜欢哪种回答。然后选择后,后续回答风格会按照用户选择的那条来回答。
- 解决如果用户没有选择哪种回答,就进行发送下一条消息、切换页面、切换会话等操作,则会自动选择详细版的哪种风格。
- 实现会话的增加,要求当前会话没有聊天记录时,不会创建会话,只有发送了至少一条聊天记录,才会新增到数据库。同时,当前会话没有聊天记录时,前端会话新增按钮要禁用,即不能再次创建一个新的会话窗口。
- 实现会话的删除,使用滑动框,左滑删除。弹出提示框,询问是否删除。并且要解决uview滑动框组件的bug,即删除一个后,该删除状态不会自动消失,还是打开的状态。
- 会话列表要实现按照今天、昨天和更早以前进行分组。
后端
由于后端代码很多,只截取片段展示。
python
def chat_response(query, user_info, session_id=None, history=None, is_chat=None, response_type=None):
if user_info is None:
return "无法找到用户信息,请检查用户ID是否正确。"
if session_id is None and is_chat == "1":
session_id = create_session(user_info['user_id'])
if is_chat == "1":
is_food_topic = is_food_related_topics(query)
print("用户输入是否是食物相关主题:" + is_food_topic)
if is_food_topic == "true":
emotions = get_user_emotions(user_info['user_id'])
emotions_str = ", ".join([f"{e['emotion']}{e['food']}" for e in emotions])
user_info_str = f"疾病: {user_info['diseases']}, 喜好的食物: {user_info['likes']}, 忌口的食物: {user_info['dislikes']}, 其他喜恶: {emotions_str}"
personalized_prompt = f"{query}\n用户信息: {user_info_str}"
print("\n============= info_prompt ===============\n")
print(personalized_prompt)
# 提取用户对食物的情感,并存到数据库中
extract_emotions_about_food(query, user_info)
else:
personalized_prompt = f"{query}"
print("\n============= no_info_prompt ===============\n")
print(personalized_prompt)
store_message(session_id, 'user', query)
# 检查用户提问的数量
messages = get_session_messages(session_id)
user_messages = [msg for msg in messages if msg['role'] == 'user']
if len(user_messages) == 3:
# 获取前三条用户提问生成标题
summary_prompt = "\n".join([msg['content'] for msg in user_messages])
title, _ = p_model.chat(tokenizer, f"请总结以下对话主题,不超过10个字,只输出一行,不要有换行:\n{summary_prompt}", top_p=1, temperature=0.01)
title = title.strip()
update_session_title(session_id, title)
print("history\n")
print(history)
if response_type == "detailed":
prompt = f"详细(100字以上)回答:\n{personalized_prompt}"
elif response_type == "concise":
prompt = f"简洁(50字以内)回答:\n{personalized_prompt}"
else:
prompt = personalized_prompt
else:
prompt = f"{query}"
print("\n============= not_chat_prompt ===============\n")
print(prompt)
response, _ = p_model.chat(tokenizer, prompt, history=history, top_p=1, temperature=0.01)
if is_chat == "1":
message_id = store_message(session_id, 'system', response) # 存储消息并返回消息ID
return response, session_id, message_id
return response
前端
由于前端代码太多,只截取部分代码展示。
vue
async sendMessage() {
if (this.userInput.trim() !== '' && !this.isTyping) {
// 检查是否需要自动选择详细版
if (this.showChoice) {
await this.autoChooseDetailed(); // 等待自动选择详细版完成
}
this.messageList.push({
role: 'user',
content: this.userInput,
avatar: 'https://xxx/avatar.png'
});
const query = this.userInput;
this.userInput = '';
// 添加ChatGPT的占位消息
const loadingMessage = {
role: 'chatgpt',
content: '',
avatar: 'https://xxx/chatai-avatar.png',
isLoading: true
};
this.messageList.push(loadingMessage);
this.handleScrollTop(); // 确保视图滚动到底部
let that = this;
this.isTyping = true;
uni.request({
url: 'https://xxx/chat',
method: 'POST',
data: { query, history: this.history, user_id: this.userId, session_id: this.currentSessionId },
header: {
'Content-Type': 'application/json',
},
success: function (resp) {
that.currentSessionId = resp.data.session_id;
const index = that.messageList.indexOf(loadingMessage);
if (index !== -1) {
that.messageList.splice(index, 1); // 删除占位消息
if (resp.data.detailed_response && resp.data.concise_response) {
// 第四次对话,展示两个回答框
that.conciseResponse = resp.data.concise_response;
that.detailedResponse = resp.data.detailed_response;
that.showChoice = true; // 设置 showChoice 为 true
console.log("showChoice")
console.log(that.showChoice)
// 插入提示语
that.messageList.push({
role: 'system',
content: '请选择您更喜欢哪种回答?',
avatar: '',
isTypingComplete: true,
responseType: 'prompt'
});
// 分别为精简版和详细版添加打字机效果
that.addTypingEffect('ChatGLM3-6B', resp.data.concise_response, resp.data.concise_message_id, 'concise', true);
that.addTypingEffect('ChatGLM3-6B', resp.data.detailed_response, resp.data.detailed_message_id, 'detailed', false);
} else {
that.addTypingEffect('ChatGLM3-6B', resp.data.response, resp.data.message_id);
}
}
that.history.push({ role: 'user', content: query });
if (that.history.filter(msg => msg.role === 'user').length === 1 || that.history.filter(msg => msg.role === 'user').length === 3) {
that.refreshSessionList();
}
},
fail: function (error) {
console.error('Error sending message:', error);
},
complete: function() {
that.isTyping = false;
}
});
}
},
addTypingEffect(user, text, messageId, responseType = null, showAvatar = true) {
if (!text) {
console.error('Error: text is undefined or empty');
return;
}
let index = 0;
const message = {
role: 'chatgpt',
content: '',
avatar: showAvatar ? 'https://xxx/chatai-avatar.png' : 'https://xxx/0b46f21fbe096b63d30f4b590d338744ebf8aca0.png',
id: messageId,
isLoading: false,
isTypingComplete: false, // 新变量,初始化为 false
responseType: responseType // 添加 responseType
};
this.messageList.push(message);
const typing = setInterval(() => {
if (index < text.length) {
message.content += text[index];
index++;
this.handleScrollTop().catch((error) => {
console.warn('Error while scrolling:', error);
});
} else {
clearInterval(typing);
this.history.push({ role: 'assistant', content: text });
message.isLoading = false; // 设置为不再加载
this.$set(message, 'isTypingComplete', true); // 打字机效果完成后设置为 true
this.handleScrollTop().catch((error) => {
console.warn('Error while scrolling:', error);
}); // 确保滚动到最底部
}
}, this.typingInterval);
},