Skip to content
Embabel 文档版本 0.3.0-SNAPSHOT 源快照 c4b0de0f 更新于 2026-09-29

21 · 使用状态(Using States)

GOAP 规划有很多好处,但让"循环"难以表达。 因此,Embabel 支持在 GOAP 计划中引入**状态(state)**的概念。

状态如何与 GOAP 协同

在每个状态内部,GOAP 规划照常工作:动作依据所需类型形成前置条件、依据产出类型形成效果, 规划器找出通往目标的最优动作序列。

当一个动作返回带 @State 注解的类时,框架会:

  • 隐藏之前的状态对象 —— 黑板上任何已有的状态对象都会被隐藏;
  • 绑定新的状态对象 —— 返回的状态被加入黑板;
  • 从新状态重新规划 —— 规划器只考虑来自新状态的动作;
  • 继续执行 —— 直到达成目标、或找不到任何计划。

上下文会跨状态迁移被保留 —— 非状态对象(例如用户消息、客户数据、对话历史)仍然可用。 只有状态对象会被隐藏,从而确保规划器只考虑"当前状态"的动作。

状态迁移会隐藏之前的状态对象,但并不清空黑板。非状态对象在新状态中依然可用。 若要清空整个黑板(例如为了循环),请在动作上使用 clearBlackboard = true。

何时使用状态

状态特别适合:

  • 线性阶段:每个阶段自然地流向下一阶段;
  • 分支工作流:一个决策点导向不同的处理路径;
  • 循环模式:处理可能需要重复(例如"修改—评审"循环);
  • 人在回路工作流:用户反馈决定下一个状态;
  • 复杂工作流:把它当作离散阶段更容易推理。

状态允许"回跳到整个状态",而一个状态可以包含一个或多个动作。 这比传统 GOAP 更灵活 —— 在传统 GOAP 中,实现循环必须小心管理前置条件。

停留在当前状态

动作可以返回 this 来停留在当前状态。这对那些"响应输入但不改变状态"的动作很有用,例如聊天处理器:

kotlin
@State
record ChitchatState(String context) {
    @Action(canRerun = true) // ①
    ChitchatState respond(UserMessage message, Ai ai) {
        var response = ai.generateText("Respond to: " + message.content());
        // ... 发送回复
        return this; // ②
    }
}

① canRerun = true 是必需的 —— 默认情况下,动作在一个进程中只运行一次 ② 返回 this 让同一个状态实例保持活跃

当一个动作返回 this:

  • 状态保持活跃,不发生迁移;
  • 黑板被保留(不被清空);
  • 该动作可以在后续规划周期再次运行(前提是 canRerun = true)。

若没有 canRerun = true,即使它返回了 this,该动作的 hasRun 标记也会阻止它再次执行。

循环状态(Looping States)

对于"动作可能返回到之前访问过的状态类型"的循环模式,请在参与循环的动作上使用 clearBlackboard = true:

kotlin
@State
record ProcessingState(String data, int iteration) implements LoopOutcome {
    @Action(clearBlackboard = true) // ①
    LoopOutcome process() {
        if (iteration >= 3) {
            return new DoneState(data); // ②
        }
        return new ProcessingState(data + "+", iteration + 1); // ③
    }
}

① clearBlackboard = true 让该动作可以回跳到同一个状态类型 ② 终止条件,退出循环 ③ 为下一轮迭代返回同类状态的新实例

若没有 clearBlackboard = true,规划器会看到该输出类型已在黑板上而跳过该动作。 清空黑板重置了上下文,从而让自然的循环成为可能。

只应在参与循环的动作上使用 clearBlackboard = true。 对线性状态迁移,默认行为(保留黑板)通常是更好的选择。

@State 注解

作为动作返回值、且应当触发状态迁移的类,必须带 @State 注解:

kotlin
@State
record ProcessingState(String data) {
    @Action
    NextState process() {
        return new NextState(data.toUpperCase());
    }
}

继承(Inheritance)

@State 注解会沿类层次结构继承。 如果某个父类或接口带 @State,则其所有子类与实现类都自动被视为状态类型。 这意味着你不必给层次结构里的每个类都加注解 —— 只注解基类型即可。

kotlin
@State
interface Stage {} // ①

record AssessStory(String content) implements Stage { ... } // ②
record ReviseStory(String content) implements Stage { ... }
record Done(String content) implements Stage { ... }

① 只有父接口需要 @State ② 实现它的 record / data class 自动被当作状态类型

