refactor(kokoro-flow): 增强对 max_wait_seconds 和超时决策的提示指导

此提交重构了 Kokoro Flow Chatter 插件中的提示,以提供更清晰、更接近人类的指导,帮助设置 `max_wait_seconds` 并处理对话超时问题。目标是改善 LLM 的决策能力,使对话节奏更自然。

主要更改包括:
- 在所有相关提示中统一并详细说明 `max_wait_seconds` 的使用,鼓励根据上下文设置,而不是默认使用 0。
- 重新设计了超时决策提示 (`kfc_SITUATION_TIMEOUT`),提供结构化的“决策指导”和“决策选项”,帮助 LLM 在等待、重新参与或结束等待之间做出更细致的选择。
- 在 NapCat 适配器中添加修复,确保 `reply` 消息段始终优先位于消息列表开头,防止潜在的发送错误。
- 对日志输出进行了小幅格式调整,以提高可读性。
This commit is contained in:
tt-P607
2025-12-01 18:20:53 +08:00
parent 18906826d4
commit 048ce37c63
5 changed files with 65 additions and 22 deletions

View File

@@ -1027,7 +1027,7 @@ class PromptBuilder:
{"type": "kfc_reply", "content": "你的回复内容"}
],
"expected_reaction": "你期待对方的反应是什么",
"max_wait_seconds": 等待时间0 表示不等待
"max_wait_seconds": "预估的等待时间(秒)"
}
```
@@ -1035,7 +1035,10 @@ class PromptBuilder:
- `thought`:你的内心独白,记录你此刻的想法和感受。要自然,不要技术性语言。
- `actions`:你要执行的动作列表。对于 `kfc_reply` 动作,**必须**填写 `content` 字段,写上你要说的话。
- `expected_reaction`:你期待对方如何回应(用于判断是否需要等待)
- `max_wait_seconds`设定等待时间(秒),0 表示不等待,超时后你会考虑是否要主动说点什么
- `max_wait_seconds`预估的等待时间(秒),这很关键,请根据对话节奏来判断:
- 如果你刚问了一个开放性问题(比如"你觉得呢?""后来怎么样了?"),或者对话明显还在兴头上,设置一个等待时间(比如 60-180 秒),给对方思考和打字的时间。
- 如果对话感觉自然结束了(比如晚安、拜拜),或者你给出了一个总结性的陈述,那就设置为 0表示你觉得可以告一段落了。
- 不要总是设为 0那会显得你很急着结束对话。
### 注意事项
- kfc_reply 的 content 字段是必填的,直接写你要发送的消息内容

View File

@@ -124,11 +124,24 @@ kfc_SITUATION_TIMEOUT = Prompt(
你原本打算最多等 {max_wait_minutes:.1f} 分钟,现在已经等了 {elapsed_minutes:.1f} 分钟了,对方还是没回。
你当时期待的反应是:"{expected_reaction}"
{timeout_context}
你需要决定:
1. 继续等待(设置新的 max_wait_seconds
2. 主动说点什么打破沉默
3. 做点别的事情(执行其他动作)
4. 算了不等了max_wait_seconds = 0
**决策指导:**
- **评估话题**:你上一条消息是结束性的,还是开启性的?是陈述句,还是疑问句?
- **考虑关系**:你们的关系有多亲近?频繁追问是否会打扰到对方?
- **保持自然**:像真人一样思考,对方可能只是暂时在忙。
**决策选项:**
1. **再等等看**:如果觉得话题还没结束,或者对方可能只是需要多点时间,可以选择再等一会。
- 使用 `do_nothing`,并设置一个新的、合理的 `max_wait_seconds`(比如 60-300 秒)。
2. **开启新话题**:如果感觉之前的话题已经告一段落,或者想轻松地打破沉默,可以主动开启一个轻松的新话题。
- 使用 `kfc_reply` 发送一条新的、不相关的问候或分享。
3. **轻轻追问**:如果你们正在讨论一件很重要的事,或者你发的上一条是关键问题,可以委婉地追问一下。
- 使用 `kfc_reply` 发送一条温柔的提醒,例如"在忙吗?"或者"刚才说到哪啦?"
4. **结束等待**:如果你觉得对话确实已经结束,或者不想再打扰对方,就自然地结束等待。
- 使用 `do_nothing`,并将 `max_wait_seconds` 设为 0。
【注意】如果已经连续多次超时,对方可能暂时不方便回复。频繁主动发消息可能会打扰到对方。
考虑是否应该暂时放下期待,让对方有空间。""",
@@ -228,7 +241,7 @@ kfc_PLANNER_OUTPUT_FORMAT = Prompt(
{{"type": "动作名称", ...动作参数}}
],
"expected_reaction": "你期待对方的反应是什么",
"max_wait_seconds": 等待时间0 表示不等待
"max_wait_seconds": "预估的等待时间(秒)"
}}
```
@@ -237,7 +250,10 @@ kfc_PLANNER_OUTPUT_FORMAT = Prompt(
- `actions`:你要执行的动作列表。每个动作是一个对象,必须包含 `type` 字段指定动作类型,其他字段根据动作类型不同而不同(参考上面每个动作的示例)。
- 对于 `kfc_reply` 动作,只需要指定 `{{"type": "kfc_reply"}}`,不需要填写 `content` 字段(回复内容会单独生成)
- `expected_reaction`:你期待对方如何回应(用于判断是否需要等待)
- `max_wait_seconds`设定等待时间(秒),0 表示不等待,超时后你会考虑是否要主动说点什么
- `max_wait_seconds`预估的等待时间(秒),这很关键,请根据对话节奏来判断:
- 如果你刚问了一个开放性问题(比如"你觉得呢?""后来怎么样了?"),或者对话明显还在兴头上,设置一个等待时间(比如 60-180 秒),给对方思考和打字的时间。
- 如果对话感觉自然结束了(比如晚安、拜拜),或者你给出了一个总结性的陈述,那就设置为 0表示你觉得可以告一段落了。
- 不要总是设为 0那会显得你很急着结束对话。
### 注意事项
- 动作参数直接写在动作对象里,不需要 `action_data` 包装
@@ -378,7 +394,7 @@ kfc_UNIFIED_OUTPUT_FORMAT = Prompt(
{{"type": "kfc_reply", "content": "你的回复内容"}}
],
"expected_reaction": "你期待对方的反应是什么",
"max_wait_seconds": 等待时间0 表示不等待
"max_wait_seconds": "预估的等待时间(秒)"
}}
```
@@ -386,7 +402,10 @@ kfc_UNIFIED_OUTPUT_FORMAT = Prompt(
- `thought`:你的内心独白,记录你此刻的想法和感受。要自然,不要技术性语言。
- `actions`:你要执行的动作列表。对于 `kfc_reply` 动作,**必须**填写 `content` 字段,写上你要说的话。
- `expected_reaction`:你期待对方如何回应(用于判断是否需要等待)
- `max_wait_seconds`设定等待时间(秒),0 表示不等待,超时后你会考虑是否要主动说点什么。如果你认为聊天没有继续的必要,或不想打扰对方,可以设为 0。
- `max_wait_seconds`预估的等待时间(秒),这很关键,请根据对话节奏来判断:
- 如果你刚问了一个开放性问题(比如"你觉得呢?""后来怎么样了?"),或者对话明显还在兴头上,设置一个等待时间(比如 60-180 秒),给对方思考和打字的时间。
- 如果对话感觉自然结束了(比如晚安、拜拜),或者你给出了一个总结性的陈述,那就设置为 0表示你觉得可以告一段落了。
- 不要总是设为 0那会显得你很急着结束对话。
### 注意事项
- kfc_reply 的 content 字段是**必填**的,直接写你要发送的消息内容

View File

@@ -266,13 +266,18 @@ def build_output_module(context_data: Optional[dict[str, str]] = None) -> str:
{
"thought": "你心里的真实想法,像日记一样自然",
"expected_user_reaction": "猜猜对方看到会怎么想",
"max_wait_seconds": 等多久60-900不等就填0,
"max_wait_seconds": "预估的等待时间(秒)",
"actions": [
{"type": "kfc_reply", "content": "你要说的话"}
]
}
```
关于 max_wait_seconds等待时间
- 如果你刚问了一个开放性问题(比如"你觉得呢?""后来怎么样了?"),或者对话明显还在兴头上,设置一个等待时间(比如 60-180 秒),给对方思考和打字的时间。
- 如果对话感觉自然结束了(比如晚安、拜拜),或者你给出了一个总结性的陈述,那就设置为 0表示你觉得可以告一段落了。
- 不要总是设为 0那会显得你很急着结束对话。
关于 thought内心想法
- 写你真正在想的,不是在分析任务
- 像心里嘀咕一样,比如"这家伙又来撒娇了~" "有点困了但还想再聊会儿"
@@ -375,20 +380,31 @@ TIMEOUT_DECISION_USER_PROMPT_TEMPLATE = """## 聊天记录
## 现在的情况
你发了消息,等了 {wait_duration_seconds:.0f} 秒({wait_duration_minutes:.1f} 分钟),对方还没回。
你之前觉得对方可能会:{expected_user_reaction}
{followup_warning}
你发的最后一条:{last_bot_message}
你发的最后一条消息是:「{last_bot_message}
---
你拿起手机看了一眼,发现对方还没回复。你想怎么办?
选项:
1. **继续等** - 用 `do_nothing`,设个 `max_wait_seconds` 等一会儿再看
2. **发消息** - 用 `reply`,不过别太频繁追问
3. **算了不等了** - 用 `do_nothing``max_wait_seconds` 设为 0
**决策指导:**
- **评估话题**:你上一条消息是结束性的,还是开启性的?是陈述句,还是疑问句?
- **考虑关系**:你们的关系有多亲近?频繁追问是否会打扰到对方?
- **保持自然**:像真人一样思考,对方可能只是暂时在忙。
用 JSON 输出你的想法和决策。"""
**决策选项:**
1. **再等等看**:如果觉得话题还没结束,或者对方可能只是需要多点时间,可以选择再等一会。
- **动作**:使用 `do_nothing`,并设置一个新的、合理的 `max_wait_seconds`(比如 60-300 秒)。
2. **开启新话题**:如果感觉之前的话题已经告一段落,或者想轻松地打破沉默,可以主动开启一个轻松的新话题。
- **动作**:使用 `kfc_reply` 发送一条新的、不相关的问候或分享。
3. **轻轻追问**:如果你们正在讨论一件很重要的事,或者你发的上一条是关键问题,可以委婉地追问一下。
- **动作**:使用 `kfc_reply` 发送一条温柔的提醒,例如"在忙吗?"或者"刚才说到哪啦?"
4. **结束等待**:如果你觉得对话确实已经结束,或者不想再打扰对方,就自然地结束等待。
- **动作**:使用 `do_nothing`,并将 `max_wait_seconds` 设为 0。
用 JSON 输出你的想法和最终决策。"""
PROACTIVE_THINKING_USER_PROMPT_TEMPLATE = """## 聊天记录

