فهرست منبع

fix(context): 按调用模型窗口独立约束多环节上下文预算

深度写作共享上下文取写作/任务书/去AI味窗口最小值;前情分析与光环预览按实际调用模型裁剪,避免依赖末级无差别截断。

Co-authored-by: Cursor <cursoragent@cursor.com>
darknessomi 1 ماه پیش
والد
کامیت
f3fcf5d3ae

+ 3 - 2
src/components/novel/character-aura-view.tsx

@@ -357,11 +357,12 @@ export function CharacterAuraView({ hideSidebar = false }: { hideSidebar?: boole
       }
       const contextPack = await buildContextPack(project.path, auraPreviewTask)
       const previewPack = { ...contextPack, characterAuras: characterAuraPreview }
+      // 预算须绑定实际发起调用的模型窗口,而非 store 里的基础 llmConfig。
+      const effectiveConfig = resolveNovelModel(llmConfig, novelConfig, "writing")
       const contextPrompt = contextPackToPrompt(previewPack, resolveContextPackTokenBudget({
-        maxContextSize: llmConfig.maxContextSize,
+        maxContextSize: effectiveConfig.maxContextSize,
         contextTokenBudget: novelConfig.contextTokenBudget,
       }))
-      const effectiveConfig = resolveNovelModel(llmConfig, novelConfig, "writing")
       const messages: ChatMessage[] = [
         {
           role: "system",

+ 14 - 1
src/lib/novel/deep-chapter-generation.ts

@@ -653,9 +653,22 @@ export async function runDeepChapterGeneration(
   );
   throwIfAborted(signal);
 
+  // 任务书(workflowConfig)、初稿/返修(writingConfig)、去AI味(deAiConfig)复用同一份
+  // outlinePrompt + contextPrompt。这三个环节可能用不同模型、各有独立上下文窗口,
+  // 因此预算取三者窗口的最小值:确保任一环节都不必依赖 llm-client 末级无差别截断
+  // (末级截断按字符砍,会绕过 ContextPack 的字段优先级),并让各环节看到一致的上下文。
+  const sharedContextWindows = [
+    writingConfig.maxContextSize,
+    workflowConfig.maxContextSize,
+    deAiConfig.maxContextSize,
+  ].filter((size): size is number => typeof size === "number" && size > 0);
+  const sharedContextWindow = sharedContextWindows.length > 0
+    ? Math.min(...sharedContextWindows)
+    : input.llmConfig.maxContextSize;
+
   // 大纲与其余上下文共用同一窗口预算:按单章目标字数×2预留输出,再分配资料包。
   const totalContextTokenBudget = computeWritingContextPackTokenBudget({
-    maxContextSize: input.llmConfig.maxContextSize,
+    maxContextSize: sharedContextWindow,
     contextTokenBudget: novelConfig.contextTokenBudget,
     chapterTargetChars: novelConfig.chapterTargetChars,
   });

+ 31 - 1
src/lib/novel/previous-chapters-analysis.ts

@@ -1,6 +1,23 @@
 import type { LlmConfig } from "@/stores/wiki-store"
 import { readFile } from "@/commands/fs"
 import { searchWiki } from "@/lib/search"
+import { computeContextBudget } from "@/lib/context-budget"
+
+/** 前情正文占分析模型窗口的比例;其余留给分析指令与模型输出。 */
+const PREVIOUS_BODY_WINDOW_FRAC = 0.5
+/** 单章正文最低保留字符数,避免窗口很小时被裁到无信息量。 */
+const PREVIOUS_PER_CHAPTER_FLOOR = 800
+
+/** 章节正文超预算时保留首尾、省略中段,避免尾部(结尾/最新状态)被整段丢弃。 */
+function clampChapterBody(body: string, maxChars: number): string {
+  const normalized = body.trim()
+  if (normalized.length <= maxChars) return normalized
+  const marker = "\n\n[中间内容已省略,保留首尾]\n\n"
+  const available = Math.max(200, maxChars - marker.length)
+  const head = Math.ceil(available * 0.6)
+  const tail = available - head
+  return `${normalized.slice(0, head).trimEnd()}${marker}${tail > 0 ? normalized.slice(-tail).trimStart() : ""}`
+}
 
 export interface PreviousChapterAnalysis {
   chapterNumber: number
@@ -43,8 +60,21 @@ export async function analyzePreviousChapters(
 
   if (previousChapters.length === 0) return ""
 
+  // 按分析模型自身的上下文窗口分配前情正文预算,均分到各章后保留首尾。
+  // 不再无界拼接全文、依赖 llm-client 末级截断。
+  const { maxCtx } = computeContextBudget(llmConfig.maxContextSize)
+  const bodyBudget = Math.floor(maxCtx * PREVIOUS_BODY_WINDOW_FRAC)
+  const perChapterBudget = Math.max(
+    PREVIOUS_PER_CHAPTER_FLOOR,
+    Math.floor(bodyBudget / previousChapters.length),
+  )
+  const budgetedChapters = previousChapters.map((ch) => ({
+    number: ch.number,
+    content: clampChapterBody(ch.content, perChapterBudget),
+  }))
+
   // 构建分析prompt
-  const analysisPrompt = buildPreviousChaptersAnalysisPrompt(previousChapters, currentChapterNumber)
+  const analysisPrompt = buildPreviousChaptersAnalysisPrompt(budgetedChapters, currentChapterNumber)
 
   // 调用LLM分析
   const { streamChat } = await import("@/lib/llm-client")