GOAPエージェント
GOAPは、総コストを最小限に抑えつつゴール条件を満たす最適なアクションシーケンスを見つけるためにA*サーチを使用する、アルゴリズムによるプランニング手法です。 LLMを使用してプランを生成するLLMベースのプランナーとは異なり、 GOAPエージェントは、事前定義されたゴールとアクションに基づいて、アルゴリズム的にアクションシーケンスを発見します。
GOAPプランナーは、主に3つの概念に基づいて動作します:
- 状態 (State): 世界の現在の状態を表します。
- アクション (Actions): 前提条件、効果(Beliefs)、コスト、および実行ロジックを含み、実行可能な内容を定義します。
- ゴール (Goals): 目標条件、ヒューリスティックコスト、および評価関数を定義します。
前提条件
環境およびプロジェクトが以下の要件を満たしていることを確認してください:
JDK 17+
Kotlin 2.2.0+
Gradle 8.0+ または Maven 3.8+ Koogパッケージを依存関係として追加します:
dependencies {
// 安定版
implementation("ai.koog:koog-agents:1.2.0")
// ベータ版
implementation("ai.koog:koog-agents-additions:1.2.0-beta")
}
dependencies {
// 安定版
implementation 'ai.koog:koog-agents:1.2.0'
// ベータ版
implementation 'ai.koog:koog-agents-additions:1.2.0-beta'
}
<dependency>
<!-- 安定版 -->
<dependency>
<groupId>ai.koog</groupId>
<artifactId>koog-agents-jvm</artifactId>
<version>1.2.0</version>
</dependency>
<!-- ベータ版 -->
<dependency>
<groupId>ai.koog</groupId>
<artifactId>koog-agents-additions-jvm</artifactId>
<version>1.2.0-beta</version>
</dependency>
</dependency>LLMプロバイダーからAPIキーを取得するか、Ollamaを介してローカルLLMを実行してください。
詳細については、クイックスタートを参照してください。 このページ内の例は、OPENAI_API_KEY環境変数が設定されていることを前提としています。
Koogでは、ゴールとアクションを宣言的に指定するDSLを使用して、GOAPエージェントを定義します。
GOAPエージェントを作成するには、以下の手順が必要です:
- ゴールに特有の様々な側面を表すプロパティを持つデータクラスとして、状態(State)を定義します。
- goap() 関数を使用して、GOAPPlanner インスタンスを作成します。
- プランナーを AIAgentPlannerStrategy でラップし、それを PlannerAIAgent コンストラクタに渡します。
Note
プランナーは、個々のアクションとそのシーケンスを選択します。 各アクションには、アクションが実行されるために満たされている必要がある前提条件と、予測される結果を定義するBeliefが含まれます。 Beliefの詳細については、実際の実行と比較した状態のBeliefを参照してください。
次の例では、GOAPが記事作成(アウトライン → 草稿 → レビュー → 公開)の高レベルなプランニングを処理し、LLMが各アクション内での実際のコンテンツ生成を実行します。
// コンテンツ作成の状態を定義
data class ContentState(
val topic: String,
val hasOutline: Boolean = false,
val outline: String = "",
val hasDraft: Boolean = false,
val draft: String = "",
val hasReview: Boolean = false,
val isPublished: Boolean = false
): GoapAgentState<String, String>() {
override val agentInput = topic
override fun provideOutput(): String = draft
}
// LLM駆動のアクションを持つGOAPプランナーを作成
val planner = goap("content-planner", ::ContentState) {
// 前提条件とBeliefsを持つアクションを定義
action(
name = "Create outline",
precondition = { state -> !state.hasOutline },
belief = { state -> state.copy(hasOutline = true, outline = "Outline") },
cost = { 1.0 }
) { ctx, state ->
// LLMを使用してアウトラインを作成
val response = ctx.llm.writeSession {
appendPrompt {
user("Create a detailed outline for an article about: ${state.topic}")
}
requestLLM()
}
state.copy(hasOutline = true, outline = response.parts.filterIsInstance<MessagePart.Text>().joinToString("
") { it.text })
}
action(
name = "Write draft",
precondition = { state -> state.hasOutline && !state.hasDraft },
belief = { state -> state.copy(hasDraft = true, draft = "Draft") },
cost = { 2.0 }
) { ctx, state ->
// LLMを使用して草稿を作成
val response = ctx.llm.writeSession {
appendPrompt {
user("Write an article based on this outline:
${state.outline}")
}
requestLLM()
}
state.copy(hasDraft = true, draft = response.parts.filterIsInstance<MessagePart.Text>().joinToString("
") { it.text })
}
action(
name = "Review content",
precondition = { state -> state.hasDraft && !state.hasReview },
belief = { state -> state.copy(hasReview = true) },
cost = { 1.0 }
) { ctx, state ->
// LLMを使用して草稿をレビュー
val response = ctx.llm.writeSession {
appendPrompt {
user("Review this article and suggest improvements:
${state.draft}")
}
requestLLM()
}
println("Review feedback: ${response.parts.filterIsInstance<MessagePart.Text>().joinToString("
") { it.text }}")
state.copy(hasReview = true)
}
action(
name = "Publish",
precondition = { state -> state.hasReview && !state.isPublished },
belief = { state -> state.copy(isPublished = true) },
cost = { 1.0 }
) { ctx, state ->
println("Publishing article...")
state.copy(isPublished = true)
}
// 完了条件を持つゴールを定義
goal(
name = "Published article",
description = "Complete and publish the article",
condition = { state -> state.isPublished }
)
}
// エージェントを作成して実行
val agentConfig = AIAgentConfig(
prompt = prompt("writer") {
system("You are a professional content writer.")
},
model = OpenAIModels.Chat.GPT4o,
maxAgentIterations = 20
)
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
strategy = planner,
agentConfig = agentConfig
)
suspend fun main() {
val result = agent.run("The Future of AI in Software Development")
println("Final state: $result")
}// コンテンツ作成の状態を定義
static class ContentState extends GoapAgentState<String, String> {
public String topic;
public boolean hasOutline = false;
public String outline = "";
public boolean hasDraft = false;
public String draft = "";
public boolean hasReview = false;
public boolean isPublished = false;
public ContentState(String topic) {
this.topic = topic;
}
@Override
public String getAgentInput() {
return topic;
}
public ContentState copy(boolean hasOutline, String outline, boolean hasDraft,
String draft, boolean hasReview, boolean isPublished) {
ContentState state = new ContentState(topic);
state.hasOutline = hasOutline;
state.outline = outline;
state.hasDraft = hasDraft;
state.draft = draft;
state.hasReview = hasReview;
state.isPublished = isPublished;
return state;
}
@Override
public String provideOutput() {
return draft;
}
}
public static void main(String[] args) {
var promptExecutor = PromptExecutor.builder()
.openAI("OPENAI_API_KEY")
.build();
var strategy = Planners.goap("content-planner", ContentState::new)
.action("Create outline", builder -> builder
.precondition(state -> !state.hasOutline)
.belief(state -> state.copy(true, "Outline", false, "", false, false))
.cost(state -> 1.0)
.execute((context, state) -> {
String response = context.llm().writeSession(session -> {
session.appendPrompt(prompt -> {
prompt.user("Create a detailed outline for an article about: " + state.topic);
return null;
});
return session.requestLLM().getParts().stream()
.filter(p -> p instanceof MessagePart.Text)
.map(p -> ((MessagePart.Text) p).getText())
.collect(Collectors.joining());
});
return state.copy(true, response, state.hasDraft, state.draft,
state.hasReview, state.isPublished);
})
)
.action("Write draft", builder -> builder
.precondition(state -> state.hasOutline && !state.hasDraft)
.belief(state -> state.copy(state.hasOutline, state.outline, true, "Draft", false, false))
.cost(state -> 2.0)
.execute((context, state) -> {
String response = context.llm().writeSession(session -> {
session.appendPrompt(prompt -> {
prompt.user("Write an article based on this outline:
" + state.outline);
return null;
});
return session.requestLLM().getParts().stream()
.filter(p -> p instanceof MessagePart.Text)
.map(p -> ((MessagePart.Text) p).getText())
.collect(Collectors.joining());
});
return state.copy(state.hasOutline, state.outline, true, response,
state.hasReview, state.isPublished);
})
)
.action("Review content", builder -> builder
.precondition(state -> state.hasDraft && !state.hasReview)
.belief(state -> state.copy(state.hasOutline, state.outline, state.hasDraft,
state.draft, true, false))
.cost(state -> 1.0)
.execute((context, state) -> {
String response = context.llm().writeSession(session -> {
session.appendPrompt(prompt -> {
prompt.user("Review this article and suggest improvements:
" + state.draft);
return null;
});
return session.requestLLM().getParts().stream()
.filter(p -> p instanceof MessagePart.Text)
.map(p -> ((MessagePart.Text) p).getText())
.collect(Collectors.joining());
});
System.out.println("Review feedback: " + response);
return state.copy(state.hasOutline, state.outline, state.hasDraft,
state.draft, true, state.isPublished);
})
)
.action("Publish", builder -> builder
.precondition(state -> state.hasReview && !state.isPublished)
.belief(state -> state.copy(state.hasOutline, state.outline, state.hasDraft,
state.draft, state.hasReview, true))
.cost(state -> 1.0)
.execute((context, state) -> {
System.out.println("Publishing article...");
return state.copy(state.hasOutline, state.outline, state.hasDraft,
state.draft, state.hasReview, true);
})
)
.goal("Published article", builder -> builder
.description("Complete and publish the article")
.condition(state -> state.isPublished)
)
.build();
var agent = AIAgent.builder()
.plannerStrategy(strategy)
.promptExecutor(promptExecutor)
.llmModel(OpenAIModels.Chat.GPT4o)
.systemPrompt("You are a professional content writer.")
.maxIterations(20)
.build();
String result = agent.run("The Future of AI in Software Development");
System.out.println("Final state: " + result);
}カスタムコスト関数
A*サーチは最適なアクションシーケンスを見つけるための要素としてコストを使用するため、アクションやゴールに対してカスタムコスト関数を定義してプランナーをガイドすることができます。
action(
name = "Expensive operation",
precondition = { true },
belief = { state -> state.copy(operationDone = true) },
cost = { state ->
// 状態に基づいた動的コスト
if (state.hasOptimization) 1.0 else 10.0
}
) { ctx, state ->
// アクションを実行
state.copy(operationDone = true)
}.action("Expensive operation", builder -> builder
.precondition(state -> true)
.belief(state -> state.copy(true))
.cost(state -> {
// 状態に基づいた動的コスト
return state.hasOptimization ? 1.0 : 10.0;
})
.execute((context, state) -> {
// アクションを実行
return state.copy(true);
})
)実際の実行と比較した状態のBelief
GOAPは、Belief(楽観的な予測)と実際の実行の概念を区別します。
- Belief (ビリーフ): プランナーが起こると予測していること。プランニングに使用されます。
- 実行 (Execution): 実際に起こること。実際の状態更新に使用されます。
これにより、プランナーは実際の結果を適切に処理しながら、期待される成果に基づいてプランを作成できます。
action(
name = "Attempt complex task",
precondition = { state -> !state.taskComplete },
belief = { state ->
// 楽観的なBelief:タスクは成功する
state.copy(taskComplete = true)
},
cost = { 5.0 }
) { ctx, state ->
// 実際の実行は失敗したり、異なる結果になったりする可能性がある
val success = performComplexTask()
state.copy(
taskComplete = success,
attempts = state.attempts + 1
)
}.action("Attempt complex task", builder -> builder
.precondition(state -> !state.taskComplete)
.belief(state -> {
// 楽観的なBelief:タスクは成功する
return state.copy(true, state.attempts);
})
.cost(state -> 5.0)
.execute((context, state) -> {
// 実際の実行は失敗したり、異なる結果になったりする可能性がある
boolean success = performComplexTask();
return state.copy(success, state.attempts + 1);
})
)