コールバック:エージェントの振る舞いを観察、カスタマイズ、制御する¶
はじめに:コールバックとは何か、なぜ使用するのか?¶
コールバックはADKの中核的な機能であり、エージェントの実行プロセスにフックするための強力なメカニズムを提供します。これにより、コアのADKフレームワークコードを変更することなく、特定の事前定義されたポイントでエージェントの振る舞いを観察、カスタマイズ、さらには制御することができます。
コールバックとは? 本質的に、コールバックはあなたが定義する標準的な関数です。そして、エージェントを作成する際にこれらの関数をエージェントに関連付けます。ADKフレームワークは、主要なステージであなたの関数を自動的に呼び出し、観察や介入を可能にします。エージェントのプロセスにおけるチェックポイントのようなものだと考えてください:
- エージェントがリクエストに対する主要な処理を開始する前と、終了した後: エージェントに何か(例:質問に答える)を依頼すると、エージェントは応答を導き出すために内部ロジックを実行します。
Before Agent
コールバックは、その特定のリクエストに対するこの主要な処理が始まる直前に実行されます。After Agent
コールバックは、エージェントがそのリクエストのすべてのステップを完了し、最終結果を準備した後、しかし結果が返される直前に実行されます。- この「主要な処理」とは、その1つのリクエストを処理するためのエージェントの全プロセスを網羅します。これには、LLMを呼び出す決定、LLMの実際の呼び出し、ツールの使用を決定、ツールの使用、結果の処理、そして最終的に回答を組み立てることが含まれる場合があります。これらのコールバックは、本質的に、入力の受信からその1つのインタラクションの最終的な出力を生成するまでの一連の全体をラップします。
- 大規模言語モデル(LLM)にリクエストを送信する前、または応答を受信した後: これらのコールバック(
Before Model
,After Model
)により、特にLLMとの間でやり取りされるデータを検査または変更することができます。 - ツール(Python関数や別のエージェントなど)を実行する前、または終了した後: 同様に、
Before Tool
およびAfter Tool
コールバックは、エージェントによって呼び出されるツールの実行周りに特化した制御ポイントを提供します。
なぜ使用するのか? コールバックは、大きな柔軟性を解放し、高度なエージェント機能を可能にします:
- 観察とデバッグ: 監視とトラブルシューティングのために、重要なステップで詳細な情報をログに記録します。
- カスタマイズと制御: エージェントを流れるデータ(LLMリクエストやツールの結果など)を変更したり、ロジックに基づいて特定のステップを完全にバイパスしたりします。
- ガードレールの実装: 安全規則を強制し、入力/出力を検証し、許可されていない操作を防ぎます。
- 状態の管理: 実行中にエージェントのセッション状態を読み取ったり、動的に更新したりします。
- 統合と拡張: 外部アクション(API呼び出し、通知)をトリガーしたり、キャッシングのような機能を追加したりします。
どのように追加するのか:
Code
from google.adk.agents import LlmAgent
from google.adk.agents.callback_context import CallbackContext
from google.adk.models import LlmResponse, LlmRequest
from typing import Optional
# --- Define your callback function ---
def my_before_model_logic(
callback_context: CallbackContext, llm_request: LlmRequest
) -> Optional[LlmResponse]:
print(f"Callback running before model call for agent: {callback_context.agent_name}")
# ... your custom logic here ...
return None # Allow the model call to proceed
# --- Register it during Agent creation ---
my_agent = LlmAgent(
name="MyCallbackAgent",
model="gemini-2.0-flash", # Or your desired model
instruction="Be helpful.",
# Other agent parameters...
before_model_callback=my_before_model_logic # Pass the function here
)
import com.google.adk.agents.CallbackContext;
import com.google.adk.agents.Callbacks;
import com.google.adk.agents.LlmAgent;
import com.google.adk.models.LlmRequest;
import java.util.Optional;
public class AgentWithBeforeModelCallback {
public static void main(String[] args) {
// --- Define your callback logic ---
Callbacks.BeforeModelCallbackSync myBeforeModelLogic =
(CallbackContext callbackContext, LlmRequest llmRequest) -> {
System.out.println(
"Callback running before model call for agent: " + callbackContext.agentName());
// ... your custom logic here ...
// Return Optional.empty() to allow the model call to proceed,
// similar to returning None in the Python example.
// If you wanted to return a response and skip the model call,
// you would return Optional.of(yourLlmResponse).
return Optional.empty();
};
// --- Register it during Agent creation ---
LlmAgent myAgent =
LlmAgent.builder()
.name("MyCallbackAgent")
.model("gemini-2.0-flash") // Or your desired model
.instruction("Be helpful.")
// Other agent parameters...
.beforeModelCallbackSync(myBeforeModelLogic) // Pass the callback implementation here
.build();
}
}
コールバックのメカニズム:傍受と制御¶
ADKフレームワークがコールバックを実行できるポイント(例:LLMを呼び出す直前)に遭遇すると、そのエージェントに対応するコールバック関数を提供したかどうかを確認します。提供した場合、フレームワークはあなたの関数を実行します。
コンテキストが鍵: あなたのコールバック関数は単独で呼び出されるわけではありません。フレームワークは、引数として特別なコンテキストオブジェクト(CallbackContext
またはToolContext
)を提供します。これらのオブジェクトには、呼び出しの詳細、セッション状態、アーティファクトやメモリなどのサービスへの参照を含む、エージェントの実行の現在の状態に関する重要な情報が含まれています。これらのコンテキストオブジェクトを使用して状況を理解し、フレームワークと対話します。(詳細は専用の「コンテキストオブジェクト」セクションを参照してください)。
フローの制御(コアメカニズム): コールバックの最も強力な側面は、その戻り値がエージェントの後続のアクションにどのように影響を与えるかにあります。これが、実行フローを傍受し、制御する方法です:
-
return None
(デフォルトの振る舞いを許可):- 言語によっては特定の戻り値の型が異なる場合があります。Javaでは、同等の戻り値の型は
Optional.empty()
です。言語固有のガイダンスについては、APIドキュメントを参照してください。 - これは、コールバックがその処理(ロギング、検査、可変な入力引数への軽微な変更など)を完了し、ADKエージェントが通常の操作を続行すべきであることを示す標準的な方法です。
before_*
コールバック(before_agent
、before_model
、before_tool
)の場合、None
を返すことは、シーケンスの次のステップ(エージェントロジックの実行、LLMの呼び出し、ツールの実行)が発生することを意味します。after_*
コールバック(after_agent
、after_model
、after_tool
)の場合、None
を返すことは、直前のステップで生成された結果(エージェントの出力、LLMの応答、ツールの結果)がそのまま使用されることを意味します。
- 言語によっては特定の戻り値の型が異なる場合があります。Javaでは、同等の戻り値の型は
-
<Specific Object>
を返す(デフォルトの振る舞いをオーバーライド):None
の代わりに特定の型のオブジェクトを返すことは、ADKエージェントのデフォルトの振る舞いをオーバーライドする方法です。フレームワークはあなたが返したオブジェクトを使用し、通常続くステップをスキップするか、生成されたばかりの結果を置き換えます。before_agent_callback
→types.Content
: エージェントの主要な実行ロジック(_run_async_impl
/_run_live_impl
)をスキップします。返されたContent
オブジェクトは、このターンのエージェントの最終的な出力として即座に扱われます。単純なリクエストを直接処理したり、アクセス制御を強制したりするのに役立ちます。before_model_callback
→LlmResponse
: 外部の大規模言語モデルへの呼び出しをスキップします。返されたLlmResponse
オブジェクトは、LLMからの実際の応答であるかのように処理されます。入力ガードレール、プロンプト検証、またはキャッシュされた応答の提供を実装するのに理想的です。before_tool_callback
→dict
またはMap
: 実際のツール関数(またはサブエージェント)の実行をスキップします。返されたdict
はツール呼び出しの結果として使用され、通常はLLMに返されます。ツール引数の検証、ポリシー制限の適用、またはモック/キャッシュされたツール結果を返すのに最適です。after_agent_callback
→types.Content
: エージェントの実行ロジックが生成したばかりのContent
を置き換えます。after_model_callback
→LlmResponse
: LLMから受信したLlmResponse
を置き換えます。出力のサニタイズ、標準的な免責事項の追加、またはLLMの応答構造の変更に役立ちます。after_tool_callback
→dict
またはMap
: ツールから返されたdict
の結果を置き換えます。ツール出力がLLMに返される前に、後処理や標準化を可能にします。
概念的なコード例(ガードレール):
この例は、before_model_callback
を使用したガードレールの一般的なパターンを示しています。
Code
from google.adk.agents import LlmAgent
from google.adk.agents.callback_context import CallbackContext
from google.adk.models import LlmResponse, LlmRequest
from google.adk.runners import Runner
from typing import Optional
from google.genai import types
from google.adk.sessions import InMemorySessionService
GEMINI_2_FLASH="gemini-2.0-flash"
# --- Define the Callback Function ---
def simple_before_model_modifier(
callback_context: CallbackContext, llm_request: LlmRequest
) -> Optional[LlmResponse]:
"""Inspects/modifies the LLM request or skips the call."""
agent_name = callback_context.agent_name
print(f"[Callback] Before model call for agent: {agent_name}")
# Inspect the last user message in the request contents
last_user_message = ""
if llm_request.contents and llm_request.contents[-1].role == 'user':
if llm_request.contents[-1].parts:
last_user_message = llm_request.contents[-1].parts[0].text
print(f"[Callback] Inspecting last user message: '{last_user_message}'")
# --- Modification Example ---
# Add a prefix to the system instruction
original_instruction = llm_request.config.system_instruction or types.Content(role="system", parts=[])
prefix = "[Modified by Callback] "
# Ensure system_instruction is Content and parts list exists
if not isinstance(original_instruction, types.Content):
# Handle case where it might be a string (though config expects Content)
original_instruction = types.Content(role="system", parts=[types.Part(text=str(original_instruction))])
if not original_instruction.parts:
original_instruction.parts.append(types.Part(text="")) # Add an empty part if none exist
# Modify the text of the first part
modified_text = prefix + (original_instruction.parts[0].text or "")
original_instruction.parts[0].text = modified_text
llm_request.config.system_instruction = original_instruction
print(f"[Callback] Modified system instruction to: '{modified_text}'")
# --- Skip Example ---
# Check if the last user message contains "BLOCK"
if "BLOCK" in last_user_message.upper():
print("[Callback] 'BLOCK' keyword found. Skipping LLM call.")
# Return an LlmResponse to skip the actual LLM call
return LlmResponse(
content=types.Content(
role="model",
parts=[types.Part(text="LLM call was blocked by before_model_callback.")],
)
)
else:
print("[Callback] Proceeding with LLM call.")
# Return None to allow the (modified) request to go to the LLM
return None
# Create LlmAgent and Assign Callback
my_llm_agent = LlmAgent(
name="ModelCallbackAgent",
model=GEMINI_2_FLASH,
instruction="You are a helpful assistant.", # Base instruction
description="An LLM agent demonstrating before_model_callback",
before_model_callback=simple_before_model_modifier # Assign the function here
)
APP_NAME = "guardrail_app"
USER_ID = "user_1"
SESSION_ID = "session_001"
# Session and Runner
session_service = InMemorySessionService()
session = session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
runner = Runner(agent=my_llm_agent, app_name=APP_NAME, session_service=session_service)
# Agent Interaction
def call_agent(query):
content = types.Content(role='user', parts=[types.Part(text=query)])
events = runner.run(user_id=USER_ID, session_id=SESSION_ID, new_message=content)
for event in events:
if event.is_final_response():
final_response = event.content.parts[0].text
print("Agent Response: ", final_response)
call_agent("callback example")
import com.google.adk.agents.CallbackContext;
import com.google.adk.agents.LlmAgent;
import com.google.adk.events.Event;
import com.google.adk.models.LlmRequest;
import com.google.adk.models.LlmResponse;
import com.google.adk.runner.InMemoryRunner;
import com.google.adk.sessions.Session;
import com.google.genai.types.Content;
import com.google.genai.types.GenerateContentConfig;
import com.google.genai.types.Part;
import io.reactivex.rxjava3.core.Flowable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class BeforeModelGuardrailExample {
private static final String MODEL_ID = "gemini-2.0-flash";
private static final String APP_NAME = "guardrail_app";
private static final String USER_ID = "user_1";
public static void main(String[] args) {
BeforeModelGuardrailExample example = new BeforeModelGuardrailExample();
example.defineAgentAndRun("Tell me about quantum computing. This is a test.");
}
// --- Define your callback logic ---
// Looks for the word "BLOCK" in the user prompt and blocks the call to LLM if found.
// Otherwise the LLM call proceeds as usual.
public Optional<LlmResponse> simpleBeforeModelModifier(
CallbackContext callbackContext, LlmRequest llmRequest) {
System.out.println("[Callback] Before model call for agent: " + callbackContext.agentName());
// Inspect the last user message in the request contents
String lastUserMessageText = "";
List<Content> requestContents = llmRequest.contents();
if (requestContents != null && !requestContents.isEmpty()) {
Content lastContent = requestContents.get(requestContents.size() - 1);
if (lastContent.role().isPresent() && "user".equals(lastContent.role().get())) {
lastUserMessageText =
lastContent.parts().orElse(List.of()).stream()
.flatMap(part -> part.text().stream())
.collect(Collectors.joining(" ")); // Concatenate text from all parts
}
}
System.out.println("[Callback] Inspecting last user message: '" + lastUserMessageText + "'");
String prefix = "[Modified by Callback] ";
GenerateContentConfig currentConfig =
llmRequest.config().orElse(GenerateContentConfig.builder().build());
Optional<Content> optOriginalSystemInstruction = currentConfig.systemInstruction();
Content conceptualModifiedSystemInstruction;
if (optOriginalSystemInstruction.isPresent()) {
Content originalSystemInstruction = optOriginalSystemInstruction.get();
List<Part> originalParts =
new ArrayList<>(originalSystemInstruction.parts().orElse(List.of()));
String originalText = "";
if (!originalParts.isEmpty()) {
Part firstPart = originalParts.get(0);
if (firstPart.text().isPresent()) {
originalText = firstPart.text().get();
}
originalParts.set(0, Part.fromText(prefix + originalText));
} else {
originalParts.add(Part.fromText(prefix));
}
conceptualModifiedSystemInstruction =
originalSystemInstruction.toBuilder().parts(originalParts).build();
} else {
conceptualModifiedSystemInstruction =
Content.builder()
.role("system")
.parts(List.of(Part.fromText(prefix)))
.build();
}
// This demonstrates building a new LlmRequest with the modified config.
llmRequest =
llmRequest.toBuilder()
.config(
currentConfig.toBuilder()
.systemInstruction(conceptualModifiedSystemInstruction)
.build())
.build();
System.out.println(
"[Callback] Conceptually modified system instruction is: '"
+ llmRequest.config().get().systemInstruction().get().parts().get().get(0).text().get());
// --- Skip Example ---
// Check if the last user message contains "BLOCK"
if (lastUserMessageText.toUpperCase().contains("BLOCK")) {
System.out.println("[Callback] 'BLOCK' keyword found. Skipping LLM call.");
LlmResponse skipResponse =
LlmResponse.builder()
.content(
Content.builder()
.role("model")
.parts(
List.of(
Part.builder()
.text("LLM call was blocked by before_model_callback.")
.build()))
.build())
.build();
return Optional.of(skipResponse);
}
System.out.println("[Callback] Proceeding with LLM call.");
// Return Optional.empty() to allow the (modified) request to go to the LLM
return Optional.empty();
}
public void defineAgentAndRun(String prompt) {
// --- Create LlmAgent and Assign Callback ---
LlmAgent myLlmAgent =
LlmAgent.builder()
.name("ModelCallbackAgent")
.model(MODEL_ID)
.instruction("You are a helpful assistant.") // Base instruction
.description("An LLM agent demonstrating before_model_callback")
.beforeModelCallbackSync(this::simpleBeforeModelModifier) // Assign the callback here
.build();
// Session and Runner
InMemoryRunner runner = new InMemoryRunner(myLlmAgent, APP_NAME);
// InMemoryRunner automatically creates a session service. Create a session using the service
Session session = runner.sessionService().createSession(APP_NAME, USER_ID).blockingGet();
Content userMessage =
Content.fromParts(Part.fromText(prompt));
// Run the agent
Flowable<Event> eventStream = runner.runAsync(USER_ID, session.id(), userMessage);
// Stream event response
eventStream.blockingForEach(
event -> {
if (event.finalResponse()) {
System.out.println(event.stringifyContent());
}
});
}
}
None
を返すことと特定のオブジェクトを返すことのこのメカニズムを理解することで、エージェントの実行パスを正確に制御でき、コールバックはADKで洗練された信頼性の高いエージェントを構築するための不可欠なツールとなります。