跳转至

基于图的智能体

通过基于图的智能体,您可以将行为建模为显式的状态机: 图策略的节点代表操作(LLM 调用、工具执行), 边代表节点之间的数据流。

基于图的智能体的主要优势包括:

  • 易于可视化
  • 状态持久化
  • 可组合的架构
先决条件

确保您的环境和项目满足以下要求:

  • JDK 17+
  • Kotlin 2.2.0+
  • Gradle 8.0+ 或 Maven 3.8+

添加 Koog 包 作为依赖项:

build.gradle.kts
dependencies {
    implementation("ai.koog:koog-agents:0.7.1")
}
build.gradle
dependencies {
    implementation 'ai.koog:koog-agents:0.7.1'
}
pom.xml
<dependency>
    <groupId>ai.koog</groupId>
    <artifactId>koog-agents-jvm</artifactId>
    <version>0.7.1</version>
</dependency>

LLM 提供商获取 API 密钥,或通过 Ollama 运行本地 LLM。 更多信息请参阅 快速入门

本页示例假设您通过 Ollama 在本地运行 Llama 3.2。

本页描述如何重新构建基础智能体所使用的策略图。 该图会向 LLM 发送请求,然后根据情况执行以下操作: 若 LLM 返回助理消息则直接输出响应; 若 LLM 请求工具调用则执行对应工具。 对于工具调用场景,智能体会将工具执行结果发送给 LLM, 随后根据返回内容决定输出响应或继续执行工具。

以下是策略图的示意图:

---
config:
  flowchart:
    defaultRenderer: "elk"
---
graph TB
    subgraph nodeStart
        Input
    end

    subgraph nodeFinish
        Output
    end

    subgraph nodeSendInput
        llmRequest(Request LLM)
    end

    subgraph nodeExecuteTool
        executeTool(Execute tool call)
    end

    subgraph nodeSendToolResult
        sendToolResult(Request LLM)
    end

    Input --String--> llmRequest
    llmRequest --Message.Response--> onToolCall{{onToolCall}}
    llmRequest --Message.Response--> onAssistantMessage{{onAssistantMessage}}
    onAssistantMessage --String--> Output
    onToolCall --Message.Tool.Call--> executeTool --ReceivedToolResult--> sendToolResult
    sendToolResult --Message.Response--> onToolCall
    sendToolResult --Message.Response--> onAssistantMessage

构建策略图

在 Koog 中,您可以通过 AIAgentGraphStrategyBuilder 实现策略。 正如每个节点都定义输入和输出类型, 策略整体也需要定义输入输出类型。 本例假设输入输出类型均为字符串, 这意味着实现该策略的智能体将接收字符串输入并返回字符串输出。

要创建策略,请使用 strategy() 函数并指定两个泛型作为输入输出类型, 为策略提供唯一标识符,并定义节点与边。

val calculatorAgentStrategy = strategy<String, String>("Simple calculator") {
    val nodeSendInput by nodeLLMRequest()
    val nodeExecuteTool by nodeExecuteTool()
    val nodeSendToolResult by nodeLLMSendToolResult()

    edge(nodeStart forwardTo nodeSendInput)
    edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true })
    edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true })
    edge(nodeExecuteTool forwardTo nodeSendToolResult)
    edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })
    edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })
}

```java var calculatorAgentStrategy = AIAgentGraphStrategy.builder("Simple calculator") .withInput(String.class) .withOutput(String.class);

var nodeSendInput = AIAgentNode.llmRequest(true, "nodeSendInput"); var nodeExecuteTool = AIAgentNode.executeTool("nodeExecuteTool"); var nodeSendToolResult = AIAgentNode.llmSendToolResult("nodeSendToolResult");

```calculatorAgentStrategy.edge(calculatorAgentStrategy.nodeStart, nodeSendInput); calculatorAgentStrategy.edge(AIAgentEdge.builder() .from(nodeSendInput)
.to(calculatorAgentStrategy.nodeFinish) .onIsInstance(Message.Assistant.class) .transformed(Message.Assistant::getContent) .build()); calculatorAgentStrategy.edge(AIAgentEdge.builder() .from(nodeSendInput) .to(nodeExecuteTool) .onIsInstance(Message.Tool.Call.class) .build()); calculatorAgentStrategy.edge(nodeExecuteTool, nodeSendToolResult); calculatorAgentStrategy.edge(AIAgentEdge.builder() .from(nodeSendToolResult) .to(calculatorAgentStrategy.nodeFinish) .onIsInstance(Message.Assistant.class) .transformed(Message.Assistant::getContent) .build()); calculatorAgentStrategy.edge(AIAgentEdge.builder() .from(nodeSendToolResult) .to(nodeExecuteTool) .onIsInstance(Message.Tool.Call.class) .build());

