Skip to content

Commit e219750

Browse files
ChangingSelfclaude
andcommitted
feat(prompts): 实现 MaiBot 提示词覆盖系统
实现 maim_message template_info 机制覆盖 MaiBot 核心提示词, 使 MaiBot 在 VTuber 直播场景下具有一致的人设和回复风格。 主要功能: - TemplateOverrideService 构建 TemplateInfo 对象 - 自动剥离 YAML frontmatter 元数据 - 支持 replyer_prompt 和 replyer_prompt_0 覆盖 - 添加 group_info 标识群聊(直播间)场景 - 详细的调试日志输出 模板文件: - replyer_prompt.md - VTuber 群聊回复主模板 - replyer_prompt_0.md - 轻量回复模板 (think_level=0) - private_replyer_prompt.md - 私聊回复模板 - chat_target_group1/2.md - 群聊场景描述 - chat_target_private1/2.md - 私聊场景描述 配置示例: [providers.decision.maicore] enable_prompt_override = true override_templates = ["replyer_prompt", "replyer_prompt_0", ...] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bd8c442 commit e219750

16 files changed

Lines changed: 834 additions & 0 deletions

config-template.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,17 @@ user_nickname = "语音"
321321
# connect_timeout = 10.0
322322
# reconnect_interval = 5.0
323323

