项目实训(7) - 对话中的“异常处理”与“引导”
流式输出搞定之后,AI模拟面试的“面子”工程看起来好多了。但“里子”问题还是不少。最近发现两个比较头疼的情况:
-
AI 卡壳或钻牛角尖:
- 有时候,AI 对一个问题追问个没完,或者连续几个问题都非常相似,感觉像是陷入了某种循环。
- 或者,AI 的回复突然变得非常简短,甚至牛头不对马嘴,好像上下文丢失了。
-
用户回答太随意或跑题:
- 用户可能只回一两个字,比如“嗯”、“是的”、“不了解”。AI 很难基于这个进行有意义的追问。
- 或者用户扯得太远,完全偏离了面试的主题。
这些情况都会让模拟面试的体验大打折扣。纯靠优化 Prompt 好像也解决不了所有问题,毕竟用户的输入是不可控的。
我的应对思路:前端辅助 + 温和引导
1. 针对 AI 卡壳:
-
“换个问题”按钮:当用户觉得 AI 的提问不合适或者重复时,可以点击这个按钮。
- 实现:点击后,前端向 API 发送一个特殊的指令,比如在
messages历史的最后追加一条userrole (或systemrole) 的消息:“请忽略你上一个问题,换一个角度或话题进行提问。” 或者更具体:“请针对[原始议题],从[新的方面]提一个不同的问题。” - 这个指令也需要精心设计,不然可能把 AI 搞得更糊涂。
- 实现:点击后,前端向 API 发送一个特殊的指令,比如在
-
“重置对话(保留上下文)”按钮? 这个有点复杂。意思是清除掉最近几轮可能“污染”了上下文的对话,回到一个相对干净的状态,让AI重新思考。但哪些算“污染”的对话?不好界定。暂时先不考虑这个。
-
监测重复性? 前端能不能简单地检测 AI 最近几次的回复是否有高度相似性?比如用字符串相似度算法。如果发现重复,可以自动触发一次“请换个话题”的内部指令。这个感觉有点过度设计,而且“相似”的阈值不好定。
2. 针对用户回答不佳:
-
智能提示/引导语料:当用户输入框为空,或者输入内容过短时,可以在输入框下方显示一些提示。
- 比如:“可以详细说明一下当时遇到的挑战吗?”、“不妨展开讲讲你是如何解决这个问题的?”、“可以举一个具体的例子吗?”
- 这些提示语可以根据当前面试的阶段和AI上一个问题的类型动态生成(或者预设几套)。
-
AI 主动引导:如果用户回答很简略,能不能让 AI 在下一轮回复时,除了追问,也附带一句引导?
- 修改 Prompt:在 System Prompt 中加入一条规则:“如果候选人的回答过于简短或不清晰,请在追问的同时,鼓励候选人提供更多细节或具体实例。”
- 前端判断后辅助:如果前端检测到用户回答字数过少,可以在发送给 API 的
messages中,在用户回答后面,追加一条system指令给 AI:“(提示:候选人刚才的回答比较简洁,请你引导他/她展开说明。)”
代码层面的小尝试 (以“换个问题”为例):
// AIChatWindow.vue
// ... template 里加个按钮 ...
// <a-button @click="requestDifferentQuestion" :disabled="isLoading || messages.length === 0">换个问题</a-button>
const requestDifferentQuestion = () => {
if (messages.value.length === 0 || isLoading.value) return;
const lastAiMessage = messages.value.filter(m => m.sender === 'ai').pop();
if (!lastAiMessage) {
// alert("还没有AI的提问呢"); // 或者禁用按钮
return;
}
// 构造一条“用户”消息,内容是请求换问题的指令
const instructionForAI = "不好意思,关于上一个问题,我想我们能否换一个角度或者话题来讨论?";
// 也可以更直接,但可能生硬:
// const instructionForAI = "请忽略你的上一个问题,并重新提出一个不同的问题。";
const userInstructionMessage: Message = {
id: Date.now().toString(),
text: instructionForAI,
sender: 'user', // 以用户身份发出这个请求
timestamp: new Date(),
};
// messages.value.push(userInstructionMessage); // 这条指令消息可以不显示在聊天记录里,或者用特殊样式显示
// 如果不显示,那就要直接把它加入到要发送给API的列表里
isLoading.value = true; // 开始请求
// 构造发送给API的消息列表
// 包含历史消息 + 这条新的指令性用户消息
const messagesToSendToAI = messages.value.map(m => ({role: m.sender, content: m.text}));
messagesToSendToAI.push({role: 'user', content: instructionForAI});
// actuallySendToAI(messagesToSendToAI);
// 注意:actuallySendToAI 内部可能需要区分这条消息是普通用户回复还是特殊指令
// 或者,我们直接调用一个精简版的发送函数,它只负责把这条指令和必要的上下文发出去
// 为了简单,这里假设 actuallySendToAI 能正确处理(比如把这条指令当成用户的最新“回答”)
// 简化版:直接把这条指令作为“用户的最新发言”来发送
const tempUserInput = userInput.value; // 保存用户可能正在输入的内容
userInput.value = instructionForAI; // 用指令覆盖
// 调用之前的 handleUserInputSend,它会把 userInput.value 发出去
// 但这样会把指令显示在聊天记录里,可能也不是最佳选择
// 更好的做法是直接调用 actuallySendToAI,并把这条指令消息加入 apiMessages 构造过程
// 决定采用:直接构造apiMessages并调用发送函数,这条指令不进messages.value(即不在UI显示)
const currentApiMessages = messages.value.map(msg => ({
role: msg.sender === 'user' ? 'user' : 'assistant',
content: msg.text
}));
currentApiMessages.push({ role: 'user', content: instructionForAI });
actuallySendToAI(currentApiMessages, true); // true 表示这是一条特殊指令触发的,可能不需要更新userInput等
// actuallySendToAI(messagesToSendToAI, isSpecialInstruction: boolean)
// isSpecialInstruction 可以用来控制UI行为,比如不把这条指令加入 messages.value
// 或者,更简单粗暴,就是加进去,然后用户看到自己发了“换个问题”,AI再回复,也还行。
};
// actuallySendToAI 需要稍微改造,能接收一个完整的待发送消息列表
// async function actuallySendToAI(messagesForAPI: APIMessage[], isFromSpecialInstruction = false) {
// isLoading.value = true;
// if (!isFromSpecialInstruction) { // 如果是正常用户输入
// // 把用户最新的输入(如果有)也加入显示列表
// }
// // ... 后续的 fetch, stream 处理 ...
// }
上面的“换个问题”实现还很粗糙,特别是怎么把这条“指令”自然地融入对话流,同时不污染 messages.value (如果不想显示它的话),需要细致处理。
反思: 感觉在做 AI 应用时,前端的职责不仅仅是展示界面和调API了。很多时候,我们需要在前端做一些“胶水”逻辑,弥补当前 AI 模型在理解复杂指令、维持长期对话焦点、处理异常输入等方面的不足。 这种“引导”和“纠错”机制,有点像给 AI 装上了“护栏”,防止它和用户都“掉沟里”。
目前这些想法还比较初级,具体效果怎么样,还得在实际场景中不断测试和调整。AI 产品经理不好当啊(我这是兼职的)。
#AI模拟面试 #对话管理 #异常处理 #用户引导 #Prompt技巧 #DeepSeek #Vue3 #前端的救赎