crystelf-plugin/lib/ai/reactEngine.js
2025-12-14 10:26:26 +08:00

600 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import ConfigControl from '../config/configControl.js';
import toolRegistry from './tools/toolRegistry.js';
import OpenaiChat from '../../modules/openai/openaiChat.js';
/**
* ReAct引擎
* Reasoning + Acting 循环
*/
class ReactEngine {
constructor() {
this.maxIterations = 8;
this.timeout = 45000;
this.enableThinking = false;
}
async init() {
try {
const config = await ConfigControl.get('ai');
const reactConfig = config?.reactConfig || {};
this.maxIterations = reactConfig.maxIterations || 8;
this.timeout = reactConfig.timeout || 45000;
this.enableThinking = reactConfig.enableThinking || false;
logger.info('[crystelf-ai] ReAct引擎初始化完成');
} catch (error) {
logger.error(`[crystelf-ai] ReAct引擎初始化失败: ${error.message}`);
}
}
/**
* 执行标准ReAct循环
* @param {string} userInput - 用户输入
* @param {Object} context - 执行上下文
* @returns {Promise<Object>} 执行结果
*/
async execute(userInput, context) {
const startTime = Date.now();
const responseQueue = [];
const thinkingHistory = [];
try {
logger.info(`[crystelf-ai] 开始标准ReAct循环用户输入: ${userInput.substring(0, 50)}...`);
// 第一阶段:思考和信息收集
const phase1Result = await this.executePhase1_ThinkingAndCollection(userInput, context, thinkingHistory);
if (!phase1Result.success) {
return {
success: false,
error: phase1Result.error,
responses: [{
type: 'message',
data: '抱歉,我在分析您的问题时遇到了困难...',
at: -1,
quote: -1,
recall: false
}],
thinkingHistory: this.enableThinking ? thinkingHistory : []
};
}
// 第二阶段:基于收集的信息生成回复
const phase2Result = await this.executePhase2_ResponseGeneration(
userInput,
context,
phase1Result.collectedInfo,
phase1Result.conversationMessages,
responseQueue
);
return {
success: true,
responses: responseQueue,
thinkingHistory: this.enableThinking ? thinkingHistory : [],
collectedInfo: phase1Result.collectedInfo,
conversationMessages: this.enableThinking ? phase1Result.conversationMessages : [],
duration: Date.now() - startTime
};
} catch (error) {
logger.error(`[crystelf-ai] ReAct执行失败: ${error.message}`);
return {
success: false,
error: error.message,
responses: [{
type: 'message',
data: '抱歉,我在处理您的请求时遇到了问题...',
at: -1,
quote: -1,
recall: false
}],
thinkingHistory: this.enableThinking ? thinkingHistory : []
};
}
}
/**
* 第一阶段:思考和信息收集
* 只包含信息收集工具,不包含回复工具和人设
*/
async executePhase1_ThinkingAndCollection(userInput, context, thinkingHistory) {
const startTime = Date.now();
// 构建第一阶段的消息历史
const conversationMessages = await this.buildPhase1Messages(userInput, context);
// 获取信息收集工具(不包含回复工具)
const collectionTools = this.getCollectionTools();
logger.info(`[crystelf-ai] 第一阶段开始 - 思考和信息收集`);
let foundAnswer = false;
let collectedInfo = '';
// 🔥 物理循环for 循环提供执行框架
for (let iteration = 0; iteration < this.maxIterations; iteration++) {
// 检查超时
if (Date.now() - startTime > this.timeout) {
logger.warn(`[crystelf-ai] 第一阶段超时,已迭代 ${iteration}`);
break;
}
logger.info(`[crystelf-ai] 第一阶段第${iteration + 1}AI思考和决策`);
try {
// 🔥 每轮都是独立的 AI 调用
const aiResponse = await this.callAIWithTools(conversationMessages, collectionTools);
// 将AI的回复添加到对话历史
conversationMessages.push(aiResponse);
// 🔥 AI 可以做出不同决策
if (aiResponse.tool_calls && aiResponse.tool_calls.length > 0) {
// 决策1调用工具继续探索
logger.info(`[crystelf-ai] AI决定调用 ${aiResponse.tool_calls.length} 个工具`);
// 并行执行所有工具
const toolResults = await this.executeToolsParallel(
aiResponse.tool_calls,
conversationMessages,
context
);
// 记录思考步骤
thinkingHistory.push({
phase: 1,
iteration: iteration + 1,
aiThought: aiResponse.content || '进行工具调用',
toolCalls: aiResponse.tool_calls,
toolResults: toolResults,
timestamp: Date.now()
});
// 检查AI是否通过工具调用标记找到答案
const answerResult = toolResults.find(result =>
result.toolName === 'found_answer' || result.toolName === 'not_enough_info'
);
if (answerResult) {
if (answerResult.toolName === 'found_answer') {
// 决策2找到答案标记找到答案跳出循环到答案回复
foundAnswer = true;
collectedInfo = answerResult.result.answerSummary || '';
logger.info(`[crystelf-ai] AI找到答案跳出循环进入第二阶段`);
break; // 🔥 跳出物理循环
} else {
// AI明确表示信息不足结束收集
logger.info(`[crystelf-ai] AI认为信息不足结束收集阶段`);
return {
success: false,
error: `信息收集失败: ${answerResult.result.reason}`,
collectedInfo: '',
conversationMessages
};
}
}
} else {
// 决策3继续思考下一轮再决策
logger.info(`[crystelf-ai] AI进行纯思考: ${aiResponse.content?.substring(0, 100)}...`);
thinkingHistory.push({
phase: 1,
iteration: iteration + 1,
aiThought: aiResponse.content || '继续思考',
toolCalls: [],
toolResults: [],
timestamp: Date.now()
});
// continue 到下一轮循环
}
} catch (error) {
logger.error(`[crystelf-ai] 第一阶段第${iteration + 1}轮失败: ${error.message}`);
break;
}
}
// 如果循环结束仍未找到答案,进行最终评估
if (!foundAnswer) {
logger.info(`[crystelf-ai] 达到最大迭代次数,进行最终评估`);
const finalEvaluation = await this.performFinalEvaluation(conversationMessages, collectionTools);
if (finalEvaluation.foundAnswer) {
foundAnswer = true;
collectedInfo = finalEvaluation.answerSummary;
} else {
return {
success: false,
error: `经过${this.maxIterations}轮思考仍未找到足够信息`,
collectedInfo: '',
conversationMessages
};
}
}
return {
success: foundAnswer,
collectedInfo,
conversationMessages,
iterations: this.maxIterations
};
}
/**
* 第二阶段:基于收集的信息生成回复
* 包含人设和回复工具,基于第一阶段的思考结果
*/
async executePhase2_ResponseGeneration(userInput, context, collectedInfo, phase1Messages, responseQueue) {
logger.info(`[crystelf-ai] 第二阶段开始 - 基于收集信息生成回复`);
// 构建第二阶段的消息历史(包含人设和第一阶段结果)
const phase2Messages = await this.buildPhase2Messages(userInput, context, collectedInfo, phase1Messages);
// 获取回复工具(包含所有回复相关工具)
const responseTools = this.getResponseTools();
try {
// AI基于收集的信息生成回复
const aiResponse = await this.callAIWithTools(phase2Messages, responseTools);
if (aiResponse.tool_calls && aiResponse.tool_calls.length > 0) {
// AI调用回复工具
logger.info(`[crystelf-ai] AI调用 ${aiResponse.tool_calls.length} 个回复工具`);
await this.executeResponseTools(aiResponse.tool_calls, responseQueue, context);
} else if (aiResponse.content) {
// AI直接给出文本回复
responseQueue.push({
type: 'message',
data: aiResponse.content,
at: -1,
quote: -1,
recall: false
});
}
return { success: true };
} catch (error) {
logger.error(`[crystelf-ai] 第二阶段失败: ${error.message}`);
// 添加默认回复
responseQueue.push({
type: 'message',
data: '我已经收集了相关信息,但在生成回复时遇到了问题...',
at: -1,
quote: -1,
recall: false
});
return { success: false, error: error.message };
}
}
/**
* 构建第一阶段消息(思考阶段,不包含人设)
*/
async buildPhase1Messages(userInput, context) {
const systemPrompt = `你是一个信息分析专家,需要分析用户的问题并收集相关信息。
=== 分析任务 ===
用户问题:"${userInput}"
请分析:
1. 用户想要什么信息?
2. 需要搜索哪些相关内容?
3. 应该使用什么工具来收集信息?
=== 可用工具 ===
- search_memory: 搜索用户的历史记忆和偏好
- get_chat_history: 获取最近的聊天记录
- found_answer: 当收集到足够信息时标记找到答案
- not_enough_info: 当认为信息不足时标记
=== 重要规则 ===
1. 可以同时调用多个工具来并行收集信息
2. 仔细分析每个工具的结果
3. 当有足够信息回答用户问题时调用found_answer
4. 如果多次尝试仍无法找到足够信息调用not_enough_info
5. 专注于信息收集,不要尝试直接回答用户
请开始分析用户问题并收集相关信息。`;
return [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `请分析这个问题并收集相关信息:"${userInput}"` }
];
}
/**
* 构建第二阶段消息(回复阶段,包含人设和第一阶段结果)
*/
async buildPhase2Messages(userInput, context, collectedInfo, phase1Messages) {
const { e } = context;
const config = await ConfigControl.get();
const botInfo = {
id: e.bot?.uin || '未知',
name: config?.profile?.nickName || '晶灵',
};
const userInfo = {
id: e.user_id || e.sender?.user_id || '未知',
name: e.sender?.card || e.sender?.nickname || '用户',
isMaster: e.isMaster,
};
const now = Date.now();
const date = new Date(now);
const formatDate = date.toLocaleDateString('zh-CN');
const formatTime = date.toLocaleTimeString('zh-CN');
// 提取第一阶段的思考过程摘要
const thinkingProcess = this.extractThinkingProcess(phase1Messages);
const systemPrompt = `你是一个名为${botInfo.name}的智能助手,具有以下特征:
1. 性格温和友善,喜欢帮助用户解决问题
2. 知识渊博,能够回答各种问题
3. 偶尔会使用一些可爱的表情和语气
4. 会记住与用户的对话内容,提供个性化的回复
5. 能够理解中文语境和网络用语
6. 回复简洁明了,避免过于冗长
=== 当前对话信息 ===
- 你的昵称:${botInfo.name}
- 你的QQ号${botInfo.id}
- 用户昵称:${userInfo.name}
- 用户QQ号${userInfo.id}
- 用户是否为主人:${userInfo.isMaster ? '是' : '否'}
- 当前时间:${formatDate} ${formatTime}
=== 用户问题 ===
"${userInput}"
=== 已收集的信息 ===
${collectedInfo}
=== 思考过程摘要 ===
${thinkingProcess}
=== 回复指南 ===
基于以上收集的信息,请给用户一个准确、友好的回复。你可以使用以下工具:
- send_message: 发送文本消息(支持@用户和引用消息)
- send_meme: 发送表情包
- render_code: 渲染代码为图片
- render_markdown: 渲染Markdown为图片
- generate_image: 生成图片
- store_memory: 存储重要信息
- poke_user: 戳一戳用户
请根据收集的信息给出最合适的回复。`;
return [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `请基于收集的信息回复用户的问题:"${userInput}"` }
];
}
/**
* 获取信息收集工具(第一阶段使用)
*/
getCollectionTools() {
const allTools = toolRegistry.getToolSchemas();
const collectionToolNames = [
'search_memory',
'get_chat_history',
'found_answer',
'not_enough_info'
];
return allTools.filter(tool =>
collectionToolNames.includes(tool.function.name)
);
}
/**
* 获取回复工具(第二阶段使用)
*/
getResponseTools() {
const allTools = toolRegistry.getToolSchemas();
const responseToolNames = [
'send_message',
'send_meme',
'render_code',
'render_markdown',
'generate_image',
'store_memory',
'poke_user'
];
return allTools.filter(tool =>
responseToolNames.includes(tool.function.name)
);
}
/**
* 并行执行工具
*/
async executeToolsParallel(toolCalls, conversationMessages, context) {
const toolPromises = toolCalls.map(async (toolCall) => {
const toolName = toolCall.function.name;
const parameters = JSON.parse(toolCall.function.arguments);
logger.info(`[crystelf-ai] 执行工具: ${toolName}(${JSON.stringify(parameters)})`);
try {
const result = await toolRegistry.executeTool(toolName, parameters, context);
// 将工具结果添加到对话历史
conversationMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({
success: result.success,
message: result.message || result.result,
data: result
})
});
return {
toolName,
parameters,
result,
success: result.success !== false,
toolCallId: toolCall.id
};
} catch (error) {
logger.error(`[crystelf-ai] 工具执行失败: ${toolName} - ${error.message}`);
conversationMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({
success: false,
error: error.message
})
});
return {
toolName,
parameters,
result: { success: false, message: error.message },
success: false,
toolCallId: toolCall.id,
error: error.message
};
}
});
return await Promise.all(toolPromises);
}
/**
* 执行回复工具
*/
async executeResponseTools(toolCalls, responseQueue, context) {
for (const toolCall of toolCalls) {
const toolName = toolCall.function.name;
const parameters = JSON.parse(toolCall.function.arguments);
try {
const result = await toolRegistry.executeTool(toolName, parameters, {
...context,
responseQueue
});
logger.info(`[crystelf-ai] 回复工具执行成功: ${toolName}`);
} catch (error) {
logger.error(`[crystelf-ai] 回复工具执行失败: ${toolName} - ${error.message}`);
}
}
}
/**
* 最终评估(当达到最大迭代次数时)
*/
async performFinalEvaluation(conversationMessages, collectionTools) {
logger.info(`[crystelf-ai] 执行最终评估`);
// 添加最终评估提示
conversationMessages.push({
role: 'user',
content: '已达到最大搜索次数请基于目前收集的所有信息做最终判断是否有足够信息回答用户问题请调用found_answer或not_enough_info工具。'
});
try {
const aiResponse = await this.callAIWithTools(conversationMessages, collectionTools);
if (aiResponse.tool_calls && aiResponse.tool_calls.length > 0) {
const finalTool = aiResponse.tool_calls[0];
const toolName = finalTool.function.name;
const parameters = JSON.parse(finalTool.function.arguments);
if (toolName === 'found_answer') {
return {
foundAnswer: true,
answerSummary: parameters.answer_summary || '基于收集的信息可以回答'
};
}
}
return { foundAnswer: false };
} catch (error) {
logger.error(`[crystelf-ai] 最终评估失败: ${error.message}`);
return { foundAnswer: false };
}
}
/**
* 提取思考过程摘要
*/
extractThinkingProcess(phase1Messages) {
const thinkingSteps = [];
for (let i = 0; i < phase1Messages.length; i++) {
const message = phase1Messages[i];
if (message.role === 'assistant' && message.tool_calls) {
const toolNames = message.tool_calls.map(tc => tc.function.name).join(', ');
thinkingSteps.push(`调用了工具: ${toolNames}`);
} else if (message.role === 'tool') {
try {
const toolResult = JSON.parse(message.content);
if (toolResult.success) {
thinkingSteps.push(`获得结果: ${toolResult.message}`);
}
} catch (e) {
// 忽略解析错误
}
}
}
return thinkingSteps.length > 0 ? thinkingSteps.join('\n') : '进行了信息收集和分析';
}
/**
* 调用AI工具调用模式
*/
async callAIWithTools(messages, tools) {
const config = await ConfigControl.get('ai');
const apiCaller = new OpenaiChat();
apiCaller.init(config.apiKey, config.baseApi);
try {
const completion = await apiCaller.openai.chat.completions.create({
model: config.modelType,
messages: messages,
tools: tools,
tool_choice: 'auto',
temperature: config.temperature || 0.7,
parallel_tool_calls: true
});
const message = completion.choices[0].message;
if (message.tool_calls && message.tool_calls.length > 0) {
logger.info(`[crystelf-ai] AI调用工具: ${message.tool_calls.map(tc => tc.function.name).join(', ')}`);
} else {
logger.info(`[crystelf-ai] AI文本回复: ${message.content?.substring(0, 50)}...`);
}
return message;
} catch (error) {
logger.error(`[crystelf-ai] AI调用失败: ${error.message}`);
throw error;
}
}
}
export default new ReactEngine();