feat(planner): 重构动作决策逻辑与参数提取机制

本次提交对动作规划器(Planner)和动作(Action)的执行流程进行了重大重构,旨在提高决策的准确性和可靠性,使机器人能更精确地响应用户指令。

核心变更:
- **决策与参数提取分离**: 规划器(Planner)现在专注于根据用户意图选择最合适的动作,不再负责提取动作参数。
- **动作参数自解析**: `RemindAction` 等动作现在通过内部调用 LLM,从用户消息中自行解析所需参数,使其更加独立和健壮。
- **优化决策Prompt**: 引入“最高优先级检查”和“互斥原则”,强制优先执行由明确意图触发的特定动作(如 `set_reminder`),并在此情况下禁止选择 `reply`,避免重复响应。
- **增强调试**: 在处理循环中增加了日志,以清晰地记录LLM最终选择的动作组合,方便调试。
This commit is contained in:
tt-P607
2025-09-16 14:00:33 +08:00
committed by Windpicker-owo
parent d0b630b212
commit cad85959d9
3 changed files with 78 additions and 41 deletions

View File

@@ -205,6 +205,13 @@ class CycleProcessor:
raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成")
with Timer("规划器", cycle_timers):
actions, _ = await self.action_planner.plan(mode=mode)
# 在这里添加日志,清晰地显示最终选择的动作
if actions:
chosen_actions = [a.get("action_type", "unknown") for a in actions]
logger.info(f"{self.log_prefix} LLM最终选择的动作: {chosen_actions}")
else:
logger.info(f"{self.log_prefix} LLM最终没有选择任何动作")
async def execute_action(action_info):
"""执行单个动作的通用函数"""

View File

@@ -35,10 +35,11 @@ def init_prompts():
2. **辅助动作 (可选)**: 这是为了增强表达效果的附加动作,例如 `emoji`(发送表情包)或 `poke_user`(戳一戳)。
**决策流程:**
1. 首先,决定是否要进行 `reply`
2. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格
3. 如果需要,选择一个最合适的辅助动作与 `reply` 组合
4. 如果用户明确要求了某个动作,请务必优先满足
1. **最高优先级检查**: 首先,检查是否有由 **关键词** 或 **LLM判断** 激活的特定动作(除了通用的 `reply`, `emoji` 等)。这些动作代表了用户的明确意图
2. **执行明确意图**: 如果存在这类特定动作,你 **必须** 优先选择它作为主要响应。这比常规的文本回复 (`reply`) 更重要
3. **常规回复**: 如果没有被特定意图激活的动作,再决定是否要进行 `reply`。
4. **辅助动作**: 在确定了主要动作后(无论是特定动作还是 `reply`),再评估是否需要 `emoji` 或 `poke_user` 等辅助动作来增强表达效果
5. **互斥原则**: 当你选择了一个由明确意图激活的特定动作(如 `set_reminder`)时,你 **绝不能** 再选择 `reply` 动作,因为特定动作的执行结果(例如,设置提醒后的确认消息)本身就是一种回复。这是必须遵守的规则。
**重要概念:将“理由”作为“内心思考”的体现**
`reason` 字段是本次决策的核心。它并非一个简单的“理由”,而是 **一个模拟人类在回应前,头脑中自然浮现的、未经修饰的思绪流**。你需要完全代入 {identity_block} 的角色,将那一刻的想法自然地记录下来。
@@ -100,6 +101,18 @@ def init_prompts():
}}
]
**单动作示例 (特定动作):**
[
{{
"action": "set_reminder",
"target_message_id": "m456",
"reason": "用户说‘提醒维尔薇下午三点去工坊’,这是一个非常明确的指令。根据决策流程,我必须优先执行这个特定动作,而不是进行常规回复。",
"user_name": "维尔薇",
"remind_time": "下午三点",
"event_details": "去工坊"
}}
]
**重要规则:**
**重要规则:**
当 `reply` 和 `emoji` 动作同时被选择时,`emoji` 动作的 `reason` 字段也应该体现出你的思考过程,并与 `reply` 的思考保持连贯。

View File

