ClojureとADK for JavaでつくるAI Agent
この記事は Uzabase Advent Calendar 2025 の記事です。
はじめに
AI Agentを構築する際、多くの場合Pythonを使うことが多いと思いますが、今回はClojureからADK for Javaを使ってAI Agentを作ることを目指します。ADKのドキュメントが読めて、JavaとClojureに明るければそんなに難しい問題ではないのですが、こういう奇妙なことをする例があるときっとClojureでAI Agentを作ろうと思ってくれる人も増える気がするので書いておきます。
プロジェクトの概要
ゴールはADK for Java を Clojure から使って、簡単な計算ツールが使えるエージェントの実装までです。
実装
1. プロジェクトのセットアップ
まず、プロジェクトのディレクトリ構造を作成します。
code:./
your-project/
├── deps.edn
└── src/
└── hello_adk/
├── agent.clj
└── core.clj
次に、deps.edn でプロジェクトの依存関係を定義します。
code:deps.edn
{:paths "src" "target/classes"
:deps {org.clojure/clojure {:mvn/version "1.12.3"}
;; Ring HTTP server
info.sunng/ring-jetty9-adapter {:mvn/version "0.33.4"}
ring/ring-core {:mvn/version "1.15.3"}
;; JSON handling
metosin/jsonista {:mvn/version "0.3.13"}
;; ADK for Java
com.google.adk/google-adk {:mvn/version "0.4.0"}}
:aliases
{:run {:main-opts "-m" "hello-adk.core"}
:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.11" :git/sha "c6c670a"}}
:ns-default build}}}
:paths に "src/java" と "target/classes" を追加しておきます(後でJavaのコードを追加するため)。Javaのコードをビルドするためのコードも書いておきます。
code:build.clj
(ns build
(:require clojure.tools.build.api :as b))
(def class-dir "target/classes")
(def basis (b/create-basis {:project "deps.edn"}))
(def java-src-dirs "src/java")
(defn clean
"Clean the target directory"
_
(b/delete {:path "target"}))
(defn compile-java
"Compile Java sources"
_
(b/javac {:src-dirs java-src-dirs
:class-dir class-dir
:basis basis
:javac-opts "-source" "25" "-target" "25"}))
(defn build
"Clean and compile Java sources"
_
(clean nil)
(compile-java nil))
2. シンプルなLLMエージェントの実装
まずは、ツールを持たないシンプルなエージェントから始めます。質問に答えるだけのエージェントを作成します。
code:src/hello_adk/agent.clj
(ns hello-adk.agent
(:import com.google.adk.agents LlmAgent
com.google.adk.runner InMemoryRunner
com.google.adk.agents RunConfig
com.google.genai.types Content Part))
(defn create-agent
"Create a simple ADK agent without tools"
[]
(-> (LlmAgent/builder)
(.name "hello-agent")
(.description "A simple conversational agent")
(.instruction "You are a helpful assistant. Answer user questions politely and concisely.")
(.model "gemini-2.5-flash")
(.build)))
(defn query-agent
"Send a query to the agent and return the response"
agent-instance user-query
(let [run-config (-> (RunConfig/builder) (.build))
runner (InMemoryRunner. agent-instance)
session (-> runner
(.sessionService)
(.createSession (.appName runner) "user1234")
(.blockingGet))
user-content (Content/fromParts (into-array Part (Part/fromText user-query)))
events (.runAsync runner (.userId session) (.id session) user-content run-config)
responses (atom [])]
;; Collect all final responses
(.blockingForEach events
(reify io.reactivex.rxjava3.functions.Consumer
(accept event
(when (.finalResponse event)
(swap! responses conj (.stringifyContent event))))))
;; Return the last response
(last @responses)))
このあたりまではADKのドキュメントをJavaからClojureに翻訳するだけなので、そんなに難しくもないと思います。少しだけ面倒なのはADK for JavaではRxJavaを使っているのでRxJavaを適切に扱うのは少々面倒かもしれません(今回はそこまで踏み込まずブロックしてしまいます)。
3. HTTPサーバーの実装
次に、Ringを使ってHTTPサーバーを構築します。
code:src/hello_adk/core.clj
(ns hello-adk.core
(:require [ring.adapter.jetty9 :refer run-jetty]
[ring.util.response :refer response status content-type]
jsonista.core :as json
hello-adk.agent :as agent)
(:gen-class))
(def agent-instance
"The ADK agent instance, created once at startup"
(delay (agent/create-agent)))
(defn hello-adk-handler
"Handle requests to /hello-adk endpoint"
request
(try
(let [body (json/read-value (slurp (:body request)) json/keyword-keys-object-mapper)
query (:query body "")
agent-response (agent/query-agent @agent-instance query)]
(-> {:status "success"
:query query
:response agent-response}
json/write-value-as-string
response
(content-type "application/json")))
(catch Exception e
(-> {:status "error"
:message (.getMessage e)}
json/write-value-as-string
response
(status 500)
(content-type "application/json")))))
(defn app-handler
"Main application handler"
[{:keys uri request-method :as request}]
(cond
(and (= uri "/hello-adk") (= request-method :post))
(hello-adk-handler request)
:else
(-> {:status "error"
:message "Not found"}
json/write-value-as-string
response
(status 404)
(content-type "application/json"))))
(defn -main
"Start the HTTP server on port 3000"
args
(println "Initializing ADK agent...")
@agent-instance
(println "Starting server on port 3000...")
(run-jetty app-handler {:port 3000 :join? true}))
簡単に動作確認をします。 clj -M:run でサーバーを起動して(認証の周りは後述します)、以下のようなリクエストを送信してみます。最近のGeminiはちょっとした計算くらいなら間違えないので結果を返してくれますね。
code:bash
$ curl -X POST http://localhost:3000/hello-adk -H "Content-Type: application/json" -d '{"query": "938271と19203を足した結果は"}'
{"status":"success","query":"938271と19203を足した結果は","response":"938271と19203を足すと、957474になります。"}
4. 計算ツールの追加
ここまでで、基本的な会話ができるエージェントが完成しました。次に、ADKの強力な機能である「ツール」を追加することにします。ADK Javaでは、ツールは @Schema アノテーションを使ったメソッドとして定義します。Clojureから直接Javaのアノテーションを使うのは難しいため、ADK Javaとの接合部分のみをJavaで実装します。
code:src/java/hello_adk/BinaryOperationTool.java
package hello_adk;
import com.google.adk.tools.Annotations.Schema;
import clojure.lang.IFn;
import java.util.Map;
public class BinaryOperationTool {
private final IFn operationFn;
public BinaryOperationTool(IFn fn) {
this.operationFn = fn;
}
@Schema(description = "Perform a binary operation on two numbers")
public Map<String, String> operate(
@Schema(name = "x", description = "First number")
double x,
@Schema(name = "y", description = "Second number")
double y) {
Object result = operationFn.invoke(x, y);
return Map.of(
"x", String.valueOf(x),
"y", String.valueOf(y),
"result", String.valueOf(result)
);
}
}
このツールはコンストラクタでClojureの関数として fn を取って初期化できるようにしておきます。ツールとなる関数は2つの数値 x と y を受け取り、Clojureの関数を通して計算した結果を返します。@Schema アノテーションでツールの説明やパラメータの情報を定義することで、LLMがこのツールの使い方を理解できるようになります。
5. エージェントにツールを統合
あわせてエージェントを作成する部分も修正しておきます。
code:src/hello_adk/agent.clj
(ns hello-adk.agent
(:import com.google.adk.agents LlmAgent
com.google.adk.runner InMemoryRunner
com.google.adk.agents RunConfig
com.google.genai.types Content Part
com.google.adk.tools FunctionTool ; 追加
hello_adk BinaryOperationTool)) ; 追加
(defn create-agent
"Create an ADK agent with a binary operation tool"
([]
(create-agent +)) ; デフォルトは足し算
(operation-fn
;; 関数を渡してツールインスタンスを作成
(let tool-instance (BinaryOperationTool. operation-fn)
(.. (LlmAgent/builder)
(name "calculator-agent")
(description "An agent that can perform binary operations on numbers")
(instruction "You are a helpful assistant that can perform calculations. Use the 'operate' tool when users ask you to calculate operations on two numbers.")
(model "gemini-2.5-flash")
(tools (into-array (FunctionTool/create tool-instance "operate")))
(build)))))
;; query-agent関数は変更なし
これでツールを足すことができました。BinaryOperationTool には適当な関数をなんでも入れることができるので、Clojureの柔軟性を活用した実装をすることができます。
ビルドと実行
ビルドしてつくったAI Agentを起動します。
code:bash
$ clj -P
$ clj -T:build build
$ gcloud auth application-default login
$ GOOGLE_GENAI_USE_VERTEXAI=TRUE \
GOOGLE_CLOUD_PROJECT=your-project-id \
GOOGLE_CLOUD_LOCATION=global \
clj -M:run
シンプルな会話(ツールなし)。
code:bash
curl -X POST http://localhost:3000/hello-adk \
-H "Content-Type: application/json" \
-d '{"query": "こんにちは!"}'
計算ツールを使った会話。
code:bash
curl -X POST http://localhost:3000/hello-adk \
-H "Content-Type: application/json" \
-d '{"query": "5と3を足してください"}'
まとめ
ADK for Javaを利用してAI Agentを作ることができました。これでClojureからADK for Javaを利用することは不可能ではないということが示せたかなと思います。ただ、説明を省略しましたがRxJavaとの繋ぎこみを適切に行うことや、Dev UIなどを起動しようとするともう少し面倒だったりします。
プロジェクトさえ立ち上げてしまえば、あとはほとんどClojureを書くだけの幸せが訪れるのでおすすめです。