324+
# --- MaiCore DecisionProvider 提示词覆盖配置 ---
325+
# 覆盖 MaiBot 默认回复提示词,使 VTuber 人设更一致(默认启用)
326+
# enable_prompt_override = true
327+
# override_template_name = "amaidesu_override"
328+
# override_templates = [
329+
# "replyer_prompt", # think_level=1 时使用
330+
# "replyer_prompt_0", # think_level=0 时使用(轻量回复)
331+
# "chat_target_group1",
332+
# "chat_target_group2",
333+
# ]
334+
324335
# --- LLM DecisionProvider ---
325336
# [providers.decision.llm]
326337
# fallback_mode = "simple" # simple | echo | error
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# MaiBot 提示词覆盖功能
2+
3+
## 功能概述
4+
5+
Amaidesu 支持通过 `maim_message``template_info` 机制覆盖 MaiBot 的回复提示词,使 VTuber 在直播场景下具有更一致的个性和回复风格。
6+
7+
**默认启用**: 该功能默认启用,确保所有消息都使用 VTuber 风格的提示词。
8+
9+
## 工作原理
10+
11+
1. Amaidesu 发送消息到 MaiBot 时,携带 `template_info``group_info`
12+
2. `template_info` 包含自定义的提示词模板
13+
3. `group_info` 标识消息来自直播间(群聊),确保使用群聊提示词
14+
4. MaiBot 收到消息后,在 `async_message_scope` 作用域内注册这些模板
15+
5. 处理该消息时,MaiBot 使用覆盖后的提示词而非默认提示词
16+
17+
**重要**: 模板变量(如 `{bot_name}`, `{identity}` 等)由 MaiBot 端填充,Amaidesu 只需提供原始模板字符串。
18+
19+
### 群聊 vs 私聊
20+
21+
MaiBot 通过 `group_info` 判断消息类型:
22+
- **`group_info`**: 使用群聊模板(`replyer_prompt`, `chat_target_group1/2`
23+
- **`group_info`**: 使用私聊模板(`private_replyer_prompt`, `chat_target_private1/2`
24+
25+
Amaidesu 默认为所有消息设置 `group_info``group_id="live_room"`, `group_name="直播间"`),确保使用群聊模板。
26+
27+
## 配置方法
28+
29+
`config.toml``[providers.decision.maicore]` 段添加:
30+
31+
```toml
32+
[providers.decision.maicore]
33+
# 现有配置...
34+
host = "127.0.0.1"
35+
port = 8000
36+
37+
# 提示词覆盖配置
38+
enable_prompt_override = true
39+
override_template_name = "amaidesu_override"
40+
override_templates = [
41+
"replyer_prompt", # think_level=1 时使用
42+
"replyer_prompt_0", # think_level=0 时使用(轻量回复)
43+
"chat_target_group1",
44+
"chat_target_group2",
45+
]
46+
```
47+
48+
### 配置项说明
49+
50+
| 配置项 | 类型 | 默认值 | 说明 |
51+
|--------|------|--------|------|
52+
| `enable_prompt_override` | bool | `true` | 是否启用提示词覆盖 |
53+
| `override_template_name` | str | `"amaidesu_override"` | 作用域名称 |
54+
| `override_templates` | list | 见上方 | 要覆盖的模板列表 |
55+
56+
### 重要:replyer_prompt vs replyer_prompt_0
57+
58+
MaiBot 根据消息复杂度选择不同的回复模板:
59+
- **`think_level=0`**:使用 `replyer_prompt_0`(轻量回复,简短平淡)
60+
- **`think_level=1`**:使用 `replyer_prompt`(标准回复,日常口语化)
61+
62+
**必须同时覆盖两个模板**,否则在某些场景下仍会使用默认的 QQ 群聊提示词。
63+
64+
## 可覆盖的提示词
65+
66+
### 核心回复模板
67+
68+
| 模板名称 | 用途 |
69+
|---------|------|
70+
| `replyer_prompt` | 群聊回复(主模板) |
71+
| `replyer_prompt_0` | 群聊回复(备选) |
72+
| `private_replyer_prompt` | 私聊回复 |
73+
74+
### 场景描述模板
75+
76+
| 模板名称 | 用途 |
77+
|---------|------|
78+
| `chat_target_group1` | 群聊场景描述 |
79+
| `chat_target_group2` | 群聊场景简短描述 |
80+
| `chat_target_private1` | 私聊场景描述 |
81+
| `chat_target_private2` | 私聊场景简短描述 |
82+
83+
## 自定义模板
84+
85+
模板文件位于 `src/modules/prompts/templates/maibot_override/` 目录。
86+
87+
### 模板格式
88+
89+
使用 YAML frontmatter 定义元数据:
90+
91+
```markdown
92+
---
93+
name: replyer_prompt
94+
version: "1.0"
95+
description: "VTuber 场景回复提示词"
96+
variables:
97+
- bot_name
98+
- identity
99+
- dialogue_prompt
100+
---
101+
102+
{knowledge_prompt}
103+
你是一位正在直播的 VTuber {bot_name}...
104+
{dialogue_prompt}
105+
现在,你说:
106+
```
107+
108+
### 注意事项
109+
110+
1. **保留所有变量**: 模板中的变量占位符(如 `{identity}`)必须保留,由 MaiBot 端填充
111+
2. **使用原始字符串**: 服务类使用 `get_raw()` 获取模板,不进行变量渲染
112+
3. **模板缺失**: 缺失的模板会被跳过并记录警告日志
113+
114+
## 常见问题
115+
116+
### Q: 覆盖后 MaiBot 回复不符合预期?
117+
118+
检查:
119+
1. 模板是否包含所有必需的变量占位符
120+
2. `override_templates` 配置是否包含目标模板名称
121+
3. 查看 Amaidesu 日志确认 template_info 已注入
122+
123+
### Q: 如何临时禁用覆盖?
124+
125+
设置 `enable_prompt_override = false` 或注释掉该配置项。
126+
127+
### Q: 多个 DecisionProvider 会冲突吗?
128+
129+
不会。`template_info``MaiCoreDecisionProvider` 层注入,其他 DecisionProvider(如 LLMDecisionProvider)不使用此机制。
130+
131+
## 相关文件
132+
133+
- 服务类: `src/modules/prompts/template_override_service.py`
134+
- Provider 修改: `src/domains/decision/providers/maicore/maicore_decision_provider.py`
135+
- 模板目录: `src/modules/prompts/templates/maibot_override/`

docs/development/provider-guide.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,37 @@ class MyProvider(OutputProvider):
472472
| `maicraft` | Maicraft 决策 | `src/domains/decision/providers/maicraft/` |
473473
| `replay` | 回放决策(调试用) | `src/domains/decision/providers/replay/` |
474474

475+
### MaiCoreDecisionProvider 提示词覆盖
476+
477+
MaiCoreDecisionProvider 支持通过 `template_info` 机制覆盖 MaiBot 的回复提示词,使 VTuber 在直播场景下具有更一致的个性。
478+
479+
#### 启用方式
480+
481+
`config.toml` 中配置:
482+
483+
```toml
484+
[providers.decision.maicore]
485+
host = "127.0.0.1"
486+
port = 8000
487+
488+
# 启用提示词覆盖
489+
enable_prompt_override = true
490+
override_template_name = "amaidesu_override"
491+
override_templates = ["replyer_prompt", "chat_target_group1", "chat_target_group2"]
492+
```
493+
494+
#### 配置项
495+
496+
| 配置项 | 说明 |
497+
|--------|------|
498+
| `enable_prompt_override` | 是否启用提示词覆盖 |
499+
| `override_template_name` | 作用域名称 |
500+
| `override_templates` | 要覆盖的模板列表 |
501+
502+
#### 自定义模板
503+
504+
模板文件位于 `src/modules/prompts/templates/maibot_override/` 目录。详细说明请参阅 [MaiBot 提示词覆盖文档](maibot-override.md)
505+
475506
### OutputProvider(12个)
476507

477508
| 名称 | 说明 | 位置 |

src/domains/decision/providers/maicore/maicore_decision_provider.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
from src.modules.config.schemas.base import BaseProviderConfig
2121
from src.modules.logging import get_logger
2222
from src.modules.types.base.decision_provider import DecisionProvider
23+
from src.modules.prompts.template_override_service import (
24+
TemplateOverrideService,
25+
OverrideConfig,
26+
)
2327

2428
from .router_adapter import RouterAdapter
2529

@@ -81,6 +85,25 @@ class ConfigSchema(BaseProviderConfig):
8185
action_cooldown_seconds: float = Field(default=5.0, gt=0.0, description="同一 Action 的最小间隔(秒)")
8286
max_suggested_actions: int = Field(default=3, ge=1, le=10, description="每条消息最大建议数量")
8387

88+
# 提示词覆盖配置
89+
enable_prompt_override: bool = Field(
90+
default=True,
91+
description="是否启用 MaiBot 提示词覆盖功能"
92+
)
93+
override_template_name: str = Field(
94+
default="amaidesu_override",
95+
description="覆盖模板组名(作用域名称)"
96+
)
97+
override_templates: list[str] = Field(
98+
default=[
99+
"replyer_prompt", # think_level=1 时使用
100+
"replyer_prompt_0", # think_level=0 时使用(轻量回复)
101+
"chat_target_group1",
102+
"chat_target_group2",
103+
],
104+
description="要覆盖的提示词名称列表"
105+
)
106+
84107
@classmethod
85108
def get_registration_info(cls) -> Dict[str, Any]:
86109
"""
@@ -133,6 +156,14 @@ def __init__(self, config: Dict[str, Any], context: "ProviderContext"):
133156
# EventBus 引用(用于事件通知)
134157
self._event_bus: Optional["EventBus"] = None
135158

159+
# 初始化提示词覆盖服务
160+
override_config = OverrideConfig(
161+
enabled=self.typed_config.enable_prompt_override,
162+
template_name=self.typed_config.override_template_name,
163+
templates=self.typed_config.override_templates,
164+
)
165+
self._override_service = TemplateOverrideService(override_config)
166+
136167
async def init(self) -> None:
137168
"""
138169
初始化 MaiCoreDecisionProvider
@@ -242,6 +273,20 @@ def _normalized_to_message_base(self, normalized: "NormalizedMessage") -> Option
242273
"original_platform": normalized.source, # 保留原始平台信息
243274
"original_message_id": message_id, # 存储原始 message_id 用于关联响应
244275
}
276+
277+
# 构建 template_info(如果启用)
278+
template_info = self._override_service.build_template_info()
279+
280+
# 构建 group_info(直播间作为群聊处理)
281+
# MaiBot 通过 group_info 是否存在来判断是群聊还是私聊
282+
# 群聊会使用 replyer_prompt,私聊会使用 private_replyer_prompt
283+
from maim_message import GroupInfo
284+
group_info = GroupInfo(
285+
platform=self.platform,
286+
group_id="live_room", # 直播间群组ID
287+
group_name="直播间", # 直播间名称
288+
)
289+
245290
message = MessageBase(
246291
message_info=BaseMessageInfo(
247292
message_id=message_id,
@@ -250,11 +295,27 @@ def _normalized_to_message_base(self, normalized: "NormalizedMessage") -> Option
250295
time=normalized.timestamp,
251296
format_info=format_info,
252297
additional_config=additional_config,
298+
template_info=template_info, # 新增:注入 template_info
299+
group_info=group_info, # 新增:注入 group_info(群聊标识)
253300
),
254301
message_segment=seg,
255302
raw_message=normalized.text, # 添加原始消息内容
256303
)
257304