<!--- KNIT exampleGraphAgentsJava01.java -->

此示例仅使用了[预定义节点](../nodes-and-components.md),
但你也可以创建[自定义节点](../custom-nodes.md)。

每个策略图都必须有一条从 `nodeStart` 到 `nodeFinish` 的路径,由[边](../custom-strategy-graphs.md#edges)连接。
边可以设置条件来决定何时遵循特定边。
边还可以在将数据传递给下一个节点之前,对上一个节点的输出进行转换。
这对于连接输出和输入类型不匹配的节点是必要的。

在前面的示例中,`onToolCall { true }` 表示仅当上一个节点返回工具调用 `Message.Tool.Call` 时,该边才会被遵循。

当使用 `onAssistantMessage { true }` 时,仅当上一个节点返回助手消息 `Message.Assistant` 时,该边才会被遵循。
此函数还会提取助手消息的内容,
从而将 `Message.Assistant` 转换为 `String`,因为 `nodeFinish` 期望接收字符串。

!!! tip

    除了使用 `onAssistantMessage {true}`,你还可以这样做:

    <!--- INCLUDE
    /**
    -->
    <!--- SUFFIX
    **/
    -->
    ```kotlin
    onIsInstance(Message.Assistant::class) transformed { it.content }
    ```
    <!--- KNIT example-graph-agents-02.kt -->

    或者:

    <!--- INCLUDE
    /**
    -->
    <!--- SUFFIX
    **/
    -->
    ```kotlin
    onCondition { it is Message.Assistant } transformed { it.asAssistantMessage().content }
    ```
    <!--- KNIT example-graph-agents-03.kt -->

## 创建并运行智能体 { #create-and-run-the-agent }

让我们使用此策略创建一个智能体实例并运行它:

=== "Kotlin"

    <!--- INCLUDE
    import ai.koog.agents.core.agent.AIAgent
    import ai.koog.agents.core.dsl.builder.forwardTo
    import ai.koog.agents.core.dsl.builder.strategy
    import ai.koog.agents.core.dsl.extension.*
    import ai.koog.agents.core.dsl.extension.nodeExecuteTool
    import ai.koog.agents.core.dsl.extension.nodeLLMRequest
    import ai.koog.agents.core.dsl.extension.nodeLLMSendToolResult
    import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor
    import ai.koog.prompt.executor.ollama.client.OllamaModels
    import kotlinx.coroutines.runBlocking
    -->
    ```kotlin
    val calculatorAgentStrategy = strategy<String, String>("Simple calculator") {
        val nodeSendInput by nodeLLMRequest()
        val nodeExecuteTool by nodeExecuteTool()
        val nodeSendToolResult by nodeLLMSendToolResult()

        edge(nodeStart forwardTo nodeSendInput)
        edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true })
        edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true })
        edge(nodeExecuteTool forwardTo nodeSendToolResult)
        edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })
        edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })
    }

```val mathAgent = AIAgent(
        promptExecutor = simpleOllamaAIExecutor(),
        llmModel = OllamaModels.Meta.LLAMA_3_2,
        strategy = calculatorAgentStrategy
    )

    fun main() = runBlocking {
        val result = mathAgent.run("将3乘以4,然后将结果乘以5,再加10,再加123。")
        println(result)
    }
    ```
    <!--- KNIT example-graph-agents-04.kt -->

=== "Java"

    <!--- INCLUDE
    import ai.koog.agents.core.agent.AIAgent;
    import ai.koog.agents.core.agent.entity.AIAgentEdge;
    import ai.koog.agents.core.agent.entity.AIAgentGraphStrategy;
    import ai.koog.agents.core.agent.entity.AIAgentNode;
    import ai.koog.prompt.executor.ollama.client.OllamaModels;
    import ai.koog.prompt.message.Message;
    import ai.koog.prompt.executor.model.PromptExecutor;
    class exampleGraphAgentsJava02 {
        public static void main(String[] args) {
    -->
    <!--- SUFFIX
        }
    }
    -->
    ```java
    var calculatorAgentStrategy = AIAgentGraphStrategy.builder("Simple calculator")
        .withInput(String.class)
        .withOutput(String.class);

    var nodeSendInput = AIAgentNode.llmRequest(true, "nodeSendInput");
    var nodeExecuteTool = AIAgentNode.executeTool("nodeExecuteTool");
    var nodeSendToolResult = AIAgentNode.llmSendToolResult("nodeSendToolResult");

    calculatorAgentStrategy.edge(calculatorAgentStrategy.nodeStart, nodeSendInput);
    calculatorAgentStrategy.edge(AIAgentEdge.builder()
        .from(nodeSendInput)   
        .to(calculatorAgentStrategy.nodeFinish)
        .onIsInstance(Message.Assistant.class)
        .transformed(Message.Assistant::getContent)
        .build());
    calculatorAgentStrategy.edge(AIAgentEdge.builder()
        .from(nodeSendInput)
        .to(nodeExecuteTool)
        .onIsInstance(Message.Tool.Call.class)
        .build());
    calculatorAgentStrategy.edge(nodeExecuteTool, nodeSendToolResult);
    calculatorAgentStrategy.edge(AIAgentEdge.builder()
        .from(nodeSendToolResult)
        .to(calculatorAgentStrategy.nodeFinish)
        .onIsInstance(Message.Assistant.class)
        .transformed(Message.Assistant::getContent)
        .build());
    calculatorAgentStrategy.edge(AIAgentEdge.builder()
        .from(nodeSendToolResult)
        .to(nodeExecuteTool)
        .onIsInstance(Message.Tool.Call.class)
        .build());

    var promptExecutor = PromptExecutor.builder()
        .ollama("http://localhost:11434")
        .build();

    AIAgent<String, String> mathAgent = AIAgent.builder()
        .promptExecutor(promptExecutor)
        .llmModel(OllamaModels.Meta.LLAMA_3_2)
        .graphStrategy(calculatorAgentStrategy.build())
        .build();

        String result = mathAgent.run("将3乘以4,然后将结果乘以5,再加10,再加123。", null);
        System.out.println(result);
    ```
    <!--- KNIT exampleGraphAgentsJava02.java -->

当你运行这个智能体时,它会返回类似这样的结果:

```text
To calculate this, I'll follow the order of operations:

1. Multiply 3 by 4: 3 * 4 = 12
2. Multiply the result by 5: 12 * 5 = 60
3. Add 10: 60 + 10 = 70
4. Add 123: 70 + 123 = 193

The final answer is 193.

然而,由于这个智能体没有任何工具,LLM 永远不会返回工具调用, 而是直接生成整个答案。 实际发生的情况如下:

---
config:
  flowchart:
    defaultRenderer: "elk"
---
graph LR
    subgraph nodeStart
        Input
    end

    subgraph nodeFinish
        Output
    end

    subgraph nodeSendInput
        llmRequest(Request LLM)
    end

    Input --String--> llmRequest --Message.Response--> onAssistantMessage{{onAssistantMessage}} --String--> Output

尽管在这种情况下答案是正确的,但结果将依赖于底层 LLM 的算术能力。 为了确保计算准确,我们应该为智能体提供数学工具。 这样 LLM 就能够决定调用工具来执行确定性的计算。

添加工具

定义用于执行数学运算的工具,并将其添加到ToolRegistry中:

=== "Kotlin"

@LLMDescription("用于执行数学运算的工具")
class MathTools : ToolSet {
    @Tool
    @LLMDescription("将两个数字相加并返回结果")
    fun add(a: Int, b: Int): Int {
        // 这并非必需,但有助于在控制台输出中查看工具调用
        println("正在计算 $a$b 的和...")
        return a + b
    }
    @Tool
    @LLMDescription("将两个数字相乘并返回结果")
    fun multiply(a: Int, b: Int): Int {
        // 这并非必需,但有助于在控制台输出中查看工具调用
        println("正在计算 $a$b 的乘积...")
        return a * b
    }
}

val toolRegistry = ToolRegistry {
    tools(MathTools())
}

@LLMDescription("用于执行数学运算的工具")
public static class MathTools implements ToolSet {
    @Tool
    @LLMDescription("将两个数字相加并返回结果")
    public int add(int a, int b) {
        // 这并非必需,但有助于在控制台输出中查看工具调用
        System.out.println("正在计算 " + a + " 与 " + b + " 的和...");
        return a + b;
    }

    @Tool
    @LLMDescription("将两个数字相乘并返回结果")
    public int multiply(int a, int b) {
        // 这并非必需,但有助于在控制台输出中查看工具调用
        System.out.println("正在计算 " + a + " 与 " + b + " 的乘积...");
        return a * b;
    }
}
public static void main(String[] args) {
    ToolRegistry toolRegistry = ToolRegistry.builder()
        .tools(new MathTools())
        .build();
}

将工具注册表添加到智能体配置中:

<!--- INCLUDE import ai.koog.agents.core.agent.AIAgent import ai.koog.agents.core.dsl.builder.forwardTo import ai.koog.agents.core.dsl.builder.strategy import ai.koog.agents.core.dsl.extension.* import ai.koog.agents.core.dsl.extension.nodeExecuteTool import ai.koog.agents.core.dsl.extension.nodeLLMRequest import ai.koog.agents.core.dsl.extension.nodeLLMSendToolResult import ai.koog.agents.core.tools.ToolRegistry import ai.koog.agents.core.tools.annotations.LLMDescription import ai.koog.agents.core.tools.annotations.Tool import ai.koog.agents.core.tools.reflect.ToolSet import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor import ai.koog.prompt.executor.ollama.client.OllamaModels import kotlinx.coroutines.runBlocking

@LLMDescription("Tools for performing math operations") class MathTools : ToolSet { @Tool @LLMDescription("Adds two numbers and returns the result") fun add(a: Int, b: Int): Int { // This is not necessary, but it helps to see the tool call in the console output println("Adding $a and $b...") return a + b } @Tool @LLMDescription("Multiplies two numbers and returns the result") fun multiply(a: Int, b: Int): Int { // This is not necessary, but it helps to see the tool call in the console output println("Multiplying $a and $b...") return a * b } }

val toolRegistry = ToolRegistry { tools(MathTools()) }

val calculatorAgentStrategy = strategy("Simple calculator") { val nodeSendInput by nodeLLMRequest() val nodeExecuteTool by nodeExecuteTool() val nodeSendToolResult by nodeLLMSendToolResult()

edge(nodeStart forwardTo nodeSendInput)
edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true })
edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true })
edge(nodeExecuteTool forwardTo nodeSendToolResult)
edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })

} --> kotlin val mathAgent = AIAgent( promptExecutor = simpleOllamaAIExecutor(), llmModel = OllamaModels.Meta.LLAMA_3_2, strategy = calculatorAgentStrategy, toolRegistry = toolRegistry ) fun main() = runBlocking { val result = mathAgent.run("将3乘以4,然后将结果乘以5,接着加上10,最后加上123。") println(result) }

