mirror of
https://github.com/Jerryplusy/crystelf-plugin.git
synced 2026-01-29 01:07:27 +00:00
600 lines
19 KiB
JavaScript
600 lines
19 KiB
JavaScript
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(); |