@@ -1,6 +1,6 @@
import asyncio
from datetime import datetime
from typing import List, Tuple, Type
from typing import List, Tuple, Type, Optional
from dateutil.parser import parse as parse_datetime
from src.common.logger import get_logger
@@ -22,10 +22,11 @@ logger = get_logger(__name__)
# ============================ AsyncTask ============================
class ReminderTask(AsyncTask):
def __init__(self, delay: float, stream_id: str, is_group: bool, target_user_id: str, target_user_name: str, event_details: str, creator_name: str):
def __init__(self, delay: float, stream_id: str, group_id: Optional[str], is_group: bool, target_user_id: str, target_user_name: str, event_details: str, creator_name: str):
super().__init__(task_name=f"ReminderTask_{target_user_id}_{datetime.now().timestamp()}")
self.delay = delay
self.stream_id = stream_id
self.group_id = group_id
self.is_group = is_group
self.target_user_id = target_user_id
self.target_user_name = target_user_name
@@ -44,14 +45,13 @@ class ReminderTask(AsyncTask):
if self.is_group:
# 在群聊中,构造 @ 消息段并发送
group_id = self.stream_id.split('_')[-1] if '_' in self.stream_id else self.stream_id
message_payload = [
{"type": "at", "data": {"qq": self.target_user_id}},
{"type": "text", "data": {"text": f" {reminder_text}"}}
]
await send_api.adapter_command_to_stream(
action="send_group_msg",
params={"group_id": group_id, "message": message_payload},
params={"group_id": self.group_id, "message": message_payload},
stream_id=self.stream_id
)
else:
@@ -83,35 +83,8 @@ class RemindAction(BaseAction):
)
# === LLM 判断与参数提取 ===
llm_judge_prompt = """
你是一个严格的提醒意图分类器。你的任务是判断用户是否明确意图设置一个未来的提醒。这是一个最高优先级的任务。
**规则:**
1. 必须包含一个明确的、指向未来的时间点或时间段例如“十分钟后”、“明天下午3点”、“周五”、“待会儿”、“一分钟后”
2. 必须包含一个需要被提醒的具体事件或动作(例如:“开会”、“喝水”、“睡觉”、“去吃饭”)。
3. 如果文本同时满足规则1和2你必须且只能回答“是”。
4. 任何不满足上述两个核心规则的文本,都回答“否”。
**正面示例(必须回答“是”):**
- "半小时后提醒我开会"
- "两分钟后叫我喝水"
- "爱莉,提醒一闪一分钟后去睡觉"
- "别忘了周五把报告交了"
- "待会儿记得和我说一声"
**负面示例(必须回答“否”):**
- "现在几点了?" (只是询问时间)
- "我明天下午有空" (陈述事实,没有要求提醒)
- "提醒呢?" (询问提醒状态,而不是设置新提醒)
- "我记得了" (表示自己记住了而不是让bot记住)
请严格按照规则进行分类,只回答""""
"""
action_parameters = {
"user_name": "需要被提醒的人的称呼或名字,如果没有明确指定给某人,则默认为'自己'",
"remind_time": "描述提醒时间的自然语言字符串,例如'十分钟后''明天下午3点'",
"event_details": "需要提醒的具体事件内容"
}
llm_judge_prompt = ""
action_parameters = {}
action_require = [
"当用户请求在未来的某个时间点提醒他/她或别人某件事时使用",
"适用于包含明确时间信息和事件描述的对话",
@@ -120,9 +93,52 @@ class RemindAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
"""执行设置提醒的动作"""
user_name = self.action_data.get("user_name")
remind_time_str = self.action_data.get("remind_time")
event_details = self.action_data.get("event_details")
try:
# 获取所有可用的模型配置
available_models = llm_api.get_available_models()
if "planner" not in available_models:
raise ValueError("未找到 'planner' 决策模型配置,无法解析时间")
model_to_use = available_models["planner"]
prompt = f"""
从以下用户输入中提取提醒事件的关键信息。
用户输入: "{self.chat_stream.context.message.processed_plain_text}"
请以JSON格式返回提取的信息包含以下字段:
- "user_name": 需要被提醒的人的姓名。如果未指定,则默认为"自己"
- "remind_time": 描述提醒时间的自然语言字符串。
- "event_details": 需要提醒的具体事件内容。
如果无法提取完整信息请返回一个包含空字符串的JSON对象例如{{"user_name": "", "remind_time": "", "event_details": ""}}
"""
success, response, _, _ = await llm_api.generate_with_model(
prompt,
model_config=model_to_use,
request_type="plugin.reminder.parameter_extractor"
)
if not success or not response:
raise ValueError(f"LLM未能返回有效的参数: {response}")
import json
import re
try:
# 提取JSON部分
json_match = re.search(r"\{.*\}", response, re.DOTALL)
if not json_match:
raise ValueError("LLM返回的内容中不包含JSON")
action_data = json.loads(json_match.group(0))
except json.JSONDecodeError:
logger.error(f"[ReminderPlugin] LLM返回的不是有效的JSON: {response}")
return False, "LLM返回的不是有效的JSON"
user_name = action_data.get("user_name")
remind_time_str = action_data.get("remind_time")
event_details = action_data.get("event_details")
except Exception as e:
logger.error(f"[ReminderPlugin] 解析参数时出错: {e}", exc_info=True)
return False, "解析参数时出错"
if not all([user_name, remind_time_str, event_details]):
missing_params = [p for p, v in {"user_name": user_name, "remind_time": remind_time_str, "event_details": event_details}.items() if not v]
@@ -208,7 +224,8 @@ class RemindAction(BaseAction):
reminder_task = ReminderTask(
delay=delay_seconds,
stream_id=self.chat_id,
stream_id=self.chat_stream.stream_id,
group_id=self.chat_stream.group_info.group_id if self.is_group and self.chat_stream.group_info else None,
is_group=self.is_group,
target_user_id=str(user_id_to_remind),
target_user_name=str(user_name_to_remind),