feat:优化记忆查询表现

pull/1364/head
SengokuCola 2025-11-18 19:38:45 +08:00
parent a392608b7e
commit 43754b5c18
6 changed files with 95 additions and 110 deletions

View File

@ -98,8 +98,13 @@ class QAManager:
return result, ppr_node_weights
async def get_knowledge(self, question: str) -> Optional[str]:
"""获取知识"""
async def get_knowledge(self, question: str, limit: int = 5) -> Optional[str]:
"""获取知识
Args:
question: 查询问题
limit: 返回的相关知识条数
"""
# 处理查询
processed_result = await self.process_query(question)
if processed_result is not None:
@ -109,6 +114,8 @@ class QAManager:
logger.debug("知识库查询结果为空,可能是知识库中没有相关内容")
return None
limit = max(1, limit) if isinstance(limit, int) else 5
knowledge = [
(
self.embed_manager.paragraphs_embedding_store.store[res[0]].str,
@ -116,9 +123,18 @@ class QAManager:
)
for res in query_res
]
found_knowledge = "\n".join(
[f"{i + 1}条知识:{k[0]}\n 该条知识对于问题的相关性:{k[1]}" for i, k in enumerate(knowledge)]
)
# max_score = max([k[1] for k in knowledge]) if knowledge else None
selected_knowledge = knowledge[:limit]
formatted_knowledge = [
f"{i + 1}条知识:{k[0]}\n 该条知识对于问题的相关性:{k[1]}"
for i, k in enumerate(selected_knowledge)
]
# if max_score is not None:
# formatted_knowledge.insert(0, f"最高相关系数:{max_score}")
found_knowledge = "\n".join(formatted_knowledge)
if len(found_knowledge) > MAX_KNOWLEDGE_LENGTH:
found_knowledge = found_knowledge[:MAX_KNOWLEDGE_LENGTH] + "\n"
return found_knowledge

View File

