GraphAgent 图驱动的 Agent 执行循环
学习目标
理解 LobeHub 中 GraphAgent 如何利用声明式有向图驱动 Agent 执行流程,包括 agent/llm 两种节点类型、回溯限制和结构化输出提取机制。
前置知识
本章涉及 Agent 核心循环和状态图的通用原理,建议先阅读:
下文假设你已理解上述概念,直接聚焦 LobeHub 的具体实现。
项目实践
双层 Agent 架构
LobeHub 的 agent-runtime 包提供两种 Agent 实现:
GeneralChatAgent 实现传统的 phase-driven 循环(LLM 决定流程),而 GraphAgent 用声明式图替代 LLM 自主决策,内部委托 GeneralChatAgent 处理 agent 节点的工具调用循环。
GraphAgent 核心循环
GraphAgent 的执行循环分为三个阶段:
// 伪代码:GraphAgent runnerasync runner(context, state): Promise<AgentInstruction> { const gc = this.getGraphContext(state);
// 1. 首次调用:初始化图 if (!gc) return this.initGraph(context, state);
const node = this.graph.states[gc.currentNode];
// 2. agent 节点:委托 GeneralChatAgent 处理工具调用循环 if (gc.nodeActive && node.type === "agent") { const instruction = await this.innerAgent.runner(context, state); // 拦截 finish — 工具循环完成后提取结构化输出 if (instruction.type === "finish") { return this.startExtraction(state, gc); } return instruction; }
// 3. llm 节点:直接单轮 LLM 调用 if (gc.nodeActive && node.type === "llm") { if (context.phase === "llm_result") { return this.onNodeComplete(state, gc); } return this.innerAgent.runner(context, state); }
// 4. 过渡阶段:开始下一个节点 return this.startNode(gc, state);}Agent 节点 vs LLM 节点
GraphAgent 的 ReasoningGraph 包含两种节点类型:
| 节点类型 | Prompt 发送 | 工具 | 输出 |
|---|---|---|---|
| agent | 不含 JSON schema | 启用(自由调用) | 工具循环结束后额外 LLM 调用提取结构化输出 |
| llm | 含 JSON schema | 禁用 | 单轮结构化输出 |
设计决策:agent 节点不发送 JSON schema 约束,让 Agent 自由使用工具;工具循环结束后再做一次 LLM 调用(extraction phase)从对话中提取结构化输出。这比直接在 agent 节点发送 JSON schema 更灵活——Agent 可以先搜索、分析、再总结,而不是被 schema 约束。
回溯限制机制
GraphAgent 支持图中回溯(backtrack),但有数量限制:
// 伪代码:回溯检查gc.visitCount[gc.currentNode]++;if (gc.visitCount[gc.currentNode] > 1) { gc.backtrackCount++;}// 在 evaluateTransitions 中:if (isBacktrack && gc.backtrackCount >= this.graph.maxBacktracks) continue;回溯时清除中间节点存储的数据,避免使用过期的中间结果。
图上下文持久化
GraphAgent 将图执行状态存储在 state.metadata 中:
// 伪代码:图上下文interface GraphContext { currentNode: string; // 当前节点 ID nodeActive: boolean; // 节点是否活跃 store: Record<string, any>; // 节点输出存储 backtrackCount: number; // 回溯计数器 visitCount: Record<string, number>; // 每节点访问计数 input: string; // 用户输入 extracting: boolean; // 是否在提取阶段}问题与规避
| 问题 | 场景 | 规避策略 |
|---|---|---|
| JSON 解析失败 | agent 节点提取阶段,LLM 未返回合法 JSON | fallback 到 { _raw: content },保留原始内容 |
| 无有效转换 | 所有 transition condition 都不满足 | fallback 到线性推进(下一个节点) |
| 回溯超限 | 图中循环导致无限回溯 | 跳过超限转换,继续线性推进 |
| 节点不存在 | 图定义中的节点 ID 不存在 | 返回 error_recovery finish |
设计取舍
为什么用装饰器模式而非重写?
GraphAgent 内部委托 GeneralChatAgent 处理工具调用循环,而非重写整个循环逻辑。
优势:
- agent 节点复用 GeneralChatAgent 的完整功能(干预检查、压缩、abort 处理)
- 减少代码重复
- 两个 Agent 可以独立测试
代价:
- GraphAgent 需要拦截 GeneralChatAgent 的 finish 指令
- 图上下文与 GeneralChatAgent 的状态管理耦合
为什么 agent 节点不用 JSON schema 约束?
直接在 agent 节点发送 JSON schema 会限制 Agent 使用工具的灵活性。LobeHub 选择在工具循环结束后做额外的 LLM 调用提取结构化输出。
代价:每次 agent 节点完成需要额外一次 LLM 调用。 收益:Agent 可以自由搜索、分析,最终输出才受 schema 约束。
参考来源
- LobeHub GraphAgent 源码 —
packages/agent-runtime/src/agents/GraphAgent.ts - State Graph Agent Loop — 通用状态图 Agent 循环原理