그래프 에이전트
네이티브 ADK-Rust 통합과 함께 LangGraph 스타일 오케스트레이션을 사용하여 복잡하고 상태를 가지는 워크플로를 구축하세요.
개요
GraphAgent를 사용하면 노드와 엣지가 있는 방향 그래프로 워크플로를 정의할 수 있으며, 다음을 지원합니다:
- AgentNode: 사용자 지정 입력/출력 매퍼를 사용해 LLM 에이전트를 그래프 노드로 감싸기
- 순환 워크플로: 루프와 반복적 추론에 대한 네이티브 지원 (ReAct 패턴)
- 조건부 라우팅: 상태에 기반한 동적 엣지 라우팅
- 상태 관리: 리듀서가 있는 타입 지정 상태(덮어쓰기, 추가, 합계, 사용자 지정)
- 체크포인팅: 장애 복원력과 human-in-the-loop를 위한 지속적 상태
- 스트리밍: 여러 스트림 모드(values, updates, messages, debug)
adk-graph 크레이트는 복잡하고 상태를 가지는 에이전트 워크플로를 구축하기 위한 LangGraph 스타일 워크플로 오케스트레이션을 제공합니다. 이는 ADK-Rust 생태계에 그래프 기반 워크플로 기능을 제공하면서도 ADK의 에이전트 시스템과 완전한 호환성을 유지합니다.
주요 이점:
- 시각적 워크플로 설계: 복잡한 로직을 직관적인 노드-엣지 그래프로 정의
- 병렬 실행: 더 나은 성능을 위해 여러 노드를 동시에 실행 가능
- 상태 지속성: 장애 복원력과 human-in-the-loop를 위한 내장 체크포인팅
- LLM 통합: ADK 에이전트를 그래프 노드로 감싸는 네이티브 지원
- 유연한 라우팅: 정적 엣지, 조건부 라우팅, 동적 의사결정
만들게 될 것
이 가이드에서는 번역과 요약을 병렬로 실행하는 텍스트 처리 파이프라인을 만들게 됩니다:
┌─────────────────────┐
User Input │ │
────────────────▶ │ START │
│ │
└──────────┬──────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ TRANSLATOR │ │ SUMMARIZER │
│ │ │ │
│ 🇫🇷 French │ │ 📝 One sentence │
│ Translation │ │ Summary │
└─────────┬────────┘ └─────────┬────────┘
│ │
└───────────────┬───────────────┘
│
▼
┌─────────────────────┐
│ COMBINE │
│ │
│ 📋 Merge Results │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ END │
│ │
│ ✅ Complete │
└─────────────────────┘
핵심 개념:
- 노드 - 작업을 수행하는 처리 단위 (LLM 에이전트, 함수, 또는 사용자 지정 로직)
- 엣지 - 노드 간 제어 흐름 (정적 연결 또는 조건부 라우팅)
- 상태 - 그래프를 따라 흐르고 노드 간에 유지되는 공유 데이터
- 병렬 실행 - 더 나은 성능을 위해 여러 노드를 동시에 실행 가능
핵심 구성 요소 이해하기
🔧 노드: 작업자 노드는 실제 작업이 수행되는 곳입니다. 각 노드는 다음을 할 수 있습니다:
- AgentNode: 자연어를 처리하기 위해 LLM 에이전트를 감싸기
- 함수 노드: 데이터 처리를 위해 사용자 지정 Rust 코드 실행
- 내장 노드: 카운터나 검증기 같은 미리 정의된 로직 사용
노드를 조립 라인의 특화된 작업자라고 생각하면 됩니다. 각 노드는 특정한 역할과 전문성을 가집니다.
🔀 엣지: 흐름 제어 엣지는 실행이 그래프를 따라 어떻게 이동하는지 결정합니다:
- 정적 엣지: 직접 연결 (
A → B → C) - 조건부 엣지: 상태에 기반한 동적 라우팅 (
if sentiment == "positive" → positive_handler) - 병렬 엣지: 하나의 노드에서 여러 경로 (
START → [translator, summarizer])
엣지는 작업 흐름을 지시하는 신호등과 도로 표지판과 같습니다.
💾 상태: 공유 메모리 상태는 모든 노드가 읽고 쓸 수 있는 키-값 저장소입니다:
- 입력 데이터: 그래프로 전달되는 초기 정보
- 중간 결과: 한 노드의 출력이 다른 노드의 입력이 됨
- 최종 출력: 모든 처리가 끝난 후의 완성된 결과
상태는 노드들이 다른 노드가 사용할 정보를 남길 수 있는 공유 화이트보드처럼 동작합니다.
⚡ 병렬 실행: 속도 향상 하나의 노드에서 여러 엣지가 나가면, 해당 대상 노드들이 동시에 실행됩니다:
- 더 빠른 처리: 독립적인 작업을 동시에 실행
- 리소스 효율성: CPU와 I/O의 더 나은 활용
- 확장성: 선형적인 속도 저하 없이 더 복잡한 워크플로 처리
이는 줄을 서서 기다리는 대신 여러 작업자가 작업의 서로 다른 부분을 동시에 처리하는 것과 같습니다.
빠른 시작
1. 프로젝트 만들기
cargo new graph_demo
cd graph_demo
Cargo.toml에 의존성을 추가하세요:
[dependencies]
adk-graph = { version = "2.0.0", features = ["sqlite"] }
adk-agent = "2.0.0"
adk-model = "2.0.0"
adk-core = "2.0.0"
tokio = { version = "1", features = ["full"] }
dotenvy = "0.15"
serde_json = "1.0"
API 키를 사용해 .env을 생성하세요:
echo 'GOOGLE_API_KEY=your-api-key' > .env
2. 병렬 처리 예제
다음은 텍스트를 병렬로 처리하는 완전한 동작 예제입니다:
use adk_agent::LlmAgentBuilder;
use adk_graph::{
agent::GraphAgent,
edge::{END, START},
node::{AgentNode, ExecutionConfig, NodeOutput},
state::State,
};
use adk_model::GeminiModel;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
// Create specialized LLM agents
let translator_agent = Arc::new(
LlmAgentBuilder::new("translator")
.description("Translates text to French")
.model(model.clone())
.instruction("Translate the input text to French. Only output the translation.")
.build()?,
);
let summarizer_agent = Arc::new(
LlmAgentBuilder::new("summarizer")
.description("Summarizes text")
.model(model.clone())
.instruction("Summarize the input text in one sentence.")
.build()?,
);
// Wrap agents as graph nodes with input/output mappers
let translator_node = AgentNode::new(translator_agent)
.with_input_mapper(|state| {
let text = state.get("input").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
if !text.is_empty() {
updates.insert("translation".to_string(), json!(text));
}
}
}
updates
});
let summarizer_node = AgentNode::new(summarizer_agent)
.with_input_mapper(|state| {
let text = state.get("input").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
if !text.is_empty() {
updates.insert("summary".to_string(), json!(text));
}
}
}
updates
});
// Build the graph with parallel execution
let agent = GraphAgent::builder("text_processor")
.description("Processes text with translation and summarization in parallel")
.channels(&["input", "translation", "summary", "result"])
.node(translator_node)
.node(summarizer_node)
.node_fn("combine", |ctx| async move {
let translation = ctx.get("translation").and_then(|v| v.as_str()).unwrap_or("N/A");
let summary = ctx.get("summary").and_then(|v| v.as_str()).unwrap_or("N/A");
let result = format!(
"=== Processing Complete ===\n\n\
French Translation:\n{}\n\n\
Summary:\n{}",
translation, summary
);
Ok(NodeOutput::new().with_update("result", json!(result)))
})
// Parallel execution: both nodes start simultaneously
.edge(START, "translator")
.edge(START, "summarizer")
.edge("translator", "combine")
.edge("summarizer", "combine")
.edge("combine", END)
.build()?;
// Execute the graph
let mut input = State::new();
input.insert("input".to_string(), json!("AI is transforming how we work and live."));
let result = agent.invoke(input, ExecutionConfig::new("thread-1")).await?;
println!("{}", result.get("result").and_then(|v| v.as_str()).unwrap_or(""));
Ok(())
}
예제 출력:
=== Processing Complete ===
French Translation:
L'IA transforme notre façon de travailler et de vivre.
Summary:
AI is revolutionizing work and daily life through technological transformation.
그래프 실행 방식
전체 그림
그래프 에이전트는 슈퍼 스텝 단위로 실행됩니다. 준비된 모든 노드가 병렬로 실행된 다음, 그래프는 다음 단계로 넘어가기 전에 모든 노드가 완료될 때까지 기다립니다:
Step 1: START ──┬──▶ translator (running)
└──▶ summarizer (running)
⏳ Wait for both to complete...
Step 2: translator ──┬──▶ combine (running)
summarizer ──┘
⏳ Wait for combine to complete...
Step 3: combine ──▶ END ✅
노드를 통한 상태 흐름
각 노드는 공유 상태를 읽고 쓸 수 있습니다:
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 1: Initial state │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ State: { "input": "AI is transforming how we work" } │
│ │
│ ↓ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ translator │ │ summarizer │ │
│ │ reads "input" │ │ reads "input" │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 2: After parallel execution │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ State: { │
│ "input": "AI is transforming how we work", │
│ "translation": "L'IA transforme notre façon de travailler", │
│ "summary": "AI is revolutionizing work through technology" │
│ } │
│ │
│ ↓ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ combine │ │
│ │ reads "translation" + "summary" │ │
│ │ writes "result" │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 3: Final state │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ State: { │
│ "input": "AI is transforming how we work", │
│ "translation": "L'IA transforme notre façon de travailler", │
│ "summary": "AI is revolutionizing work through technology", │
│ "result": "=== Processing Complete ===\n\nFrench..." │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────┘
작동하게 만드는 요소
| 구성 요소 | 역할 |
|---|---|
AgentNode | 입력/출력 매퍼로 LLM 에이전트를 감쌉니다 |
input_mapper | state → 에이전트 입력 Content를 변환합니다 |
output_mapper | 에이전트 이벤트 → 상태 업데이트로 변환합니다 |
channels | 그래프가 사용할 상태 필드를 선언합니다 |
edge() | 노드 간 실행 흐름을 정의합니다 |
ExecutionConfig | 체크포인팅을 위한 스레드 ID를 제공합니다 |
LLM 분류를 사용한 조건부 라우팅
LLMs가 실행 경로를 결정하는 스마트 라우팅 시스템을 구축하세요:
시각적 요소: 감정 기반 라우팅
┌─────────────────────┐
User Feedback │ │
────────────────▶ │ CLASSIFIER │
│ 🧠 Analyze tone │
└──────────┬──────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ POSITIVE │ │ NEGATIVE │ │ NEUTRAL │
│ │ │ │ │ │
│ 😊 Thank you! │ │ 😔 Apologize │ │ 😐 Ask more │
│ Celebrate │ │ Help fix │ │ questions │
└──────────────────┘ └──────────────────┘ └──────────────────┘
전체 예제 코드
use adk_agent::LlmAgentBuilder;
use adk_graph::{
edge::{END, Router, START},
graph::StateGraph,
node::{AgentNode, ExecutionConfig},
state::State,
};
use adk_model::GeminiModel;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
// Create classifier agent
let classifier_agent = Arc::new(
LlmAgentBuilder::new("classifier")
.description("Classifies text sentiment")
.model(model.clone())
.instruction(
"You are a sentiment classifier. Analyze the input text and respond with \
ONLY one word: 'positive', 'negative', or 'neutral'. Nothing else.",
)
.build()?,
);
// Create response agents for each sentiment
let positive_agent = Arc::new(
LlmAgentBuilder::new("positive")
.description("Handles positive feedback")
.model(model.clone())
.instruction(
"You are a customer success specialist. The customer has positive feedback. \
Express gratitude, reinforce the positive experience, and suggest ways to \
share their experience. Be warm and appreciative. Keep response under 3 sentences.",
)
.build()?,
);
let negative_agent = Arc::new(
LlmAgentBuilder::new("negative")
.description("Handles negative feedback")
.model(model.clone())
.instruction(
"You are a customer support specialist. The customer has a complaint. \
Acknowledge their frustration, apologize sincerely, and offer help. \
Be empathetic. Keep response under 3 sentences.",
)
.build()?,
);
let neutral_agent = Arc::new(
LlmAgentBuilder::new("neutral")
.description("Handles neutral feedback")
.model(model.clone())
.instruction(
"You are a customer service representative. The customer has neutral feedback. \
Ask clarifying questions to better understand their needs. Be helpful and curious. \
Keep response under 3 sentences.",
)
.build()?,
);
// Create AgentNodes with mappers
let classifier_node = AgentNode::new(classifier_agent)
.with_input_mapper(|state| {
let text = state.get("feedback").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("")
.to_lowercase()
.trim()
.to_string();
let sentiment = if text.contains("positive") { "positive" }
else if text.contains("negative") { "negative" }
else { "neutral" };
updates.insert("sentiment".to_string(), json!(sentiment));
}
}
updates
});
// Response nodes (similar pattern for each)
let positive_node = AgentNode::new(positive_agent)
.with_input_mapper(|state| {
let text = state.get("feedback").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
updates.insert("response".to_string(), json!(text));
}
}
updates
});
// Build graph with conditional routing
let graph = StateGraph::with_channels(&["feedback", "sentiment", "response"])
.add_node(classifier_node)
.add_node(positive_node)
// ... add negative_node and neutral_node similarly
.add_edge(START, "classifier")
.add_conditional_edges(
"classifier",
Router::by_field("sentiment"), // Route based on sentiment field
[
("positive", "positive"),
("negative", "negative"),
("neutral", "neutral"),
],
)
.add_edge("positive", END)
.add_edge("negative", END)
.add_edge("neutral", END)
.compile()?;
// Test with different feedback
let mut input = State::new();
input.insert("feedback".to_string(), json!("Your product is amazing! I love it!"));
let result = graph.invoke(input, ExecutionConfig::new("feedback-1")).await?;
println!("Sentiment: {}", result.get("sentiment").and_then(|v| v.as_str()).unwrap_or(""));
println!("Response: {}", result.get("response").and_then(|v| v.as_str()).unwrap_or(""));
Ok(())
}
예제 흐름:
Input: "Your product is amazing! I love it!"
↓
Classifier: "positive"
↓
Positive Agent: "Thank you so much for the wonderful feedback!
We're thrilled you love our product.
Would you consider leaving a review to help others?"
ReAct 패턴: 추론 + 행동
복잡한 문제를 해결하기 위해 도구를 반복적으로 사용할 수 있는 에이전트를 구축하세요:
시각적 요소: ReAct 사이클
┌─────────────────────┐
User Question │ │
────────────────▶ │ REASONER │
│ 🧠 Think + Act │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Has tool calls? │
│ │
└──────────┬──────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ YES │ │ NO │
│ │ │ │
│ 🔄 Loop back │ │ ✅ Final answer │
│ to reasoner │ │ END │
└─────────┬────────┘ └──────────────────┘
│
└─────────────────┐
│
▼
┌─────────────────────┐
│ REASONER │
│ 🧠 Think + Act │
│ (next iteration) │
└─────────────────────┘
전체 ReAct 예제
use adk_agent::LlmAgentBuilder;
use adk_core::{Part, Tool};
use adk_graph::{
edge::{END, START},
graph::StateGraph,
node::{AgentNode, ExecutionConfig, NodeOutput},
state::State,
};
use adk_model::GeminiModel;
use adk_tool::FunctionTool;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
// Create tools
let weather_tool = Arc::new(FunctionTool::new(
"get_weather",
"Get the current weather for a location. Takes a 'location' parameter (city name).",
|_ctx, args| async move {
let location = args.get("location").and_then(|v| v.as_str()).unwrap_or("unknown");
Ok(json!({
"location": location,
"temperature": "72°F",
"condition": "Sunny",
"humidity": "45%"
}))
},
)) as Arc<dyn Tool>;
let calculator_tool = Arc::new(FunctionTool::new(
"calculator",
"Perform mathematical calculations. Takes an 'expression' parameter (string).",
|_ctx, args| async move {
let expr = args.get("expression").and_then(|v| v.as_str()).unwrap_or("0");
let result = match expr {
"2 + 2" => "4",
"10 * 5" => "50",
"100 / 4" => "25",
"15 - 7" => "8",
_ => "Unable to evaluate",
};
Ok(json!({ "result": result, "expression": expr }))
},
)) as Arc<dyn Tool>;
// Create reasoner agent with tools
let reasoner_agent = Arc::new(
LlmAgentBuilder::new("reasoner")
.description("Reasoning agent with tools")
.model(model.clone())
.instruction(
"You are a helpful assistant with access to tools. Use tools when needed to answer questions. \
When you have enough information, provide a final answer without using more tools.",
)
.tool(weather_tool)
.tool(calculator_tool)
.build()?,
);
// Create reasoner node that detects tool usage
let reasoner_node = AgentNode::new(reasoner_agent)
.with_input_mapper(|state| {
let question = state.get("question").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(question)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
let mut has_tool_calls = false;
let mut response = String::new();
for event in events {
if let Some(content) = event.content() {
for part in &content.parts {
match part {
Part::FunctionCall { .. } => {
has_tool_calls = true;
}
Part::Text { text } => {
response.push_str(text);
}
_ => {}
}
}
}
}
updates.insert("has_tool_calls".to_string(), json!(has_tool_calls));
updates.insert("response".to_string(), json!(response));
updates
});
// Build ReAct graph with cycle
let graph = StateGraph::with_channels(&["question", "has_tool_calls", "response", "iteration"])
.add_node(reasoner_node)
.add_node_fn("counter", |ctx| async move {
let i = ctx.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
Ok(NodeOutput::new().with_update("iteration", json!(i + 1)))
})
.add_edge(START, "counter")
.add_edge("counter", "reasoner")
.add_conditional_edges(
"reasoner",
|state| {
let has_tools = state.get("has_tool_calls").and_then(|v| v.as_bool()).unwrap_or(false);
let iteration = state.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
// Safety limit
if iteration >= 5 { return END.to_string(); }
if has_tools {
"counter".to_string() // Loop back for more reasoning
} else {
END.to_string() // Done - final answer
}
},
[("counter", "counter"), (END, END)],
)
.compile()?
.with_recursion_limit(10);
// Test the ReAct agent
let mut input = State::new();
input.insert("question".to_string(), json!("What's the weather in Paris and what's 15 + 25?"));
let result = graph.invoke(input, ExecutionConfig::new("react-1")).await?;
println!("Final answer: {}", result.get("response").and_then(|v| v.as_str()).unwrap_or(""));
println!("Iterations: {}", result.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0));
Ok(())
}
예제 흐름:
Question: "What's the weather in Paris and what's 15 + 25?"
Iteration 1:
Reasoner: "I need to get weather info and do math"
→ Calls get_weather(location="Paris") and calculator(expression="15 + 25")
→ has_tool_calls = true → Loop back
Iteration 2:
Reasoner: "Based on the results: Paris is 72°F and sunny, 15 + 25 = 40"
→ No tool calls → has_tool_calls = false → END
Final Answer: "The weather in Paris is 72°F and sunny with 45% humidity.
And 15 + 25 equals 40."
AgentNode
임의의 ADK Agent(일반적으로 LlmAgent)을 그래프 노드로 감쌉니다:
에이전트가 보는 것
그래프 내부의 에이전트는 그래프를 시작한 호출에서 파생된 컨텍스트 아래에서 실행되므로, 그래프 밖에서와 동일하게 동작합니다. Runner이 GraphAgent을 호출하면, 호출자의 식별 정보와 서비스가 자동으로 전달됩니다:
| 전달됨 | 주석 |
|---|---|
app_name, user_id, session_id | 합성된 것이 아니라 호출자의 것 |
| 스코프와 요청 메타데이터 | 따라서 스코프 검사가 호출자의 권한을 보게 됨 |
| 비밀 서비스, 메모리, 아티팩트, 공유 상태 | 그래프 외부에서와 정확히 동일하게 사용 가능 |
| 취소 | Runner::interrupt가 노드로 실행 중인 agent에 도달함 |
RunConfig | 호출자로부터 상속됨 |
branch | 파생됨, {caller_branch}.{agent_name}로, 노드의 이벤트가 귀속될 수 있음 |
직접 호출된 그래프 — graph.invoke(state, ExecutionConfig::new("thread")) —
는 상속할 호출이 없습니다. 이것이 독립 실행 모드입니다: 노드는
user_id = "graph_user", app_name = "graph_app", 분기 main, 비밀 정보 없음, 그리고
메모리 없음으로 실행됩니다. 이는 Runner 밖에서 그래프를 실행하기 위한 의도적인 모드이며, 프로덕션에서 꺼내 쓸 대체 수단이 아닙니다.
예를 들어 자신만의 실행기에서 그래프를 구동할 때처럼 수동으로 연결하려면, 호출을 명시적으로 전달하세요:
let config = ExecutionConfig::new(ctx.session_id()).with_parent_context(ctx.clone());
참고: 노드는 여전히 자체 인메모리 그래프 세션에서 실행되므로, 노드 내부의 에이전트 대화 기록은 호출자 세션에 추가되지 않고 노드에만 범위가 지정됩니다.
let node = AgentNode::new(llm_agent)
.with_input_mapper(|state| {
// Transform graph state to agent input Content
let text = state.get("input").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
// Transform agent events to state updates
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
updates.insert("output".to_string(), json!(text));
}
}
updates
});
함수 노드
상태를 처리하는 단순한 비동기 함수:
.node_fn("process", |ctx| async move {
let input = ctx.state.get("input").unwrap();
let output = process_data(input).await?;
Ok(NodeOutput::new().with_update("output", output))
})
엣지 유형
정적 엣지
노드 간의 직접 연결:
.edge(START, "first_node")
.edge("first_node", "second_node")
.edge("second_node", END)
조건부 엣지
상태에 기반한 동적 라우팅:
.conditional_edge(
"router",
|state| {
match state.get("next").and_then(|v| v.as_str()) {
Some("research") => "research_node".to_string(),
Some("write") => "write_node".to_string(),
_ => END.to_string(),
}
},
[
("research_node", "research_node"),
("write_node", "write_node"),
(END, END),
],
)
라우터 헬퍼
일반적인 패턴에는 내장 라우터를 사용하세요:
use adk_graph::edge::Router;
// Route based on a state field value
.conditional_edge("classifier", Router::by_field("sentiment"), [
("positive", "positive_handler"),
("negative", "negative_handler"),
("neutral", "neutral_handler"),
])
// Route based on boolean field
.conditional_edge("check", Router::by_bool("approved"), [
("true", "execute"),
("false", "reject"),
])
// Limit iterations
.conditional_edge("loop", Router::max_iterations("count", 5), [
("continue", "process"),
("done", END),
])
병렬 실행
단일 노드에서 나가는 여러 엣지는 병렬로 실행됩니다:
let agent = GraphAgent::builder("parallel_processor")
.channels(&["input", "translation", "summary", "analysis"])
.node(translator_node)
.node(summarizer_node)
.node(analyzer_node)
.node(combiner_node)
// All three start simultaneously
.edge(START, "translator")
.edge(START, "summarizer")
.edge(START, "analyzer")
// Wait for all to complete before combining
.edge("translator", "combiner")
.edge("summarizer", "combiner")
.edge("analyzer", "combiner")
.edge("combiner", END)
.build()?;
순환 그래프 (ReAct 패턴)
순환을 사용해 반복적 추론 에이전트를 구축하세요:
use adk_core::Part;
// Create agent with tools
let reasoner = Arc::new(
LlmAgentBuilder::new("reasoner")
.model(model)
.instruction("Use tools to answer questions. Provide final answer when done.")
.tool(search_tool)
.tool(calculator_tool)
.build()?
);
let reasoner_node = AgentNode::new(reasoner)
.with_input_mapper(|state| {
let question = state.get("question").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(question)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
let mut has_tool_calls = false;
let mut response = String::new();
for event in events {
if let Some(content) = event.content() {
for part in &content.parts {
match part {
Part::FunctionCall { name, .. } => {
has_tool_calls = true;
}
Part::Text { text } => {
response.push_str(text);
}
_ => {}
}
}
}
}
updates.insert("has_tool_calls".to_string(), json!(has_tool_calls));
updates.insert("response".to_string(), json!(response));
updates
});
// Build graph with cycle
let react_agent = StateGraph::with_channels(&["question", "has_tool_calls", "response", "iteration"])
.add_node(reasoner_node)
.add_node_fn("counter", |ctx| async move {
let i = ctx.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
Ok(NodeOutput::new().with_update("iteration", json!(i + 1)))
})
.add_edge(START, "counter")
.add_edge("counter", "reasoner")
.add_conditional_edges(
"reasoner",
|state| {
let has_tools = state.get("has_tool_calls").and_then(|v| v.as_bool()).unwrap_or(false);
let iteration = state.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
// Safety limit
if iteration >= 5 { return END.to_string(); }
if has_tools {
"counter".to_string() // Loop back
} else {
END.to_string() // Done
}
},
[("counter", "counter"), (END, END)],
)
.compile()?
.with_recursion_limit(10);
멀티 에이전트 슈퍼바이저
작업을 전문 에이전트로 라우팅하세요:
// Create supervisor agent
let supervisor = Arc::new(
LlmAgentBuilder::new("supervisor")
.model(model.clone())
.instruction("Route tasks to: researcher, writer, or coder. Reply with agent name only.")
.build()?
);
let supervisor_node = AgentNode::new(supervisor)
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("")
.to_lowercase();
let next = if text.contains("researcher") { "researcher" }
else if text.contains("writer") { "writer" }
else if text.contains("coder") { "coder" }
else { "done" };
updates.insert("next_agent".to_string(), json!(next));
}
}
updates
});
// Build supervisor graph
let graph = StateGraph::with_channels(&["task", "next_agent", "research", "content", "code"])
.add_node(supervisor_node)
.add_node(researcher_node)
.add_node(writer_node)
.add_node(coder_node)
.add_edge(START, "supervisor")
.add_conditional_edges(
"supervisor",
Router::by_field("next_agent"),
[
("researcher", "researcher"),
("writer", "writer"),
("coder", "coder"),
("done", END),
],
)
// Agents report back to supervisor
.add_edge("researcher", "supervisor")
.add_edge("writer", "supervisor")
.add_edge("coder", "supervisor")
.compile()?;
상태 관리
리듀서를 사용한 상태 스키마
상태 업데이트가 어떻게 병합되는지 제어합니다:
let schema = StateSchema::builder()
.channel("current_step") // Overwrite (default)
.list_channel("messages") // Append to list
.channel_with_reducer("count", Reducer::Sum) // Sum values
.channel_with_reducer("data", Reducer::Custom(Arc::new(|old, new| {
// Custom merge logic
merge_json(old, new)
})))
.build();
let agent = GraphAgent::builder("stateful")
.state_schema(schema)
// ... nodes and edges
.build()?;
리듀서 유형
| Reducer | 동작 |
|---|---|
Overwrite | 이전 값을 새 값으로 교체 (기본값) |
Append | 목록에 추가 |
Sum | 숫자 값 추가 |
Custom | 사용자 정의 병합 함수 |
체크포인팅
장애 복원력과 인간 개입을 위해 지속 상태를 활성화합니다:
메모리 내(개발)
use adk_graph::checkpoint::MemoryCheckpointer;
let checkpointer = Arc::new(MemoryCheckpointer::new());
let graph = StateGraph::with_channels(&["task", "result"])
// ... nodes and edges
.compile()?
.with_checkpointer_arc(checkpointer.clone());
SQLite(프로덕션)
use adk_graph::checkpoint::SqliteCheckpointer;
let checkpointer = SqliteCheckpointer::new("checkpoints.db").await?;
let graph = StateGraph::with_channels(&["task", "result"])
// ... nodes and edges
.compile()?
.with_checkpointer(checkpointer);
체크포인트가 기록하는 것
체크포인트는 누적 상태, 단계 번호, 그리고 프론티어를 저장합니다 — 즉, 아직 실행되어야 하는 노드들입니다. 이는 프론티어가 진행된 뒤에 기록되므로, 재개 시 이미 완료된 노드를 다시 실행하지 않으며 업데이트를 두 번 적용하지도 않습니다. 완료된 실행은 빈 프론티어를 체크포인트하므로, 완료된 스레드를 재개하면 그래프를 다시 시작하는 대신 최종 상태가 반환됩니다.
두 가지 경우에는 의도적으로 다음 노드가 아니라 실행 중이던 프론티어를 체크포인트합니다. 중단된 노드는 아직 업데이트를 생성하지 않았고, 재개 시 다시 실행되어야 하기 때문입니다:
| 상황 | 저장된 프론티어 |
|---|---|
| 슈퍼 스텝 완료 | 실행할 다음 노드 |
| 실행 완료 | 비어 있음 |
| 인터럽트 발생(블로킹 또는 스트리밍) | 실행 중이던 노드 |
스트리밍 실행은 중단 실행과 동일한 일정으로 체크포인트를 기록하며, 인터럽트가 스트림을 종료하는 경우에도 마찬가지입니다. 따라서 human-in-the-loop 일시 중지는 어느 실행 모드에서든 재개할 수 있습니다.
체크포인트 히스토리(Time Travel)
읽기 전용.
TimeTravelHandle::state_history(from, to)은 각 체크포인트 단계에 저장된 상태를 반환합니다. 아무것도 실행하지 않습니다 — 노드도 실행되지 않고, 이벤트도 재생성되지 않으며, 부작용도 반복되지 않습니다. 기록의 한 지점부터 다시 실행하려면fork_at을 사용해 해당 체크포인트를 분기하고 포크된 스레드에서 그래프를 호출하세요. 이 메서드는 이전에replay라는 이름이었고 그래프를 재실행한다고 문서화되었지만, 실제로는 그렇지 않았습니다.
체크포인트는 durable resume도 가능하게 합니다 — 그래프 실행이 충돌하거나 프로세스가 재시작되더라도, 실행은 처음부터 다시 시작하는 대신 마지막으로 영속화된 체크포인트에서 이어집니다. 충돌에 안전한 지속성을 위해 SqliteCheckpointer 또는 PostgresCheckpointer을 사용하세요.
// List all checkpoints for a thread
let checkpoints = checkpointer.list("thread-id").await?;
for cp in checkpoints {
println!("Step {}: {:?}", cp.step, cp.state.get("status"));
}
// Load a specific checkpoint
if let Some(checkpoint) = checkpointer.load_by_id(&checkpoint_id).await? {
println!("State at step {}: {:?}", checkpoint.step, checkpoint.state);
}
Human-in-the-Loop
동적 인터럽트를 사용하여 인간의 승인을 위해 실행을 일시 중지합니다:
use adk_graph::{error::GraphError, node::NodeOutput};
// Planner agent assesses risk
let planner_node = AgentNode::new(planner_agent)
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
// Extract risk level from LLM response
let risk = if text.to_lowercase().contains("risk: high") { "high" }
else if text.to_lowercase().contains("risk: medium") { "medium" }
else { "low" };
updates.insert("plan".to_string(), json!(text));
updates.insert("risk_level".to_string(), json!(risk));
}
}
updates
});
// Review node with dynamic interrupt
let graph = StateGraph::with_channels(&["task", "plan", "risk_level", "approved", "result"])
.add_node(planner_node)
.add_node(executor_node)
.add_node_fn("review", |ctx| async move {
let risk = ctx.get("risk_level").and_then(|v| v.as_str()).unwrap_or("low");
let approved = ctx.get("approved").and_then(|v| v.as_bool());
// Already approved - continue
if approved == Some(true) {
return Ok(NodeOutput::new());
}
// High/medium risk - interrupt for approval
if risk == "high" || risk == "medium" {
return Ok(NodeOutput::interrupt_with_data(
&format!("{} RISK: Human approval required", risk.to_uppercase()),
json!({
"plan": ctx.get("plan"),
"risk_level": risk,
"action": "Set 'approved' to true to continue"
})
));
}
// Low risk - auto-approve
Ok(NodeOutput::new().with_update("approved", json!(true)))
})
.add_edge(START, "planner")
.add_edge("planner", "review")
.add_edge("review", "executor")
.add_edge("executor", END)
.compile()?
.with_checkpointer_arc(checkpointer.clone());
// Execute - may pause for approval
let thread_id = "task-001";
let result = graph.invoke(input, ExecutionConfig::new(thread_id)).await;
match result {
Err(GraphError::Interrupted(interrupt)) => {
println!("*** EXECUTION PAUSED ***");
println!("Reason: {}", interrupt.interrupt);
println!("Plan awaiting approval: {:?}", interrupt.state.get("plan"));
// Human reviews and approves...
// Update state with approval
graph.update_state(thread_id, [("approved".to_string(), json!(true))]).await?;
// Resume execution
let final_result = graph.invoke(State::new(), ExecutionConfig::new(thread_id)).await?;
println!("Final result: {:?}", final_result.get("result"));
}
Ok(result) => {
println!("Completed without interrupt: {:?}", result);
}
Err(e) => {
println!("Error: {}", e);
}
}
정적 인터럽트
필수 일시 중지 지점에는 interrupt_before 또는 interrupt_after을 사용하세요:
let graph = StateGraph::with_channels(&["task", "plan", "result"])
.add_node(planner_node)
.add_node(executor_node)
.add_edge(START, "planner")
.add_edge("planner", "executor")
.add_edge("executor", END)
.compile()?
.with_interrupt_before(&["executor"]); // Always pause before execution
스트리밍 실행
그래프가 실행되는 동안 이벤트를 스트리밍합니다:
use futures::StreamExt;
use adk_graph::stream::StreamMode;
let stream = agent.stream(input, config, StreamMode::Updates);
while let Some(event) = stream.next().await {
match event? {
StreamEvent::NodeStart(name) => println!("Starting: {}", name),
StreamEvent::Updates { node, updates } => {
println!("{} updated state: {:?}", node, updates);
}
StreamEvent::NodeEnd(name) => println!("Completed: {}", name),
StreamEvent::Done(state) => println!("Final state: {:?}", state),
_ => {}
}
}
스트림 모드
| 모드 | 설명 |
|---|---|
Values | 각 노드 이후 전체 상태를 스트리밍 |
Updates | 상태 변경 사항만 스트리밍 |
Messages | 메시지 유형 업데이트 스트리밍 |
Debug | 모든 내부 이벤트 스트리밍 |
Messages 모드는 Node::execute_stream에서 생성되는 토큰을 그대로 읽습니다.
이 모드에서는 각 노드가 super-step마다 한 번만 실행됩니다. 노드는 스트림에서 상태 업데이트를 StreamEvent::Updates 이벤트로 보고하고, executor는 이를 수집하기 위해 노드를 두 번째로 실행하는 대신 그 업데이트를 적용합니다. 이는 특히 AgentNode에서 중요합니다. 두 번째 실행은 노드마다 두 번째로 요금이 부과되는 모델 호출을 의미하기 때문입니다.
중요:
execute_stream를 재정의하는 사용자 정의Node는 상태 업데이트를 담은StreamEvent::Updates이벤트를 반환해야 합니다. 그렇지 않으면 노드는 이벤트를 스트리밍하지만Messages모드에서는 상태에 기여하지 않습니다.execute를 감싸는 기본execute_stream는 이를 자동으로 처리합니다.
타임아웃 정책은 스트리밍 실행 자체에 적용됩니다. 스트림의 경우, idle_timeout는 제한 시간 내에 이벤트가 생성되지 않았음을 의미합니다.
ADK 통합
GraphAgent은 ADK Agent 트레이트를 구현하므로 다음과 함께 사용할 수 있습니다:
- Runner: 표준 실행을 위해
adk-runner와 함께 사용 - Callbacks: before/after 콜백을 완전히 지원
- Sessions: 대화 기록을 위해
adk-session와 함께 동작 - Streaming: ADK
EventStream를 반환
use adk_runner::Runner;
let graph_agent = GraphAgent::builder("workflow")
.before_agent_callback(|ctx| async {
println!("Starting graph execution for session: {}", ctx.session_id());
Ok(())
})
.after_agent_callback(|ctx, event| async {
if let Some(content) = event.content() {
println!("Graph completed with content");
}
Ok(())
})
// ... graph definition
.build()?;
// GraphAgent implements Agent trait - use with Launcher or Runner
// See adk-runner README for Runner configuration
사례
이 저장소에서 검증된 graph 예제:
cargo run --manifest-path examples/tier_examples/standard/Cargo.toml --bin 11-standard-graph
cargo run --manifest-path examples/tier_examples/standard/Cargo.toml --bin 12-standard-sequential
cargo run --manifest-path examples/competitive_graph_resume/Cargo.toml
실제 LLM 통합을 사용하는 전체 그래프 갤러리는 이 사이트에 포함된 ADK-Rust Playground에서 확인할 수 있습니다.
LangGraph와의 비교
| 기능 | LangGraph | adk-graph |
|---|---|---|
| 상태 관리 | TypedDict + Reducers | StateSchema + Reducers |
| 실행 모델 | Pregel 슈퍼 스텝 | Pregel 슈퍼 스텝 |
| 체크포인팅 | Memory, SQLite, Postgres | Memory, SQLite |
| Human-in-Loop | interrupt_before/after | interrupt_before/after + 동적 |
| 스트리밍 | 5가지 모드 | 5가지 모드 |
| 순환 | 기본 지원 | 기본 지원 |
| 타입 안전성 | Python 타이핑 | Rust 타입 시스템 |
| LLM 통합 | LangChain | AgentNode + ADK 에이전트 |
이전: ← Multi-Agent Systems | 다음: Realtime Agents →