@ -11,6 +11,7 @@ from src.plugin_system.apis import llm_api
from src.common.database.database_model import ThinkingBack
from json_repair import repair_json
from src.memory_system.retrieval_tools import get_tool_registry, init_all_tools
from src.memory_system.retrieval_tools.query_lpmm_knowledge import query_lpmm_knowledge
from src.llm_models.payload_content.message import MessageBuilder, RoleType, Message
logger = get_logger("memory_retrieval")
@ -63,20 +64,17 @@ def init_memory_retrieval_prompt():
1. 对话中是否提到了过去发生的事情人物事件或信息
2. 是否有需要回忆的内容比如"之前说过""上次""以前"
3. 是否有需要查找历史信息的问题
4. 是否需要查找某人的信息person: 如果对话中提到人名昵称用户ID等需要查询该人物的详细信息
5. 是否有问题可以搜集信息帮助你聊天
6. 对话中是否包含黑话俚语缩写等可能需要查询的概念
4. 是否有问题可以搜集信息帮助你聊天
5. 对话中是否包含黑话俚语缩写等可能需要查询的概念
重要提示
- **每次只能提出一个问题**选择最需要查询的关键问题
- 如果"最近已查询的问题和结果"中已经包含了类似的问题请避免重复生成相同或相似的问题
- 如果"最近已查询的问题和结果"中已经包含了类似的问题并得到了答案请避免重复生成相同或相似的问题不需要重复查询
- 如果之前已经查询过某个问题但未找到答案可以尝试用不同的方式提问或更具体的问题
- 如果之前已经查询过某个问题并找到了答案可以直接参考已有结果不需要重复查询
如果你认为需要从记忆中检索信息来回答
1. 先识别对话中可能需要查询的概念黑话/俚语/缩写/专有名词等关键词放入"concepts"字段
2. 识别对话中提到的人物名称人名昵称等放入"person"字段
3. 然后根据上下文提出**一个**最关键的问题来帮助你回复目标消息放入"questions"字段
1. 识别对话中可能需要查询的概念黑话/俚语/缩写/专有名词等关键词放入"concepts"字段
2. 根据上下文提出**一个**最关键的问题来帮助你回复目标消息放入"questions"字段
问题格式示例
- "xxx在前几天干了什么"
@ -84,17 +82,11 @@ def init_memory_retrieval_prompt():
- "xxxx和xxx的关系是什么"
- "xxx在某个时间点发生了什么"
请输出JSON格式包含三个字段
- "concepts": 需要检索的概念列表字符串数组如果不需要检索概念则输出空数组[]
- "person": 需要查询的人物名称列表字符串数组如果不需要查询人物信息则输出空数组[]
- "questions": 问题数组字符串数组如果不需要检索记忆则输出空数组[]如果需要检索则只输出包含一个问题的数组
输出格式示例需要检索时
```json
{{
"concepts": ["AAA", "BBB", "CCC"],
"person": ["张三", "李四"],
"questions": ["张三在前几天干了什么"]
"concepts": ["AAA", "BBB", "CCC"], #需要检索的概念列表(字符串数组),如果不需要检索概念则输出空数组[]
"questions": ["张三在前几天干了什么"] #问题数组(字符串数组),如果不需要检索记忆则输出空数组[],如果需要检索则只输出包含一个问题的数组
}}
```
@ -102,7 +94,6 @@ def init_memory_retrieval_prompt():
```json
{{
"concepts": [],
"person": [],
"questions": []
}}
```
@ -114,10 +105,8 @@ def init_memory_retrieval_prompt():
# 第二步ReAct Agent prompt使用function calling要求先思考再行动
Prompt(
"""
你的名字是{bot_name}现在是{time_now}
"""你的名字是{bot_name}。现在是{time_now}
你正在参与聊天你需要搜集信息来回答问题帮助你参与聊天
你需要通过思考(Think)行动(Action)观察(Observation)的循环来回答问题
**重要限制**
- 最大查询轮数{max_iterations}当前第{current_iteration}剩余{remaining_iterations}
@ -130,7 +119,6 @@ def init_memory_retrieval_prompt():
{collected_info}
**执行步骤**
**第一步思考Think**
在思考中分析
- 当前信息是否足够回答问题
@ -139,6 +127,10 @@ def init_memory_retrieval_prompt():
- **如果已有信息不足或无法找到答案**在思考中给出not_enough_info(reason="信息不足或无法找到答案的原因")
**第二步行动Action**
- 如果涉及过往事件可以使用聊天记录查询工具查询过往事件
- 如果涉及概念可以用jargon查询或根据关键词检索聊天记录
- 如果涉及人物可以使用人物信息查询工具查询人物信息
- 如果不确定查询类别也可以使用lpmm知识库查询
- 如果信息不足且需要继续查询说明最需要查询什么并输出为纯文本说明然后调用相应工具查询可并行调用多个工具
**重要规则**
@ -151,14 +143,8 @@ def init_memory_retrieval_prompt():
# 额外如果最后一轮迭代ReAct Agent prompt使用function calling要求先思考再行动
Prompt(
"""
你的名字是{bot_name}现在是{time_now}
你正在参与聊天你需要搜集信息来回答问题帮助你参与聊天
**重要限制**
- 你已经经过几轮查询尝试了信息搜集现在你需要总结信息选择回答问题或判断问题无法回答
- 思考要简短直接切入要点
- 必须严格使用检索到的信息回答问题不要编造信息
"""你的名字是{bot_name}。现在是{time_now}
你正在参与聊天你需要根据搜集到的信息判断问题是否可以回答问题
当前问题{question}
已收集的信息
@ -171,6 +157,9 @@ def init_memory_retrieval_prompt():
- **如果信息不足或无法找到答案**在思考中给出not_enough_info(reason="信息不足或无法找到答案的原因")
**重要规则**
- 你已经经过几轮查询尝试了信息搜集现在你需要总结信息选择回答问题或判断问题无法回答
- 必须严格使用检索到的信息回答问题不要编造信息
- 答案必须精简不要过多解释
- **只有在检索到明确具体的答案时才使用found_answer**
- **如果信息不足无法确定找不到相关信息必须使用not_enough_info不要使用found_answer**
- 答案必须给出格式为 found_answer(answer="...") not_enough_info(reason="...")
@ -308,46 +297,6 @@ async def _retrieve_concepts_with_jargon(
return ""
async def _retrieve_persons_info(
persons: List[str],
chat_id: str
) -> str:
"""对人物列表进行信息检索
Args:
persons: 人物名称列表
chat_id: 聊天ID
Returns:
str: 检索结果字符串
"""
if not persons:
return ""
from src.memory_system.retrieval_tools.query_person_info import query_person_info
results = []
for person in persons:
person = person.strip()
if not person:
continue
try:
person_info = await query_person_info(person)
if person_info and "未找到" not in person_info:
results.append(f"{person}\n{person_info}")
logger.info(f"查询到人物信息: {person}")
else:
# 未找到时不插入占位信息
logger.info(f"未找到人物信息: {person}")
except Exception as e:
logger.error(f"查询人物信息失败: {person}, 错误: {e}")
if results:
return "【人物信息检索结果】\n" + "\n\n".join(results) + "\n"
return ""
async def _react_agent_solve_question(
question: str,
chat_id: str,
@ -468,11 +417,10 @@ async def _react_agent_solve_question(
content_type = "未知"
# 构建单条消息的日志信息
msg_info = f"\n[消息 {idx}] 角色: {role_name}"
msg_info += f"\n 内容类型: {content_type}"
msg_info = f"\n[消息 {idx}] 角色: {role_name} 内容类型: {content_type}\n========================================"
if full_content:
msg_info += f"\n 内容:\n{full_content}"
msg_info += f"\n{full_content}"
if msg.tool_calls:
msg_info += f"\n 工具调用: {len(msg.tool_calls)}"
@ -1014,6 +962,22 @@ async def _process_single_question(
logger.info(f"开始处理问题: {question}")
_cleanup_stale_not_found_thinking_back()
question_initial_info = initial_info or ""
# 预先进行一次LPMM知识库查询作为后续ReAct Agent的辅助信息
if global_config.lpmm_knowledge.enable:
try:
lpmm_result = await query_lpmm_knowledge(question, limit=2)
if lpmm_result and lpmm_result.startswith("你从LPMM知识库中找到"):
if question_initial_info:
question_initial_info += "\n"
question_initial_info += f"【LPMM知识库预查询】\n{lpmm_result}"
logger.info(f"LPMM预查询命中问题: {question[:50]}...")
else:
logger.info(f"LPMM预查询未命中或未找到信息问题: {question[:50]}...")
except Exception as e:
logger.error(f"LPMM预查询失败问题: {question[:50]}... 错误: {e}")
# 先检查thinking_back数据库中是否有现成答案
cached_result = _query_thinking_back(chat_id, question)
@ -1051,7 +1015,7 @@ async def _process_single_question(
chat_id=chat_id,
max_iterations=global_config.memory.max_agent_iterations,
timeout=120.0,
initial_info=initial_info
initial_info=question_initial_info
)
# 存储到数据库(超时时不存储)
@ -1132,10 +1096,9 @@ async def build_memory_retrieval_prompt(
logger.error(f"LLM生成问题失败: {response}")
return ""
# 解析概念列表、人物列表和问题列表
concepts, persons, questions = _parse_questions_json(response)
# 解析概念列表和问题列表
concepts, questions = _parse_questions_json(response)
logger.info(f"解析到 {len(concepts)} 个概念: {concepts}")
logger.info(f"解析到 {len(persons)} 个人物: {persons}")
logger.info(f"解析到 {len(questions)} 个问题: {questions}")
# 对概念进行jargon检索作为初始信息
@ -1149,22 +1112,13 @@ async def build_memory_retrieval_prompt(
else:
logger.info("概念检索未找到任何结果")
# 对人物进行信息检索,添加到初始信息
if persons:
logger.info(f"开始对 {len(persons)} 个人物进行信息检索")
person_info = await _retrieve_persons_info(persons, chat_id)
if person_info:
initial_info += person_info
logger.info(f"人物信息检索完成,结果: {person_info[:200]}...")
else:
logger.info("人物信息检索未找到任何结果")
# 获取缓存的记忆与question时使用相同的时间窗口和数量限制
cached_memories = _get_cached_memories(chat_id, time_window_seconds=300.0)
if not questions:
logger.debug("模型认为不需要检索记忆或解析失败")
# 即使没有当次查询,也返回缓存的记忆、概念检索结果和人物信息检索结果
# 即使没有当次查询,也返回缓存的记忆和概念检索结果
all_results = []
if initial_info:
all_results.append(initial_info.strip())
@ -1174,7 +1128,7 @@ async def build_memory_retrieval_prompt(
if all_results:
retrieved_memory = "\n\n".join(all_results)
end_time = time.time()
logger.info(f"无当次查询,返回缓存记忆、概念检索和人物信息检索结果,耗时: {(end_time - start_time):.3f}")
logger.info(f"无当次查询,返回缓存记忆和概念检索结果,耗时: {(end_time - start_time):.3f}")
return f"你回忆起了以下信息:\n{retrieved_memory}\n如果与回复内容相关,可以参考这些回忆的信息。\n"
else:
return ""
@ -1236,14 +1190,14 @@ async def build_memory_retrieval_prompt(
return ""
def _parse_questions_json(response: str) -> Tuple[List[str], List[str], List[str]]:
"""解析问题JSON返回概念列表、人物列表和问题列表
def _parse_questions_json(response: str) -> Tuple[List[str], List[str]]:
"""解析问题JSON返回概念列表和问题列表
Args:
response: LLM返回的响应
Returns:
Tuple[List[str], List[str], List[str]]: (概念列表, 人物列表, 问题列表)
Tuple[List[str], List[str]]: (概念列表, 问题列表)
"""
try:
# 尝试提取JSON可能包含在```json代码块中
@ -1262,30 +1216,26 @@ def _parse_questions_json(response: str) -> Tuple[List[str], List[str], List[str
# 解析JSON
parsed = json.loads(repaired_json)
# 只支持新格式包含concepts、person和questions的对象
# 只支持新格式包含concepts和questions的对象
if not isinstance(parsed, dict):
logger.warning(f"解析的JSON不是对象格式: {parsed}")
return [], [], []
return [], []
concepts_raw = parsed.get("concepts", [])
persons_raw = parsed.get("person", [])
questions_raw = parsed.get("questions", [])
# 确保是列表
if not isinstance(concepts_raw, list):
concepts_raw = []
if not isinstance(persons_raw, list):
persons_raw = []
if not isinstance(questions_raw, list):
questions_raw = []
# 确保所有元素都是字符串
concepts = [c for c in concepts_raw if isinstance(c, str) and c.strip()]
persons = [p for p in persons_raw if isinstance(p, str) and p.strip()]
questions = [q for q in questions_raw if isinstance(q, str) and q.strip()]
return concepts, persons, questions
return concepts, questions
except Exception as e:
logger.error(f"解析问题JSON失败: {e}, 响应内容: {response[:200]}...")
return [], [], []
return [], []

View File

@ -193,7 +193,7 @@ def register_tool():
"""注册工具"""
register_memory_retrieval_tool(
name="query_chat_history",
description="根据时间或关键词在chat_history表的聊天记录概述库中查询。可以查询某个时间点发生了什么、某个时间范围内的事件,或根据关键词搜索消息概述。支持两种匹配模式:模糊匹配(默认,只要包含任意一个关键词即匹配)和全匹配(必须包含所有关键词才匹配)",
description="根据时间或关键词在聊天记录中查询。可以查询某个时间点发生了什么、某个时间范围内的事件,或根据关键词搜索消息概述。支持两种匹配模式:模糊匹配(默认,只要包含任意一个关键词即匹配)和全匹配(必须包含所有关键词才匹配)",
parameters=[
{
"name": "keyword",

View File

@ -10,7 +10,7 @@ from .tool_registry import register_memory_retrieval_tool
logger = get_logger("memory_retrieval_tools")
async def query_lpmm_knowledge(query: str) -> str:
async def query_lpmm_knowledge(query: str, limit: int = 5) -> str:
"""在LPMM知识库中查询相关信息
Args:
@ -24,6 +24,12 @@ async def query_lpmm_knowledge(query: str) -> str:
if not content:
return "查询关键词为空"
try:
limit_value = int(limit)
except (TypeError, ValueError):
limit_value = 5
limit_value = max(1, limit_value)
if not global_config.lpmm_knowledge.enable:
logger.debug("LPMM知识库未启用")
return "LPMM知识库未启用"
@ -33,7 +39,7 @@ async def query_lpmm_knowledge(query: str) -> str:
logger.debug("LPMM知识库未初始化跳过查询")
return "LPMM知识库未初始化"
knowledge_info = await qa_manager.get_knowledge(content)
knowledge_info = await qa_manager.get_knowledge(content, limit=limit_value)
logger.debug(f"LPMM知识库查询结果: {knowledge_info}")
if knowledge_info:
@ -57,7 +63,13 @@ def register_tool():
"type": "string",
"description": "需要查询的关键词或问题",
"required": True,
}
},
{
"name": "limit",
"type": "integer",
"description": "希望返回的相关知识条数默认为5",
"required": False,
},
],
execute_func=query_lpmm_knowledge,
)

View File

@ -15,6 +15,7 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
description = "从知识库中搜索相关信息,如果你需要知识,就使用这个工具"
parameters = [
("query", ToolParamType.STRING, "搜索查询关键词", True, None),
("limit", ToolParamType.INTEGER, "希望返回的相关知识条数默认5", False, 5),
]
available_for_llm = global_config.lpmm_knowledge.enable
@ -29,6 +30,12 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
"""
try:
query: str = function_args.get("query") # type: ignore
limit = function_args.get("limit", 5)
try:
limit_value = int(limit)
except (TypeError, ValueError):
limit_value = 5
limit_value = max(1, limit_value)
# threshold = function_args.get("threshold", 0.4)
# 检查LPMM知识库是否启用
@ -38,7 +45,7 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
# 调用知识库搜索
knowledge_info = await qa_manager.get_knowledge(query)
knowledge_info = await qa_manager.get_knowledge(query, limit=limit_value)
logger.debug(f"知识库查询结果: {knowledge_info}")

View File

@ -1,5 +1,5 @@
[inner]
version = "6.21.5"
version = "6.21.6"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请递增version的值
@ -104,7 +104,7 @@ talk_value_rules = [
include_planner_reasoning = false # 是否将planner推理加入replyer默认关闭不加入
[memory]
max_agent_iterations = 5 # 记忆思考深度最低为1不深入思考
max_agent_iterations = 3 # 记忆思考深度最低为1不深入思考
[jargon]
all_global = true # 是否开启全局黑话模式,注意,此功能关闭后,已经记录的全局黑话不会改变,需要手动删除