这适用于:

  • 接口:实现 @State 接口的类是状态类型;
  • 抽象类:继承 @State 抽象类的类是状态类型;
  • 具体类:继承 @State 类的类是状态类型;
  • 深层继承:该注解会沿多个层级继承。

行为(Behavior)

当一个动作返回带 @State 注解的类(或继承了 @State 的类)时:

  • 之前的状态对象会从黑板上被隐藏(不是删除,而是不再可见);
  • 返回的对象被绑定到黑板(以 it 为名);
  • 规划只考虑定义在当前状态类内部的动作;
  • 该状态内的任何 @AchievesGoal 方法都成为潜在目标。

上下文(非状态对象)跨状态迁移被保留。 这意味着用户消息、客户数据、对话历史等在新状态中依然可用。 只有状态对象被隐藏 —— 这提供了状态作用域(state scoping),确保只考虑当前状态的动作。

对于"返回到之前访问过的状态类型"的循环状态,请在参与循环的动作上使用 @Action(clearBlackboard = true) —— 它会清空黑板(包括 hasRun 条件)并使循环得以继续。详见循环状态。

父状态接口模式(Parent State Interface Pattern)

要在多个状态之间动态选择,请定义一个由各子状态实现的父接口(或 sealed interface/class)。 得益于继承,你只需注解父接口 —— 所有实现类都自动成为状态类型:

kotlin
@State
interface Stage {} // ①

record AssessStory(String content) implements Stage { // ②
    @Action
    Stage assess() {
        if (isAcceptable()) {
            return new Done(content);
        } else {
            return new ReviseStory(content);
        }
    }
}

record ReviseStory(String content) implements Stage {
    @Action
    AssessStory revise() {
        return new AssessStory(improvedContent());
    }
}

record Done(String content) implements Stage {
    @AchievesGoal(description = "Processing complete")
    @Action
    Output complete() {
        return new Output(content);
    }
}

① 在父接口上加 @State ② 实现它的 record/data class 不需要再加 @State —— 它们从 Stage 继承

这个模式带来了:

  • 多态返回类型:动作可以返回父接口的任意实现;
  • 动态路由:由运行时值决定进入哪个状态;
  • 循环:状态可以返回其它状态,最终又循环回来。

框架会自动发现父接口的所有实现,并把它们的动作注册为可能的下一步。

示例:WriteAndReviewAgent

下面的示例展示了一个完整的"写作 + 评审"工作流,包含:

  • 带循环的基于状态的流程控制;
  • 用 WaitFor 实现的人在回路反馈;
  • LLM 驱动的内容生成与评估;
  • 并通过状态传递可配置属性。
kotlin
abstract class Personas { // ①
    static final RoleGoalBackstory WRITER = RoleGoalBackstory
            .withRole("Creative Storyteller")
            .andGoal("Write engaging and imaginative stories")
            .andBackstory("Has a PhD in French literature; used to work in a circus");

    static final Persona REVIEWER = new Persona(
            "Media Book Review",
            "New York Times Book Reviewer",
            "Professional and insightful",
            "Help guide readers toward good stories"
    );
}

@Agent(description = "Generate a story based on user input and review it")
public class WriteAndReviewAgent {

    public record Story(String text) {}

    public record ReviewedStory(
            Story story,
            String review,
            Persona reviewer
    ) implements HasContent, Timestamped {
        // ... 内容格式化方法
    }

    @State
    interface Stage {} // ②

    record Properties( // ③
            int storyWordCount,
            int reviewWordCount
    ) {}

    private final Properties properties;

    WriteAndReviewAgent(
            @Value("${storyWordCount:100}") int storyWordCount,
            @Value("${reviewWordCount:100}") int reviewWordCount
    ) {
        this.properties = new Properties(storyWordCount, reviewWordCount);
    }

    @Action
    AssessStory craftStory(UserInput userInput, Ai ai) { // ④
        var draft = ai
                .withLlm(LlmOptions.withAutoLlm().withTemperature(.7))
                .withPromptContributor(Personas.WRITER)
                .createObject(String.format("""
                        Craft a short story in %d words or less.
                        The story should be engaging and imaginative.
                        Use the user's input as inspiration if possible.

                        # User input
                        %s
                        """,
                        properties.storyWordCount,
                        userInput.getContent()
                ).trim(), Story.class);
        return new AssessStory(userInput, draft, properties); // ⑤
    }

