perf: 拆分规划策略
This commit is contained in:
@@ -44,15 +44,33 @@ func BuildCoordinator(
|
||||
tools.NewSaveVolumeSummaryTool(store),
|
||||
}
|
||||
|
||||
architect := agentcore.SubAgentConfig{
|
||||
Name: "architect",
|
||||
Description: "世界构建师:生成小说前提、大纲和角色档案",
|
||||
architectShort := agentcore.SubAgentConfig{
|
||||
Name: "architect_short",
|
||||
Description: "短篇规划师:为单卷、单冲突、高密度故事生成紧凑设定与扁平大纲",
|
||||
Model: model,
|
||||
SystemPrompt: prompts.Architect,
|
||||
SystemPrompt: prompts.ArchitectShort,
|
||||
Tools: architectTools,
|
||||
MaxTurns: 10,
|
||||
}
|
||||
|
||||
architectMid := agentcore.SubAgentConfig{
|
||||
Name: "architect_mid",
|
||||
Description: "中篇规划师:为多阶段但篇幅受控的故事生成可推进的设定与阶段化大纲",
|
||||
Model: model,
|
||||
SystemPrompt: prompts.ArchitectMid,
|
||||
Tools: architectTools,
|
||||
MaxTurns: 12,
|
||||
}
|
||||
|
||||
architectLong := agentcore.SubAgentConfig{
|
||||
Name: "architect_long",
|
||||
Description: "长篇规划师:为连载型、可持续升级的故事生成分层设定与卷弧大纲",
|
||||
Model: model,
|
||||
SystemPrompt: prompts.ArchitectLong,
|
||||
Tools: architectTools,
|
||||
MaxTurns: 14,
|
||||
}
|
||||
|
||||
// 动态拼接风格指令到 Writer prompt
|
||||
writerPrompt := prompts.Writer
|
||||
if style, ok := styles[cfg.Style]; ok {
|
||||
@@ -77,7 +95,7 @@ func BuildCoordinator(
|
||||
MaxTurns: 10,
|
||||
}
|
||||
|
||||
subagentTool := agentcore.NewSubAgentTool(architect, writer, editor)
|
||||
subagentTool := agentcore.NewSubAgentTool(architectShort, architectMid, architectLong, writer, editor)
|
||||
|
||||
agent := agentcore.NewAgent(
|
||||
agentcore.WithModel(model),
|
||||
|
||||
@@ -7,22 +7,24 @@ import (
|
||||
|
||||
// Config 小说应用配置。
|
||||
type Config struct {
|
||||
Prompt string // 用户的小说需求
|
||||
NovelName string // 小说名(用作输出目录名)
|
||||
OutputDir string // 输出根目录,默认 output/{NovelName}
|
||||
Provider string // LLM 提供商:openai / anthropic / gemini
|
||||
ModelName string // LLM 模型名
|
||||
APIKey string // API Key
|
||||
BaseURL string // API Base URL(可选)
|
||||
Style string // 写作风格(default/suspense/fantasy/romance)
|
||||
Prompt string // 用户的小说需求
|
||||
NovelName string // 小说名(用作输出目录名)
|
||||
OutputDir string // 输出根目录,默认 output/{NovelName}
|
||||
Provider string // LLM 提供商:openai / anthropic / gemini
|
||||
ModelName string // LLM 模型名
|
||||
APIKey string // API Key
|
||||
BaseURL string // API Base URL(可选)
|
||||
Style string // 写作风格(default/suspense/fantasy/romance)
|
||||
}
|
||||
|
||||
// Prompts 嵌入的提示词。
|
||||
type Prompts struct {
|
||||
Coordinator string
|
||||
Architect string
|
||||
Writer string
|
||||
Editor string
|
||||
Coordinator string
|
||||
ArchitectShort string
|
||||
ArchitectMid string
|
||||
ArchitectLong string
|
||||
Writer string
|
||||
Editor string
|
||||
}
|
||||
|
||||
// Validate 校验配置(CLI 模式,要求 Prompt 非空)。
|
||||
|
||||
59
app/run.go
59
app/run.go
@@ -92,7 +92,10 @@ func Run(cfg Config, refs tools.References, prompts Prompts, styles map[string]s
|
||||
return fmt.Errorf("init progress: %w", err)
|
||||
}
|
||||
log.Printf("新建模式:%s", cfg.NovelName)
|
||||
promptText := fmt.Sprintf("请创作一部小说,章节数量由你根据故事需要自行决定。要求如下:\n\n%s", cfg.Prompt)
|
||||
promptText := fmt.Sprintf(
|
||||
"请创作一部小说,章节数量由你根据故事需要自行决定。若题材与冲突天然适合长篇连载,请优先规划为分层长篇结构,而不是压缩成短篇式梗概。要求如下:\n\n%s",
|
||||
cfg.Prompt,
|
||||
)
|
||||
if err := coordinator.Prompt(promptText); err != nil {
|
||||
return fmt.Errorf("prompt: %w", err)
|
||||
}
|
||||
@@ -204,6 +207,22 @@ func registerSubscription(coordinator *agentcore.Agent, store *state.Store, prov
|
||||
})
|
||||
}
|
||||
|
||||
func planningTierGuidance(runMeta *domain.RunMeta) string {
|
||||
if runMeta == nil {
|
||||
return ""
|
||||
}
|
||||
switch runMeta.PlanningTier {
|
||||
case domain.PlanningTierShort:
|
||||
return "当前规划级别:short。如需调整设定或重做大纲,优先调用 architect_short。"
|
||||
case domain.PlanningTierMid:
|
||||
return "当前规划级别:mid。如需调整设定或重做大纲,优先调用 architect_mid。"
|
||||
case domain.PlanningTierLong:
|
||||
return "当前规划级别:long。如需调整设定或重做大纲,优先调用 architect_long,并保持分层大纲的一致性。"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// submitSteer 提交用户干预(CLI 和 Runtime 共用)。
|
||||
func submitSteer(store *state.Store, coordinator *agentcore.Agent, text string) {
|
||||
log.Printf("[steer] 用户干预: %s", text)
|
||||
@@ -219,8 +238,17 @@ func submitSteer(store *state.Store, coordinator *agentcore.Agent, text string)
|
||||
if err := store.SetFlow(domain.FlowSteering); err != nil {
|
||||
log.Printf("[warn] 设置流程状态失败: %v", err)
|
||||
}
|
||||
runMeta, err := store.LoadRunMeta()
|
||||
if err != nil {
|
||||
log.Printf("[warn] 读取运行元信息失败: %v", err)
|
||||
}
|
||||
guidance := planningTierGuidance(runMeta)
|
||||
message := fmt.Sprintf("[用户干预] %s\n请评估影响范围,决定是否需要修改设定或重写已有章节。", text)
|
||||
if guidance != "" {
|
||||
message += "\n" + guidance
|
||||
}
|
||||
coordinator.Steer(agentcore.UserMsg(fmt.Sprintf(
|
||||
"[用户干预] %s\n请评估影响范围,决定是否需要修改设定或重写已有章节。", text)))
|
||||
"%s", message)))
|
||||
}
|
||||
|
||||
// recoveryResult 恢复链的判断结果。
|
||||
@@ -236,14 +264,21 @@ func determineRecovery(progress *domain.Progress, runMeta *domain.RunMeta) recov
|
||||
if progress == nil {
|
||||
return recoveryResult{IsNew: true}
|
||||
}
|
||||
guidance := planningTierGuidance(runMeta)
|
||||
withGuidance := func(prompt string) string {
|
||||
if guidance == "" {
|
||||
return prompt
|
||||
}
|
||||
return prompt + "\n" + guidance
|
||||
}
|
||||
|
||||
if progress.InProgressChapter > 0 {
|
||||
ch := progress.InProgressChapter
|
||||
scenes := len(progress.CompletedScenes)
|
||||
return recoveryResult{
|
||||
PromptText: fmt.Sprintf(
|
||||
PromptText: withGuidance(fmt.Sprintf(
|
||||
"第 %d 章正在进行中,已完成 %d 个场景。请调用 writer 从场景 %d 继续写作。总共需要写 %d 章。",
|
||||
ch, scenes, scenes+1, progress.TotalChapters),
|
||||
ch, scenes, scenes+1, progress.TotalChapters)),
|
||||
Label: fmt.Sprintf("场景级恢复:第 %d 章已完成 %d 个场景", ch, scenes),
|
||||
}
|
||||
}
|
||||
@@ -254,18 +289,18 @@ func determineRecovery(progress *domain.Progress, runMeta *domain.RunMeta) recov
|
||||
verb = "打磨"
|
||||
}
|
||||
return recoveryResult{
|
||||
PromptText: fmt.Sprintf(
|
||||
PromptText: withGuidance(fmt.Sprintf(
|
||||
"有 %d 章待%s(受影响章节:%v)。原因:%s。请逐章调用 writer %s后继续正常写作。总共需要写 %d 章。",
|
||||
len(progress.PendingRewrites), verb, progress.PendingRewrites, progress.RewriteReason, verb, progress.TotalChapters),
|
||||
len(progress.PendingRewrites), verb, progress.PendingRewrites, progress.RewriteReason, verb, progress.TotalChapters)),
|
||||
Label: fmt.Sprintf("%s恢复:%d 章待处理 %v", verb, len(progress.PendingRewrites), progress.PendingRewrites),
|
||||
}
|
||||
}
|
||||
|
||||
if progress.Flow == domain.FlowReviewing {
|
||||
return recoveryResult{
|
||||
PromptText: fmt.Sprintf(
|
||||
PromptText: withGuidance(fmt.Sprintf(
|
||||
"上次审阅中断,请重新调用 editor 对已完成章节进行全局审阅。已完成 %d 章,共 %d 字。总共需要写 %d 章。",
|
||||
len(progress.CompletedChapters), progress.TotalWordCount, progress.TotalChapters),
|
||||
len(progress.CompletedChapters), progress.TotalWordCount, progress.TotalChapters)),
|
||||
Label: "审阅恢复:上次审阅中断",
|
||||
}
|
||||
}
|
||||
@@ -273,9 +308,9 @@ func determineRecovery(progress *domain.Progress, runMeta *domain.RunMeta) recov
|
||||
if progress.IsResumable() && runMeta != nil && runMeta.PendingSteer != "" {
|
||||
next := progress.NextChapter()
|
||||
return recoveryResult{
|
||||
PromptText: fmt.Sprintf(
|
||||
PromptText: withGuidance(fmt.Sprintf(
|
||||
"从第 %d 章继续写作。之前已完成 %d 章,共 %d 字。总共需要写 %d 章。\n\n[用户干预-恢复] %s\n请评估影响范围,决定是否需要修改设定或重写已有章节。",
|
||||
next, len(progress.CompletedChapters), progress.TotalWordCount, progress.TotalChapters, runMeta.PendingSteer),
|
||||
next, len(progress.CompletedChapters), progress.TotalWordCount, progress.TotalChapters, runMeta.PendingSteer)),
|
||||
Label: "Steer 恢复:上次干预未完成,重新注入",
|
||||
}
|
||||
}
|
||||
@@ -283,9 +318,9 @@ func determineRecovery(progress *domain.Progress, runMeta *domain.RunMeta) recov
|
||||
if progress.IsResumable() {
|
||||
next := progress.NextChapter()
|
||||
return recoveryResult{
|
||||
PromptText: fmt.Sprintf(
|
||||
PromptText: withGuidance(fmt.Sprintf(
|
||||
"从第 %d 章继续写作。之前已完成 %d 章,共 %d 字。总共需要写 %d 章。",
|
||||
next, len(progress.CompletedChapters), progress.TotalWordCount, progress.TotalChapters),
|
||||
next, len(progress.CompletedChapters), progress.TotalWordCount, progress.TotalChapters)),
|
||||
Label: fmt.Sprintf("恢复模式:从第 %d 章继续(已完成 %d 章,共 %d 字)",
|
||||
next, len(progress.CompletedChapters), progress.TotalWordCount),
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/voocel/agentcore"
|
||||
@@ -122,3 +123,31 @@ func TestCreateModelUsesOpenRouterProvider(t *testing.T) {
|
||||
t.Fatalf("expected provider openrouter, got %q", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetermineRecoveryIncludesPlanningTierGuidance(t *testing.T) {
|
||||
progress := &domain.Progress{
|
||||
Phase: domain.PhaseWriting,
|
||||
CurrentChapter: 3,
|
||||
CompletedChapters: []int{1, 2},
|
||||
TotalWordCount: 2400,
|
||||
TotalChapters: 12,
|
||||
}
|
||||
runMeta := &domain.RunMeta{
|
||||
PlanningTier: domain.PlanningTierLong,
|
||||
}
|
||||
|
||||
recovery := determineRecovery(progress, runMeta)
|
||||
if !strings.Contains(recovery.PromptText, "architect_long") {
|
||||
t.Fatalf("expected architect_long guidance, got %q", recovery.PromptText)
|
||||
}
|
||||
if !strings.Contains(recovery.PromptText, "分层大纲") {
|
||||
t.Fatalf("expected layered-outline guidance, got %q", recovery.PromptText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanningTierGuidanceForMid(t *testing.T) {
|
||||
guidance := planningTierGuidance(&domain.RunMeta{PlanningTier: domain.PlanningTierMid})
|
||||
if !strings.Contains(guidance, "architect_mid") {
|
||||
t.Fatalf("expected architect_mid guidance, got %q", guidance)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,10 @@ func (rt *Runtime) Start(prompt string) error {
|
||||
return fmt.Errorf("init progress: %w", err)
|
||||
}
|
||||
|
||||
promptText := fmt.Sprintf("请创作一部小说,章节数量由你根据故事需要自行决定。要求如下:\n\n%s", prompt)
|
||||
promptText := fmt.Sprintf(
|
||||
"请创作一部小说,章节数量由你根据故事需要自行决定。若题材与冲突天然适合长篇连载,请优先规划为分层长篇结构,而不是压缩成短篇式梗概。要求如下:\n\n%s",
|
||||
prompt,
|
||||
)
|
||||
if err := rt.coordinator.Prompt(promptText); err != nil {
|
||||
return fmt.Errorf("prompt: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user