AIAgent<String, String> mathAgent = AIAgent.builder()
    .promptExecutor(promptExecutor)
    .llmModel(OllamaModels.Meta.LLAMA_3_2)
    .graphStrategy(calculatorAgentStrategy.build())
    .toolRegistry(toolRegistry)
    .build();

String result = mathAgent.run("将3乘以4,然后将结果乘以5,接着加上10,最后加上123。", null);
System.out.println(result);

现在运行智能体时,它将返回类似以下的结果:

Multiplying 3 and 4...
The output from the first operation was multiplied by 5:
5 * 12 = 60

Then, 10 was added to the result:
60 + 10 = 70

Finally, 123 was added to the result:
70 + 123 = 193

根据此输出,智能体正确执行了计算,但它仅调用了一次 multiply 工具, 而未对每个操作调用相应的工具。 我们可以通过描述其角色并在系统提示中提供使用适当工具的说明来帮助智能体。

提供系统提示

系统提示定义了智能体的角色和执行任务的说明。 在我们的示例中,描述智能体应如何处理复杂的多步骤计算非常重要:

<!--- INCLUDE import ai.koog.agents.core.agent.AIAgent import ai.koog.agents.core.dsl.builder.forwardTo import ai.koog.agents.core.dsl.builder.strategy import ai.koog.agents.core.dsl.extension.* import ai.koog.agents.core.dsl.extension.nodeExecuteTool import ai.koog.agents.core.dsl.extension.nodeLLMRequest import ai.koog.agents.core.dsl.extension.nodeLLMSendToolResult import ai.koog.agents.core.tools.ToolRegistry import ai.koog.agents.core.tools.annotations.LLMDescription import ai.koog.agents.core.tools.annotations.Tool import ai.koog.agents.core.tools.reflect.ToolSet import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor import ai.koog.prompt.executor.ollama.client.OllamaModels import kotlinx.coroutines.runBlocking

