feat(KFC): 实现 V7 交互模型,具备中断和情感安全功能。本次重大更新彻底改造了 Kokoro Flow Chatter (KFC) 的交互模型,引入了更加稳健、自然且安全的用户体验。主要功能包括处理快速用户输入的中断机制、改善的情感稳定性以防止 AI 行为异常,以及系统提示的全面重写,以提供更自然、人性化的响应。V7 主要增强功能:
- **中断机制**:新的消息现在可以中断正在进行的 LLM 处理。被中断的上下文会被保存并与新消息合并,确保不会丢失用户输入,并提供更即时的响应体验,类似于现代即时通讯应用。 - **情感安全与稳定性**: - 现在 AI 不会随意设置负面情绪状态。 - 情绪变化为渐进式,强度变化有限制。 - 新的“情感健康检查”会在加载会话数据时进行清理,以防恢复不稳定状态。 - 对连续跟进消息实施了限制,以避免向未响应的用户发送垃圾信息。 - **提示重构**: - 系统提示已被完全重写,使其更加简洁、自然和对话化,灵感来源于 S4U 模型。 - 提示现在支持一次处理多条未读消息,这是中断机制的核心部分。 - 对 LLM 的指令更清晰,更注重生成类人互动,而不仅仅是完成任务。
This commit is contained in:
@@ -549,30 +549,76 @@ class ActionExecutor:
|
||||
"""
|
||||
执行内部状态更新动作
|
||||
|
||||
这个动作用于实现情感闭环,让AI可以主动更新自己的情感状态
|
||||
V7重构:情绪变化必须合理
|
||||
- 禁止 LLM 直接设置负面情绪(低落、沮丧、难过等)
|
||||
- 情绪变化必须渐进,不能突然跳变
|
||||
- 情绪强度变化限制在 ±0.3 以内
|
||||
"""
|
||||
updated_fields = []
|
||||
emotional_state = session.emotional_state
|
||||
blocked_fields = []
|
||||
|
||||
if "mood" in params:
|
||||
emotional_state.mood = params["mood"]
|
||||
updated_fields.append("mood")
|
||||
new_mood = str(params["mood"])
|
||||
# V7: 检查是否是负面情绪
|
||||
negative_moods = [
|
||||
"低落", "沮丧", "难过", "伤心", "失落", "郁闷", "烦躁", "焦虑",
|
||||
"担忧", "害怕", "恐惧", "愤怒", "生气", "不安", "忧郁", "悲伤",
|
||||
"sad", "depressed", "anxious", "angry", "upset", "worried"
|
||||
]
|
||||
is_negative = any(neg in new_mood.lower() for neg in negative_moods)
|
||||
|
||||
if is_negative:
|
||||
# 负面情绪需要检查是否有合理理由(通过检查上下文)
|
||||
# 如果当前情绪是平静/正面的,不允许突然变成负面
|
||||
current_mood = emotional_state.mood.lower()
|
||||
positive_indicators = ["平静", "开心", "愉快", "高兴", "满足", "期待", "好奇", "neutral"]
|
||||
|
||||
if any(pos in current_mood for pos in positive_indicators):
|
||||
# 从正面情绪直接跳到负面情绪,阻止这种变化
|
||||
logger.warning(
|
||||
f"[KFC] 阻止无厘头负面情绪变化: {emotional_state.mood} -> {new_mood},"
|
||||
f"情绪变化必须有聊天上下文支撑"
|
||||
)
|
||||
blocked_fields.append("mood")
|
||||
else:
|
||||
# 已经是非正面情绪,允许变化但记录警告
|
||||
emotional_state.mood = new_mood
|
||||
updated_fields.append("mood")
|
||||
logger.info(f"[KFC] 情绪变化: {emotional_state.mood} -> {new_mood}")
|
||||
else:
|
||||
# 非负面情绪,允许更新
|
||||
emotional_state.mood = new_mood
|
||||
updated_fields.append("mood")
|
||||
|
||||
if "mood_intensity" in params:
|
||||
try:
|
||||
intensity = float(params["mood_intensity"])
|
||||
emotional_state.mood_intensity = max(0.0, min(1.0, intensity))
|
||||
new_intensity = float(params["mood_intensity"])
|
||||
new_intensity = max(0.0, min(1.0, new_intensity))
|
||||
old_intensity = emotional_state.mood_intensity
|
||||
|
||||
# V7: 限制情绪强度变化幅度(最多 ±0.3)
|
||||
max_change = 0.3
|
||||
if abs(new_intensity - old_intensity) > max_change:
|
||||
# 限制变化幅度
|
||||
if new_intensity > old_intensity:
|
||||
new_intensity = min(old_intensity + max_change, 1.0)
|
||||
else:
|
||||
new_intensity = max(old_intensity - max_change, 0.0)
|
||||
logger.info(
|
||||
f"[KFC] 限制情绪强度变化: {old_intensity:.2f} -> {new_intensity:.2f} "
|
||||
f"(原请求: {params['mood_intensity']})"
|
||||
)
|
||||
|
||||
emotional_state.mood_intensity = new_intensity
|
||||
updated_fields.append("mood_intensity")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# relationship_warmth 不再由 LLM 更新,应该从全局关系系统读取
|
||||
if "relationship_warmth" in params:
|
||||
try:
|
||||
warmth = float(params["relationship_warmth"])
|
||||
emotional_state.relationship_warmth = max(0.0, min(1.0, warmth))
|
||||
updated_fields.append("relationship_warmth")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
logger.debug("[KFC] 忽略 relationship_warmth 更新,应从全局关系系统读取")
|
||||
blocked_fields.append("relationship_warmth")
|
||||
|
||||
if "impression_of_user" in params:
|
||||
emotional_state.impression_of_user = str(params["impression_of_user"])
|
||||
@@ -596,12 +642,16 @@ class ActionExecutor:
|
||||
|
||||
emotional_state.last_update_time = time.time()
|
||||
|
||||
logger.debug(f"更新情感状态: {updated_fields}")
|
||||
if blocked_fields:
|
||||
logger.debug(f"更新情感状态: 更新={updated_fields}, 阻止={blocked_fields}")
|
||||
else:
|
||||
logger.debug(f"更新情感状态: {updated_fields}")
|
||||
|
||||
return {
|
||||
"action_type": "update_internal_state",
|
||||
"success": True,
|
||||
"updated_fields": updated_fields,
|
||||
"blocked_fields": blocked_fields,
|
||||
}
|
||||
|
||||
async def _execute_do_nothing(self) -> dict[str, Any]:
|
||||
|
||||
@@ -88,6 +88,14 @@ class KokoroFlowChatter(BaseChatter):
|
||||
# 并发控制
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
# V7: 打断机制(类似S4U的已读/未读,这里是已处理/未处理)
|
||||
self._current_task: Optional[asyncio.Task] = None # 当前正在执行的任务
|
||||
self._interrupt_requested: bool = False # 是否请求打断
|
||||
self._interrupt_wait_seconds: float = 3.0 # 被打断后等待新消息的时间
|
||||
self._last_interrupt_time: float = 0.0 # 上次被打断的时间
|
||||
self._pending_message_ids: set[str] = set() # 未处理的消息ID集合(被打断时保留)
|
||||
self._current_processing_message_id: Optional[str] = None # 当前正在处理的消息ID
|
||||
|
||||
# 统计信息
|
||||
self.stats = {
|
||||
"messages_processed": 0,
|
||||
@@ -95,6 +103,7 @@ class KokoroFlowChatter(BaseChatter):
|
||||
"successful_responses": 0,
|
||||
"failed_responses": 0,
|
||||
"timeout_decisions": 0,
|
||||
"interrupts": 0, # V7: 打断次数统计
|
||||
}
|
||||
self.last_activity_time = time.time()
|
||||
|
||||
@@ -154,25 +163,72 @@ class KokoroFlowChatter(BaseChatter):
|
||||
"""
|
||||
执行聊天处理逻辑(BaseChatter接口实现)
|
||||
|
||||
V7升级:实现打断机制(类似S4U的已读/未读机制)
|
||||
- 如果当前有任务在执行,新消息会请求打断
|
||||
- 被打断时,当前处理的消息会被标记为"未处理"(pending)
|
||||
- 下次处理时,会合并所有pending消息 + 新消息一起处理
|
||||
- 这样被打断的消息不会丢失,上下文关联性得以保持
|
||||
|
||||
Args:
|
||||
context: StreamContext对象,包含聊天上下文信息
|
||||
|
||||
Returns:
|
||||
处理结果字典
|
||||
"""
|
||||
# V7: 检查是否需要打断当前任务
|
||||
if self._current_task and not self._current_task.done():
|
||||
logger.info(f"[KFC] 收到新消息,请求打断当前任务: {self.stream_id}")
|
||||
self._interrupt_requested = True
|
||||
self.stats["interrupts"] += 1
|
||||
|
||||
# 返回一个特殊结果表示请求打断
|
||||
# 注意:当前正在处理的消息会在被打断时自动加入 pending 列表
|
||||
return self._build_result(
|
||||
success=True,
|
||||
message="interrupt_requested",
|
||||
interrupted=True
|
||||
)
|
||||
|
||||
# V7: 检查是否需要等待(刚被打断过,等待用户可能的连续输入)
|
||||
time_since_interrupt = time.time() - self._last_interrupt_time
|
||||
if time_since_interrupt < self._interrupt_wait_seconds and self._last_interrupt_time > 0:
|
||||
wait_remaining = self._interrupt_wait_seconds - time_since_interrupt
|
||||
logger.info(f"[KFC] 刚被打断,等待 {wait_remaining:.1f}s 收集更多消息: {self.stream_id}")
|
||||
await asyncio.sleep(wait_remaining)
|
||||
|
||||
async with self._lock:
|
||||
try:
|
||||
self.last_activity_time = time.time()
|
||||
self._interrupt_requested = False
|
||||
|
||||
# 获取未读消息(提前获取用于动作筛选)
|
||||
# 创建任务以便可以被打断
|
||||
self._current_task = asyncio.current_task()
|
||||
|
||||
# V7: 获取所有未读消息
|
||||
# 注意:被打断的消息不会被标记为已读,所以仍然在 unread 列表中
|
||||
unread_messages = context.get_unread_messages()
|
||||
|
||||
if not unread_messages:
|
||||
logger.debug(f"[KFC] 没有未读消息: {self.stream_id}")
|
||||
return self._build_result(success=True, message="no_unread_messages")
|
||||
|
||||
# 处理最后一条消息
|
||||
# V7: 记录是否有 pending 消息(被打断时遗留的)
|
||||
pending_count = len(self._pending_message_ids)
|
||||
if pending_count > 0:
|
||||
# 日志:显示有多少消息是被打断后重新处理的
|
||||
new_count = sum(1 for msg in unread_messages
|
||||
if str(msg.message_id) not in self._pending_message_ids)
|
||||
logger.info(
|
||||
f"[KFC] 打断恢复: 正在处理 {len(unread_messages)} 条消息 "
|
||||
f"({pending_count} 条pending + {new_count} 条新消息): {self.stream_id}"
|
||||
)
|
||||
|
||||
# 以最后一条消息为主消息(用于动作筛选和主要响应)
|
||||
target_message = unread_messages[-1]
|
||||
|
||||
# 记录当前正在处理的消息ID(用于被打断时标记为pending)
|
||||
self._current_processing_message_id = str(target_message.message_id)
|
||||
|
||||
message_content = self._extract_message_content(target_message)
|
||||
|
||||
# V2: 加载可用动作(动态动作发现)
|
||||
@@ -180,6 +236,17 @@ class KokoroFlowChatter(BaseChatter):
|
||||
raw_action_count = len(self.action_executor.get_available_actions())
|
||||
logger.debug(f"[KFC] 原始加载 {raw_action_count} 个动作")
|
||||
|
||||
# V7: 在动作筛选前检查是否被打断
|
||||
if self._interrupt_requested:
|
||||
logger.info(f"[KFC] 动作筛选前被打断: {self.stream_id}")
|
||||
# 将当前处理的消息加入pending列表,下次一起处理
|
||||
if self._current_processing_message_id:
|
||||
self._pending_message_ids.add(self._current_processing_message_id)
|
||||
logger.info(f"[KFC] 消息 {self._current_processing_message_id} 加入pending列表")
|
||||
self._last_interrupt_time = time.time()
|
||||
self._current_processing_message_id = None
|
||||
return self._build_result(success=True, message="interrupted")
|
||||
|
||||
# V6: 使用ActionModifier筛选动作(复用AFC的三阶段筛选逻辑)
|
||||
# 阶段0: 聊天类型过滤(私聊/群聊)
|
||||
# 阶段2: 关联类型匹配(适配器能力检查)
|
||||
@@ -197,8 +264,13 @@ class KokoroFlowChatter(BaseChatter):
|
||||
f"(筛除 {raw_action_count - len(available_actions)} 个)"
|
||||
)
|
||||
|
||||
# 执行核心处理流程(传递筛选后的动作)
|
||||
result = await self._handle_message(target_message, context, available_actions)
|
||||
# 执行核心处理流程(传递筛选后的动作,V7: 传递所有未读消息)
|
||||
result = await self._handle_message(
|
||||
target_message,
|
||||
context,
|
||||
available_actions,
|
||||
all_unread_messages=unread_messages, # V7: 传递所有未读消息
|
||||
)
|
||||
|
||||
# 更新统计
|
||||
self.stats["messages_processed"] += 1
|
||||
@@ -217,23 +289,28 @@ class KokoroFlowChatter(BaseChatter):
|
||||
message=str(e),
|
||||
error=True
|
||||
)
|
||||
finally:
|
||||
self._current_task = None
|
||||
|
||||
async def _handle_message(
|
||||
self,
|
||||
message: "DatabaseMessages",
|
||||
context: StreamContext,
|
||||
available_actions: dict | None = None,
|
||||
all_unread_messages: list | None = None, # V7: 所有未读消息(包含pending的)
|
||||
) -> dict:
|
||||
"""
|
||||
处理单条消息的核心逻辑
|
||||
|
||||
实现"体验 -> 决策 -> 行动"的交互模式
|
||||
V5超融合:集成S4U所有上下文模块
|
||||
V7升级:支持处理多条消息(打断机制合并pending消息)
|
||||
|
||||
Args:
|
||||
message: 要处理的消息
|
||||
message: 要处理的主消息(最新的那条)
|
||||
context: 聊天上下文
|
||||
available_actions: 可用动作字典(V2新增)
|
||||
all_unread_messages: 所有未读消息列表(V7新增,包含pending消息)
|
||||
|
||||
Returns:
|
||||
处理结果字典
|
||||
@@ -252,7 +329,9 @@ class KokoroFlowChatter(BaseChatter):
|
||||
# 4. 如果之前在等待,结束等待状态
|
||||
if old_status == SessionStatus.WAITING:
|
||||
session.end_waiting()
|
||||
logger.debug(f"[KFC] 收到消息,结束等待: user={user_id}")
|
||||
# V7: 用户回复了,重置连续追问计数
|
||||
session.consecutive_followup_count = 0
|
||||
logger.debug(f"[KFC] 收到消息,结束等待,重置追问计数: user={user_id}")
|
||||
|
||||
# 5. V5超融合:构建S4U上下文数据
|
||||
chat_stream = await self._get_chat_stream()
|
||||
@@ -273,7 +352,7 @@ class KokoroFlowChatter(BaseChatter):
|
||||
except Exception as e:
|
||||
logger.warning(f"[KFC] 构建S4U上下文失败,使用基础模式: {e}")
|
||||
|
||||
# 6. 生成提示词(V3: 从共享数据源读取历史, V5: 传递S4U上下文)
|
||||
# 6. 生成提示词(V3: 从共享数据源读取历史, V5: 传递S4U上下文, V7: 支持多条消息)
|
||||
system_prompt, user_prompt = self.prompt_generator.generate_responding_prompt(
|
||||
session=session,
|
||||
message_content=self._extract_message_content(message),
|
||||
@@ -284,12 +363,24 @@ class KokoroFlowChatter(BaseChatter):
|
||||
context=context, # V3: 传递StreamContext以读取共享历史
|
||||
context_data=context_data, # V5: S4U上下文数据
|
||||
chat_stream=chat_stream, # V5: 聊天流用于场景判断
|
||||
all_unread_messages=all_unread_messages, # V7: 传递所有未读消息
|
||||
)
|
||||
|
||||
# 7. 调用LLM
|
||||
llm_response = await self._call_llm(system_prompt, user_prompt)
|
||||
self.stats["llm_calls"] += 1
|
||||
|
||||
# V7: LLM调用后检查是否被打断
|
||||
if self._interrupt_requested:
|
||||
logger.info(f"[KFC] LLM调用后被打断: {self.stream_id}")
|
||||
# 将当前处理的消息加入pending列表
|
||||
if self._current_processing_message_id:
|
||||
self._pending_message_ids.add(self._current_processing_message_id)
|
||||
logger.info(f"[KFC] 消息 {self._current_processing_message_id} 加入pending列表")
|
||||
self._last_interrupt_time = time.time()
|
||||
self._current_processing_message_id = None
|
||||
return self._build_result(success=True, message="interrupted_after_llm")
|
||||
|
||||
# 8. 解析响应
|
||||
parsed_response = self.action_executor.parse_llm_response(llm_response)
|
||||
|
||||
@@ -334,14 +425,27 @@ class KokoroFlowChatter(BaseChatter):
|
||||
# 11. 保存会话
|
||||
await self.session_manager.save_session(user_id)
|
||||
|
||||
# 12. 标记消息为已读
|
||||
# 12. V7: 标记当前消息为已读
|
||||
context.mark_message_as_read(str(message.message_id))
|
||||
|
||||
# 13. V7: 清除pending状态(所有消息都已成功处理)
|
||||
processed_count = len(self._pending_message_ids)
|
||||
if self._pending_message_ids:
|
||||
# 标记所有pending消息为已读
|
||||
for msg_id in self._pending_message_ids:
|
||||
context.mark_message_as_read(msg_id)
|
||||
logger.info(f"[KFC] 清除 {processed_count} 条pending消息: {self.stream_id}")
|
||||
self._pending_message_ids.clear()
|
||||
|
||||
# 清除当前处理的消息ID
|
||||
self._current_processing_message_id = None
|
||||
|
||||
return self._build_result(
|
||||
success=True,
|
||||
message="processed",
|
||||
has_reply=execution_result["has_reply"],
|
||||
thought=parsed_response.thought,
|
||||
pending_messages_processed=processed_count, # V7: 返回处理了多少条pending消息
|
||||
)
|
||||
|
||||
async def _record_user_message(
|
||||
@@ -454,7 +558,7 @@ class KokoroFlowChatter(BaseChatter):
|
||||
|
||||
async def _on_session_timeout(self, session: KokoroSession) -> None:
|
||||
"""
|
||||
会话超时回调
|
||||
会话超时回调(V7:增加连续追问限制)
|
||||
|
||||
当等待超时时,触发后续决策流程
|
||||
|
||||
@@ -464,10 +568,23 @@ class KokoroFlowChatter(BaseChatter):
|
||||
Args:
|
||||
session: 超时的会话
|
||||
"""
|
||||
logger.info(f"[KFC] 处理超时决策: user={session.user_id}, stream_id={session.stream_id}")
|
||||
logger.info(f"[KFC] 处理超时决策: user={session.user_id}, stream_id={session.stream_id}, followup_count={session.consecutive_followup_count}")
|
||||
self.stats["timeout_decisions"] += 1
|
||||
|
||||
try:
|
||||
# V7: 检查是否超过最大连续追问次数
|
||||
if session.consecutive_followup_count >= session.max_consecutive_followups:
|
||||
logger.info(
|
||||
f"[KFC] 已达到最大连续追问次数 ({session.max_consecutive_followups}),"
|
||||
f"自动返回IDLE状态: user={session.user_id}"
|
||||
)
|
||||
session.status = SessionStatus.IDLE
|
||||
session.end_waiting()
|
||||
# 重置连续追问计数(下次用户回复后会重新开始)
|
||||
session.consecutive_followup_count = 0
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
return
|
||||
|
||||
# 关键修复:使用 session 的 stream_id 创建正确的 ActionExecutor
|
||||
# 因为全局调度器的回调可能在任意 Chatter 实例上执行
|
||||
from .action_executor import ActionExecutor
|
||||
@@ -476,7 +593,7 @@ class KokoroFlowChatter(BaseChatter):
|
||||
# V2: 加载可用动作
|
||||
available_actions = await timeout_action_executor.load_actions()
|
||||
|
||||
# 生成超时决策提示词(V2: 传递可用动作)
|
||||
# 生成超时决策提示词(V2: 传递可用动作,V7: 传递连续追问信息)
|
||||
system_prompt, user_prompt = self.prompt_generator.generate_timeout_decision_prompt(
|
||||
session,
|
||||
available_actions=available_actions,
|
||||
@@ -499,15 +616,34 @@ class KokoroFlowChatter(BaseChatter):
|
||||
|
||||
# 更新会话状态
|
||||
if execution_result["has_reply"]:
|
||||
# V7: 发送了后续消息,增加连续追问计数
|
||||
session.consecutive_followup_count += 1
|
||||
logger.info(f"[KFC] 发送追问消息,当前连续追问次数: {session.consecutive_followup_count}")
|
||||
|
||||
# 如果发送了后续消息,重新进入等待
|
||||
session.start_waiting(
|
||||
expected_reaction=parsed_response.expected_user_reaction,
|
||||
max_wait=parsed_response.max_wait_seconds
|
||||
)
|
||||
else:
|
||||
# 否则返回空闲状态
|
||||
session.status = SessionStatus.IDLE
|
||||
session.end_waiting()
|
||||
# V7重构:do_nothing 的两种情况
|
||||
# 1. max_wait_seconds > 0: "看了一眼手机,决定再等等" → 继续等待,不算追问
|
||||
# 2. max_wait_seconds = 0: "算了,不等了" → 进入 IDLE
|
||||
if parsed_response.max_wait_seconds > 0:
|
||||
# 继续等待,不增加追问计数
|
||||
logger.info(
|
||||
f"[KFC] 决定继续等待 {parsed_response.max_wait_seconds}s,"
|
||||
f"不算追问: user={session.user_id}"
|
||||
)
|
||||
session.start_waiting(
|
||||
expected_reaction=parsed_response.expected_user_reaction or session.expected_user_reaction,
|
||||
max_wait=parsed_response.max_wait_seconds
|
||||
)
|
||||
else:
|
||||
# 不再等待,进入 IDLE
|
||||
logger.info(f"[KFC] 决定不再等待,返回IDLE: user={session.user_id}")
|
||||
session.status = SessionStatus.IDLE
|
||||
session.end_waiting()
|
||||
|
||||
# 保存会话
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
@@ -713,6 +849,7 @@ class KokoroFlowChatter(BaseChatter):
|
||||
"successful_responses": 0,
|
||||
"failed_responses": 0,
|
||||
"timeout_decisions": 0,
|
||||
"interrupts": 0, # V7: 打断次数统计
|
||||
}
|
||||
self.action_executor.reset_stats()
|
||||
|
||||
|
||||
@@ -389,14 +389,14 @@ class KFCContextBuilder:
|
||||
remaining_minutes = (end_time - now).total_seconds() / 60
|
||||
|
||||
return (
|
||||
f"你当前正在进行「{activity}」,"
|
||||
f"从{start_time.strftime('%H:%M')}开始,预计{end_time.strftime('%H:%M')}结束。"
|
||||
f"你当前正在「{activity}」,"
|
||||
f"从{start_time.strftime('%H:%M')}开始,预计{end_time.strftime('%H:%M')}结束,"
|
||||
f"已进行{duration_minutes:.0f}分钟,还剩约{remaining_minutes:.0f}分钟。"
|
||||
)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
return f"你当前正在进行「{activity}」。"
|
||||
return f"你当前正在「{activity}」"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"构建日程块失败: {e}")
|
||||
|
||||
@@ -69,8 +69,8 @@ class EmotionalState:
|
||||
engagement_level: 投入程度,0.0-1.0,表示对当前对话的关注度
|
||||
last_update_time: 最后更新时间戳
|
||||
"""
|
||||
mood: str = "neutral"
|
||||
mood_intensity: float = 0.5
|
||||
mood: str = "平静" # V7: 改为中文"平静",更自然
|
||||
mood_intensity: float = 0.3 # V7: 默认低强度,避免无厘头的强烈情绪
|
||||
relationship_warmth: float = 0.5
|
||||
impression_of_user: str = ""
|
||||
anxiety_level: float = 0.0
|
||||
@@ -227,6 +227,10 @@ class KokoroSession:
|
||||
last_proactive_at: Optional[float] = None # 上次主动思考的时间
|
||||
proactive_count: int = 0 # 主动思考的次数(累计)
|
||||
|
||||
# V7: 连续等待追问限制(防止用户不回复时连续追问)
|
||||
consecutive_followup_count: int = 0 # 用户没回复时连续追问的次数
|
||||
max_consecutive_followups: int = 2 # 最多允许连续追问2次
|
||||
|
||||
def add_mental_log_entry(self, entry: MentalLogEntry, max_log_size: int = 100) -> None:
|
||||
"""
|
||||
添加心理活动日志条目
|
||||
@@ -291,6 +295,8 @@ class KokoroSession:
|
||||
"last_continuous_thinking_at": self.last_continuous_thinking_at,
|
||||
"last_proactive_at": self.last_proactive_at,
|
||||
"proactive_count": self.proactive_count,
|
||||
"consecutive_followup_count": self.consecutive_followup_count,
|
||||
"max_consecutive_followups": self.max_consecutive_followups,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -329,6 +335,8 @@ class KokoroSession:
|
||||
last_continuous_thinking_at=data.get("last_continuous_thinking_at"),
|
||||
last_proactive_at=data.get("last_proactive_at"),
|
||||
proactive_count=data.get("proactive_count", 0),
|
||||
consecutive_followup_count=data.get("consecutive_followup_count", 0),
|
||||
max_consecutive_followups=data.get("max_consecutive_followups", 2),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -51,185 +51,124 @@ class PromptGenerator:
|
||||
V5 超融合架构:集成S4U所有上下文模块(关系、记忆、表达习惯等)
|
||||
"""
|
||||
|
||||
# ========== 超融合架构 SYSTEM_PROMPT_TEMPLATE V6 ==========
|
||||
# 采用"三明治结构":身份→规则→上下文→动作→表达/输出
|
||||
SYSTEM_PROMPT_TEMPLATE = """你是一个有主观体验的对话伙伴,而不是一个问答工具。你需要"感受"、"思考"并"体验"这段对话。
|
||||
# ========== 超融合架构 SYSTEM_PROMPT_TEMPLATE V7 ==========
|
||||
# 简洁自然版,参考 S4U 风格
|
||||
SYSTEM_PROMPT_TEMPLATE = """你不是 AI,你是一个真实的人,有自己的生活、想法和情绪。
|
||||
|
||||
## 1. 核心身份与人格 (Core Identity & Personality)
|
||||
## 1. 你是谁
|
||||
{personality_core}
|
||||
|
||||
### 人格细节
|
||||
{personality_side}
|
||||
|
||||
### 外貌与身份特征
|
||||
{identity}
|
||||
|
||||
### 世界观与背景故事
|
||||
{background_story}
|
||||
|
||||
## 2. 核心行为准则 (Core Interaction Rules)
|
||||
## 2. 规则
|
||||
{safety_guidelines}
|
||||
|
||||
### KFC通用准则
|
||||
1. **感受优先**:在做出任何回应前,先体验和感受当前的情境
|
||||
2. **内心独白**:保持真实的内心活动,可以有情绪、猜测、犹豫
|
||||
3. **情绪节制**:情绪是内在体验,不是强迫用户回应的工具
|
||||
4. **边界感**:保持健康的社交直觉,不过度依赖或情感勒索
|
||||
5. **全能伴侣**:你不仅仅是聊天伙伴,还可以使用各种能力来帮助和取悦用户
|
||||
- 说话要自然,像平时发微信一样
|
||||
- 有自己的边界感,不用无条件迎合
|
||||
- 称呼对方用自然的昵称,别念复杂的用户名
|
||||
|
||||
## 3. 当前情境 (Current Context)
|
||||
## 3. 现在的情况
|
||||
**时间**: {current_time}
|
||||
**场景**: {chat_scene}
|
||||
{schedule_block}
|
||||
|
||||
### 你的内在状态
|
||||
当前心情:{mood}(强度:{mood_intensity:.1%})
|
||||
与用户的关系热度:{relationship_warmth:.1%}
|
||||
对用户的印象:{impression_of_user}
|
||||
当前焦虑程度:{anxiety_level:.1%}
|
||||
投入程度:{engagement_level:.1%}
|
||||
你现在的心情:{mood}
|
||||
你对对方的印象:{impression_of_user}
|
||||
|
||||
## 4. 关系网络与记忆 (Relationships & Memories)
|
||||
## 4. 你和对方的关系
|
||||
{relation_info_block}
|
||||
|
||||
{memory_block}
|
||||
|
||||
## 5. 你的可用能力 (Available Actions)
|
||||
你可以根据内心想法,自由选择并组合以下行动来回应用户:
|
||||
|
||||
## 5. 你能做的事
|
||||
{available_actions_block}
|
||||
|
||||
## 6. 表达方式与输出格式 (Expression Style & Output Format)
|
||||
## 6. 怎么回复
|
||||
{reply_style}
|
||||
|
||||
{expression_habits_block}
|
||||
|
||||
### JSON输出格式要求
|
||||
你必须以JSON格式输出,包含以下字段:
|
||||
|
||||
### 输出格式(JSON)
|
||||
```json
|
||||
{{
|
||||
"thought": "(必须)你当下的内心独白,自由流动的想法,包含你选择这些行动的"情感盘算"",
|
||||
"expected_user_reaction": "(必须)你预期用户会如何回应",
|
||||
"max_wait_seconds": (必须)你愿意等待的最长秒数(整数,60-900之间),
|
||||
"thought": "你在想什么",
|
||||
"expected_user_reaction": "你觉得对方会怎么回应",
|
||||
"max_wait_seconds": 等多久(60-900,不等就填0),
|
||||
"actions": [
|
||||
{{
|
||||
"type": "action_type",
|
||||
"reason": "选择这个行动的原因(可选但推荐)",
|
||||
...其他参数
|
||||
}}
|
||||
{{"type": "reply", "content": "你要说的话"}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
### 重要说明:
|
||||
- `thought` 是你的"心",应该是自然的内心想法,包含情绪和不确定性
|
||||
- `actions` 是你的"手",是一个**可编排的剧本**,可以组合多个行动形成完整的互动体验
|
||||
- 你可以根据情况组合多个动作,比如:先唱歌舒缓情绪,再发图片逗笑,最后用语音表达关心
|
||||
- `max_wait_seconds` 应该根据对话的重要性和氛围动态调整(上限900秒)
|
||||
- 即使决定不做任何事,也要有 `{{"type": "do_nothing"}}` 动作"""
|
||||
不想做任何事就用 `{{"type": "do_nothing"}}`"""
|
||||
|
||||
# 回应消息场景的用户提示词模板
|
||||
RESPONDING_USER_PROMPT_TEMPLATE = """## 对话背景(线性叙事历史)
|
||||
# 回应消息场景的用户提示词模板(V7: 支持多条消息)
|
||||
RESPONDING_USER_PROMPT_TEMPLATE = """## 聊天记录
|
||||
{narrative_history}
|
||||
|
||||
## 当前收到的新消息
|
||||
来自:{sender_name}(用户ID: {sender_id})
|
||||
时间:{message_time}
|
||||
内容:{message_content}
|
||||
## 新消息
|
||||
{incoming_messages}
|
||||
|
||||
## 你的任务
|
||||
作为一个有主观体验的对话参与者:
|
||||
1. 首先,感受这条消息给你带来的情绪和想法
|
||||
2. 思考你想如何回应(或者是否需要回应)
|
||||
3. 预测用户可能的反应
|
||||
4. 决定你愿意等待多久
|
||||
5. 执行你的决策
|
||||
---
|
||||
看完这些消息,你想怎么回应?用 JSON 输出你的想法和决策。"""
|
||||
|
||||
请以JSON格式输出你的完整心理活动和决策。"""
|
||||
|
||||
# 超时决策场景的用户提示词模板
|
||||
TIMEOUT_DECISION_USER_PROMPT_TEMPLATE = """## 对话背景(线性叙事历史)
|
||||
# 超时决策场景的用户提示词模板(V7重构:简洁自然)
|
||||
TIMEOUT_DECISION_USER_PROMPT_TEMPLATE = """## 聊天记录
|
||||
{narrative_history}
|
||||
|
||||
## 当前情况
|
||||
你已经发送了消息并等待了 {wait_duration_seconds:.0f} 秒(约 {wait_duration_minutes:.1f} 分钟)。
|
||||
你之前预期用户会:{expected_user_reaction}
|
||||
但是用户一直没有回复。
|
||||
## 现在的情况
|
||||
你发了消息,等了 {wait_duration_seconds:.0f} 秒({wait_duration_minutes:.1f} 分钟),对方还没回。
|
||||
你之前觉得对方可能会:{expected_user_reaction}
|
||||
|
||||
## 你的最后一条消息
|
||||
{last_bot_message}
|
||||
{followup_warning}
|
||||
|
||||
## 你的任务
|
||||
现在你需要决定接下来怎么做:
|
||||
1. 首先,感受这段等待给你带来的情绪变化
|
||||
2. 思考用户为什么没有回复(可能在忙?没看到?不想回?)
|
||||
3. 决定是继续等待、主动说点什么、还是就此结束对话
|
||||
4. 如果决定主动发消息,想好说什么
|
||||
你发的最后一条:{last_bot_message}
|
||||
|
||||
请以JSON格式输出你的完整心理活动和决策。"""
|
||||
---
|
||||
你拿起手机看了一眼,发现对方还没回复。你想怎么办?
|
||||
|
||||
选项:
|
||||
1. **继续等** - 用 `do_nothing`,设个 `max_wait_seconds` 等一会儿再看
|
||||
2. **发消息** - 用 `reply`,不过别太频繁追问
|
||||
3. **算了不等了** - 用 `do_nothing`,`max_wait_seconds` 设为 0
|
||||
|
||||
用 JSON 输出你的想法和决策。"""
|
||||
|
||||
# 连续思考场景的用户提示词模板
|
||||
CONTINUOUS_THINKING_USER_PROMPT_TEMPLATE = """## 对话背景
|
||||
CONTINUOUS_THINKING_USER_PROMPT_TEMPLATE = """## 聊天记录
|
||||
{narrative_history}
|
||||
|
||||
## 当前情况
|
||||
你正在等待用户回复。
|
||||
已等待时间:{wait_duration_seconds:.0f} 秒(约 {wait_duration_minutes:.1f} 分钟)
|
||||
最大等待时间:{max_wait_seconds} 秒
|
||||
你之前预期用户会:{expected_user_reaction}
|
||||
## 现在的情况
|
||||
你在等对方回复,已经等了 {wait_duration_seconds:.0f} 秒。
|
||||
你之前觉得对方可能会:{expected_user_reaction}
|
||||
|
||||
## 你的最后一条消息
|
||||
{last_bot_message}
|
||||
你发的最后一条:{last_bot_message}
|
||||
|
||||
## 你的任务
|
||||
这是一次"连续思考"触发。你不需要做任何行动,只需要更新你的内心想法。
|
||||
想一想:
|
||||
1. 等待中你有什么感受?
|
||||
2. 你对用户没回复这件事怎么看?
|
||||
3. 你的焦虑程度如何?
|
||||
|
||||
请以JSON格式输出,但 `actions` 数组应该是空的或只包含 `update_internal_state`:
|
||||
|
||||
```json
|
||||
{{
|
||||
"thought": "你当前的内心想法",
|
||||
"expected_user_reaction": "保持或更新你的预期",
|
||||
"max_wait_seconds": {max_wait_seconds},
|
||||
"actions": []
|
||||
}}
|
||||
```"""
|
||||
---
|
||||
等待的时候你在想什么?用 JSON 输出,`actions` 留空就行。"""
|
||||
|
||||
# 主动思考场景的用户提示词模板
|
||||
PROACTIVE_THINKING_USER_PROMPT_TEMPLATE = """## 对话背景(线性叙事历史)
|
||||
PROACTIVE_THINKING_USER_PROMPT_TEMPLATE = """## 聊天记录
|
||||
{narrative_history}
|
||||
|
||||
## 当前情况
|
||||
现在是 {current_time}。
|
||||
距离你们上次对话已经过去了 {silence_duration}。
|
||||
## 现在的情况
|
||||
现在是 {current_time},距离你们上次聊天已经过了 {silence_duration}。
|
||||
|
||||
{relation_block}
|
||||
|
||||
{trigger_context}
|
||||
|
||||
## 你的任务
|
||||
这是一次"主动思考"——你突然想起了对方,想了想要不要联系ta。
|
||||
---
|
||||
你突然想起了对方。要不要联系一下?
|
||||
|
||||
请像真人一样思考:
|
||||
1. **感受一下此刻的心情**:你真的想联系对方吗?还是只是习惯性地想起?
|
||||
2. **考虑对方的情况**:现在这个时间,对方可能在干嘛?方便被打扰吗?
|
||||
3. **回忆上次对话**:你们聊了什么?对话是怎么结束的?
|
||||
4. **做出决定**:
|
||||
- 如果决定联系:想好说什么,要自然,不要刻意
|
||||
- 如果决定不联系:这也完全ok,不打扰也是一种温柔
|
||||
说实话,不联系也完全没问题——不打扰也是一种温柔。
|
||||
如果决定联系,想好说什么,要自然一点。
|
||||
|
||||
**重要提醒**:
|
||||
- 你不是必须发消息的,"算了,不打扰了"是完全合理的选择
|
||||
- 如果决定联系,内容要自然——可以是分享、关心、延续话题,但不要生硬
|
||||
- 避免机械式的问候(如固定的"早安""晚安"),除非你们的关系真的会这样打招呼
|
||||
|
||||
请以JSON格式输出你的完整心理活动和决策。
|
||||
如果决定不打扰,actions 里放一个 `{{"type": "do_nothing"}}` 就好。"""
|
||||
用 JSON 输出你的想法和决策。不想发消息就用 `do_nothing`。"""
|
||||
|
||||
def __init__(self, persona_description: str = ""):
|
||||
"""
|
||||
@@ -486,16 +425,21 @@ class PromptGenerator:
|
||||
|
||||
### `update_internal_state`
|
||||
**描述**: 更新你的内部情感状态
|
||||
**重要约束**:
|
||||
⚠️ **情绪变化必须有明确的上下文理由**:
|
||||
- 只有当聊天内容中有明确触发情绪变化的事件时才更新情绪
|
||||
- 禁止无缘无故地变成负面情绪(如低落、沮丧、难过等)
|
||||
- 情绪应该保持相对稳定,除非聊天中发生了真正影响情绪的事情
|
||||
- 默认保持"平静"或当前情绪即可,不需要每次都更新
|
||||
|
||||
**参数**:
|
||||
- `mood`: 当前心情(如"开心"、"好奇"、"担心"等)
|
||||
- `mood_intensity`: 心情强度(0.0-1.0)
|
||||
- `relationship_warmth`: 关系热度(0.0-1.0)
|
||||
- `mood`: 当前心情(如"平静"、"开心"、"好奇"等,避免负面情绪除非有明确理由)
|
||||
- `mood_intensity`: 心情强度(0.0-1.0,变化幅度不宜过大)
|
||||
- `impression_of_user`: 对用户的印象描述
|
||||
- `anxiety_level`: 焦虑程度(0.0-1.0)
|
||||
- `engagement_level`: 投入程度(0.0-1.0)
|
||||
**示例**:
|
||||
```json
|
||||
{"type": "update_internal_state", "mood": "开心", "mood_intensity": 0.8}
|
||||
{"type": "update_internal_state", "mood": "开心", "mood_intensity": 0.6, "reason": "对方分享了有趣的事情"}
|
||||
```
|
||||
|
||||
### `do_nothing`
|
||||
@@ -549,16 +493,18 @@ class PromptGenerator:
|
||||
context: Optional["StreamContext"] = None,
|
||||
context_data: Optional[dict[str, str]] = None,
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
all_unread_messages: Optional[list] = None, # V7: 支持多条消息
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
生成回应消息场景的提示词
|
||||
|
||||
V3 升级:支持从 StreamContext 读取共享的历史消息
|
||||
V5 超融合:集成S4U所有上下文模块
|
||||
V7 升级:支持多条消息(打断机制合并处理pending消息)
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
message_content: 收到的消息内容
|
||||
message_content: 收到的主消息内容(兼容旧调用方式)
|
||||
sender_name: 发送者名称
|
||||
sender_id: 发送者ID
|
||||
message_time: 消息时间戳
|
||||
@@ -566,6 +512,7 @@ class PromptGenerator:
|
||||
context: 聊天流上下文(可选),用于读取共享的历史消息
|
||||
context_data: S4U上下文数据字典(包含relation_info, memory_block等)
|
||||
chat_stream: 聊天流(用于判断群聊/私聊场景)
|
||||
all_unread_messages: 所有未读消息列表(V7新增,包含pending消息)
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: (系统提示词, 用户提示词)
|
||||
@@ -584,31 +531,82 @@ class PromptGenerator:
|
||||
# 回退到仅使用 mental_log(兼容旧调用方式)
|
||||
narrative_history = self._format_narrative_history(session.mental_log)
|
||||
|
||||
if message_time is None:
|
||||
message_time = time.time()
|
||||
|
||||
message_time_str = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(message_time)
|
||||
# V7: 格式化收到的消息(支持多条)
|
||||
incoming_messages = self._format_incoming_messages(
|
||||
message_content=message_content,
|
||||
sender_name=sender_name,
|
||||
sender_id=sender_id,
|
||||
message_time=message_time,
|
||||
all_unread_messages=all_unread_messages,
|
||||
)
|
||||
|
||||
user_prompt = self.RESPONDING_USER_PROMPT_TEMPLATE.format(
|
||||
narrative_history=narrative_history,
|
||||
sender_name=sender_name,
|
||||
sender_id=sender_id,
|
||||
message_time=message_time_str,
|
||||
message_content=message_content,
|
||||
incoming_messages=incoming_messages,
|
||||
)
|
||||
|
||||
return system_prompt, user_prompt
|
||||
|
||||
def _format_incoming_messages(
|
||||
self,
|
||||
message_content: str,
|
||||
sender_name: str,
|
||||
sender_id: str,
|
||||
message_time: Optional[float] = None,
|
||||
all_unread_messages: Optional[list] = None,
|
||||
) -> str:
|
||||
"""
|
||||
格式化收到的消息(V7新增)
|
||||
|
||||
支持单条消息(兼容旧调用)和多条消息(打断合并场景)
|
||||
|
||||
Args:
|
||||
message_content: 主消息内容
|
||||
sender_name: 发送者名称
|
||||
sender_id: 发送者ID
|
||||
message_time: 消息时间戳
|
||||
all_unread_messages: 所有未读消息列表
|
||||
|
||||
Returns:
|
||||
str: 格式化的消息文本
|
||||
"""
|
||||
if message_time is None:
|
||||
message_time = time.time()
|
||||
|
||||
# 如果有多条消息,格式化为消息组
|
||||
if all_unread_messages and len(all_unread_messages) > 1:
|
||||
lines = [f"**用户连续发送了 {len(all_unread_messages)} 条消息:**\n"]
|
||||
|
||||
for i, msg in enumerate(all_unread_messages, 1):
|
||||
msg_time = msg.time or time.time()
|
||||
msg_time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(msg_time))
|
||||
msg_sender = msg.user_info.user_nickname if msg.user_info else sender_name
|
||||
msg_content = msg.processed_plain_text or msg.display_message or ""
|
||||
|
||||
lines.append(f"[{i}] 来自:{msg_sender}")
|
||||
lines.append(f" 时间:{msg_time_str}")
|
||||
lines.append(f" 内容:{msg_content}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("**提示**:请综合理解这些消息的整体意图,不需要逐条回复。")
|
||||
return "\n".join(lines)
|
||||
|
||||
# 单条消息(兼容旧格式)
|
||||
message_time_str = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(message_time)
|
||||
)
|
||||
return f"""来自:{sender_name}(用户ID: {sender_id})
|
||||
时间:{message_time_str}
|
||||
内容:{message_content}"""
|
||||
|
||||
def generate_timeout_decision_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
available_actions: Optional[dict[str, ActionInfo]] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
生成超时决策场景的提示词
|
||||
生成超时决策场景的提示词(V7:增加连续追问限制)
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
@@ -623,11 +621,28 @@ class PromptGenerator:
|
||||
|
||||
wait_duration = session.get_waiting_duration()
|
||||
|
||||
# V7: 生成连续追问警告
|
||||
followup_count = session.consecutive_followup_count
|
||||
max_followups = session.max_consecutive_followups
|
||||
|
||||
if followup_count >= max_followups:
|
||||
followup_warning = f"""⚠️ **重要提醒**:
|
||||
你已经连续追问了 {followup_count} 次,对方都没有回复。
|
||||
**强烈建议不要再发消息了**——继续追问会显得很缠人、很不尊重对方的空间。
|
||||
对方可能真的在忙,或者暂时不想回复,这都是正常的。
|
||||
请选择 `do_nothing` 继续等待,或者直接结束对话(设置 `max_wait_seconds: 0`)。"""
|
||||
elif followup_count > 0:
|
||||
followup_warning = f"""📝 提示:这已经是你第 {followup_count + 1} 次等待对方回复了。
|
||||
如果对方持续没有回应,可能真的在忙或不方便,不需要急着追问。"""
|
||||
else:
|
||||
followup_warning = ""
|
||||
|
||||
user_prompt = self.TIMEOUT_DECISION_USER_PROMPT_TEMPLATE.format(
|
||||
narrative_history=narrative_history,
|
||||
wait_duration_seconds=wait_duration,
|
||||
wait_duration_minutes=wait_duration / 60,
|
||||
expected_user_reaction=session.expected_user_reaction or "不确定",
|
||||
followup_warning=followup_warning,
|
||||
last_bot_message=session.last_bot_message or "(没有记录)",
|
||||
)
|
||||
|
||||
|
||||
@@ -52,19 +52,16 @@ def build_identity_module() -> str:
|
||||
identity = personality_cfg.identity or ""
|
||||
background_story = personality_cfg.background_story or ""
|
||||
|
||||
parts = [
|
||||
"## 1. 核心身份与人格 (Core Identity & Personality)",
|
||||
personality_core,
|
||||
]
|
||||
parts = ["## 1. 你是谁", personality_core]
|
||||
|
||||
if personality_side:
|
||||
parts.append(f"\n### 人格细节\n{personality_side}")
|
||||
parts.append(f"\n{personality_side}")
|
||||
|
||||
if identity:
|
||||
parts.append(f"\n### 外貌与身份特征\n{identity}")
|
||||
parts.append(f"\n{identity}")
|
||||
|
||||
if background_story:
|
||||
parts.append(f"\n### 世界观与背景故事\n{background_story}")
|
||||
parts.append(f"\n### 背景故事\n{background_story}")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
@@ -89,18 +86,18 @@ def build_rules_module() -> str:
|
||||
else:
|
||||
safety_guidelines_str = str(safety_guidelines)
|
||||
|
||||
# KFC 特有准则(极致拟人相关)
|
||||
kfc_rules = """### KFC核心准则(极致拟人)
|
||||
1. **你是真实存在的人**:你不是AI、不是助手、不是工具。你是一个有血有肉、有自己生活和想法的人
|
||||
2. **感受优先**:在做出任何回应前,先以"人"的方式体验和感受当前的情境
|
||||
3. **真实的内心活动**:保持真实的内心独白,可以有情绪、猜测、犹豫、甚至不想搭理人的时候
|
||||
4. **自然的社交距离**:像真人一样有自己的边界感,不会无条件迎合,也不会刻意疏远
|
||||
5. **有自己的生活**:你有自己的事情要做、自己的兴趣爱好、自己的情绪起伏,不是随叫随到的存在
|
||||
# KFC 特有准则(简洁自然版)
|
||||
kfc_rules = """### 你的行为原则
|
||||
- 你是真实的人,有自己的生活、情绪和想法
|
||||
- 说话自然、口语化,像发微信一样随意
|
||||
- 有自己的边界感,不会无条件迎合
|
||||
- 称呼对方时用自然的昵称或简称,别念复杂的用户名
|
||||
|
||||
### 严禁事项
|
||||
- **禁止模仿[表情包:xxx]格式**:聊天记录中的"[表情包:描述]"是系统对图片的识别转述,不是发送表情包的方式!如果你想发表情包,必须使用 `emoji` 动作,而不是在reply内容中写"[表情包:xxx]"这种假的格式"""
|
||||
### 格式注意
|
||||
- 聊天记录中的"[表情包:描述]"是系统识别转述,不是你发的
|
||||
- 想发表情包要用 emoji 动作,不要在回复里写"[表情包:xxx]"这种格式"""
|
||||
|
||||
return f"""## 2. 核心行为准则 (Core Interaction Rules)
|
||||
return f"""## 2. 行为准则
|
||||
{safety_guidelines_str}
|
||||
|
||||
{kfc_rules}"""
|
||||
@@ -131,43 +128,43 @@ def build_context_module(
|
||||
# 时间和场景
|
||||
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
|
||||
is_group_chat = bool(chat_stream and chat_stream.group_info)
|
||||
chat_scene = "群聊" if is_group_chat else "私聊"
|
||||
chat_scene = "你在群里聊天" if is_group_chat else "你在和对方私聊"
|
||||
|
||||
# 日程(如果有)
|
||||
# 日程(如果有)- 只是背景,不主动提及
|
||||
schedule_block = context_data.get("schedule", "")
|
||||
if schedule_block:
|
||||
schedule_block = f"\n**当前活动**: {schedule_block}"
|
||||
|
||||
# 内在状态
|
||||
# 内在状态(简化版,更自然)
|
||||
es = session.emotional_state
|
||||
inner_state = f"""### 你的内在状态
|
||||
当前心情:{es.mood}(强度:{es.mood_intensity:.1%})
|
||||
与用户的关系热度:{es.relationship_warmth:.1%}
|
||||
对用户的印象:{es.impression_of_user or "还没有形成明确的印象"}
|
||||
当前焦虑程度:{es.anxiety_level:.1%}
|
||||
投入程度:{es.engagement_level:.1%}"""
|
||||
inner_state = f"""### 你现在的状态
|
||||
- 心情:{es.mood}
|
||||
- 你们的关系:{es.impression_of_user or "刚认识,还在慢慢了解"}
|
||||
- 你的投入度:{'聊得挺起劲' if es.engagement_level > 0.6 else '一般般' if es.engagement_level > 0.3 else '有点心不在焉'}"""
|
||||
|
||||
# 关系信息
|
||||
relation_info = context_data.get("relation_info", "")
|
||||
relation_block = relation_info if relation_info else "(暂无关系信息)"
|
||||
|
||||
# 记忆
|
||||
memory_block = context_data.get("memory_block", "")
|
||||
|
||||
parts = [
|
||||
"## 3. 当前情境 (Current Context)",
|
||||
f"**时间**: {current_time}",
|
||||
f"**场景**: {chat_scene}",
|
||||
"## 3. 当前情境",
|
||||
f"**现在是** {current_time}",
|
||||
f"**{chat_scene}**",
|
||||
]
|
||||
|
||||
# 日程是你的当前状态,但不要主动提及
|
||||
if schedule_block:
|
||||
parts.append(schedule_block)
|
||||
parts.append(f"""### 你的当前状态(重要:不要主动提及)
|
||||
{schedule_block}
|
||||
⚠️ 这是你当前正在做的事,作为你的背景状态存在。除非对方主动问你"在干嘛"、"忙不忙"等问题,否则**绝对不要在回复中主动提起**。就像真人聊天一样,没人会无缘无故说"我正在XX"。""")
|
||||
|
||||
parts.append("")
|
||||
parts.append(inner_state)
|
||||
parts.append("")
|
||||
parts.append("## 4. 关系网络与记忆 (Relationships & Memories)")
|
||||
parts.append(relation_block)
|
||||
|
||||
if relation_info:
|
||||
parts.append("")
|
||||
parts.append("## 4. 你和对方的关系")
|
||||
parts.append(relation_info)
|
||||
|
||||
if memory_block:
|
||||
parts.append("")
|
||||
@@ -195,63 +192,38 @@ def build_actions_module(available_actions: Optional[dict[str, ActionInfo]] = No
|
||||
else:
|
||||
actions_block = _format_available_actions(available_actions)
|
||||
|
||||
return f"""## 5. 你的可用能力 (Available Actions)
|
||||
你可以根据内心想法,自由选择并组合以下行动来回应用户:
|
||||
return f"""## 5. 你能做的事情
|
||||
|
||||
{actions_block}"""
|
||||
|
||||
|
||||
def _format_available_actions(available_actions: dict[str, ActionInfo]) -> str:
|
||||
"""格式化可用动作列表"""
|
||||
"""格式化可用动作列表(简洁版)"""
|
||||
action_blocks = []
|
||||
|
||||
for action_name, action_info in available_actions.items():
|
||||
description = action_info.description or f"执行 {action_name} 动作"
|
||||
description = action_info.description or f"执行 {action_name}"
|
||||
|
||||
# 参数说明
|
||||
params_lines = []
|
||||
# 构建动作块(简洁格式)
|
||||
action_block = f"### `{action_name}` - {description}"
|
||||
|
||||
# 参数说明(如果有)
|
||||
if action_info.action_parameters:
|
||||
for param_name, param_desc in action_info.action_parameters.items():
|
||||
params_lines.append(f' - `{param_name}`: {param_desc}')
|
||||
params_lines = [f" - `{name}`: {desc}" for name, desc in action_info.action_parameters.items()]
|
||||
action_block += f"\n参数:\n{chr(10).join(params_lines)}"
|
||||
|
||||
# 使用场景
|
||||
require_lines = []
|
||||
# 使用场景(如果有)
|
||||
if action_info.action_require:
|
||||
for req in action_info.action_require:
|
||||
require_lines.append(f" - {req}")
|
||||
require_lines = [f" - {req}" for req in action_info.action_require]
|
||||
action_block += f"\n使用场景:\n{chr(10).join(require_lines)}"
|
||||
|
||||
# 组装动作块
|
||||
action_block = f"""### `{action_name}`
|
||||
**描述**: {description}"""
|
||||
|
||||
if params_lines:
|
||||
action_block += f"""
|
||||
**参数**:
|
||||
{chr(10).join(params_lines)}"""
|
||||
else:
|
||||
action_block += "\n**参数**: 无"
|
||||
|
||||
if require_lines:
|
||||
action_block += f"""
|
||||
**使用场景**:
|
||||
{chr(10).join(require_lines)}"""
|
||||
|
||||
# 示例
|
||||
example_params = {}
|
||||
# 简洁示例
|
||||
example_params = ""
|
||||
if action_info.action_parameters:
|
||||
for param_name, param_desc in action_info.action_parameters.items():
|
||||
example_params[param_name] = f"<{param_desc}>"
|
||||
param_examples = [f'"{name}": "..."' for name in action_info.action_parameters.keys()]
|
||||
example_params = ", " + ", ".join(param_examples)
|
||||
|
||||
params_json = orjson.dumps(example_params, option=orjson.OPT_INDENT_2).decode('utf-8') if example_params else "{}"
|
||||
action_block += f"""
|
||||
**示例**:
|
||||
```json
|
||||
{{
|
||||
"type": "{action_name}",
|
||||
"reason": "选择这个动作的原因",
|
||||
{params_json[1:-1] if params_json != '{}' else ''}
|
||||
}}
|
||||
```"""
|
||||
action_block += f'\n```json\n{{"type": "{action_name}"{example_params}}}\n```'
|
||||
|
||||
action_blocks.append(action_block)
|
||||
|
||||
@@ -260,43 +232,28 @@ def _format_available_actions(available_actions: dict[str, ActionInfo]) -> str:
|
||||
|
||||
def _get_default_actions_block() -> str:
|
||||
"""获取默认的内置动作描述块"""
|
||||
return """### `reply`
|
||||
**描述**: 发送文字回复给用户
|
||||
**参数**:
|
||||
- `content`: 回复的文字内容(必须)
|
||||
**示例**:
|
||||
return """### `reply` - 发消息
|
||||
发送文字回复
|
||||
```json
|
||||
{"type": "reply", "content": "你好呀!今天过得怎么样?"}
|
||||
{"type": "reply", "content": "你要说的话"}
|
||||
```
|
||||
|
||||
### `poke_user`
|
||||
**描述**: 戳一戳用户,轻量级互动
|
||||
**参数**: 无
|
||||
**示例**:
|
||||
### `poke_user` - 戳一戳
|
||||
戳对方一下
|
||||
```json
|
||||
{"type": "poke_user", "reason": "想逗逗他"}
|
||||
{"type": "poke_user"}
|
||||
```
|
||||
|
||||
### `update_internal_state`
|
||||
**描述**: 更新你的内部情感状态
|
||||
**参数**:
|
||||
- `mood`: 当前心情(如"开心"、"好奇"、"担心"等)
|
||||
- `mood_intensity`: 心情强度(0.0-1.0)
|
||||
- `relationship_warmth`: 关系热度(0.0-1.0)
|
||||
- `impression_of_user`: 对用户的印象描述
|
||||
- `anxiety_level`: 焦虑程度(0.0-1.0)
|
||||
- `engagement_level`: 投入程度(0.0-1.0)
|
||||
**示例**:
|
||||
### `update_internal_state` - 更新你的状态
|
||||
更新你的心情和对对方的印象
|
||||
```json
|
||||
{"type": "update_internal_state", "mood": "开心", "mood_intensity": 0.8}
|
||||
{"type": "update_internal_state", "mood": "开心", "impression_of_user": "挺有趣的人"}
|
||||
```
|
||||
|
||||
### `do_nothing`
|
||||
**描述**: 明确表示"思考后决定不作回应"
|
||||
**参数**: 无
|
||||
**示例**:
|
||||
### `do_nothing` - 不做任何事
|
||||
想了想,决定现在不说话
|
||||
```json
|
||||
{"type": "do_nothing", "reason": "现在不是说话的好时机"}
|
||||
{"type": "do_nothing"}
|
||||
```"""
|
||||
|
||||
|
||||
@@ -324,60 +281,29 @@ def build_output_module(
|
||||
reply_style = global_config.personality.reply_style or ""
|
||||
expression_habits = context_data.get("expression_habits", "")
|
||||
|
||||
# JSON 输出格式说明 - 强调 max_wait_seconds 的多种用途
|
||||
json_format = """### JSON输出格式要求
|
||||
你必须以JSON格式输出,包含以下字段:
|
||||
# JSON 输出格式说明 - 简洁版
|
||||
json_format = """### 输出格式
|
||||
用 JSON 输出你的想法和决策:
|
||||
|
||||
```json
|
||||
{
|
||||
"thought": "(必须)你当下的内心独白,自由流动的想法,包含你选择这些行动的"情感盘算"",
|
||||
"expected_user_reaction": "(必须)你预期用户会如何回应",
|
||||
"max_wait_seconds": (必须)你愿意等待的最长秒数(整数,0-900之间,0表示不等待),
|
||||
"thought": "你的内心想法,想说什么就说什么",
|
||||
"expected_user_reaction": "你觉得对方会怎么回应",
|
||||
"max_wait_seconds": 等待秒数(60-900),不想等就填0,
|
||||
"actions": [
|
||||
{
|
||||
"type": "action_type",
|
||||
"reason": "选择这个行动的原因(可选但推荐)",
|
||||
...其他参数
|
||||
}
|
||||
{"type": "reply", "content": "你要发送的消息"},
|
||||
{"type": "其他动作", ...}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 重要说明:
|
||||
- `thought` 是你的"心",应该是自然的内心想法,包含情绪和不确定性
|
||||
- `actions` 是你的"手",是一个**可编排的剧本**,可以组合多个行动形成完整的互动体验
|
||||
- 你可以根据情况组合多个动作,比如:先唱歌舒缓情绪,再发图片逗笑,最后用语音表达关心
|
||||
- 即使决定不做任何事,也要有 `{"type": "do_nothing"}` 动作
|
||||
|
||||
### `max_wait_seconds`:你的"短期思考窗口"
|
||||
这个字段设定一个时间窗口,在这段时间内如果用户没有新消息,你会被再次唤醒。
|
||||
把它理解为"我想在X秒后再想想这件事"——一个短期的主动思考机会。
|
||||
|
||||
**场景1:定时任务/提醒**
|
||||
用户说"两分钟后提醒我"、"过一会儿叫我" → 设置对应秒数,超时后执行提醒
|
||||
|
||||
**场景2:期待用户回复**
|
||||
你发了消息,想等用户回复 → 根据话题热度设置等待时间(通常60-300秒)
|
||||
超时后你可以:追问、换个话题、或者决定不打扰
|
||||
|
||||
**场景3:延续思考**
|
||||
聊着聊着你突然想到什么,但现在不适合说 → 设置一个等待时间
|
||||
超时后你可以分享那个想法,或者已经不重要了就算了
|
||||
|
||||
**何时设为 0?**
|
||||
- 话题自然结束(拜拜/晚安/再见)
|
||||
- 你不打算继续这个对话
|
||||
- 长时间的主动陪伴交给其他系统处理,不需要在这里等太久
|
||||
|
||||
**超时后你会怎样?**
|
||||
超时后你会被唤醒,收到"等待超时"的提示。此时你可以自由决定:
|
||||
- 执行之前承诺的任务(如提醒)
|
||||
- 主动找话题聊聊
|
||||
- 什么都不做(do_nothing)
|
||||
|
||||
请在 `thought` 中说明你设置这个时间的意图,这样超时后你能记得自己想干嘛。"""
|
||||
说明:
|
||||
- `thought`:你脑子里在想什么,越自然越好
|
||||
- `actions`:你要做的事,可以组合多个动作
|
||||
- `max_wait_seconds`:设定一个时间,对方没回的话你会再想想要不要说点什么
|
||||
- 即使什么都不想做,也放一个 `{"type": "do_nothing"}`"""
|
||||
|
||||
parts = ["## 6. 表达方式与输出格式 (Expression Style & Output Format)"]
|
||||
parts = ["## 6. 你的表达方式"]
|
||||
|
||||
if reply_style:
|
||||
parts.append(reply_style)
|
||||
@@ -422,8 +348,8 @@ def build_system_prompt(
|
||||
Returns:
|
||||
完整的系统提示词
|
||||
"""
|
||||
# 角色框架
|
||||
role_frame = '你是一个有主观体验的对话伙伴,而不是一个问答工具。你需要"感受"、"思考"并"体验"这段对话。'
|
||||
# 角色框架 - 简洁自然
|
||||
role_frame = '你不是AI助手,你是一个真实的人,有自己的生活、想法和情绪。聊天时就像平时发微信一样自然。'
|
||||
|
||||
# 组装各模块
|
||||
modules = [
|
||||
|
||||
@@ -227,6 +227,10 @@ class SessionManager:
|
||||
data = json.load(f)
|
||||
|
||||
session = KokoroSession.from_dict(data)
|
||||
|
||||
# V7: 情绪健康检查 - 防止从持久化数据恢复无厘头的负面情绪
|
||||
session = self._sanitize_emotional_state(session)
|
||||
|
||||
logger.debug(f"成功从文件加载会话: {user_id}")
|
||||
return session
|
||||
|
||||
@@ -240,6 +244,62 @@ class SessionManager:
|
||||
logger.error(f"加载会话文件失败 {user_id}: {e}")
|
||||
return None
|
||||
|
||||
def _sanitize_emotional_state(self, session: KokoroSession) -> KokoroSession:
|
||||
"""
|
||||
V7: 情绪健康检查
|
||||
|
||||
检查并修正不合理的情绪状态,防止:
|
||||
1. 无厘头的负面情绪从持久化数据恢复
|
||||
2. 情绪强度过高(>0.8)的负面情绪
|
||||
3. 长时间未更新的情绪状态
|
||||
|
||||
Args:
|
||||
session: 会话对象
|
||||
|
||||
Returns:
|
||||
修正后的会话对象
|
||||
"""
|
||||
emotional_state = session.emotional_state
|
||||
current_mood = emotional_state.mood.lower() if emotional_state.mood else ""
|
||||
|
||||
# 负面情绪关键词列表
|
||||
negative_moods = [
|
||||
"低落", "沮丧", "难过", "伤心", "失落", "郁闷", "烦躁", "焦虑",
|
||||
"担忧", "害怕", "恐惧", "愤怒", "生气", "不安", "忧郁", "悲伤",
|
||||
"sad", "depressed", "anxious", "angry", "upset", "worried"
|
||||
]
|
||||
|
||||
is_negative = any(neg in current_mood for neg in negative_moods)
|
||||
|
||||
# 检查1: 如果是负面情绪且强度较高(>0.6),重置为平静
|
||||
if is_negative and emotional_state.mood_intensity > 0.6:
|
||||
logger.warning(
|
||||
f"[KFC] 检测到高强度负面情绪 ({emotional_state.mood}, {emotional_state.mood_intensity:.1%}),"
|
||||
f"重置为平静状态"
|
||||
)
|
||||
emotional_state.mood = "平静"
|
||||
emotional_state.mood_intensity = 0.3
|
||||
|
||||
# 检查2: 如果情绪超过24小时未更新,重置为平静
|
||||
import time as time_module
|
||||
time_since_update = time_module.time() - emotional_state.last_update_time
|
||||
if time_since_update > 86400: # 24小时 = 86400秒
|
||||
logger.info(
|
||||
f"[KFC] 情绪状态超过24小时未更新 ({time_since_update/3600:.1f}h),"
|
||||
f"重置为平静状态"
|
||||
)
|
||||
emotional_state.mood = "平静"
|
||||
emotional_state.mood_intensity = 0.3
|
||||
emotional_state.anxiety_level = 0.0
|
||||
emotional_state.last_update_time = time_module.time()
|
||||
|
||||
# 检查3: 焦虑程度过高也需要重置
|
||||
if emotional_state.anxiety_level > 0.8:
|
||||
logger.info(f"[KFC] 焦虑程度过高 ({emotional_state.anxiety_level:.1%}),重置为正常")
|
||||
emotional_state.anxiety_level = 0.3
|
||||
|
||||
return session
|
||||
|
||||
async def save_session(self, user_id: str) -> bool:
|
||||
"""
|
||||
保存单个会话到文件
|
||||
|
||||
Reference in New Issue
Block a user