    record HumanFeedback(String comments) {} // ⑥

    private record AssessmentOfHumanFeedback(boolean acceptable) {}

    @State
    record AssessStory(UserInput userInput, Story story, Properties properties) implements Stage {

        @Action
        HumanFeedback getFeedback() { // ⑦
            return WaitFor.formSubmission("""
                    Please provide feedback on the story
                    %s
                    """.formatted(story.text),
                    HumanFeedback.class);
        }

        @Action(clearBlackboard = true) // ⑧
        Stage assess(HumanFeedback feedback, Ai ai) {
            var assessment = ai.withDefaultLlm().createObject("""
                    Based on the following human feedback, determine if the story is acceptable.
                    Return true if the story is acceptable, false otherwise.

                    # Story
                    %s

                    # Human feedback
                    %s
                    """.formatted(story.text(), feedback.comments),
                    AssessmentOfHumanFeedback.class);
            if (assessment.acceptable) {
                return new Done(userInput, story, properties); // ⑨
            } else {
                return new ReviseStory(userInput, story, feedback, properties); // ⑩
            }
        }
    }

    @State
    record ReviseStory(UserInput userInput, Story story, HumanFeedback humanFeedback,
                       Properties properties) implements Stage {

        @Action(clearBlackboard = true) // ⑪
        AssessStory reviseStory(Ai ai) {
            var draft = ai
                    .withLlm(LlmOptions.withAutoLlm().withTemperature(.7))
                    .withPromptContributor(Personas.WRITER)
                    .createObject(String.format("""
                                    Revise a short story in %d words or less.
                                    Use the user's input as inspiration if possible.

                                    # User input
                                    %s

                                    # Previous story
                                    %s

                                    # Revision instructions
                                    %s
                                    """,
                            properties.storyWordCount,
                            userInput.getContent(),
                            story.text(),
                            humanFeedback.comments
                    ).trim(), Story.class);
            return new AssessStory(userInput, draft, properties); // ⑫
        }
    }

    @State
    record Done(UserInput userInput, Story story, Properties properties) implements Stage {

        @AchievesGoal( // ⑬
                description = "The story has been crafted and reviewed by a book reviewer",
                export = @Export(remote = true, name = "writeAndReviewStory"))
        @Action
        ReviewedStory reviewStory(Ai ai) {
            var review = ai
                    .withAutoLlm()
                    .withPromptContributor(Personas.REVIEWER)
                    .generateText(String.format("""
                                    You will be given a short story to review.
                                    Review it in %d words or less.
                                    Consider whether the story is engaging, imaginative, and well-written.

                                    # Story
                                    %s

                                    # User input that inspired the story
                                    %s
                                    """,
                            properties.reviewWordCount,
                            story.text(),
                            userInput.getContent()
                    ).trim());
            return new ReviewedStory(story, review, Personas.REVIEWER);
        }
    }
}

注记:

  • ① Personas:可复用的 prompt contributor,给 LLM 提供关于自身角色的上下文;
  • ② 父状态接口:让动作可以动态返回任意实现状态;
  • ③ Properties record:把配置打包在一起,便于跨状态传递;
  • ④ 入口动作:用 LLM 生成故事初稿;
  • ⑤ 状态迁移:返回携带全部必要数据的 AssessStory;
  • ⑥ HITL 数据类型:用于承载人类反馈的简单 record/data class;
  • ⑦ WaitFor 集成:暂停执行,等待用户提交反馈表单;
  • ⑧ 循环动作:clearBlackboard = true 使"返回到之前访问过的状态类型"成为可能;
  • ⑨ 终止分支:若可接受,迁移到 Done 状态;
  • ⑩ 循环分支:若不可接受,带着反馈迁移到 ReviseStory;
  • ⑪ 循环动作:clearBlackboard = true 使"回跳到 AssessStory"成为可能;
  • ⑫ 回跳:返回新的 AssessStory,进入下一轮反馈;
  • ⑬ 目标达成:产出评审后故事并导出它的最终动作。

执行流程(Execution Flow)