@LLMDescription("Tools for performing math operations") class MathTools : ToolSet { @Tool @LLMDescription("Adds two numbers and returns the result") fun add(a: Int, b: Int): Int { // This is not necessary, but it helps to see the tool call in the console output println("Adding $a and $b...") return a + b } @Tool @LLMDescription("Multiplies two numbers and returns the result") fun multiply(a: Int, b: Int): Int { // This is not necessary, but it helps to see the tool call in the console output println("Multiplying $a and $b...") return a * b } }

val toolRegistry = ToolRegistry { tools(MathTools()) }

val calculatorAgentStrategy = strategy("Simple calculator") { val nodeSendInput by nodeLLMRequest() val nodeExecuteTool by nodeExecuteTool() val nodeSendToolResult by nodeLLMSendToolResult()

edge(nodeStart forwardTo nodeSendInput)
edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true })
edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true })
edge(nodeExecuteTool forwardTo nodeSendToolResult)
edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })

} --> kotlin val mathAgent = AIAgent( promptExecutor = simpleOllamaAIExecutor(), llmModel = OllamaModels.Meta.LLAMA_3_2, systemPrompt = """ 你是一个简单的计算器助手。 你可以使用“add”和“multiply”工具对两个数字进行加法和乘法运算。 当用户提供输入时,提取他们请求的数字和操作。 对第一个操作使用适当的工具,然后处理下一个操作,依此类推,直到计算出结果。 始终以清晰、友好的消息回应,显示计算过程和结果。 """.trimIndent(), toolRegistry = toolRegistry, strategy = calculatorAgentStrategy ) fun main() = runBlocking { val result = mathAgent.run("将3乘以4,然后将结果乘以5,再加10,再加123。") println(result) }

AIAgent<String, String> mathAgent = AIAgent.builder()
    .promptExecutor(promptExecutor)
    .llmModel(OllamaModels.Meta.LLAMA_3_2)
    .systemPrompt("你是一个简单的计算器助手。你可以使用“add”和“multiply”工具对两个数字进行加法和乘法运算。当用户提供输入时,提取他们请求的数字和操作。对第一个操作使用适当的工具,然后处理下一个操作,依此类推,直到计算出结果。始终以清晰、友好的消息回应,显示计算过程和结果。")
    .graphStrategy(calculatorAgentStrategy.build())
    .toolRegistry(toolRegistry)
    .build();

String result = mathAgent.run("将3乘以4,然后将结果乘以5,再加10,再加123。", null);
System.out.println(result);

现在运行智能体时,它将返回类似以下内容:

Multiplying 3 and 4...
Multiplying 12 and 5...
Adding 60 and 10...
Adding 70 and 123...
The final result is: 193

如你所见,智能体现在为每个操作正确调用了相应的工具, 确保以确定性的方式执行计算,而不是冒险产生幻觉结果。

后续步骤