feat:精简表达选择,优化replyer表现,优化缓存命中率

This commit is contained in:
SengokuCola
2026-04-24 21:27:45 +08:00
parent 3b6d30cd5e
commit 7719353984
11 changed files with 188 additions and 142 deletions

View File

@@ -268,6 +268,11 @@ class MaisakaExpressionSelector:
reply_reason: str,
sub_agent_runner: Optional[SubAgentRunner],
) -> MaisakaExpressionSelectionResult:
del chat_history
del reply_message
del reply_reason
del sub_agent_runner
if not session_id:
logger.info("表达方式选择已跳过:缺少 session_id")
return MaisakaExpressionSelectionResult()
@@ -280,48 +285,10 @@ class MaisakaExpressionSelector:
logger.info(f"表达方式选择已跳过本地候选不足session_id={session_id}")
return MaisakaExpressionSelectionResult()
if not global_config.expression.advanced_chosen:
return self._build_direct_selection_result(
session_id=session_id,
candidates=candidates,
)
if sub_agent_runner is None:
logger.info(f"表达方式选择已跳过:缺少 sub_agent_runnersession_id={session_id}")
return MaisakaExpressionSelectionResult()
logger.info(
f"表达方式选择开始session_id={session_id} 候选数={len(candidates)} "
f"候选预览={self._format_candidate_preview(candidates)}"
)
selector_prompt = self._build_selector_prompt(
chat_history=chat_history,
reply_message=reply_message,
reply_reason=reply_reason,
return self._build_direct_selection_result(
session_id=session_id,
candidates=candidates,
)
try:
raw_response = await sub_agent_runner(selector_prompt)
except Exception:
logger.exception("表达方式选择子代理执行失败")
return MaisakaExpressionSelectionResult()
selected_ids = self._parse_selected_ids(raw_response, candidates)
if not selected_ids:
logger.info(f"表达方式选择完成但未命中session_id={session_id}")
return MaisakaExpressionSelectionResult()
selected_expressions = [candidate for candidate in candidates if candidate.get("id") in selected_ids]
self._update_last_active_time(selected_ids)
logger.info(
f"表达方式选择完成session_id={session_id} 已选数={len(selected_ids)} "
f"selected_ids={selected_ids!r} 已选预览={self._format_candidate_preview(selected_expressions)}"
)
return MaisakaExpressionSelectionResult(
expression_habits=self._build_expression_habits_block(selected_expressions),
selected_expression_ids=selected_ids,
)
maisaka_expression_selector = MaisakaExpressionSelector()

View File

@@ -18,6 +18,14 @@ from src.common.data_models.reply_generation_data_models import (
ReplyGenerationResult,
build_reply_monitor_detail,
)
from src.common.data_models.message_component_data_model import (
AtComponent,
EmojiComponent,
ImageComponent,
ReplyComponent,
TextComponent,
VoiceComponent,
)
from src.common.logger import get_logger
from src.common.utils.utils_session import SessionUtils
from src.config.config import global_config
@@ -100,6 +108,30 @@ class BaseMaisakaReplyGenerator:
del message
return ""
@staticmethod
def _strip_guided_reply_formatting(content: str) -> str:
"""移除 guided_reply 可见文本中的引用包装,仅保留真正回复正文。"""
normalized_content = content.strip()
if not normalized_content:
return ""
reply_body_marker = "[发言内容]"
if normalized_content.startswith("[引用]quote_id=") or normalized_content.startswith("[引用消息]"):
marker_index = normalized_content.find(reply_body_marker)
if marker_index >= 0:
return normalized_content[marker_index + len(reply_body_marker) :].strip()
newline_index = normalized_content.find("\n")
if newline_index < 0:
return ""
normalized_content = normalized_content[newline_index + 1 :].lstrip()
if normalized_content.startswith(reply_body_marker):
normalized_content = normalized_content[len(reply_body_marker) :].lstrip()
return normalized_content.strip()
def _extract_guided_bot_reply(self, message: SessionBackedMessage) -> str:
# 只能根据结构化来源字段判断是否为 bot 自身写回的历史消息,
# 不能依赖昵称/群名片等可控文本,避免误判和提示注入。
@@ -108,7 +140,9 @@ class BaseMaisakaReplyGenerator:
plain_text = message.processed_plain_text.strip()
_, body = parse_speaker_content(plain_text)
normalized_body = body.strip() or plain_text
normalized_body = self._strip_guided_reply_formatting(body) or self._strip_guided_reply_formatting(
plain_text
)
return self._normalize_content(normalized_body) if normalized_body else ""
def _build_target_message_block(self, reply_message: Optional[SessionMessage]) -> str:
@@ -118,18 +152,62 @@ class BaseMaisakaReplyGenerator:
user_info = reply_message.message_info.user_info
sender_name = user_info.user_cardname or user_info.user_nickname or user_info.user_id
target_message_id = reply_message.message_id.strip() if reply_message.message_id else "未知"
target_content = self._normalize_content((reply_message.processed_plain_text or "").strip(), limit=300)
target_time = reply_message.timestamp.strftime("%Y-%m-%d %H:%M:%S")
target_content = self._normalize_content(self._build_target_message_content(reply_message), limit=300)
if not target_content:
target_content = "[无可见文本内容]"
return (
"【本次回复目标】\n"
f"- 目标消息ID{target_message_id}\n"
f"- 发送者{sender_name}\n"
f"- 消息内容{target_content}\n"
"- 你这次要回复的就是这条目标消息,请结合整段上下文理解,但不要把其他历史消息当成当前回复对象。"
f"- msg_id{target_message_id}\n"
f"- 时间{target_time}\n"
f"- 用户名{sender_name}\n"
f"- 发言内容:{target_content}\n\n"
"你这次要回复的就是这条目标消息,请结合整段上下文理解,但不要把其他历史消息当成当前回复对象。"
)
@staticmethod
def _render_target_at_component(component: AtComponent) -> str:
target_name = component.target_user_cardname or component.target_user_nickname or component.target_user_id
return f"@{target_name}".strip()
def _build_target_message_content(self, reply_message: SessionMessage) -> str:
rendered_parts: List[str] = []
for component in reply_message.raw_message.components:
if isinstance(component, TextComponent):
if component.text:
rendered_parts.append(component.text)
continue
if isinstance(component, ReplyComponent):
target_message_id = component.target_message_id.strip()
if target_message_id:
rendered_parts.append(f"[引用:quote_id={target_message_id}]")
continue
if isinstance(component, AtComponent):
rendered_at = self._render_target_at_component(component)
if rendered_at:
rendered_parts.append(rendered_at)
continue
if isinstance(component, ImageComponent):
rendered_parts.append(component.content.strip() or "[图片]")
continue
if isinstance(component, EmojiComponent):
rendered_parts.append(component.content.strip() or "[表情包]")
continue
if isinstance(component, VoiceComponent):
rendered_parts.append(component.content.strip() or "[语音消息]")
normalized_content = " ".join(part.strip() for part in rendered_parts if part and part.strip()).strip()
if normalized_content:
return normalized_content
return (reply_message.processed_plain_text or "").strip()
@staticmethod
def _get_chat_prompt_for_chat(chat_id: str, is_group_chat: Optional[bool]) -> str:
"""根据聊天流 ID 获取匹配的额外 prompt。"""
@@ -221,8 +299,10 @@ class BaseMaisakaReplyGenerator:
expression_habits: str = "",
stream_id: Optional[str] = None,
) -> str:
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
target_message_block = self._build_target_message_block(reply_message)
del reply_message
del reply_reason
del reference_info
del expression_habits
session_id = self._resolve_session_id(stream_id)
try:
@@ -231,16 +311,29 @@ class BaseMaisakaReplyGenerator:
bot_name=global_config.bot.nickname,
group_chat_attention_block=self._build_group_chat_attention_block(session_id),
replyer_at_block=self._build_replyer_at_block(),
time_block=f"当前时间:{current_time}",
identity=self._personality_prompt,
reply_style=global_config.personality.reply_style,
)
except Exception:
system_prompt = "你是一个友好的 AI 助手,请根据聊天记录自然回复。"
sections: List[str] = []
return system_prompt
def _build_reply_instruction(self) -> str:
return "请自然地回复。不要输出多余说明、括号、@ 或额外标记,只输出实际要发送的内容。"
def _build_final_user_message(
self,
reply_message: Optional[SessionMessage],
reply_reason: str,
reference_info: str = "",
expression_habits: str = "",
) -> str:
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sections: List[str] = [f"当前时间:{current_time}"]
if expression_habits.strip():
sections.append(expression_habits.strip())
target_message_block = self._build_target_message_block(reply_message)
if target_message_block:
sections.append(target_message_block)
reply_reference_lines: List[str] = []
@@ -250,12 +343,8 @@ class BaseMaisakaReplyGenerator:
reply_reference_lines.append(f"【参考信息】\n{reference_info.strip()}")
if reply_reference_lines:
sections.append("【回复信息参考】\n" + "\n\n".join(reply_reference_lines))
if not sections:
return system_prompt
return f"{system_prompt}\n\n" + "\n\n".join(sections)
def _build_reply_instruction(self) -> str:
return "请自然地回复。不要输出多余说明、括号、@ 或额外标记,只输出实际要发送的内容。"
sections.append(self._build_reply_instruction())
return "\n\n".join(sections)
def _build_history_messages(
self,
@@ -311,11 +400,16 @@ class BaseMaisakaReplyGenerator:
expression_habits=expression_habits,
stream_id=stream_id,
)
instruction = self._build_reply_instruction()
final_user_message = self._build_final_user_message(
reply_message=reply_message,
reply_reason=reply_reason,
reference_info=reference_info,
expression_habits=expression_habits,
)
messages.append(MessageBuilder().set_role(RoleType.System).add_text_content(system_prompt).build())
messages.extend(self._build_history_messages(chat_history, enable_visual_message))
messages.append(MessageBuilder().set_role(RoleType.User).add_text_content(instruction).build())
messages.append(MessageBuilder().set_role(RoleType.User).add_text_content(final_user_message).build())
return messages
def _resolve_enable_visual_message(self, model_info: Optional[ModelInfo] = None) -> bool:
@@ -403,10 +497,10 @@ class BaseMaisakaReplyGenerator:
result.error_message = "聊天历史为空"
return finalize(False)
logger.info(
f"Maisaka 回复器开始生成: 流={stream_id} 原因={reply_reason!r} "
f"历史条数={len(chat_history)} 目标ID={reply_message.message_id if reply_message else None}"
)
# logger.info(
# f"Maisaka 回复器开始生成: 流={stream_id} 原因={reply_reason!r} "
# f"历史条数={len(chat_history)} 目标ID={reply_message.message_id if reply_message else None}"
# )
filtered_history = [
message
@@ -444,9 +538,9 @@ class BaseMaisakaReplyGenerator:
else list(reply_context.selected_expression_ids)
)
logger.info(
f"回复上下文完成 流={stream_id} 已选表达={result.selected_expression_ids!r}"
)
# logger.info(
# f"回复上下文完成 流={stream_id} 已选表达={result.selected_expression_ids!r}"
# )
prompt_started_at = time.perf_counter()
try: