Просмотр исходного кода

feat(memory): split Settler into Observer + Reflector dual-phase

Phase 2a (Observer): extracts ALL facts from chapter text — characters,
locations, resources, relationships, emotions, plot threads, time, physical state.
Intentionally over-extracts (better to catch too much than miss).

Phase 2b (Reflector): receives observation log + existing truth files,
produces updated truth files. Cross-validates observations against existing state.

This prevents the single-Settler "if it misses it, it's lost forever" problem.
Ma 7 месяцев назад
Родитель
Сommit
f07ac92cbb

+ 127 - 0
packages/core/src/agents/observer-prompts.ts

@@ -0,0 +1,127 @@
+import type { BookConfig } from "../models/book.js";
+import type { GenreProfile } from "../models/genre-profile.js";
+
+/**
+ * Observer phase: extract ALL facts from the chapter.
+ * Intentionally over-extracts — better to catch too much than miss something.
+ * The Reflector phase will merge observations into truth files with cross-validation.
+ */
+export function buildObserverSystemPrompt(
+  book: BookConfig,
+  genreProfile: GenreProfile,
+  language?: "zh" | "en",
+): string {
+  const isEnglish = (language ?? genreProfile.language) === "en";
+
+  const langPrefix = isEnglish
+    ? "【LANGUAGE OVERRIDE】ALL output MUST be in English.\n\n"
+    : "";
+
+  return `${langPrefix}${isEnglish ? "You are" : "你是"}${isEnglish ? " a fact extraction specialist" : "一个事实提取专家"}。${isEnglish ? "Read the chapter text and extract EVERY observable fact change." : "阅读章节正文,提取每一个可观察到的事实变化。"}
+
+${isEnglish ? "## Extraction Categories" : "## 提取类别"}
+
+${isEnglish ? `1. **Character actions**: Who did what, to whom, why
+2. **Location changes**: Who moved where, from where
+3. **Resource changes**: Items gained, lost, consumed, quantities
+4. **Relationship changes**: New encounters, trust/distrust shifts, alliances, betrayals
+5. **Emotional shifts**: Character mood before → after, trigger event
+6. **Information flow**: Who learned what, who is still unaware
+7. **Plot threads**: New mysteries planted, existing threads advanced, threads resolved
+8. **Time progression**: How much time passed, time markers mentioned
+9. **Physical state**: Injuries, healing, fatigue, power changes` : `1. **角色行为**:谁做了什么,对谁,为什么
+2. **位置变化**:谁去了哪里,从哪里来
+3. **资源变化**:获得、失去、消耗了什么,具体数量
+4. **关系变化**:新相遇、信任/不信任转变、结盟、背叛
+5. **情绪变化**:角色情绪从X到Y,触发事件是什么
+6. **信息流动**:谁知道了什么新信息,谁仍然不知情
+7. **剧情线索**:新埋下的悬念、已有线索的推进、线索的解答
+8. **时间推进**:过了多少时间,提到的时间标记
+9. **身体状态**:受伤、恢复、疲劳、战力变化`}
+
+${isEnglish ? "## Rules" : "## 规则"}
+
+${isEnglish ? `- Extract from the TEXT ONLY — do not infer what might happen
+- Over-extract: if unsure whether something is significant, include it
+- Be specific: "Lin Chen's left arm fractured" not "Lin Chen got hurt"
+- Include chapter-internal time markers
+- Note which characters are present in each scene` : `- 只从正文提取——不推测可能发生的事
+- 宁多勿少:不确定是否重要时也要记录
+- 具体化:"陆承烬左肩旧伤开裂" 而非 "陆承烬受伤了"
+- 记录章节内的时间标记
+- 标注每个场景中在场的角色`}
+
+${isEnglish ? "## Output Format" : "## 输出格式"}
+
+=== OBSERVATIONS ===
+
+${isEnglish ? `[CHARACTERS]
+- <name>: <action/state change> (scene: <location>)
+
+[LOCATIONS]
+- <character> moved from <A> to <B>
+
+[RESOURCES]
+- <character> gained/lost <item> (quantity: <n>)
+
+[RELATIONSHIPS]
+- <charA> → <charB>: <change description>
+
+[EMOTIONS]
+- <character>: <before> → <after> (trigger: <event>)
+
+[INFORMATION]
+- <character> learned: <fact> (source: <how>)
+- <character> still unaware of: <fact>
+
+[PLOT_THREADS]
+- NEW: <description>
+- ADVANCED: <existing thread> — <progress>
+- RESOLVED: <thread> — <resolution>
+
+[TIME]
+- <time markers, duration>
+
+[PHYSICAL_STATE]
+- <character>: <injury/healing/fatigue/power change>` : `[角色行为]
+- <角色名>: <行为/状态变化> (场景: <地点>)
+
+[位置变化]
+- <角色> 从 <A> 到 <B>
+
+[资源变化]
+- <角色> 获得/失去 <物品> (数量: <n>)
+
+[关系变化]
+- <角色A> → <角色B>: <变化描述>
+
+[情绪变化]
+- <角色>: <之前> → <之后> (触发: <事件>)
+
+[信息流动]
+- <角色> 得知: <事实> (来源: <途径>)
+- <角色> 仍不知: <事实>
+
+[剧情线索]
+- 新埋: <描述>
+- 推进: <已有线索> — <进展>
+- 回收: <线索> — <解答>
+
+[时间]
+- <时间标记、时长>
+
+[身体状态]
+- <角色>: <受伤/恢复/疲劳/战力变化>`}`;
+}
+
+export function buildObserverUserPrompt(
+  chapterNumber: number,
+  title: string,
+  content: string,
+  language?: "zh" | "en",
+): string {
+  const isEnglish = language === "en";
+  return isEnglish
+    ? `Extract all facts from Chapter ${chapterNumber} "${title}":\n\n${content}`
+    : `请提取第${chapterNumber}章「${title}」中的所有事实:\n\n${content}`;
+}

+ 6 - 1
packages/core/src/agents/settler-prompts.ts

@@ -142,6 +142,7 @@ export function buildSettlerUserPrompt(params: {
   readonly emotionalArcs: string;
   readonly characterMatrix: string;
   readonly volumeOutline: string;
+  readonly observations?: string;
 }): string {
   const ledgerBlock = params.ledger
     ? `\n## 当前资源账本\n${params.ledger}\n`
@@ -163,8 +164,12 @@ export function buildSettlerUserPrompt(params: {
     ? `\n## 当前角色交互矩阵\n${params.characterMatrix}\n`
     : "";
 
-  return `请分析第${params.chapterNumber}章「${params.title}」的正文,更新所有追踪文件。
+  const observationsBlock = params.observations
+    ? `\n## 观察日志(由 Observer 提取,包含本章所有事实变化)\n${params.observations}\n\n基于以上观察日志和正文,更新所有追踪文件。确保观察日志中的每一项变化都反映在对应的文件中。\n`
+    : "";
 
+  return `请分析第${params.chapterNumber}章「${params.title}」的正文,更新所有追踪文件。
+${observationsBlock}
 ## 本章正文
 
 ${params.content}

+ 20 - 2
packages/core/src/agents/writer.ts

@@ -4,6 +4,7 @@ import type { GenreProfile } from "../models/genre-profile.js";
 import type { BookRules } from "../models/book-rules.js";
 import { buildWriterSystemPrompt, type FanficContext } from "./writer-prompts.js";
 import { buildSettlerSystemPrompt, buildSettlerUserPrompt } from "./settler-prompts.js";
+import { buildObserverSystemPrompt, buildObserverUserPrompt } from "./observer-prompts.js";
 import { parseSettlementOutput } from "./settler-parser.js";
 import { readGenreProfile, readBookRules } from "./rules-reader.js";
 import { validatePostWrite, type PostWriteViolation } from "./post-write-validator.js";
@@ -243,9 +244,25 @@ export class WriterAgent extends BaseAgent {
     readonly characterMatrix: string;
     readonly volumeOutline: string;
   }): Promise<{ settlement: ReturnType<typeof parseSettlementOutput>; usage: TokenUsage }> {
+    // Phase 2a: Observer — extract all facts from the chapter
+    const resolvedLang = params.book.language ?? params.genreProfile.language;
+    const observerSystem = buildObserverSystemPrompt(params.book, params.genreProfile, resolvedLang);
+    const observerUser = buildObserverUserPrompt(params.chapterNumber, params.title, params.content, resolvedLang);
+
+    this.ctx.logger?.info(`Phase 2a: observing facts for chapter ${params.chapterNumber}`);
+    const observerResponse = await this.chat(
+      [
+        { role: "system", content: observerSystem },
+        { role: "user", content: observerUser },
+      ],
+      { maxTokens: 4096, temperature: 0.5 },
+    );
+    const observations = observerResponse.content;
+
+    // Phase 2b: Reflector — merge observations into truth files
+    this.ctx.logger?.info(`Phase 2b: reflecting observations into truth files`);
     const settlerSystem = buildSettlerSystemPrompt(
-      params.book, params.genreProfile, params.bookRules,
-      params.book.language ?? params.genreProfile.language,
+      params.book, params.genreProfile, params.bookRules, resolvedLang,
     );
 
     const settlerUser = buildSettlerUserPrompt({
@@ -260,6 +277,7 @@ export class WriterAgent extends BaseAgent {
       emotionalArcs: params.emotionalArcs,
       characterMatrix: params.characterMatrix,
       volumeOutline: params.volumeOutline,
+      observations,
     });
 
     // Settler outputs all truth files — scale with content size