From dbd89f39ab16f04aa63d90fca37bcab682d18e7a Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 22 Jun 2026 20:25:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=BA=E6=A0=BC=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=8F=8C=E5=B1=82=E6=9E=B6=E6=9E=84=20=E2=80=94=20persona.md?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E6=B3=A8=E5=85=A5system=20prompt=20+=20RAG?= =?UTF-8?q?=E9=BB=91=E5=90=8D=E5=8D=95=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PersonaConfig 新增 NarrativePersona 字段,在 system prompt 的「你的身份」和「你的性格」之间插入「## 你的叙事人格」 - Loader.NewLoader 自动读取同目录 persona.md 注入所有 config;Reload 同步热更新 - RAG IngestDirectory 跳过 persona.md 和 _index.md,避免重复检索浪费token - 新增 docs/dev-plan/12-next-phase-optimization.md 下一阶段优化大纲 Co-Authored-By: Claude --- backend/ai-core/internal/persona/injector.go | 10 +- backend/ai-core/internal/persona/loader.go | 18 ++ docs/dev-plan/12-next-phase-optimization.md | 253 +++++++++++++++++++ 3 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 docs/dev-plan/12-next-phase-optimization.md diff --git a/backend/ai-core/internal/persona/injector.go b/backend/ai-core/internal/persona/injector.go index bed43fe..e9487de 100644 --- a/backend/ai-core/internal/persona/injector.go +++ b/backend/ai-core/internal/persona/injector.go @@ -17,6 +17,11 @@ type PersonaConfig struct { ThinkingGuidelines ThinkingGuidelines `yaml:"thinking_guidelines"` MemoryGuidelines MemoryGuidelines `yaml:"memory_guidelines"` ReflectionGuidelines ReflectionGuidelines `yaml:"reflection_guidelines"` + + // NarrativePersona is the full text of persona.md, injected verbatim + // into the system prompt as the narrative personality layer. + // Not parsed from YAML — loaded from persona.md alongside the YAML config. + NarrativePersona string `yaml:"-"` } // BuildSystemPrompt 构建系统Prompt (向后兼容,不含心情) @@ -43,8 +48,11 @@ func (pc *PersonaConfig) BuildSystemPromptWithMood(userName string, affectionLev ## 你的身份 %s +## 你的叙事人格 +%s + ## 你的性格 -`, pc.Identity.TrueName, pc.Identity.Essence) +`, pc.Identity.TrueName, pc.Identity.Essence, pc.NarrativePersona) for _, trait := range pc.Personality.CoreTraits { prompt += fmt.Sprintf("- %s: %s\n", trait.Name, trait.Description) diff --git a/backend/ai-core/internal/persona/loader.go b/backend/ai-core/internal/persona/loader.go index 2e62d45..c5dbc21 100644 --- a/backend/ai-core/internal/persona/loader.go +++ b/backend/ai-core/internal/persona/loader.go @@ -3,6 +3,7 @@ package persona import ( "fmt" "os" + "path/filepath" "sync" "gopkg.in/yaml.v3" @@ -54,6 +55,17 @@ func NewLoader(personaDir string) (*Loader, error) { return nil, fmt.Errorf("未找到任何人格配置文件") } + // Load narrative persona from persona.md (same directory as YAML configs). + // This is the full-text personality narrative injected verbatim into the system prompt. + personaMDPath := filepath.Join(personaDir, "persona.md") + if data, err := os.ReadFile(personaMDPath); err == nil { + // Attach to every loaded persona config. + narrative := string(data) + for _, cfg := range l.configs { + cfg.NarrativePersona = narrative + } + } + return l, nil } @@ -82,6 +94,12 @@ func (l *Loader) Reload(name string, path string) error { } l.mu.Lock() + // Re-read persona.md to keep narrative persona in sync. + personaDir := filepath.Dir(path) + personaMDPath := filepath.Join(personaDir, "persona.md") + if data, err := os.ReadFile(personaMDPath); err == nil { + cfg.NarrativePersona = string(data) + } l.configs[name] = &cfg l.mu.Unlock() diff --git a/docs/dev-plan/12-next-phase-optimization.md b/docs/dev-plan/12-next-phase-optimization.md new file mode 100644 index 0000000..1e277f8 --- /dev/null +++ b/docs/dev-plan/12-next-phase-optimization.md @@ -0,0 +1,253 @@ +# 12 — 下一阶段优化大纲 + +> **日期**:2026-06-22 +> **来源**:对照 Cyrene 人格、记忆及功能拓展架构设计文档,逐项比对现状后梳理 +> **状态**:大纲,待拆分为详细实施计划 + +--- + +## 目录 + +1. [P0 — 自主思考分层](#1-p0--自主思考分层) +2. [P1 — LLM 可自主操作提醒与日程](#2-p1--llm-可自主操作提醒与日程) +3. [P2 — 对话后处理补全](#3-p2--对话后处理补全) +4. [P3 — 群聊并发(协会议话)](#4-p3--群聊并协会议话) +5. [P4 — 多平台前缀统一抽象](#5-p4--多平台前缀统一抽象) +6. [P5 — 工具子会话增强](#6-p5--工具子会话增强) +7. [实施顺序建议](#7-实施顺序建议) + +--- + +## 1. P0 — 自主思考分层 + +### 现状 + +所有后台思考(periodic / silence / post_chat / startup)共用同一个 LLM 模型和同一套 system prompt。高频短思考(如每分钟检查状态)和低频深度反思(如每 5 分钟整理记忆)没有区分,浪费 token。 + +### 目标 + +``` +┌─────────────┐ 唤醒 ┌─────────────┐ +│ 简单思考 │ ──────────────► │ 复杂思考 │ +│ (下位模型) │ │ (思考模型) │ +│ 高频/轻量 │ │ 低频/深度 │ +│ 例: 每分钟 │ │ 例: 每5分钟 │ +└──────┬───────┘ └──────┬───────┘ + │ 可调用工具 │ 可调用工具 + │ 可主动消息 │ 可主动消息 + │ 可追加继续 │ 可追加继续 + ▼ ▼ + 共享 ProactiveGuard 频率控制 +``` + +### 要点 + +- **模型配置**:简单思考用 fast/flash 模型(temperature 0.3),复杂思考用 thinking 模型(temperature 0.7) +- **周期**:简单思考 30-60s / 复杂思考 300s(可配置) +- **唤醒**:简单思考可判定"需要深思"→ 触发一次复杂思考 +- **追加**:两种思考都允许 `continue` 信号追加(当前只有 post_chat 可追加) +- **后处理**:每次复杂思考结束后触发记忆整理 + +### 涉及文件 + +| 文件 | 改动 | +|------|------| +| `background/thinker.go` | Thinker 拆分为 SimpleThinker + DeepThinker 或增加双模式循环 | +| `config/loader.go` → `models.json` | 新增 `simple_think` / `deep_think` 路由 | +| `background/thinker.go` → `buildThinkingSystemPrompt` | 简单/复杂两套 prompt | + +--- + +## 2. P1 — LLM 可自主操作提醒与日程 + +### 现状 + +Gateway 有 Reminder CRUD API,但 LLM 无法调用——没有注册 `reminder_create` / `reminder_update` / `schedule_query` 工具。昔涟不能在思考中说"明天早上 8 点提醒开拓者开会",因为这句话无法转化为实际的定时提醒。 + +### 目标 + +在 AI-Core 的 ToolRegistry 中新增 3-4 个提醒/日程工具,LLM 可在对话中或自主思考中调用。 + +### 要点 + +- **工具列表**: + - `reminder_create(time, content, repeat?)` → 创建提醒 + - `reminder_list()` → 列出当前用户的提醒 + - `reminder_delete(id)` → 删除提醒 + - `schedule_query(date)` → 查询某一天的日程 +- **实现方式**:HTTP 调用 Gateway `/api/v1/reminders` API(已有后端 CRUD) +- **自主思考可调用**:加入 `AutonomousToolPolicy.AllowedTools` 白名单 +- **防止阻塞**:配置 `THINK_SCHEDULE_WINDOW` 环境变量,在提醒时间窗口内确保简单思考被唤醒 + +### 涉及文件 + +| 文件 | 改动 | +|------|------| +| `ai-core/internal/tools/reminder_tool.go` | **新建** — 3-4 个 reminders 工具 | +| `ai-core/cmd/main.go` | 注册提醒工具到 ToolRegistry | +| `background/thinker.go` → `AutonomousToolPolicy` | 允许提醒工具在自主思考中调用 | + +--- + +## 3. P2 — 对话后处理补全 + +### 3.1 自主思考后触发记忆整理 + +**现状**:记忆提取(`ExtractMemoriesOnly`)只在 `platform_silent` 和正常对话后触发。自主思考(periodic/silence/startup)结束后不触发。 + +**目标**:复杂思考结束后自动调用一次记忆整理。 + +**涉及文件**:`background/thinker.go` → `performThink` 的结尾逻辑 + +### 3.2 对话后思考可追加 + +**现状**:`post_chat` 思考支持追加(`allowContinuation` + `triggerReason == "post_chat"`),但 `periodic` / `silence` 不支持。 + +**目标**:所有思考类型都支持 `continue` 信号追加,通过 `MaxContinuationRounds` 限制防止死循环。 + +**涉及文件**:`background/thinker.go` → 思考循环的 `allowContinuation` 条件 + +--- + +## 4. P3 — 群聊并发(协会议话) + +### 现状 + +一个 session 的消息处理是严格线性的。当多个 QQ 用户同时在群里 @ 昔涟时: +- 消息按到达顺序排队 +- 每条的 LLM 合成延迟 3-4s +- 第 3 条消息要等前面 2 条处理完才开始回复(6-8s 延迟) + +### 目标 + +``` +主会话 (session_id=platform_qq_) + │ + ├─ 消息A → 主会话处理 (LLM 合成中...) + │ + ├─ 消息B (主会话繁忙) → 协会议话 B (独立 LLM 合成) + │ └─ 完成 → 结果写入主会话上下文 + │ + └─ 消息C (主会话繁忙) → 协会议话 C (独立 LLM 合成) + └─ 完成 → 结果写入主会话上下文 +``` + +### 要点 + +- **并发检测**:收到新消息时,检查主会话是否正在处理(`isProcessing` 标记) +- **协会议话**:独立 goroutine + 独立 LLM 调用,共享主会话的对话历史 +- **结果合并**:协会议话完成后,回复通过 platform-bridge 发送,上下文摘要写回主会话 +- **数量限制**:最多 3 个协会议话并行,超出则排队 +- **身份一致性**:每个协会议话携带相同的 persona config + 群聊上下文 + +### 涉及文件 + +| 文件 | 改动 | +|------|------| +| `ai-core/orchestrator/orchestrator.go` | ProcessInput 增加并发检测 + 协会议话创建 | +| `ai-core/orchestrator/concurrent_handler.go` | **新建** — 协会议话管理器 | +| `platform-bridge/cmd/main.go` | 可能需要支持协会议话的回复路由 | + +--- + +## 5. P4 — 多平台前缀统一抽象 + +### 现状 + +QQ 平台标识(`【QQ群聊:xxx】`、`【QQ私聊:xxx】`)硬编码在 `thinker.go` 的 `buildThinkingUserPrompt` 里,`extractProactiveMessage` 里的 `qqTargetRe` 也只匹配 QQ。如果要接入 Telegram / Discord / 企业微信,需要在多个位置添加硬编码。 + +### 目标 + +将"平台前缀"抽象为一个统一的 `PlatformRouteFormat` 接口,每个 adapter 注册时声明自己的格式。 + +### 要点 + +- `PlatformRouteFormat` 接口: + - `ProactiveMarker(chatType, channelID, atUserID string) string` — 主动消息标记格式 + - `ChannelPrefix(channelID string) string` — 消息来源前缀(如 `【QQ群聊:xxx】`) +- 注册在 `PlatformAdapter` 接口扩展中 +- thinker prompt 和 extractProactiveMessage 从 router 获取当前已注册的格式列表 +- 后向兼容:QQ 当前行为不变 + +### 涉及文件 + +| 文件 | 改动 | +|------|------| +| `platform-bridge/bridge/adapter.go` | PlatformAdapter 接口扩展 | +| `platform-bridge/bridge/router.go` | GetRouteFormats() 方法 | +| `platform-bridge/handler/bridge_handler.go` | API 端点暴露格式信息 | +| `ai-core/background/thinker.go` | 从 bridge 获取格式替代硬编码 | +| `ai-core/cmd/main.go` | 启动时拉取格式配置 | + +--- + +## 6. P5 — 工具子会话增强 + +### 6.1 子会话返回时间戳 + +**现状**:子会话结果(SubSessionResult)没有记录创建时间和结束时间。 + +**目标**:每个子会话结果增加 `CreatedAt` / `FinishedAt` 字段。 + +**涉及文件**:`model/sub_session.go` + 各 provider 的 `Execute()` 方法 + +### 6.2 工具调用不阻塞主会话 + +**现状**:`Synthesizer.ChatWithTools` 的 LLM 工具调用循环(最多 5 轮)在主 goroutine 中执行,长工具调用(如 `web_search` + `web_fetch` 链)会阻塞流式输出。 + +**目标**:工具调用结果通过 channel 异步返回,Synthesizer 先输出等待提示(如"让我查一下..."),收到结果后再继续流式生成。 + +**涉及文件**:`orchestrator/synthesizer.go` — `ChatWithTools` 的调用模型改为 publish/subscribe + +--- + +## 7. 实施顺序建议 + +``` +Phase A (本节可做) ───────────────────────────── + P2.2 对话后思考可追加 ~10 行改动 + P2.1 自主思考后触发记忆整理 ~5 行改动 + P5.1 子会话返回时间戳 ~20 行改动 + +Phase B (需要跨服务协调) ─────────────────────── + P1 LLM 可自主操作提醒与日程 新建 1 文件 + 注册工具 + P0 自主思考分层 重构 thinker 核心循环 + +Phase C (架构级改动) ─────────────────────────── + P4 多平台前缀统一抽象 跨 ai-core + platform-bridge + P3 群聊并发(协会议话) 跨 ai-core + platform-bridge + +Phase D (后续优化) ───────────────────────────── + P5.2 工具调用不阻塞主会话 改造 synthesizer 调用模型 +``` + +--- + +## 附:与架构设计文档的对照清单 + +| # | 设计要求 | 状态 | 对应章节 | +|---|---------|------|---------| +| 1 | 基础出人格库 | ✅ | — | +| 2 | 故事背景知识库 | ✅ | — | +| 3 | 多优先级自主增删改记忆 | ⚠️ 缺自主删改 | 待定 | +| 4 | 用户手动增删改知识库 | ✅ | — | +| 5 | IoT 功能调用知识库 | ✅ | — | +| 6 | 插件功能调用知识库 | ✅ | — | +| 7 | 定时任务模块 | ❌ | P1 | +| 8 | 定时任务自主增删改 | ❌ | P1 | +| 9 | 日程自主增删改 | ❌ | P1 | +| 10 | 简单思考(高频下位模型) | ❌ | P0 | +| 11 | 复杂思考(低频思考模型) | ⚠️ 有但未分层 | P0 | +| 12 | 简单思考可唤醒复杂思考 | ❌ | P0 | +| 13 | 两种思考均可追加 | ⚠️ 仅 post_chat | P2.2 | +| 14 | 两种思考均可调用工具 | ✅ | — | +| 15 | 主动通讯(客户端+平台) | ✅ | — | +| 16 | 思考后触发记忆整理 | ❌ | P2.1 | +| 17 | 回复一句一句发 | ✅ | — | +| 18 | 工具调用不阻塞主会话 | ⚠️ 部分阻塞 | P5.2 | +| 19 | 工具子会话返回时间戳 | ❌ | P5.1 | +| 20 | 群聊并发协会议话 | ❌ | P3 | +| 21 | 多平台消息前缀 | ⚠️ QQ 硬编码 | P4 | +| 22 | 多平台发送方式 | ✅ | — | + +> 共 22 项设计要求:✅ 已完成 9 项,⚠️ 部分完成 5 项,❌ 未实现 8 项。