305+
# 调试日志
306+
if template_info:
307+
self.logger.debug(
308+
f"消息携带 template_info: "
309+
f"template_name={template_info.template_name}, "
310+
f"templates={list(template_info.template_items.keys())}"
311+
)
312+
if group_info:
313+
self.logger.debug(
314+
f"消息携带 group_info: "
315+
f"group_id={group_info.group_id}, "
316+
f"group_name={group_info.group_name}"
317+
)
318+
258319
return message
259320
except Exception as e:
260321
self.logger.error(f"转换为 MessageBase 失败: {e}", exc_info=True)
@@ -281,6 +342,22 @@ async def decide(self, normalized_message: "NormalizedMessage") -> None:
281342
return
282343

283344
try:
345+
# 打印发送的 maim-message 结构(调试用)
346+
self.logger.info("发送到 MaiCore 的消息结构:")
347+
self.logger.info(f" - message_id: {message.message_info.message_id}")
348+
self.logger.info(f" - platform: {message.message_info.platform}")
349+
self.logger.info(f" - user_info: {message.message_info.user_info}")
350+
self.logger.info(f" - group_info: {message.message_info.group_info}")
351+
self.logger.info(f" - template_info: {message.message_info.template_info}")
352+
if message.message_info.group_info:
353+
self.logger.info(f" - group_id: {message.message_info.group_info.group_id}")
354+
self.logger.info(f" - group_name: {message.message_info.group_info.group_name}")
355+
if message.message_info.template_info:
356+
self.logger.info(f" - template_default: {message.message_info.template_info.template_default}")
357+
self.logger.info(f" - template_name: {message.message_info.template_info.template_name}")
358+
self.logger.info(f" - template_items keys: {list(message.message_info.template_info.template_items.keys()) if message.message_info.template_info.template_items else None}")
359+
# 打印完整消息字典(调试用)
360+
self.logger.debug(f"完整消息字典: {message.to_dict()}")
284361
await self._router_adapter.send(message)
285362
self.logger.debug(f"消息已发送至 MaiCore (id: {message.message_info.message_id})")
286363
except Exception as e:

