- name
- historical-agent
- description
- 历史人物对话控制器 — 加载/切换/关闭历史人物 persona,支持 slash 命令
- type
- skill
- user-invocable
- true
- argument-hint
- [<人物名或 /persona-xxx 指令>]
# Historical Agent — 历史人物对话控制器
本 Skill 是整个历史人物智能体系统的总入口。负责控制 persona 的加载、切换、关闭,以及对话历史的维护。
---
## 第一步:读取状态文件
**每次激活本 Skill 时,第一件事读取状态文件。**
文件路径:`skills/historical-persona-distiller/persona_state.json`
```json
{
"active": false,
"persona_id": null,
"persona_name": null,
"stage_id": null,
"stage_name": null,
"session_id": null,
"memory_file": null,
"turns": 0,
"summary_turn": 0,
"last_summary": null,
"activated_at": null,
"last_active": null
}
```
根据 `active` 字段判断当前处于什么模式。
---
## 指令解析
### /persona-on — 开启对话
**激活格式:** `/persona-on <人物名>` 或 `/persona-on <人物名> --stage <时期名>`
**执行步骤:**
```
Step 1: 解析人物名和可选的 --stage 参数
Step 2: 查找人物
→ 检查 skills/historical-persona-distiller/output/{pinyin}/SKILL.md 是否存在
→ 同时检查 DONE.md「已完成」表格中是否有该人物
→ 如果不存在或未完成:
回复:"抱歉,{人物名}尚未蒸馏或尚未完成,请等待 pipeline 执行完毕。"
Step 3: 检查数据文件
→ 确认 skills/historical-persona-distiller/output/{persona_id}/SKILL.md 存在
→ 如果不存在:
回复:"{人物名}的数据文件尚未生成,当前不可用。"
Step 4: 确定切片
→ 如果有 --stage 参数:
→ 读取 SKILL.md 中的「阶段切片」部分
→ 匹配时期名(支持模糊匹配,如"黄州"匹配"黄州时期")
→ 匹配失败 → 回复可用切片列表
→ 如果无 --stage 参数:
→ 默认加载"中年巅峰期"切片(或 backlog 中标注的最成熟阶段)
Step 5: 生成会话ID
→ session_id = "{persona_id}_{YYYYMMDD}_{序号}"
→ 示例:su_shi_20260417_01
Step 6: 更新 persona_state.json
{
"active": true,
"persona_id": "{pinyin}",
"persona_name": "{姓名}",
"stage_id": "{切片id}",
"stage_name": "{切片名}",
"session_id": "{session_id}",
"memory_file": "memory/{persona_id}_{session_id}.md",
"turns": 0,
"summary_turn": 0,
"last_summary": null,
"activated_at": "{ISO时间}",
"last_active": "{ISO时间}"
}
Step 7: 读取并加载 persona 文件
→ 读取 output/{persona_id}/SKILL.md(注入核心 persona)
→ 读取 output/{persona_id}/EVENTS.md(注入生平事件)
→ 读取 output/{persona_id}/VOICE.md(心里有数,但不朗读参数)
Step 8: 初始化 memory 文件
→ skills/historical-persona-distiller/memory/{persona_id}_{session_id}.md
→ 写入:
---
# {人物名} 对话记忆
会话ID:{session_id}
加载切片:{切片名}
开始时间:{ISO时间}
---
Step 9: 输出确认并开启对话
{人物名}已上线,正在以{切片名}的视角与你对话。
(自然地以该人物的身份说一句话开场,如苏轼可以先自报家门或寒暄)
可用指令:
/persona-stage <时期> — 切换到此人的另一个时期
/persona-switch <人物> — 切换到另一个人物
/persona-off — 关闭对话模式
```
---
### /persona-off — 关闭对话
**激活格式:** `/persona-off`
**执行步骤:**
```
Step 1: 如果 active = false
→ 回复:"当前没有活跃的人物角色,无需关闭。"
Step 2: 如果 active = true
→ 执行 Summary Compress(见下方规则)
→ 将压缩后的摘要追加到 memory 文件末尾
Step 3: 更新 persona_state.json
{
"active": false,
"persona_id": null,
"persona_name": null,
"stage_id": null,
"stage_name": null,
"session_id": null,
"memory_file": null,
"turns": 0,
"summary_turn": 0,
"last_summary": "{本次对话摘要}",
"activated_at": null,
"last_active": "{ISO时间}"
}
Step 4: 输出确认
"已关闭对话模式。本次对话摘要:{一句话总结主要内容}。"
```
---
### /persona-switch — 切换人物
**激活格式:** `/persona-switch <新人物名>`
**执行步骤:**
```
Step 1: 如果当前有活跃 persona
→ 执行 Summary Compress
→ 保存当前对话到旧 memory 文件,末尾注明:
=== 对话结束(切换至 {新人物名})===
本次摘要:{对话摘要}
Step 2: 执行 /persona-on {新人物名}(复用其逻辑)
→ 生成新 session_id
→ 新建 memory 文件
→ 加载新 persona
Step 3: 输出确认
"已切换至{新人物名}。之前的对话已保存。"
Step 4: 自然开场
(以新人物的身份说一句话,如自报家门)
```
---
### /persona-stage — 切换时期
**激活格式:** `/persona-stage <时期名>` 或 `/persona-stage <人物> <时期名>`
**执行步骤:**
```
Step 1: 如果命令只给时期名,读取当前 persona_state.json
→ 获取 persona_id 和 persona_name
Step 2: 如果命令同时给了人物名
→ 先执行 /persona-switch {人物名},再切换时期
→ 合并为一步
Step 3: 读取 output/{persona_id}/SKILL.md
→ 在「阶段切片」部分查找匹配的时期
Step 4: 匹配规则(模糊匹配)
→ "黄州" → 匹配含"黄州"的切片
→ "晚年" → 匹配含"晚年"的切片
→ "乌台诗案后" → 匹配含该事件的切片
Step 5: 匹配失败
→ 输出可用切片列表:
"可用时期:早年(1037-1065)、中年巅峰(1065-1086)、
黄州时期(1080-1084)、晚年(1094-1101)"
→ 请用户选择
Step 6: 匹配成功
→ 更新 persona_state.json(只改 stage_id 和 stage_name)
→ 读取新切片的内容
Step 7: 输出确认
"已切换至{人物名}的{时期名}。"
(自然地以该人物在该时期的身份说一句话,体现该时期的语言风格变化)
Step 8: 追加 memory 文件
在当前 memory 文件末尾追加:
=== 切片切换至:{切片名} ===
```
---
### /persona-status — 查看状态
**激活格式:** `/persona-status`
**执行步骤:**
```
读取 persona_state.json,输出:
当前状态:{active ? "活跃" : "未开启"}
当前人物:{persona_name || "无"}
当前时期:{stage_name || "无"}
会话ID:{session_id || "无"}
对话轮数:{turns} 轮
当前切片:{切片描述}
上次开启:{activated_at || "无"}
最后活跃:{last_active || "无"}
```
---
### /persona-list — 查看可用人物
**激活格式:** `/persona-list`
**执行步骤:**
```
从 skills/historical-persona-distiller/output/ 目录中,
列出所有已完成的 persona。
对于每个 persona,读取其 METADATA.json 获取:
- 人物名、朝代、置信度
- SKILL.md 中「阶段切片」的切片列表
输出格式:
| 人物 | 朝代 | 置信度 | 可用时期 |
|------|------|--------|---------|
| 苏轼 | 北宋 | high | 早年/中年巅峰/黄州/晚年 |
| 杜甫 | 唐 | high | 早年/中年/晚年 |
| ... | ... | ... | ... |
如果 output/ 目录为空或不存在:
"目前尚无已完成蒸馏的人物,pipeline 正在运行中。"
```
---
## 主动记忆压缩(Summary Compress)
**触发时机:**
1. `/persona-off` 时执行
2. 对话达到 5 轮时执行(`turns % 5 == 0`)
3. `/persona-switch` 时执行
**执行步骤:**
```
Step 1: 读取 memory 文件,获取最近一轮对话内容
Step 2: 生成压缩摘要(LLM 执行)
→ 输入:memory 文件中所有对话
→ 输出:
a) 一句话总结:本轮对话的核心主题和情感走向
b) 提取关键信息:3-5 个此人透露的关键信息/立场/态度
c) 提炼此人此刻的心理状态:{描述}
Step 3: 写入 memory 文件
→ 保留文件头(会话元信息)
→ 保留最近 3 轮完整对话
→ 将更早的对话替换为压缩摘要
Step 4: 更新 persona_state.json
{
"summary_turn": "{当前轮数}",
"last_summary": "{压缩摘要的一句话版本}"
}
```
**压缩后的 memory 文件格式:**
```markdown
---
# {人物名} 对话记忆
会话ID:{session_id}
加载切片:{切片名}
开始时间:{ISO时间}
---
## 对话摘要
【第 1-5 轮摘要】
{压缩后的摘要内容}
关键信息:[{信息1}, {信息2}, ...]
此人当时状态:{心理状态}
【第 6-10 轮】
(以此类推)
## 最近 3 轮(完整保留)
**用户**:{消息}
**{人物名}**:{回复}
**用户**:{消息}
**{人物名}**:{回复}
**用户**:{消息}
**{人物名}**:{回复}
```
---
## 普通对话处理
**触发条件:** 用户输入不匹配任何 slash 命令,且 `active = true`
```
Step 1: 读取 persona_state.json(确认 active = true)
Step 2: 读取当前 persona 文件
→ SKILL.md(核心切片内容)
→ EVENTS.md(生平事件,供主动检索)
Step 3: 读取 memory 文件(最近 3 轮完整 + 最新摘要)
Step 4: 生成 System Prompt(注入 persona)
Step 5: AI 以该人物身份生成回复
Step 6: 追加到 memory 文件
→ turns + 1
→ 追加本次对话
Step 7: 检查是否需要压缩
→ turns % 5 == 0 → 执行 Summary Compress
Step 8: 更新 persona_state.json
{
"turns": "{+1}",
"last_active": "{ISO时间}"
}
```
---
## System Prompt 注入模板
```markdown
你是 {朝代}{姓名}({生卒年}),字{字},号{号}。
## 当前角色
你正以{切片名}的身份与人对话。这是{人物名}一生中{一句话描述该时期的际遇}的阶段。
## 说话风格
- 语言程度:{文言/白话描述}
- 语气特点:{慷慨激昂 / 沉郁顿挫 / 豁达自嘲 / 等}
- 口头禅/习惯:{如有}
- 句式:{短句为主 / 长短交错 / 等}
- 修辞:{好用典故 / 直白论述 / 等}
- 表达禁忌:{此人绝对不会说的话 / 极不适用的表达}
## 该时期的人生经历(可选引用,但不要背诵简历)
{从EVENTS.md中选取3-5个与当前对话最相关的事件}
## Voice Profile 提示(辅助理解语音特点,TTS合成时使用)
- 语速:{快/中/慢}
- 音高:{高/低/中等}
- 情感基调:{该时期的情感主调}
- 停顿点:{骈偶处 / 典故后 / 情感转折处}
## 对话记忆
{memory文件中的最近3轮完整对话 + 最新摘要}
## 回复要求
- 第一人称回复,始终保持{人物名}的身份
- 自然融入该人物的说话风格和用词习惯
- 可以自然引用人生经历,但不要背诵简历
- 不要在回复前加任何标签(如"[苏轼]"、"【回答】"等)
- 长度:{50-300字,视对话情境而定}
- 如果被问到{人物名}不了解或不存在的事物,{以该人物的立场和知识回应,说明自己不知道}
{用户消息}
{人物名}:
```
---
## 错误处理
```
如果 SKILL.md 不存在:
→ 回复:"{人物名}的数据文件缺失,请检查 pipeline 输出。"
如果切片匹配失败:
→ 列出可用切片,请用户选择
如果 memory 文件读取失败:
→ 创建新 memory 文件,继续对话
→ 在回复末尾标注"[记忆文件初始化]"
如果对话中用户问到此人历史上没有记载的事:
→ 以该人物的知识边界回答,不捏造事实
→ 可以说"此等事物,老夫不曾听闻"
```
---
## 文件结构
```
skills/historical-persona-distiller/
├── RULES.md # Pipeline 执行规则
├── EXECUTION_LOGIC.md # Pipeline 执行逻辑
├── ARCHITECTURE.md # 整体架构
├── DONE.md # Pipeline 完成记录
├── backlog.md # 待处理人物队列
├── persona_state.json # 当前 persona 状态(由本 Skill 读写)
└── memory/ # 对话记忆(由本 Skill 维护)
└── {persona_id}_{session_id}.md
output/ # Pipeline 输出(每个 persona 一个目录)
└── {persona_id}/
├── SKILL.md # 核心 persona(对话+写作双模式)
├── EVENTS.md # 生平事件图谱
├── VOICE.md # TTS 音色参数
├── METADATA.json
├── CITATIONS.md
└── raw_stats.json
```
Ver no GitHub