View File

@@ -561,7 +561,7 @@ def _log_pretty_response(response: LLMResponse, stream_id: str | None = None) ->
for i, reply in enumerate(replies):
if len(replies) > 1:
logger.info(f"[KFC] 💬[{i+1}] {reply}")
logger.info(f"[KFC] 💬 [{i+1}] {reply}")
else:
logger.info(f"[KFC] 💬 {reply}")
@@ -569,7 +569,7 @@ def _log_pretty_response(response: LLMResponse, stream_id: str | None = None) ->
logger.info(f"[KFC] 🎯 {', '.join(actions)}")
if response.max_wait_seconds > 0 or response.expected_reaction:
meta = f"{response.max_wait_seconds}s" if response.max_wait_seconds > 0 else ""
meta = f" {response.max_wait_seconds}s" if response.max_wait_seconds > 0 else ""
if response.expected_reaction:
meta += f" 预期: {response.expected_reaction}"
logger.info(f"[KFC] {meta.strip()}")

View File

@@ -90,6 +90,11 @@ class SendHandler:
logger.critical("现在暂时不支持解析此回复!")
return None
# 🔧 确保 reply 消息段始终在列表最前面
# 排序原则reply 类型优先级最高(排序值为 0其他类型保持原有顺序排序值为 1
# 使用 stable sort 确保非 reply 元素的相对顺序不变
processed_message.sort(key=lambda seg: 0 if isinstance(seg, dict) and seg.get("type") == "reply" else 1)
if group_info and group_info.get("group_id"):
logger.debug("发送群聊消息")
target_id = int(group_info["group_id"])