构建层:图 DSL、预制节点与预制策略
30 秒导读: 上一章(01-graph-engine.md)讲的是引擎——图被建好之后怎么跑。这一章讲开发者实际怎么把这张图写出来:用
strategy{}开一张图,用node{}/ 预制节点当积木,用edge(A forwardTo B onXxx{...})把积木连成有条件的箭头。Koog 自带的singleRunStrategy、reActStrategy不是引擎内置的黑盒,而正是用这套 DSL 拼出来的现成范例,你可以照着抄。
1. 这是什么(先建立直觉)
一句话: 这是 Koog 的声明式建图层——你不写 while 循环、不手动调度节点,而是声明"哪些节点、谁连谁、什么条件下走哪条边",引擎(01 章)照着这张图跑。
它解决的问题: 一个 agent 的控制流,本质是"调 LLM → 看它想调工具还是想收尾 → 调工具 → 把结果喂回去 → 再问 LLM …"。如果用命令式代码写,很快就是一坨嵌套 if/while,难读也难测。Koog 的做法是把它拆成节点(node)+ 边(edge) 的状态图:
- 节点 = 一步动作(发一次 LLM 请求、执行一批工具、压缩历史……)。
- 边 = 带条件的跳转("如果 LLM 这次回的是工具调用,就去执行工具节点")。
用起来什么样。 下面是 Koog 自带的最简单策略——单轮工具循环,一屏看完(agents/agents-core/.../core/agent/AIAgentSimpleStrategies.kt:29 singleRunStrategy):
strategy<String, String>("single_run") {
val nodeCallLLM by nodeLLMRequest() // 发请求
val nodeExecuteTool by nodeExecuteTools() // 执行工具
val nodeSendToolResult by nodeLLMSendToolResults() // 回填结果再问
edge(nodeStart forwardTo nodeCallLLM)
edge(nodeCallLLM forwardTo nodeExecuteTool onToolCalls { true }) // 想调工具 → 去执行
edge(nodeCallLLM forwardTo nodeFinish onTextMessage { true }) // 给了文本 → 收尾
edge(nodeExecuteTool forwardTo nodeSendToolResult)
edge(nodeSendToolResult forwardTo nodeFinish onTextMessage { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCalls { true }) // 还想调 → 回到执行
}
看不懂细节没关系,先记住三件事:strategy{} 圈出一张图;by nodeXxx() 声明节点;edge(A forwardTo B onXxx{...}) 连一条带条件的边。这三样就是本章的全部。
一句话类比: 把它当成流程图 DSL。你画的是框和箭头,只不过框是 Kotlin 里的节点变量、箭头是 forwardTo,而"箭头上的判断条件"是 onToolCalls/onTextMessage 这类谓词。
2. 顶层全景(三个构建器 + 委托机制)
这套 DSL 只有三层嵌套结构,外加一个"委托"小魔法把节点变量接到图里。
2.1 三个构建器的包含关系
strategy<In,Out>("name") { ← 整张图(一个策略)
├─ nodeStart / nodeFinish ← 图自带的起点/终点(引擎注入)
│
├─ val x by node { ... } ← 自定义节点
├─ val y by nodeLLMRequest() ← 预制节点
│
├─ subgraph<In,Out> { ... } ← 子图:图中之图,可换工具集/模型
│ └─ 内部又是 node + edge
│
└─ edge(x forwardTo y onXxx{}) ← 边:把节点连起来
}
| 构建器 | 干什么 | 入口符号 · 文件 |
|---|---|---|
strategy{} | 开一张顶层图,定死 In→Out 类型和工具选择策略 | strategy · AIAgentGraphStrategyBuilder.kt:48 |
subgraph{} | 图里嵌一张子图,可局部换工具集 / LLM 模型 / 参数 | subgraph · AIAgentSubgraphBuilder.kt:359 |
node{} | 定义一个自定义节点(一段 suspend 逻辑) | node · AIAgentSubgraphBuilder.kt:323 |
edge(...) | 连一条边(见 §3) | edge · AIAgentSubgraphBuilder.kt:74 |
strategy 和 subgraph 都继承同一个基类 AIAgentSubgraphBuilderBase(AIAgentSubgraphBuilder.kt:42),所以图和子图的写法完全一样——都有 nodeStart/nodeFinish,都能加节点和边。区别只在:顶层 strategy 是整个 agent 的入口;subgraph 是可复用、可换上下文的子块。
strategy{} 本身很薄:构造 AIAgentGraphStrategyBuilder、apply(init) 跑你的建图代码、build() 产出 AIAgentGraphStrategy(AIAgentGraphStrategyBuilder.kt:48-59)。真正的执行留给引擎。
2.2 节点委托:by 的魔法
你会注意到节点总是用 val x by nodeXxx() 声明,而不是 val x = nodeXxx()。这是 Kotlin 的属性委托,Koog 用它做两件事:
- 懒实例化——
nodeXxx()返回的是AIAgentNodeDelegate(一个"节点工厂"),真正的节点对象在第一次访问x时才建出来(AIAgentNodeDelegate.kt:47getValue)。 - 自动命名——如果你没显式给名 字,节点就用变量名当节点名:
name ?: property.name(AIAgentNodeDelegate.kt:49)。所以val nodeCallLLM by nodeLLMRequest()建出的节点名就叫"nodeCallLLM",测试和追踪时能对上号。
// 示意,非源码:by 背后发生了什么
val nodeCallLLM by nodeLLMRequest()
// ≈ 委托对象在你首次读 nodeCallLLM 时,building 出名为 "nodeCallLLM" 的真实节点
subgraph 走的是同一套委托机制(AIAgentSubgraphBuilder.kt:264 AIAgentSubgraphDelegate.getValue,同样 name ?: property.name)。
2.3 自定义节点 node{}
预制节点不够用时,自己写一个:node 接一个 suspend AIAgentGraphContextBase.(Input) -> Output 的 lambda(AIAgentSubgraphBuilder.kt:323)。lambda 的 receiver 是图上下文,里面能拿到 llm、storage、environment 等——但这些上下文 API 的细节留给 03-llm-layer.md,本章只关心"怎么把节点摆进图里"。
// 示意:一个把输入转大写的自定义节点
val shout by node<String, String> { input ->
input.uppercase() // receiver 是 AIAgentGraphContextBase,这里没用到它
}
还有两个进阶建图工具,知道有即可:
parallel(...)(AIAgentSubgraphBuilder.kt:449):把多个节点并发跑,再用merge{}合并结果。注意源码里明说并行执行不支持 checkpoint(AIAgentSubgraphBuilder.kt:470)。transform{}(AIAgentNodeDelegate.kt:68):给节点委托套一层输出转换,产出一个新委托。
3. 边 DSL:怎么把节点连起来
边是这套 DSL 里最需要讲清楚的部分。一条边回答三个问题:从哪个节点出?到哪个节点去?满足什么条件才走、走的时候要不要变形数据?
3.1 三段式:起点 forwardTo 终点 [谓词/变形]
nodeCallLLM forwardTo nodeExecuteTool onToolCalls { true }
└── 起点 ──┘ └链接符┘ └─── 终点 ───┘ └──── 条件谓词 ────┘
forwardTo(AIAgentNode.kt:145):最基础的连接符。A forwardTo B返回一个AIAgentEdgeBuilderIntermediate——一个"半成品边",默认原样透传 A 的输出给 B(Some(output),AIAgentNode.kt:151)。edge(...)(AIAgentSubgraphBuilder.kt:74):把半成品边收尾、注册进图(fromNode.addEdge)。每条边都要用edge(...)包起来才生效。
半成品边上可以链式挂两类操作:变形和过滤。
3.2 两个原语:transformed(变形)与 onCondition(过滤)
所有条件谓词最终都归结到这两个基础方法(定义在 AIAgentEdgeBuilder.kt 的 AIAgentEdgeBuilderIntermediateBase):
| 原语 |
|---|