src/domains/decision/providers/maicore/router_adapter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ async def send(self, message: MessageBase) -> None:
5555

5656
try:
5757
self.logger.debug(f"准备发送消息: {message.message_info.message_id}")
58+
# 打印序列化后的消息(调试用)
59+
msg_dict = message.to_dict()
60+
self.logger.info("RouterAdapter 发送的消息字典:")
61+
self.logger.info(f" - group_info: {msg_dict.get('message_info', {}).get('group_info')}")
62+
self.logger.info(f" - template_info: {msg_dict.get('message_info', {}).get('template_info')}")
5863
await self._router.send_message(message)
5964
self.logger.debug(f"消息 {message.message_info.message_id} 已发送")
6065
except Exception as e:

src/modules/prompts/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,17 @@
2424
get_prompt_manager,
2525
reset_prompt_manager,
2626
)
27+
from src.modules.prompts.template_override_service import (
28+
OverrideConfig,
29+
TemplateOverrideService,
30+
)
2731

2832
__all__ = [
2933
"PromptManager",
3034
"PromptTemplate",
3135
"TemplateMetadata",
3236
"get_prompt_manager",
3337
"reset_prompt_manager",
38+
"OverrideConfig",
39+
"TemplateOverrideService",
3440
]

0 commit comments

Comments
 (0)