这个 Agent 的执行流程是:

  1. craftStory 用 LLM 执行,返回 AssessStory → 进入 AssessStory 状态;
  2. getFeedback 调用 WaitFor.formSubmission() → Agent 暂停,等待用户输入;
  3. 用户提交反馈 → HumanFeedback 被加入黑板;
  4. assess 用 LLM 解释反馈:
    • 若可接受:返回 Done → 黑板被清空,进入 Done 状态;
    • 若不可接受:返回 ReviseStory → 黑板被清空,进入 ReviseStory 状态;
  5. 若处于 ReviseStory:reviseStory 用 LLM 执行,返回 AssessStory → 黑板被清空,回到第 2 步;
  6. 当处于 Done 时:reviewStory 用 LLM 执行,返回 ReviewedStory → 目标达成。

规划器会自动处理所有迁移,包括循环。 参与循环的动作(assess 与 reviseStory)使用 clearBlackboard = true 来启用"返回到之前访问过的状态类型"。

用 WaitFor 实现人在回路(Human-in-the-Loop)

WaitFor.formSubmission() 方法是人在回路工作流的关键:

kotlin
@Action
HumanFeedback getFeedback() {
    return WaitFor.formSubmission("""
            Please provide feedback on the story
            %s
            """.formatted(story.text),
            HumanFeedback.class);
}

这个动作执行时:

  • Agent 进程进入 WAITING 状态;
  • 依据 HumanFeedback record 的结构生成一个表单;
  • 用户看到提示并填写表单;
  • 提交后,HumanFeedback 实例被创建并加入黑板;
  • Agent 带着可用的反馈恢复执行。

它与状态模式自然地结合:反馈会留在当前状态内,直到下一次状态迁移。

跨状态传递数据(Passing Data Through States)

当为循环状态使用 clearBlackboard = true 时,由于黑板会被清空,所有必要的上下文都必须通过状态 record 传递:

kotlin
@State
record AssessStory(
        UserInput userInput,      // 原始用户请求
        Story story,              // 当前故事草稿
        Properties properties     // 配置
) implements Stage { ... }

@State
record ReviseStory(
        UserInput userInput,
        Story story,
        HumanFeedback humanFeedback, // 修改所需的额外上下文
        Properties properties
) implements Stage { ... }

建议用一个 Properties record/data class 把需要在多个状态间传递的配置值打包起来, 而不是重复重复单个字段。

对于非循环的状态迁移(不使用 clearBlackboard 时),黑板会被保留,数据可以直接从黑板访问。 当状态需要访问共享上下文(例如用户身份、对话历史)时,这很有用。

状态类的约束(State Class Requirements)

状态类必须是静态嵌套类(Java)或顶层类(Kotlin)。

不允许使用非静态内部类,因为它们持有对外层实例的引用,会导致序列化与持久化问题。 如果框架检测到带 @State 的非静态内部类,会抛出 IllegalStateException。

kotlin
// 好的:静态嵌套类(Java 的 record 隐式静态)
@State
record AssessStory(UserInput userInput, Story story) implements Stage { ... }

// 好的:顶层类
@State
record ProcessingState(String data) { ... }

// 不好的:非静态内部类 —— 会抛 IllegalStateException
@State
class AssessStory implements Stage { ... } // 非静态上下文中的内部类

在 Java 中,类内部声明的 record 是隐式静态的,因此很适合作为状态类。 在 Kotlin 中,类内部声明的 data class 默认是 inner,请改用顶层声明。

顶层状态类是 Kotlin 下推荐的模式。 它们可以通过 @Provided 注解访问外层组件 —— 完整文档见《@Provided 注解》。

要点(Key Points)

  • 用 @State 注解状态类(或从带 @State 的类型继承);
  • @State 沿类层次继承 —— 只注解基类型即可;
  • 使用静态嵌套类(Java 的 record)或顶层类以避免持久化问题;
  • 用父接口实现多态的状态返回;
  • 状态内的动作会被自动发现并注册;
  • 状态作用域:进入新状态时,之前的状态被隐藏 —— 只有当前状态的动作可用;
  • 上下文会被保留:非状态对象(用户数据、对话等)跨迁移依然可用;
  • 黑板会被保留:状态迁移隐藏之前的状态,但保留黑板上其它所有内容;
  • 停留在状态内:返回 this 并配合 canRerun = true,即可不迁移地停留在当前状态;
  • 对循环状态,用 @Action(clearBlackboard = true) 以启用"返回到之前访问过的状态类型";
  • 使用 clearBlackboard = true 时,把所有必要数据通过状态 record 字段传递;
  • 目标由终结状态动作上的 @AchievesGoal 定义;
  • 状态内的人在回路交互使用 WaitFor;
  • 在状态内部,正常的 GOAP 规划仍负责编排动作顺序。

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。