BLOGE
0.9.9-RC1在线版 · 编制 2026-11-02 · English
附录 E —— 智能体扩展
第 17 章讲的是怎么在 bloge 上"思考"智能体。这份附录是你写真实智能体时一直 摆在旁边的查找表:每一个 DSL 关键字、每一条记录字段、每一个 SPI 回调、每 一项默认值、每一种运行时可能写出的 finish-reason 字符串。
什么时候翻这份附录
| 问题 | 章节 |
|---|---|
agent { … } 里能用哪些 DSL 关键字? | DSL 关键字索引 |
一个 tool { … } 块怎么变成 LLM 的工具 schema? | 工具块参考 |
| 有哪些记忆策略,开销分别是多少? | 记忆策略 |
exit_condition 表达式里可以引用哪些上下文变量? | 退出条件 |
AgentOutput.finishReason 实际上会是什么值? | Finish Reason 与异常 |
| 监听器回调按什么顺序触发? | AgentExecutionListener 速查 |
| 不用 DSL,怎么用 Java 构建智能体? | Java Builder API |
| 我得满足哪些聊天模型 SPI? | LLM Provider 表面 |
| 这个模块进入 classpath 后 Spring 会自动装哪些 Bean? | Spring 装配 |
| 什么时候该用 agent,什么时候用 state machine 或 session? | 选型表 |
这是 第 17 章 —— 智能体编排 的参考层;那一章给的是概念图与走通流程。
模块表面
| 属性 | 取值 |
|---|---|
| Maven 坐标 | com.leanowtech.bloge:bloge-agent-ext:${version} |
| DSL 扩展 kind | "agent" |
| 注册方式 | AgentExtensionProvider,通过 DslExtensionProvider SPI 发现 |
| 硬依赖 | bloge-core、bloge-dsl |
| 运行依赖 | bloge-common-operators(提供 LlmProvider / llmChat / llmStreamingChat / TokenEstimator);可选 JsonCodec 以反序列化工具参数 |
当 JAR 进入 classpath,DSL 编译器会自动识别 agent 块语法,编译期无需手工
接线。运行时则要求 OperatorRegistry 已经注册了 LLM 聊天算子(同步:
"llmChat";流式:"llmStreamingChat")。
ReAct 推理循环
下面这张图把"循环 + 监听器回调点 + 流式 chunk 类型"画在一张图里:
DSL 关键字索引
| 关键字 | 作用域 | 类型 / 取值 | 默认值 | 用途 |
|---|---|---|---|---|
agent | root、graph、phase、state 体内 | 块前缀 | — | 声明一个智能体扩展块。 |
stream agent | 同上 | 块前缀 | — | agent 的简写,等价于 streaming = true。 |
model | agent 体 | String | 必填 | 传给 llmChat 的模型标识。 |
system_prompt | agent 体 | String | null | 每次 LLM 调用都会前置的 system 消息。 |
max_turns | agent 体 | int | 10 | think–act–observe 的硬上限。超过抛 MaxTurnsExceededException。 |
max_tool_concurrency | agent 体 | Integer | null(不限) | 单轮工具并发派发上限。 |
temperature | agent 体 | Double | null(用 provider 默认) | 传给 llmChat 的采样温度。 |
memory | agent 体 | 策略表达式 | full() | 对话记忆策略 —— 见下表。 |
exit_condition | agent 体 | DSL 表达式 | null(永不提前退出) | 每轮工具后求值,真值则退出循环。 |
streaming | agent 体 | boolean | false | 物化成 StreamingAgentLoopOperator 而不是 AgentLoopOperator。 |
streaming_buffer_capacity | agent 体 | Integer | 16 | 流式变体内部 LLM token 缓冲区大小。 |
tool <name> : <OperatorRef> | agent 体,可重复 | 块 | — | 声明一个工具,引用的算子就是其实现。 |
description | tool 体 | String | "" | 传给 LLM 的工具描述。 |
input { … } | tool 体 | 赋值块 | — | 把工具参数映射到算子输入。 |
tool_args | input { … } 内 | 上下文变量 | — | LLM 在本次工具调用中给出的参数对象。 |
记忆策略
memory = … 接受以下几种写法(都对应 AgentMemoryStrategy):
| DSL 写法 | 行为 | 备注 |
|---|---|---|
full() | 保留出现过的所有消息。 | 默认;CPU 最便宜,token 最贵。 |
sliding_window(n) | 保留所有 system 消息加上最近 n 条 非 system 消息。 | n 必须 ≥ 1。 |
token_budget(n) | 从最老的非 system 消息开始裁剪,直到 TokenEstimator 报告总 token 数 ≤ n。 | 需要 TokenEstimator(已作为 SPI 注册;模块也带一个启发式实现)。 |
summary(n) | 完整保留最近 n 条非 system 消息,更早的用一段 LLM 生成的摘要替代。 | 摘要本身要再发一次 LLM 调用,记得评估代价。 |
AgentMemoryStrategy.Custom(factory) | 仅 Java:传自己的 ConversationMemory.Factory。 | DSL 不可表达。 |
退出条件
exit_condition 是一个 DSL 表达式,每轮工具结束后基于以下上下文求值:
| 变量 | 类型 | 含义 |
|---|---|---|
finish_reason | String | 最近一次 LLM 响应的 finish reason。 |
content | String | 最近一次 LLM 响应的 assistant 内容字符串。 |
turn | int | 刚结束这一轮的 1 起始编号。 |
tool_calls | List<Map> | 本轮 LLM 产生的工具调用,每项包含 id、name、arguments。 |
tool_results | Map<String,Object> | 当前工具批次的结果,按工具名为 key。 |
messages | List<LlmMessage> | 完整对话状态(含刚刚追加的工具消息)。 |
求值结果为真时,listener 的 LoopExitedPayload.exitReason 为 "exit_condition",循环不再调用模型;正常返回的 AgentOutput.finishReason 仍保留最近一次 provider reason。
工具块参考
一个 tool 块会编译成一份 AgentToolRef。从中流出的有三样东西:
- 面向 LLM 的
ToolDefinition——name(DSL 标签)、description(你写的description = …)、由input { … }推导出的 JSON-schemaparameters对象。 - 一个子图,把被引用的算子包起来,让 agent 循环可以像普通节点一样 调用它。
resultNodeId,循环用它从子图里取出该工具的输出。
工具 schema 推导
编译器会遍历 input { … } 中的每一条赋值。只有 形如
tool_args.<field> 的路径会被推到 JSON-schema 里,作为必填参数;其它
赋值(常量、引用外层 ctx 等)不出现在 schema 中,会在派发时填入。
tool searchKnowledgeBase : KBSearchOperator {
description = "Search the knowledge base"
input {
query = tool_args.query
locale = ctx.user.locale // 不出现在 LLM 看到的 schema 里
}
}
LLM 看到的是一个名为 searchKnowledgeBase 的工具,要求一个必填字符串参数
query;locale 是从外层图上下文填进去的。如果你的工具参数包含嵌套对象,
要给 DSL 编译器配置一个 JsonCodec,让 arguments JSON 可以反序列化成结构
化值。
一个完整示例
最短的端到端智能体 —— DSL 加运行所需的 Java 引导代码。
agent customerSupport {
model = "gpt-4o"
system_prompt = "You are a helpful support agent."
max_turns = 15
temperature = 0.7
memory = sliding_window(20)
tool searchKnowledgeBase : KBSearchOperator {
description = "Search the knowledge base"
input { query = tool_args.query }
}
tool escalateToHuman : EscalateOperator {
description = "Escalate to a human agent"
input { reason = tool_args.reason }
}
exit_condition = finish_reason == "stop" || tool_call("escalateToHuman")
}
OperatorRegistry registry = OperatorRegistry.builder()
.register("KBSearchOperator", new KBSearchOperator())
.register("EscalateOperator", new EscalateOperator())
.register("llmChat", new LlmChatOperator(myLlmProvider))
.build();
AgentDef def = new AgentDslCompiler(registry).compile(dslSource);
AgentLoopOperator agent = new AgentLoopOperator("supportAgent", def, registry);
AgentOutput result = agent.execute("My account is locked", OperatorContext.root());
System.out.println(result.content());
System.out.println(result.finishReason()); // provider reason,例如 "stop" / "tool_calls"
流式变体在 DSL 里写 streaming = true,或在 AgentBuilder 上调
toStreamingOperator(...);运行时通过 NodeChannel 推出 AgentStreamChunk。
Java Builder API
AgentBuilder 与 DSL 关键字一一对应:
| 方法 | 用途 |
|---|---|
AgentBuilder.create(id) | 开一个全新的 builder。 |
AgentBuilder.from(def) | 从已有的 AgentDef 起手。 |
.model(String) | 设置 LLM 模型 id。 |
.systemPrompt(String) | 设置 system prompt。 |
.maxTurns(int) | 设置轮次上限。 |
.maxToolConcurrency(int) | 设置单轮工具并发上限。 |
.temperature(Double) | 设置采样温度。 |
.memory(AgentMemoryStrategy) | 选择 记忆策略。 |
.tool(AgentToolRef) | 追加一个工具。 |
.exitCondition(Expression | String) | 编译并附加退出条件表达式。 |
.streaming(boolean) | 切换到流式变体。 |
.streamingBufferCapacity(int) | 设置流式 buffer 大小。 |
.build() | 产出不可变的 AgentDef。 |
.toOperator(ref [, registry]) | 物化成 AgentLoopOperator。 |
.toStreamingOperator(ref [, registry]) | 物化成 StreamingAgentLoopOperator。 |
记录结构
所有 record 不可变;字段按声明顺序列出。
AgentDef
| 字段 | 类型 | 备注 |
|---|---|---|
id | String | DSL 中的标识符。 |
model | String | LLM 模型 id;必填。 |
systemPrompt | String | 可以为 null。 |
maxTurns | int | 默认 10。 |
maxToolConcurrency | Integer | null => 不 限并发。 |
temperature | Double | null => 使用 provider 默认。 |
memoryStrategy | AgentMemoryStrategy | 默认 Full。 |
tools | List<AgentToolRef> | 保持 DSL 顺序。 |
exitCondition | Expression | 可以为 null。 |
streaming | boolean | true => 使用流式算子。 |
streamingBufferCapacity | Integer | 流式 buffer 大小,默认 16。 |
AgentInput
| 字段 | 类型 | 备注 |
|---|---|---|
messages | List<LlmMessage> | 已规范化的形式;AgentInput.from(Object) 会把字符串、单条消息、列表都转过来。 |
AgentOutput
| 字段 | 类型 | 备注 |
|---|---|---|
content | String | 最终 assistant 内容。 |
finishReason | String | 见 Finish Reason 与异常。 |
turnsUsed | int | 循环实际跑了多少轮。 |
messages | List<LlmMessage> | 完整对话,包括 system / assistant / tool 消息。 |
toolResults | Map<String,Object> | 最近一轮工具结果,按 tool-call id 为 key。 |
checkpoint | AgentCheckpoint | 用于恢复的快照;不会为 null。 |
AgentCheckpoint
| 字段 | 类型 | 备注 |
|---|---|---|
conversation | List<LlmMessage> | 截至快照点的消息。 |
turnCount | int | 快照时的轮次。 |
toolResults | Map<String,Object> | 快照时的工具结果。 |
把它与你自己的 session / state-machine checkpoint 一起持久化,就可以在 重启后恢复一次智能体运行。
AgentToolRef
| 字段 | 类型 | 备注 |
|---|---|---|
name | String | 暴露 给 LLM 的工具名。 |
operatorRef | String | OperatorRegistry 中注册的算子名。 |
description | String | LLM 看到的同一份描述。 |
graph | Graph | 包装该算子的编译期子图。 |
resultNodeId | String | 输出作为工具结果的节点。 |
toolDefinition | LlmProvider.ToolDefinition | LLM 侧 schema(name + description + parameters)。 |
AgentStreamChunk(sealed)
| 变体 | 字段 | 触发时机 |
|---|---|---|
Token | text: String、turnNumber: int | 每个 LLM token(或一组)。 |
ToolStart | toolName: String、toolCallId: String、turnNumber: int | 工具派发之前。 |
ToolEnd | toolName: String、toolCallId: String、success: boolean、turnNumber: int | 工具完成之后。 |
Done | finalOutput: AgentOutput | 流的最后一个 chunk,循环已退出。 |