perf: 上下文分级裁剪与Agent完成性保障
This commit is contained in:
@@ -21,7 +21,7 @@ func NewCheckConsistencyTool(store *state.Store) *CheckConsistencyTool {
|
||||
|
||||
func (t *CheckConsistencyTool) Name() string { return "check_consistency" }
|
||||
func (t *CheckConsistencyTool) Description() string {
|
||||
return "加载章节内容和全部状态数据(时间线、伏笔、关系、世界规则、角色状态),供你自行对照检查一致性"
|
||||
return "加载章节内容和对照数据(世界规则、伏笔、关系、别名、最近摘要),供你检查一致性"
|
||||
}
|
||||
func (t *CheckConsistencyTool) Label() string { return "一致性检查" }
|
||||
|
||||
@@ -55,18 +55,17 @@ func (t *CheckConsistencyTool) Execute(_ context.Context, args json.RawMessage)
|
||||
result["content"] = content
|
||||
result["word_count"] = wordCount
|
||||
|
||||
// 状态数据(全部加载,Agent 自行决定怎么用)
|
||||
if timeline, _ := t.store.LoadTimeline(); len(timeline) > 0 {
|
||||
result["timeline"] = timeline
|
||||
// 对照数据:保留全局性的一致性检查数据,避免重复加载 novel_context 已有的窗口数据
|
||||
if rules, _ := t.store.LoadWorldRules(); len(rules) > 0 {
|
||||
result["world_rules"] = rules
|
||||
}
|
||||
if foreshadow, _ := t.store.LoadForeshadowLedger(); len(foreshadow) > 0 {
|
||||
if foreshadow, _ := t.store.LoadActiveForeshadow(); len(foreshadow) > 0 {
|
||||
result["foreshadow_ledger"] = foreshadow
|
||||
}
|
||||
if relationships, _ := t.store.LoadRelationships(); len(relationships) > 0 {
|
||||
result["relationships"] = relationships
|
||||
}
|
||||
if chars, _ := t.store.LoadCharacters(); len(chars) > 0 {
|
||||
result["characters"] = chars
|
||||
aliasMap := make(map[string]string)
|
||||
for _, c := range chars {
|
||||
for _, alias := range c.Aliases {
|
||||
@@ -77,12 +76,6 @@ func (t *CheckConsistencyTool) Execute(_ context.Context, args json.RawMessage)
|
||||
result["alias_map"] = aliasMap
|
||||
}
|
||||
}
|
||||
if changes, _ := t.store.LoadRecentStateChanges(a.Chapter, 5); len(changes) > 0 {
|
||||
result["recent_state_changes"] = changes
|
||||
}
|
||||
if rules, _ := t.store.LoadWorldRules(); len(rules) > 0 {
|
||||
result["world_rules"] = rules
|
||||
}
|
||||
if summaries, _ := t.store.LoadRecentSummaries(a.Chapter, 2); len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ func (t *DraftChapterTool) Execute(_ context.Context, args json.RawMessage) (jso
|
||||
"chapter": a.Chapter,
|
||||
"mode": "append",
|
||||
"word_count": utf8.RuneCountInString(full),
|
||||
"next_step": "自审后调用 commit_chapter 提交",
|
||||
})
|
||||
default: // write
|
||||
if err := t.store.SaveDraft(a.Chapter, a.Content); err != nil {
|
||||
@@ -75,6 +76,7 @@ func (t *DraftChapterTool) Execute(_ context.Context, args json.RawMessage) (jso
|
||||
"chapter": a.Chapter,
|
||||
"mode": "write",
|
||||
"word_count": utf8.RuneCountInString(a.Content),
|
||||
"next_step": "自审后调用 commit_chapter 提交",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,15 +126,9 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
warn("current_chapter_outline", err)
|
||||
}
|
||||
|
||||
// 摘要加载:分层 vs 扁平
|
||||
// 摘要加载:分层 vs 扁平窗口
|
||||
if profile.Layered {
|
||||
t.loadLayeredSummaries(result, a.Chapter, profile.SummaryWindow, warn)
|
||||
} else if profile.FullContext {
|
||||
if summaries, err := t.store.LoadAllSummaries(a.Chapter); err == nil && len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
} else {
|
||||
warn("recent_summaries", err)
|
||||
}
|
||||
} else {
|
||||
if summaries, err := t.store.LoadRecentSummaries(a.Chapter, profile.SummaryWindow); err == nil && len(summaries) > 0 {
|
||||
result["recent_summaries"] = summaries
|
||||
@@ -143,33 +137,17 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
}
|
||||
}
|
||||
|
||||
// 时间线:Layered 用窗口,其他按策略
|
||||
if profile.FullContext {
|
||||
if timeline, err := t.store.LoadTimeline(); err == nil && len(timeline) > 0 {
|
||||
result["timeline"] = timeline
|
||||
} else {
|
||||
warn("timeline", err)
|
||||
}
|
||||
// 时间线:窗口加载
|
||||
if timeline, err := t.store.LoadRecentTimeline(a.Chapter, profile.TimelineWindow); err == nil && len(timeline) > 0 {
|
||||
result["timeline"] = timeline
|
||||
} else {
|
||||
if timeline, err := t.store.LoadRecentTimeline(a.Chapter, profile.TimelineWindow); err == nil && len(timeline) > 0 {
|
||||
result["timeline"] = timeline
|
||||
} else {
|
||||
warn("timeline", err)
|
||||
}
|
||||
warn("timeline", err)
|
||||
}
|
||||
// foreshadow:短篇全量,否则只取未回收条目
|
||||
if profile.FullContext {
|
||||
if foreshadow, err := t.store.LoadForeshadowLedger(); err == nil && len(foreshadow) > 0 {
|
||||
result["foreshadow_ledger"] = foreshadow
|
||||
} else {
|
||||
warn("foreshadow_ledger", err)
|
||||
}
|
||||
// foreshadow:只取未回收条目
|
||||
if foreshadow, err := t.store.LoadActiveForeshadow(); err == nil && len(foreshadow) > 0 {
|
||||
result["foreshadow_ledger"] = foreshadow
|
||||
} else {
|
||||
if foreshadow, err := t.store.LoadActiveForeshadow(); err == nil && len(foreshadow) > 0 {
|
||||
result["foreshadow_ledger"] = foreshadow
|
||||
} else {
|
||||
warn("foreshadow_ledger", err)
|
||||
}
|
||||
warn("foreshadow_ledger", err)
|
||||
}
|
||||
// relationships:保持全量(pair-key 去重,数据量天然可控)
|
||||
if relationships, err := t.store.LoadRelationships(); err == nil && len(relationships) > 0 {
|
||||
@@ -177,8 +155,8 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
} else {
|
||||
warn("relationship_state", err)
|
||||
}
|
||||
// 状态变化:最近 5 章的角色/实体状态变化
|
||||
if changes, err := t.store.LoadRecentStateChanges(a.Chapter, 5); err == nil && len(changes) > 0 {
|
||||
// 状态变化:最近 2 章
|
||||
if changes, err := t.store.LoadRecentStateChanges(a.Chapter, 2); err == nil && len(changes) > 0 {
|
||||
result["recent_state_changes"] = changes
|
||||
} else {
|
||||
warn("recent_state_changes", err)
|
||||
@@ -231,6 +209,17 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
warn("chapter_plan", err)
|
||||
}
|
||||
|
||||
// 前章尾部:嵌入前一章末尾 ~800 字,Writer 无需额外调用 read_chapter 获取衔接上文
|
||||
if a.Chapter > 1 {
|
||||
if prevText, err := t.store.LoadChapterText(a.Chapter - 1); err == nil && prevText != "" {
|
||||
runes := []rune(prevText)
|
||||
if len(runes) > 800 {
|
||||
runes = runes[len(runes)-800:]
|
||||
}
|
||||
result["previous_tail"] = string(runes)
|
||||
}
|
||||
}
|
||||
|
||||
// 风格锚点:从前文提取代表性段落
|
||||
if anchors := t.store.ExtractStyleAnchors(3); len(anchors) > 0 {
|
||||
result["style_anchors"] = anchors
|
||||
@@ -288,11 +277,20 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
warn("volume_summaries", err)
|
||||
}
|
||||
result["references"] = t.architectReferences()
|
||||
|
||||
// 基础设定完备性检查
|
||||
result["foundation_status"] = t.foundationStatus()
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
result["_warnings"] = warnings
|
||||
}
|
||||
|
||||
// 优先级预算:总大小超过阈值时自动裁剪低优先级数据
|
||||
if a.Chapter > 0 {
|
||||
trimByBudget(result, 100*1024) // 100KB 预算
|
||||
}
|
||||
|
||||
result["_loading_summary"] = buildLoadingSummary(result, a.Chapter)
|
||||
return json.Marshal(result)
|
||||
}
|
||||
@@ -363,6 +361,9 @@ func buildLoadingSummary(result map[string]any, chapter int) string {
|
||||
if n := countSlice("recent_state_changes"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("状态变化:%d", n))
|
||||
}
|
||||
if _, ok := result["previous_tail"]; ok {
|
||||
items = append(items, "前章尾部:ok")
|
||||
}
|
||||
|
||||
// 参考资料
|
||||
if refs, ok := result["references"].(map[string]string); ok && len(refs) > 0 {
|
||||
@@ -371,6 +372,9 @@ func buildLoadingSummary(result map[string]any, chapter int) string {
|
||||
if warnings, ok := result["_warnings"].([]string); ok && len(warnings) > 0 {
|
||||
items = append(items, fmt.Sprintf("告警:%d", len(warnings)))
|
||||
}
|
||||
if trimmed, ok := result["_trimmed"].([]string); ok && len(trimmed) > 0 {
|
||||
items = append(items, fmt.Sprintf("裁剪:%s", strings.Join(trimmed, ",")))
|
||||
}
|
||||
|
||||
if len(items) > 0 {
|
||||
parts = append(parts, strings.Join(items, " "))
|
||||
@@ -520,15 +524,17 @@ func (t *ContextTool) writerReferences(chapter int) map[string]string {
|
||||
refs[k] = v
|
||||
}
|
||||
}
|
||||
// 始终加载的核心参考
|
||||
add("chapter_guide", t.refs.ChapterGuide)
|
||||
// 渐进式加载:始终保留核心参考,前 3 章额外加载完整写作指南
|
||||
add("consistency", t.refs.Consistency)
|
||||
add("hook_techniques", t.refs.HookTechniques)
|
||||
add("quality_checklist", t.refs.QualityChecklist)
|
||||
add("consistency", t.refs.Consistency)
|
||||
add("dialogue_writing", t.refs.DialogueWriting)
|
||||
add("style_reference", t.refs.StyleReference)
|
||||
if chapter <= 3 {
|
||||
add("chapter_guide", t.refs.ChapterGuide)
|
||||
add("dialogue_writing", t.refs.DialogueWriting)
|
||||
add("style_reference", t.refs.StyleReference)
|
||||
}
|
||||
|
||||
// 仅首章加载的补充参考(后续章节不再需要)
|
||||
// 仅首章加载的补充参考
|
||||
if chapter <= 1 {
|
||||
add("chapter_template", t.refs.ChapterTemplate)
|
||||
add("content_expansion", t.refs.ContentExpansion)
|
||||
@@ -551,6 +557,29 @@ func (t *ContextTool) architectReferences() map[string]string {
|
||||
return refs
|
||||
}
|
||||
|
||||
// foundationStatus 检查基础设定的完备性,返回缺失项列表。
|
||||
func (t *ContextTool) foundationStatus() map[string]any {
|
||||
status := map[string]any{"ready": true}
|
||||
var missing []string
|
||||
if p, _ := t.store.LoadPremise(); p == "" {
|
||||
missing = append(missing, "premise")
|
||||
}
|
||||
if o, _ := t.store.LoadOutline(); len(o) == 0 {
|
||||
missing = append(missing, "outline")
|
||||
}
|
||||
if c, _ := t.store.LoadCharacters(); len(c) == 0 {
|
||||
missing = append(missing, "characters")
|
||||
}
|
||||
if r, _ := t.store.LoadWorldRules(); len(r) == 0 {
|
||||
missing = append(missing, "world_rules")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
status["ready"] = false
|
||||
status["missing"] = missing
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
// ContextSummary 返回当前状态的简要摘要(供日志使用)。
|
||||
func (t *ContextTool) ContextSummary() string {
|
||||
var parts []string
|
||||
@@ -568,3 +597,43 @@ func (t *ContextTool) ContextSummary() string {
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// trimByBudget 按优先级裁剪 result,使 JSON 总大小不超过 budget 字节。
|
||||
// 优先级(从低到高):references < voice_samples < style_anchors < previous_tail < timeline
|
||||
// < recent_state_changes < foreshadow_ledger < relationship_state < 其余(不裁剪)
|
||||
// 裁剪的 key 会记录到 result["_trimmed"] 供日志排查。
|
||||
func trimByBudget(result map[string]any, budget int) {
|
||||
// 先测量当前大小
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil || len(data) <= budget {
|
||||
return
|
||||
}
|
||||
|
||||
// 按优先级从低到高列出可裁剪的 key
|
||||
trimOrder := []string{
|
||||
"references",
|
||||
"voice_samples",
|
||||
"style_anchors",
|
||||
"previous_tail",
|
||||
"timeline",
|
||||
"recent_state_changes",
|
||||
"foreshadow_ledger",
|
||||
"relationship_state",
|
||||
}
|
||||
|
||||
var trimmed []string
|
||||
for _, key := range trimOrder {
|
||||
if _, ok := result[key]; !ok {
|
||||
continue
|
||||
}
|
||||
delete(result, key)
|
||||
trimmed = append(trimmed, key)
|
||||
data, err = json.Marshal(result)
|
||||
if err != nil || len(data) <= budget {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(trimmed) > 0 {
|
||||
result["_trimmed"] = trimmed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,13 +59,14 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
}
|
||||
}
|
||||
|
||||
result := map[string]any{"saved": true, "type": a.Type, "scale": a.Scale}
|
||||
|
||||
switch a.Type {
|
||||
case "premise":
|
||||
if err := t.store.SavePremise(content); err != nil {
|
||||
return nil, fmt.Errorf("save premise: %w", err)
|
||||
}
|
||||
_ = t.store.UpdatePhase(domain.PhasePremise)
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "premise", "scale": a.Scale})
|
||||
|
||||
case "outline":
|
||||
var entries []domain.OutlineEntry
|
||||
@@ -76,14 +77,13 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
return nil, fmt.Errorf("save outline: %w", err)
|
||||
}
|
||||
_ = t.store.UpdatePhase(domain.PhaseOutline)
|
||||
// 根据大纲长度自动设定总章节数
|
||||
_ = t.store.SetTotalChapters(len(entries))
|
||||
if domain.PlanningTier(a.Scale) != domain.PlanningTierLong {
|
||||
_ = t.store.SetLayered(false)
|
||||
_ = t.store.UpdateVolumeArc(0, 0)
|
||||
_ = t.store.ClearLayeredOutline()
|
||||
}
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "outline", "chapters": len(entries), "scale": a.Scale})
|
||||
result["chapters"] = len(entries)
|
||||
|
||||
case "layered_outline":
|
||||
var volumes []domain.VolumeOutline
|
||||
@@ -93,7 +93,6 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
if err := t.store.SaveLayeredOutline(volumes); err != nil {
|
||||
return nil, fmt.Errorf("save layered_outline: %w", err)
|
||||
}
|
||||
// 展开为扁平大纲,兼容现有 GetChapterOutline
|
||||
flat := domain.FlattenOutline(volumes)
|
||||
if err := t.store.SaveOutline(flat); err != nil {
|
||||
return nil, fmt.Errorf("save flattened outline: %w", err)
|
||||
@@ -105,11 +104,8 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
if len(volumes) > 0 && len(volumes[0].Arcs) > 0 {
|
||||
_ = t.store.UpdateVolumeArc(volumes[0].Index, volumes[0].Arcs[0].Index)
|
||||
}
|
||||
return json.Marshal(map[string]any{
|
||||
"saved": true, "type": "layered_outline",
|
||||
"volumes": len(volumes), "chapters": total,
|
||||
"scale": a.Scale,
|
||||
})
|
||||
result["volumes"] = len(volumes)
|
||||
result["chapters"] = total
|
||||
|
||||
case "characters":
|
||||
var chars []domain.Character
|
||||
@@ -119,7 +115,7 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
if err := t.store.SaveCharacters(chars); err != nil {
|
||||
return nil, fmt.Errorf("save characters: %w", err)
|
||||
}
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "characters", "count": len(chars), "scale": a.Scale})
|
||||
result["count"] = len(chars)
|
||||
|
||||
case "world_rules":
|
||||
var rules []domain.WorldRule
|
||||
@@ -129,11 +125,15 @@ func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (j
|
||||
if err := t.store.SaveWorldRules(rules); err != nil {
|
||||
return nil, fmt.Errorf("save world_rules: %w", err)
|
||||
}
|
||||
return json.Marshal(map[string]any{"saved": true, "type": "world_rules", "count": len(rules), "scale": a.Scale})
|
||||
result["count"] = len(rules)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown type %q, expected premise/outline/layered_outline/characters/world_rules", a.Type)
|
||||
}
|
||||
|
||||
// 返回剩余未完成项,引导 Architect 继续
|
||||
result["remaining"] = t.remaining()
|
||||
return json.Marshal(result)
|
||||
}
|
||||
|
||||
func normalizeFoundationContent(raw json.RawMessage) (string, error) {
|
||||
@@ -151,3 +151,21 @@ func normalizeFoundationContent(raw json.RawMessage) (string, error) {
|
||||
}
|
||||
return string(raw), nil
|
||||
}
|
||||
|
||||
// remaining 检查基础设定中还缺少哪些必要项。
|
||||
func (t *SaveFoundationTool) remaining() []string {
|
||||
var missing []string
|
||||
if p, _ := t.store.LoadPremise(); p == "" {
|
||||
missing = append(missing, "premise")
|
||||
}
|
||||
if o, _ := t.store.LoadOutline(); len(o) == 0 {
|
||||
missing = append(missing, "outline")
|
||||
}
|
||||
if c, _ := t.store.LoadCharacters(); len(c) == 0 {
|
||||
missing = append(missing, "characters")
|
||||
}
|
||||
if r, _ := t.store.LoadWorldRules(); len(r) == 0 {
|
||||
missing = append(missing, "world_rules")
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user