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

@@ -206,6 +206,13 @@ class CycleProcessor:
with Timer("规划器", cycle_timers): with Timer("规划器", cycle_timers):
actions, _ = await self.action_planner.plan(mode=mode) 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): async def execute_action(action_info):
"""执行单个动作的通用函数""" """执行单个动作的通用函数"""
try: try:

View File

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

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
from datetime import datetime 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 dateutil.parser import parse as parse_datetime
from src.common.logger import get_logger from src.common.logger import get_logger
@@ -22,10 +22,11 @@ logger = get_logger(__name__)
# ============================ AsyncTask ============================ # ============================ AsyncTask ============================
class ReminderTask(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()}") super().__init__(task_name=f"ReminderTask_{target_user_id}_{datetime.now().timestamp()}")
self.delay = delay self.delay = delay
self.stream_id = stream_id self.stream_id = stream_id
self.group_id = group_id
self.is_group = is_group self.is_group = is_group
self.target_user_id = target_user_id self.target_user_id = target_user_id
self.target_user_name = target_user_name self.target_user_name = target_user_name
@@ -44,14 +45,13 @@ class ReminderTask(AsyncTask):
if self.is_group: if self.is_group:
# 在群聊中,构造 @ 消息段并发送 # 在群聊中,构造 @ 消息段并发送
group_id = self.stream_id.split('_')[-1] if '_' in self.stream_id else self.stream_id
message_payload = [ message_payload = [
{"type": "at", "data": {"qq": self.target_user_id}}, {"type": "at", "data": {"qq": self.target_user_id}},
{"type": "text", "data": {"text": f" {reminder_text}"}} {"type": "text", "data": {"text": f" {reminder_text}"}}
] ]
await send_api.adapter_command_to_stream( await send_api.adapter_command_to_stream(
action="send_group_msg", 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 stream_id=self.stream_id
) )
else: else:
@@ -83,35 +83,8 @@ class RemindAction(BaseAction):
) )
# === LLM 判断与参数提取 === # === LLM 判断与参数提取 ===
llm_judge_prompt = """ llm_judge_prompt = ""
你是一个严格的提醒意图分类器。你的任务是判断用户是否明确意图设置一个未来的提醒。这是一个最高优先级的任务。 action_parameters = {}
**规则:**
1. 必须包含一个明确的、指向未来的时间点或时间段例如“十分钟后”、“明天下午3点”、“周五”、“待会儿”、“一分钟后”
2. 必须包含一个需要被提醒的具体事件或动作(例如:“开会”、“喝水”、“睡觉”、“去吃饭”)。
3. 如果文本同时满足规则1和2你必须且只能回答“是”。
4. 任何不满足上述两个核心规则的文本,都回答“否”。
**正面示例(必须回答“是”):**
- "半小时后提醒我开会"
- "两分钟后叫我喝水"
- "爱莉,提醒一闪一分钟后去睡觉"
- "别忘了周五把报告交了"
- "待会儿记得和我说一声"
**负面示例(必须回答“否”):**
- "现在几点了?" (只是询问时间)
- "我明天下午有空" (陈述事实,没有要求提醒)
- "提醒呢?" (询问提醒状态,而不是设置新提醒)
- "我记得了" (表示自己记住了而不是让bot记住)
请严格按照规则进行分类,只回答""""
"""
action_parameters = {
"user_name": "需要被提醒的人的称呼或名字,如果没有明确指定给某人,则默认为'自己'",
"remind_time": "描述提醒时间的自然语言字符串,例如'十分钟后''明天下午3点'",
"event_details": "需要提醒的具体事件内容"
}
action_require = [ action_require = [
"当用户请求在未来的某个时间点提醒他/她或别人某件事时使用", "当用户请求在未来的某个时间点提醒他/她或别人某件事时使用",
"适用于包含明确时间信息和事件描述的对话", "适用于包含明确时间信息和事件描述的对话",
@@ -120,9 +93,52 @@ class RemindAction(BaseAction):
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
"""执行设置提醒的动作""" """执行设置提醒的动作"""
user_name = self.action_data.get("user_name") try:
remind_time_str = self.action_data.get("remind_time") # 获取所有可用的模型配置
event_details = self.action_data.get("event_details") 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]): 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] 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( reminder_task = ReminderTask(
delay=delay_seconds, 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, is_group=self.is_group,
target_user_id=str(user_id_to_remind), target_user_id=str(user_id_to_remind),
target_user_name=str(user_name_to_remind), target_user_name=str(user_name_to_remind),