MaiBot/src/memory_system/memory_retrieval.py

1057 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import time
import json
import asyncio
from typing import List, Dict, Any, Optional, Tuple
from src.common.logger import get_logger
from src.config.config import global_config, model_config
from src.prompt.prompt_manager import prompt_manager
from src.plugin_system.apis import llm_api
from src.common.database.database_model import ThinkingBack
from src.memory_system.retrieval_tools import get_tool_registry, init_all_tools
from src.llm_models.payload_content.message import MessageBuilder, RoleType, Message
from src.chat.message_receive.chat_stream import get_chat_manager
from src.bw_learner.jargon_explainer import retrieve_concepts_with_jargon
logger = get_logger("memory_retrieval")
THINKING_BACK_NOT_FOUND_RETENTION_SECONDS = 36000 # 未找到答案记录保留时长
THINKING_BACK_CLEANUP_INTERVAL_SECONDS = 3000 # 清理频率
_last_not_found_cleanup_ts: float = 0.0
def _cleanup_stale_not_found_thinking_back() -> None:
"""定期清理过期的未找到答案记录"""
global _last_not_found_cleanup_ts
now = time.time()
if now - _last_not_found_cleanup_ts < THINKING_BACK_CLEANUP_INTERVAL_SECONDS:
return
threshold_time = now - THINKING_BACK_NOT_FOUND_RETENTION_SECONDS
try:
deleted_rows = (
ThinkingBack.delete()
.where((ThinkingBack.found_answer == 0) & (ThinkingBack.update_time < threshold_time))
.execute()
)
if deleted_rows:
logger.info(f"清理过期的未找到答案thinking_back记录 {deleted_rows}")
_last_not_found_cleanup_ts = now
except Exception as e:
logger.error(f"清理未找到答案的thinking_back记录失败: {e}")
def init_memory_retrieval_sys():
"""初始化记忆检索相关工具"""
# 注册所有工具
init_all_tools()
def _log_conversation_messages(
conversation_messages: List[Message],
head_prompt: Optional[str] = None,
final_status: Optional[str] = None,
) -> None:
"""输出对话消息列表的日志
Args:
conversation_messages: 对话消息列表
head_prompt: 第一条系统消息head_prompt的内容可选
final_status: 最终结果状态描述(例如:找到答案/未找到答案),可选
"""
if not global_config.debug.show_memory_prompt:
return
log_lines: List[str] = []
# 如果有head_prompt先添加为第一条消息
if head_prompt:
msg_info = "========================================\n[消息 1] 角色: System\n-----------------------------"
msg_info += f"\n{head_prompt}"
log_lines.append(msg_info)
start_idx = 2
else:
start_idx = 1
if not conversation_messages and not head_prompt:
return
for idx, msg in enumerate(conversation_messages, start_idx):
role_name = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
# 构建单条消息的日志信息
# msg_info = f"\n========================================\n[消息 {idx}] 角色: {role_name} 内容类型: {content_type}\n-----------------------------"
msg_info = (
f"\n========================================\n[消息 {idx}] 角色: {role_name}\n-----------------------------"
)
# if full_content:
# msg_info += f"\n{full_content}"
if msg.content:
msg_info += f"\n{msg.content}"
if msg.tool_calls:
msg_info += f"\n 工具调用: {len(msg.tool_calls)}"
for tool_call in msg.tool_calls:
msg_info += f"\n - {tool_call.func_name}: {json.dumps(tool_call.args, ensure_ascii=False)}"
# if msg.tool_call_id:
# msg_info += f"\n 工具调用ID: {msg.tool_call_id}"
log_lines.append(msg_info)
total_count = len(conversation_messages) + (1 if head_prompt else 0)
log_text = f"消息列表 (共{total_count}条):{''.join(log_lines)}"
if final_status:
log_text += f"\n\n[最终结果] {final_status}"
logger.info(log_text)
async def _react_agent_solve_question(
chat_id: str,
max_iterations: int = 5,
timeout: float = 30.0,
initial_info: str = "",
chat_history: str = "",
) -> Tuple[bool, str, List[Dict[str, Any]], bool]:
"""使用ReAct架构的Agent来解决问题
Args:
chat_id: 聊天ID
max_iterations: 最大迭代次数
timeout: 超时时间(秒)
initial_info: 初始信息将作为collected_info的初始值
chat_history: 聊天记录,将传递给 ReAct Agent prompt
Returns:
Tuple[bool, str, List[Dict[str, Any]], bool]: (是否找到答案, 答案内容, 思考步骤列表, 是否超时)
"""
start_time = time.time()
collected_info = initial_info if initial_info else ""
# 构造日志前缀:[聊天流名称],用于在日志中标识聊天流
try:
chat_name = get_chat_manager().get_stream_name(chat_id) or chat_id
except Exception:
chat_name = chat_id
react_log_prefix = f"[{chat_name}] "
thinking_steps = []
is_timeout = False
conversation_messages: List[Message] = []
first_head_prompt: Optional[str] = None # 保存第一次使用的head_prompt用于日志显示
last_tool_name: Optional[str] = None # 记录最后一次使用的工具名称
# 使用 while 循环,支持额外迭代
iteration = 0
max_iterations_with_extra = max_iterations
while iteration < max_iterations_with_extra:
# 检查超时
if time.time() - start_time > timeout:
logger.warning(f"ReAct Agent超时已迭代{iteration}")
is_timeout = True
break
# 获取工具注册器
tool_registry = get_tool_registry()
# 获取bot_name
bot_name = global_config.bot.nickname
# 获取当前时间
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
# 计算剩余迭代次数
current_iteration = iteration + 1
remaining_iterations = max_iterations - current_iteration
# 提取函数调用中参数的值,支持单引号和双引号
def extract_quoted_content(text, func_name, param_name):
"""从文本中提取函数调用中参数的值,支持单引号和双引号
Args:
text: 要搜索的文本
func_name: 函数名,如 'return_information'
param_name: 参数名,如 'information'
Returns:
提取的参数值如果未找到则返回None
"""
if not text:
return None
# 查找函数调用位置(不区分大小写)
func_pattern = func_name.lower()
text_lower = text.lower()
func_pos = text_lower.find(func_pattern)
if func_pos == -1:
return None
# 查找参数名和等号
param_pattern = f"{param_name}="
param_pos = text_lower.find(param_pattern, func_pos)
if param_pos == -1:
return None
# 跳过参数名、等号和空白
start_pos = param_pos + len(param_pattern)
while start_pos < len(text) and text[start_pos] in " \t\n":
start_pos += 1
if start_pos >= len(text):
return None
# 确定引号类型
quote_char = text[start_pos]
if quote_char not in ['"', "'"]:
return None
# 查找匹配的结束引号(考虑转义)
end_pos = start_pos + 1
while end_pos < len(text):
if text[end_pos] == quote_char:
# 检查是否是转义的引号
if end_pos > start_pos + 1 and text[end_pos - 1] == "\\":
end_pos += 1
continue
# 找到匹配的引号
content = text[start_pos + 1 : end_pos]
# 处理转义字符
content = content.replace('\\"', '"').replace("\\'", "'").replace("\\\\", "\\")
return content
end_pos += 1
return None
# 正常迭代使用head_prompt决定调用哪些工具包含return_information工具
tool_definitions = tool_registry.get_tool_definitions()
# tool_names = [tool_def["name"] for tool_def in tool_definitions]
# logger.debug(f"ReAct Agent 第 {iteration + 1} 次迭代,问题: {question}|可用工具: {', '.join(tool_names)} (共{len(tool_definitions)}个)")
# head_prompt应该只构建一次使用初始的collected_info后续迭代都复用同一个
if first_head_prompt is None:
# 第一次构建使用初始的collected_info即initial_info
initial_collected_info = initial_info if initial_info else ""
# 根据配置选择使用哪个 prompt
prompt_name = (
"memory_retrieval_react_prompt_head_lpmm"
if global_config.experimental.lpmm_memory
else "memory_retrieval_react_prompt_head"
)
first_head_prompt_template = prompt_manager.get_prompt(prompt_name)
first_head_prompt_template.add_context("bot_name", bot_name)
first_head_prompt_template.add_context("time_now", time_now)
first_head_prompt_template.add_context("chat_history", chat_history)
first_head_prompt_template.add_context("collected_info", initial_collected_info)
first_head_prompt_template.add_context("current_iteration", str(current_iteration))
first_head_prompt_template.add_context("remaining_iterations", str(remaining_iterations))
first_head_prompt_template.add_context("max_iterations", str(max_iterations))
first_head_prompt = await prompt_manager.render_prompt(first_head_prompt_template)
# 后续迭代都复用第一次构建的head_prompt
head_prompt = first_head_prompt
def message_factory(
_client,
*,
_head_prompt: str = head_prompt,
_conversation_messages: List[Message] = conversation_messages,
) -> List[Message]:
messages: List[Message] = []
system_builder = MessageBuilder()
system_builder.set_role(RoleType.System)
system_builder.add_text_content(_head_prompt)
messages.append(system_builder.build())
messages.extend(_conversation_messages)
return messages
(
success,
response,
reasoning_content,
model_name,
tool_calls,
) = await llm_api.generate_with_model_with_tools_by_message_factory(
message_factory,
model_config=model_config.model_task_config.tool_use,
tool_options=tool_definitions,
request_type="memory.react",
)
# logger.info(
# f"ReAct Agent 第 {iteration + 1} 次迭代 模型: {model_name} ,调用工具数量: {len(tool_calls) if tool_calls else 0} ,调用工具响应: {response}"
# )
if not success:
logger.error(f"ReAct Agent LLM调用失败: {response}")
break
# 注意这里会检查return_information工具调用如果检测到return_information工具会根据information参数决定返回信息或退出查询
assistant_message: Optional[Message] = None
if tool_calls:
assistant_builder = MessageBuilder()
assistant_builder.set_role(RoleType.Assistant)
if response and response.strip():
assistant_builder.add_text_content(response)
assistant_builder.set_tool_calls(tool_calls)
assistant_message = assistant_builder.build()
elif response and response.strip():
assistant_builder = MessageBuilder()
assistant_builder.set_role(RoleType.Assistant)
assistant_builder.add_text_content(response)
assistant_message = assistant_builder.build()
# 记录思考步骤
step = {"iteration": iteration + 1, "thought": response, "actions": [], "observations": []}
if assistant_message:
conversation_messages.append(assistant_message)
# 记录思考过程到collected_info中
if reasoning_content or response:
thought_summary = reasoning_content or (response[:200] if response else "")
if thought_summary:
collected_info += f"\n[思考] {thought_summary}\n"
# 处理工具调用
if not tool_calls:
# 如果没有工具调用检查响应文本中是否包含return_information函数调用格式或JSON格式
if response and response.strip():
# 首先尝试解析JSON格式的return_information
def parse_json_return_information(text: str):
"""从文本中解析JSON格式的return_information返回information字符串如果未找到则返回None"""
if not text:
return None, None
try:
# 尝试提取JSON对象可能包含在代码块中或直接是JSON
json_text = text.strip()
# 如果包含代码块标记提取JSON部分
if "```json" in json_text:
start = json_text.find("```json") + 7
end = json_text.find("```", start)
if end != -1:
json_text = json_text[start:end].strip()
elif "```" in json_text:
start = json_text.find("```") + 3
end = json_text.find("```", start)
if end != -1:
json_text = json_text[start:end].strip()
# 尝试解析JSON
data = json.loads(json_text)
# 检查是否包含return_information字段
if isinstance(data, dict) and "return_information" in data:
information = data.get("information", "")
return information
except (json.JSONDecodeError, ValueError, TypeError):
# 如果JSON解析失败尝试在文本中查找JSON对象
try:
# 查找第一个 { 和最后一个 } 之间的内容更健壮的JSON提取
first_brace = text.find("{")
if first_brace != -1:
# 从第一个 { 开始,找到匹配的 }
brace_count = 0
json_end = -1
for i in range(first_brace, len(text)):
if text[i] == "{":
brace_count += 1
elif text[i] == "}":
brace_count -= 1
if brace_count == 0:
json_end = i + 1
break
if json_end != -1:
json_text = text[first_brace:json_end]
data = json.loads(json_text)
if isinstance(data, dict) and "return_information" in data:
information = data.get("information", "")
return information
except (json.JSONDecodeError, ValueError, TypeError):
pass
return None
# 尝试从文本中解析return_information函数调用
def parse_return_information_from_text(text: str):
"""从文本中解析return_information函数调用返回information字符串如果未找到则返回None"""
if not text:
return None
# 查找return_information函数调用位置不区分大小写
func_pattern = "return_information"
text_lower = text.lower()
func_pos = text_lower.find(func_pattern)
if func_pos == -1:
return None
# 解析information参数字符串使用extract_quoted_content
information = extract_quoted_content(text, "return_information", "information")
# 如果information存在即使是空字符串也返回它
return information
# 首先尝试解析JSON格式
parsed_information_json = parse_json_return_information(response)
is_json_format = parsed_information_json is not None
# 如果JSON解析成功使用JSON结果
if is_json_format:
parsed_information = parsed_information_json
else:
# 如果JSON解析失败尝试解析函数调用格式
parsed_information = parse_return_information_from_text(response)
if parsed_information is not None or is_json_format:
# 检测到return_information格式可能是JSON格式或函数调用格式
format_type = "JSON格式" if is_json_format else "函数调用格式"
# 返回信息(即使为空字符串也返回)
step["actions"].append(
{
"action_type": "return_information",
"action_params": {"information": parsed_information or ""},
}
)
if parsed_information and parsed_information.strip():
step["observations"] = [f"检测到return_information{format_type}调用,返回信息"]
thinking_steps.append(step)
logger.info(
f"{react_log_prefix}{iteration + 1} 次迭代 通过return_information{format_type}返回信息: {parsed_information[:100]}..."
)
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
final_status=f"返回信息:{parsed_information}",
)
return True, parsed_information, thinking_steps, False
else:
# 信息为空,直接退出查询
step["observations"] = [f"检测到return_information{format_type}调用,信息为空"]
thinking_steps.append(step)
logger.info(
f"{react_log_prefix}{iteration + 1} 次迭代 通过return_information{format_type}判断信息为空"
)
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
final_status="信息为空通过return_information文本格式判断信息为空",
)
return False, "", thinking_steps, False
# 如果没有检测到return_information格式记录思考过程继续下一轮迭代
step["observations"] = [f"思考完成,但未调用工具。响应: {response}"]
logger.info(f"{react_log_prefix}{iteration + 1} 次迭代 思考完成但未调用工具: {response}")
collected_info += f"思考: {response}"
else:
logger.warning(f"{react_log_prefix}{iteration + 1} 次迭代 无工具调用且无响应")
step["observations"] = ["无响应且无工具调用"]
thinking_steps.append(step)
iteration += 1 # 在continue之前增加迭代计数避免跳过iteration += 1
continue
# 处理工具调用
# 首先检查是否有return_information工具调用如果有则立即返回不再处理其他工具
return_information_info = None
for tool_call in tool_calls:
tool_name = tool_call.func_name
tool_args = tool_call.args or {}
if tool_name == "return_information":
return_information_info = tool_args.get("information", "")
# 返回信息(即使为空也返回)
step["actions"].append(
{
"action_type": "return_information",
"action_params": {"information": return_information_info},
}
)
if return_information_info and return_information_info.strip():
# 有信息,返回
step["observations"] = ["检测到return_information工具调用返回信息"]
thinking_steps.append(step)
logger.info(
f"{react_log_prefix}{iteration + 1} 次迭代 通过return_information工具返回信息: {return_information_info}"
)
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
final_status=f"返回信息:{return_information_info}",
)
return True, return_information_info, thinking_steps, False
else:
# 信息为空,直接退出查询
step["observations"] = ["检测到return_information工具调用信息为空"]
thinking_steps.append(step)
logger.info(f"{react_log_prefix}{iteration + 1} 次迭代 通过return_information工具判断信息为空")
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
final_status="信息为空通过return_information工具判断信息为空",
)
return False, "", thinking_steps, False
# 如果没有return_information工具调用继续处理其他工具
tool_tasks = []
for i, tool_call in enumerate(tool_calls):
tool_name = tool_call.func_name
tool_args = tool_call.args or {}
logger.debug(
f"{react_log_prefix}{iteration + 1} 次迭代 工具调用 {i + 1}/{len(tool_calls)}: {tool_name}({tool_args})"
)
# 跳过return_information工具调用已经在上面处理过了
if tool_name == "return_information":
continue
# 记录最后一次使用的工具名称(用于判断是否需要额外迭代)
last_tool_name = tool_name
# 普通工具调用
tool = tool_registry.get_tool(tool_name)
if tool:
# 准备工具参数需要添加chat_id如果工具需要
import inspect
sig = inspect.signature(tool.execute_func)
tool_params = tool_args.copy()
if "chat_id" in sig.parameters:
tool_params["chat_id"] = chat_id
# 创建异步任务
async def execute_single_tool(tool_instance, params, tool_name_str, iter_num):
try:
observation = await tool_instance.execute(**params)
param_str = ", ".join([f"{k}={v}" for k, v in params.items() if k != "chat_id"])
return f"查询{tool_name_str}({param_str})的结果:{observation}"
except Exception as e:
error_msg = f"工具执行失败: {str(e)}"
logger.error(f"{react_log_prefix}{iter_num + 1} 次迭代 工具 {tool_name_str} {error_msg}")
return f"查询{tool_name_str}失败: {error_msg}"
tool_tasks.append(execute_single_tool(tool, tool_params, tool_name, iteration))
step["actions"].append({"action_type": tool_name, "action_params": tool_args})
else:
error_msg = f"未知的工具类型: {tool_name}"
logger.warning(
f"{react_log_prefix}{iteration + 1} 次迭代 工具 {i + 1}/{len(tool_calls)} {error_msg}"
)
tool_tasks.append(asyncio.create_task(asyncio.sleep(0, result=f"查询{tool_name}失败: {error_msg}")))
# 并行执行所有工具
if tool_tasks:
observations = await asyncio.gather(*tool_tasks, return_exceptions=True)
# 处理执行结果
for i, (tool_call_item, observation) in enumerate(zip(tool_calls, observations, strict=False)):
if isinstance(observation, Exception):
observation = f"工具执行异常: {str(observation)}"
logger.error(f"{react_log_prefix}{iteration + 1} 次迭代 工具 {i + 1} 执行异常: {observation}")
observation_text = observation if isinstance(observation, str) else str(observation)
stripped_observation = observation_text.strip()
step["observations"].append(observation_text)
collected_info += f"\n{observation_text}\n"
if stripped_observation:
# 不再自动检测工具输出中的jargon改为通过 query_words 工具主动查询
tool_builder = MessageBuilder()
tool_builder.set_role(RoleType.Tool)
tool_builder.add_text_content(observation_text)
tool_builder.add_tool_call(tool_call_item.call_id)
conversation_messages.append(tool_builder.build())
thinking_steps.append(step)
# 检查是否需要额外迭代:如果最后一次使用的工具是 search_chat_history 且达到最大迭代次数,额外增加一回合
if iteration + 1 >= max_iterations and last_tool_name == "search_chat_history" and not is_timeout:
max_iterations_with_extra = max_iterations + 1
logger.info(
f"{react_log_prefix}达到最大迭代次数(已迭代{iteration + 1}次),最后一次使用工具为 search_chat_history额外增加一回合尝试"
)
iteration += 1
# 正常迭代结束后,如果达到最大迭代次数或超时,执行最终评估
# 最终评估单独处理,不算在迭代中
should_do_final_evaluation = False
if is_timeout:
should_do_final_evaluation = True
logger.warning(f"{react_log_prefix}超时,已迭代{iteration}次,进入最终评估")
elif iteration >= max_iterations:
should_do_final_evaluation = True
logger.info(f"{react_log_prefix}达到最大迭代次数(已迭代{iteration}次),进入最终评估")
if should_do_final_evaluation:
# 获取必要变量用于最终评估
tool_registry = get_tool_registry()
bot_name = global_config.bot.nickname
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
current_iteration = iteration + 1
remaining_iterations = 0
# 提取函数调用中参数的值,支持单引号和双引号
def extract_quoted_content(text, func_name, param_name):
"""从文本中提取函数调用中参数的值,支持单引号和双引号
Args:
text: 要搜索的文本
func_name: 函数名,如 'return_information'
param_name: 参数名,如 'information'
Returns:
提取的参数值如果未找到则返回None
"""
if not text:
return None
# 查找函数调用位置(不区分大小写)
func_pattern = func_name.lower()
text_lower = text.lower()
func_pos = text_lower.find(func_pattern)
if func_pos == -1:
return None
# 查找参数名和等号
param_pattern = f"{param_name}="
param_pos = text_lower.find(param_pattern, func_pos)
if param_pos == -1:
return None
# 跳过参数名、等号和空白
start_pos = param_pos + len(param_pattern)
while start_pos < len(text) and text[start_pos] in " \t\n":
start_pos += 1
if start_pos >= len(text):
return None
# 确定引号类型
quote_char = text[start_pos]
if quote_char not in ['"', "'"]:
return None
# 查找匹配的结束引号(考虑转义)
end_pos = start_pos + 1
while end_pos < len(text):
if text[end_pos] == quote_char:
# 检查是否是转义的引号
if end_pos > start_pos + 1 and text[end_pos - 1] == "\\":
end_pos += 1
continue
# 找到匹配的引号
content = text[start_pos + 1 : end_pos]
# 处理转义字符
content = content.replace('\\"', '"').replace("\\'", "'").replace("\\\\", "\\")
return content
end_pos += 1
return None
# 执行最终评估
evaluation_prompt_template = prompt_manager.get_prompt("memory_retrieval_react_final_prompt")
evaluation_prompt_template.add_context("bot_name", bot_name)
evaluation_prompt_template.add_context("time_now", time_now)
evaluation_prompt_template.add_context("chat_history", chat_history)
evaluation_prompt_template.add_context("collected_info", collected_info if collected_info else "暂无信息")
evaluation_prompt_template.add_context("current_iteration", str(current_iteration))
evaluation_prompt_template.add_context("remaining_iterations", str(remaining_iterations))
evaluation_prompt_template.add_context("max_iterations", str(max_iterations))
evaluation_prompt = await prompt_manager.render_prompt(evaluation_prompt_template)
(
eval_success,
eval_response,
eval_reasoning_content,
eval_model_name,
eval_tool_calls,
) = await llm_api.generate_with_model_with_tools(
evaluation_prompt,
model_config=model_config.model_task_config.tool_use,
tool_options=[], # 最终评估阶段不提供工具
request_type="memory.react.final",
)
if not eval_success:
logger.error(f"ReAct Agent 最终评估阶段 LLM调用失败: {eval_response}")
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
final_status="未找到答案最终评估阶段LLM调用失败",
)
return False, "最终评估阶段LLM调用失败", thinking_steps, is_timeout
if global_config.debug.show_memory_prompt:
logger.info(f"{react_log_prefix}最终评估Prompt: {evaluation_prompt}")
logger.info(f"{react_log_prefix}最终评估响应: {eval_response}")
# 从最终评估响应中提取return_information
return_information_content = None
if eval_response:
return_information_content = extract_quoted_content(eval_response, "return_information", "information")
# 如果提取到信息,返回(无论是否超时,都视为成功完成)
if return_information_content is not None:
eval_step = {
"iteration": current_iteration,
"thought": f"[最终评估] {eval_response}",
"actions": [
{"action_type": "return_information", "action_params": {"information": return_information_content}}
],
"observations": ["最终评估阶段检测到return_information"],
}
thinking_steps.append(eval_step)
if return_information_content and return_information_content.strip():
logger.info(f"ReAct Agent 最终评估阶段返回信息: {return_information_content}")
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
final_status=f"返回信息:{return_information_content}",
)
return True, return_information_content, thinking_steps, False
else:
logger.info("ReAct Agent 最终评估阶段判断信息为空")
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
final_status="信息为空:最终评估阶段判断信息为空",
)
return False, "", thinking_steps, False
# 如果没有明确判断视为not_enough_info返回空字符串不返回任何信息
eval_step = {
"iteration": current_iteration,
"thought": f"[最终评估] {eval_response}",
"actions": [{"action_type": "return_information", "action_params": {"information": ""}}],
"observations": ["已到达最大迭代次数,信息为空"],
}
thinking_steps.append(eval_step)
logger.info("ReAct Agent 已到达最大迭代次数,信息为空")
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
final_status="未找到答案:已到达最大迭代次数,无法找到答案",
)
return False, "", thinking_steps, is_timeout
# 如果正常迭代过程中提前找到答案返回,不会到达这里
# 如果正常迭代结束但没有触发最终评估(理论上不应该发生),直接返回
logger.warning("ReAct Agent正常迭代结束但未触发最终评估")
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
final_status="未找到答案:正常迭代结束",
)
return False, "", thinking_steps, is_timeout
def _get_recent_query_history(chat_id: str, time_window_seconds: float = 600.0) -> str:
"""获取最近一段时间内的查询历史(用于避免重复查询)
Args:
chat_id: 聊天ID
time_window_seconds: 时间窗口默认10分钟
Returns:
str: 格式化的查询历史字符串
"""
try:
current_time = time.time()
start_time = current_time - time_window_seconds
# 查询最近时间窗口内的记录,按更新时间倒序
records = (
ThinkingBack.select()
.where((ThinkingBack.chat_id == chat_id) & (ThinkingBack.update_time >= start_time))
.order_by(ThinkingBack.update_time.desc())
.limit(5) # 最多返回5条最近的记录
)
if not records.exists():
return ""
history_lines = []
history_lines.append("最近已查询的问题和结果:")
for record in records:
status = "✓ 已找到答案" if record.found_answer else "✗ 未找到答案"
answer_preview = ""
# 只有找到答案时才显示答案内容
if record.found_answer and record.answer:
# 截取答案前100字符
answer_preview = record.answer[:100]
if len(record.answer) > 100:
answer_preview += "..."
history_lines.append(f"- 问题:{record.question}")
history_lines.append(f" 状态:{status}")
if answer_preview:
history_lines.append(f" 答案:{answer_preview}")
history_lines.append("") # 空行分隔
return "\n".join(history_lines)
except Exception as e:
logger.error(f"获取查询历史失败: {e}")
return ""
def _get_recent_found_answers(chat_id: str, time_window_seconds: float = 600.0) -> List[str]:
"""获取最近一段时间内已找到答案的查询记录(用于返回给 replyer
Args:
chat_id: 聊天ID
time_window_seconds: 时间窗口默认10分钟
Returns:
List[str]: 格式化的答案列表,每个元素格式为 "问题xxx\n答案xxx"
"""
try:
current_time = time.time()
start_time = current_time - time_window_seconds
# 查询最近时间窗口内已找到答案的记录,按更新时间倒序
records = (
ThinkingBack.select()
.where(
(ThinkingBack.chat_id == chat_id)
& (ThinkingBack.update_time >= start_time)
& (ThinkingBack.found_answer == 1)
& (ThinkingBack.answer.is_null(False))
& (ThinkingBack.answer != "")
)
.order_by(ThinkingBack.update_time.desc())
.limit(3) # 最多返回5条最近的记录
)
if not records.exists():
return []
found_answers = []
for record in records:
if record.answer:
found_answers.append(f"问题:{record.question}\n答案:{record.answer}")
return found_answers
except Exception as e:
logger.error(f"获取最近已找到答案的记录失败: {e}")
return []
def _store_thinking_back(
chat_id: str, question: str, context: str, found_answer: bool, answer: str, thinking_steps: List[Dict[str, Any]]
) -> None:
"""存储或更新思考过程到数据库(如果已存在则更新,否则创建)
Args:
chat_id: 聊天ID
question: 问题
context: 上下文信息
found_answer: 是否找到答案
answer: 答案内容
thinking_steps: 思考步骤列表
"""
try:
now = time.time()
# 先查询是否已存在相同chat_id和问题的记录
existing = (
ThinkingBack.select()
.where((ThinkingBack.chat_id == chat_id) & (ThinkingBack.question == question))
.order_by(ThinkingBack.update_time.desc())
.limit(1)
)
if existing.exists():
# 更新现有记录
record = existing.get()
record.context = context
record.found_answer = found_answer
record.answer = answer
record.thinking_steps = json.dumps(thinking_steps, ensure_ascii=False)
record.update_time = now
record.save()
logger.info(f"已更新思考过程到数据库,问题: {question[:50]}...")
else:
# 创建新记录
ThinkingBack.create(
chat_id=chat_id,
question=question,
context=context,
found_answer=found_answer,
answer=answer,
thinking_steps=json.dumps(thinking_steps, ensure_ascii=False),
create_time=now,
update_time=now,
)
# logger.info(f"已创建思考过程到数据库,问题: {question[:50]}...")
except Exception as e:
logger.error(f"存储思考过程失败: {e}")
async def _process_memory_retrieval(
chat_id: str,
context: str,
initial_info: str = "",
max_iterations: Optional[int] = None,
chat_history: str = "",
) -> Optional[str]:
"""处理记忆检索
Args:
chat_id: 聊天ID
context: 上下文信息
initial_info: 初始信息将传递给ReAct Agent
max_iterations: 最大迭代次数
chat_history: 聊天记录,将传递给 ReAct Agent
Returns:
Optional[str]: 如果找到答案返回答案内容否则返回None
"""
_cleanup_stale_not_found_thinking_back()
question_initial_info = initial_info or ""
# 直接使用ReAct Agent进行记忆检索
# 如果未指定max_iterations使用配置的默认值
if max_iterations is None:
max_iterations = global_config.memory.max_agent_iterations
found_answer, answer, thinking_steps, is_timeout = await _react_agent_solve_question(
chat_id=chat_id,
max_iterations=max_iterations,
timeout=global_config.memory.agent_timeout_seconds,
initial_info=question_initial_info,
chat_history=chat_history,
)
# 不再存储到数据库,直接返回答案
if is_timeout:
logger.info("ReAct Agent超时不返回结果")
if found_answer and answer:
return answer
return None
async def build_memory_retrieval_prompt(
message: str,
sender: str,
target: str,
chat_stream,
think_level: int = 1,
unknown_words: Optional[List[str]] = None,
) -> str:
"""构建记忆检索提示
Args:
message: 聊天历史记录
sender: 发送者名称
target: 目标消息内容
chat_stream: 聊天流对象
think_level: 思考深度等级
unknown_words: Planner 提供的未知词语列表,优先使用此列表而不是从聊天记录匹配
Returns:
str: 记忆检索结果字符串
"""
start_time = time.time()
# 构造日志前缀:[聊天流名称],用于在日志中标识聊天流(优先群名称/用户昵称)
try:
group_info = chat_stream.group_info
user_info = chat_stream.user_info
# 群聊优先使用群名称
if group_info is not None and getattr(group_info, "group_name", None):
stream_name = group_info.group_name.strip() or str(group_info.group_id)
# 私聊使用用户昵称
elif user_info is not None and getattr(user_info, "user_nickname", None):
stream_name = user_info.user_nickname.strip() or str(user_info.user_id)
# 兜底使用 stream_id
else:
stream_name = chat_stream.stream_id
except Exception:
stream_name = chat_stream.stream_id
log_prefix = f"[{stream_name}] " if stream_name else ""
logger.info(f"{log_prefix}检测是否需要回忆,元消息:{message[:30]}...,消息长度: {len(message)}")
try:
chat_id = chat_stream.stream_id
# 初始阶段:使用 Planner 提供的 unknown_words 进行检索(如果提供)
initial_info = ""
if unknown_words and len(unknown_words) > 0:
# 清理和去重 unknown_words
cleaned_concepts = []
for word in unknown_words:
if isinstance(word, str):
cleaned = word.strip()
if cleaned:
cleaned_concepts.append(cleaned)
if cleaned_concepts:
# 对匹配到的概念进行jargon检索作为初始信息
concept_info = await retrieve_concepts_with_jargon(cleaned_concepts, chat_id)
if concept_info:
initial_info += concept_info
logger.info(
f"{log_prefix}使用 Planner 提供的 unknown_words{len(cleaned_concepts)} 个概念,检索结果: {concept_info[:100]}..."
)
else:
logger.debug(f"{log_prefix}unknown_words 检索未找到任何结果")
# 直接使用 ReAct Agent 进行记忆检索(跳过问题生成步骤)
base_max_iterations = global_config.memory.max_agent_iterations
# 根据think_level调整迭代次数think_level=1时不变think_level=0时减半
if think_level == 0:
max_iterations = max(1, base_max_iterations // 2) # 至少为1
else:
max_iterations = base_max_iterations
timeout_seconds = global_config.memory.agent_timeout_seconds
logger.debug(
f"{log_prefix}直接使用 ReAct Agent 进行记忆检索think_level={think_level},设置最大迭代次数: {max_iterations}(基础值: {base_max_iterations}),超时时间: {timeout_seconds}"
)
# 直接调用 ReAct Agent 处理记忆检索
try:
result = await _process_memory_retrieval(
chat_id=chat_id,
context=message,
initial_info=initial_info,
max_iterations=max_iterations,
chat_history=message,
)
except Exception as e:
logger.error(f"{log_prefix}处理记忆检索时发生异常: {e}")
result = None
end_time = time.time()
if result:
logger.info(f"{log_prefix}记忆检索成功,耗时: {(end_time - start_time):.3f}")
return f"你回忆起了以下信息:\n{result}\n如果与回复内容相关,可以参考这些回忆的信息。\n"
else:
logger.debug(f"{log_prefix}记忆检索未找到相关信息")
return ""
except Exception as e:
logger.error(f"{log_prefix}记忆检索时发生异常: {str(e)}")
return ""