理系学生日記

おまえはいつまで学生気分なのか

JavaプロジェクトでSerenaとClaude Codeが実現するセマンティックなコード理解

Claude Codeをとても便利に使っていて、実質的なコードや各種設定を自分の手で書くことが相当少なくなってきた。 仕組みというか、流行り言葉で言えばガードレールなんでしょうけど、その辺りを整えておくと楽だわ〜という感じになる。

でもそこには限界もあって、特にコード規模が大きくなってくるとClaude Codeによるコーディングも結構破綻していく。あるいは破綻しないまでも、そもそもAIが学習していないような内製フレームワークとか、共通コンポーネントとか、そういうものを使わせようとするとしんどい。Few-shotでコンポーネントを利用するコードサンプルを渡してあげても、「サンプルの引数に何を渡せばいいのかわかんねぇ...」というAIの悲鳴が聞こえてくる感じ。

それで注目されてるのがSerenaだ。話題になってから何周も過ぎた気がするけど、Language Server Protocol(LSP)とModel Context Protocol(MCP)を統合することで、既存のLLMをIDEレベルの機能を持つエージェントへと変化させてくれるオープンソースのツールになっている。実際良い。Serena。やめられない。

でも、僕はSerenaが何をしてくれてるのか、表面しか理解してなかった。そういうわけで、SerenaがClaude Codeに提供する具体的な機能とか使ってみた感じとかを書いていく。

AIコーディング支援における課題

大規模言語モデルの登場で、コード生成やバグ修正といった開発タスクにAIが活用されるようになってきた。でも、既存の多くのAIコーディングツールには根本的な問題がある。

一般的なLLMはテキストの予測と生成には優れているものの、実際の開発環境と真に相互作用する能力を欠いている。特に大規模で複雑なコードベースにおいて、AIが既存プロジェクトの複雑な依存関係を理解して適切な箇所に正確な変更を加えることは難しかった。多くの場合、AIはファイル全体を読み込んだり、単純な文字列検索や置換をする必要があって、これは大規模プロジェクトでは非効率だし、トークン消費も膨大になる。結果として前にやってたことを忘れるし、完全忘却とまでいかなくても精度が落ちる。

従来のアプローチには主に2つのパターンがある。1つはファイルベースのアプローチで、AIがファイル全体を読み込んで内容を理解し、変更を加える方法だ。このアプローチは小規模なプロジェクトでは十分に機能するが、数百または数千のファイルを含む大規模プロジェクトでは効率が極端に低下する。

もう1つはRAG(Retrieval-Augmented Generation)ベースのアプローチで、コードをベクトル化してセマンティック検索を使う方法。でも、(相当な工夫をしない限り)テキストの意味論的類似性に基づいて動作するので、コードの構造的関係やメソッドシグネチャを正確に理解できないという限界がある。コードは意味論だけで記述できるわけじゃない。

Serenaとは何か

Serenaは、Serena自体が新しいLLMを提供するんじゃなくて、既存のLLMを強化するツールだ。 Model Context Protocol(MCP)を通じてClaude CodeなどのMCP対応エージェントと統合され、セマンティックなコード取得と編集ツールを提供する。MCPはLLMがツールやデータソースと連携するための標準プロトコルで、Serenaはこのプロトコルを介してLSPの機能をClaude Codeに提供している。これにより、Claude CodeはIDEのような機能を持つようになって、ファイル全体を読み込んだり不器用な文字列置換をする必要がなくなる。代わりに、シンボルレベルでコードを理解して精密な操作ができるようになる。

  • この定数ってどこに定義されている?
  • このメソッドのbodyを書き換えてほしいんだけど

コードのセマンティクスを理解するためには各言語それぞれへの対応が必要だけど、Serenaは各言語への抽象化レイヤとしてLSPを利用する。これで、Python、Java、TypeScript、PHP、Go、Rust、C/C++などLSPに対応した多様なプログラミング言語をサポートできる。

LSP による統合アーキテクチャ

Serenaの技術的優位性は、MCPとLSPという2つの重要なプロトコルを巧みに統合したアーキテクチャにある。

Language Server Protocol(LSP)による構造的理解

SerenaにおけるLanguage Server Protocolは、コードの意味論的理解を提供する「頭脳」の役割を担っている。VS Codeなどにおける「定義へ移動」や「すべての参照を検索」といったインテリジェントな機能を支えているのがこれだ。

LSPを活用することで、Serenaは単純なテキスト検索を超えた理解を実現する。ここで言うシンボルとは、関数、クラス、変数、メソッド、定数といったプログラミング言語における構成要素を指す。LSPはこれらのシンボル間の複雑な関係をプロジェクト全体にわたって理解できるようになる。この言語サーバーは独立したプロセスとして実行されて、プログラミング言語に関する深い知識を持っている。抽象構文木やコンパイラシンボルといった言語ドメインモデルを扱ってくれるので、コーディングエージェントが個々の言語のASTを気にする必要がない。

LSPが提供する機能

LSPが提供する主要な機能には以下のようなものがある。textDocument/definitionリクエストはシンボルの定義位置を取得して、IDEの「定義へ移動」機能に対応する。textDocument/referencesリクエストはシンボルのすべての参照を検索して、「すべての参照を検索」機能を実現する。これらの機能は、言語サーバーがコードの構造的理解を持っているからこそ実現できる。

MCPとLSPの統合によって、Serenaはエージェントがファイル全体を読み込んだり、不器用な文字列置換をする必要をなくしてくれる。代わりに精密なIDEライクなツールを使えるようにすることで、トークンと時間を節約して、大規模プロジェクトにおけるコード変更の品質を向上させている。

Serenaが提供する機能

Serenaが提供する機能は、セマンティックなコード分析と編集のための包括的なツールセットになっている。と言われてもイメージわかないので、いくつか例をあげる。

最も重要なツールの1つがfind_symbolだ。実際に、コーディングとかするときに(Claude Codeが)よく使う。 このツールは、指定された名前または部分文字列を含むシンボルを検索するもので、型でフィルタリングもできる。grepやテキスト検索とは異なり、コードのセマンティックな構造を理解した上で、関数名、クラス名、変数名などを識別してくれる。なので、大規模なコードベースにおいても関連するコードを迅速に特定できる。

find_referencing_symbolsは、指定された位置のシンボルを参照しているシンボルを検索するツールだ。このツールはIDEにおける「すべての参照を検索」機能に相当する。例えば、ある関数を変更する際に、その関数がコードベース内のどこで使われているかを把握できる。

get_symbols_overviewは、指定されたファイル内で定義されているトップレベルシンボルの概要を取得するツールで、プロジェクトの構造を素早く理解するために使われる。AIがコードベースのアーキテクチャを把握するのに役立つ。

このようなツールセットによって、AIは「この関数はどこで定義されているか」「この変数はどこで使われているか」「このクラスにはどのメソッドがあるか」といった質問に、IDEと大体同じ精度で答えられるようになる。この構造的理解は大規模プロジェクトにおいて重要で、AIが依存関係を正確にナビゲートして適切なコード変更をすることを可能にする。

実用的な使用シナリオ

ここまでSerenaが提供するツールセットを見てきたが、これらのツールが実際のソフトウェア開発においてどう役立つのかを具体例で見ていく。Serenaの実用的なアプリケーションは、既存の大規模コードベースのメンテナンス、リファクタリング、機能追加といった、構造的理解が重要となるタスクにおいて顕著な効果を発揮する。

複雑なリファクタリング

複雑なリファクタリングは、Serenaの強みが最もよく表れる使用例の1つだ。例えば、プロジェクト全体で使われている関数の名前を変更する場合を考えてみる。従来のアプローチでは、AIは検索と置換を使うか、すべてのファイルを読み込んで手動で変更を加える必要があった。でもSerenaを使えば、find_referencing_symbolsツールでその関数の使用箇所を特定して、それぞれの場所へ適切な変更を加えられる。

深層的なデバッグ

複雑なバグは複数のファイルや関数にまたがるコールスタックを通じて発生することがある。Serenaを使うことで、AIはバグの発生箇所から出発して、find_symbolfind_referencing_symbolsを組み合わせて関連するコードを追跡できる。例えば、ある変数が予期しない値を持っている場合、その変数がどこで初期化され変更され使われているかを段階的に追跡して、バグの根本原因を特定できる。従来であれば開発者が手動でやる必要があった依存関係の追跡を、AIが自動的に実行してくれるわけだ。

Javaへの適用

ここまでSerenaの仕組みと理論的な効果を説明してきたが、実際のプロジェクトでどの程度機能するかが重要だ。Serenaを使った成功事例はPythonへの適用例が多く、Javaに対する適用例は僕の可測範囲では見たことがない。 実際、GitHubのREADMEにも次のように気になる記述がある。

Java (Note: startup is slow, initial startup especially so. There may be issues with java on macos and linux, we are working on it.)

GitHub - oraios/serena: A powerful coding agent toolkit providing semantic retrieval and editing capabilities (MCP server & other integrations)

ただ、使ってみた感じ、問題なく使えそうだ。

LSP

Serenaが内部で使うJava用のLSPサーバはEclipse JDT Language Serverで、自前のJavaコンパイラ基盤(JDT Core)を使ってソースの解析をする形になっている。これ、VS CodeのJava用拡張であるvscode-javaでも使われているものなので、その信頼性たるやという感じ。

遅延

Serenaの公式READMEにもあるように、Javaに対してはstartup is slow, initial startup especially so. ということで、どのくらい遅いのかと思って試してみた。対象リポジトリはこちらで、2つのバックエンドアプリケーション、1つのフロントエンドアプリケーションが同梱されたmonorepoになっている。

Serenaはダッシュボードを持っていて、そこで起動ログも確認が取れるんだけど、LSPを初期化するのに大体6秒といったところだった。早くはないが、我慢できない時間ではない。どうせTwitter(X)やってる時間のほうが支配的だろ。

INFO  2025-10-19 12:31:10,329 [SerenaAgentExecutor_0] serena.agent:stop:336 - Language server initialization completed in 6.133 seconds
INFO  2025-10-19 12:31:10,330 [SerenaAgentExecutor_0] serena.agent:stop:336 - Task-1[init_language_server] completed in 6.135 seconds

モノレポを読み解けるか

上記の通り、今回解析対象としたexample-chatはmonorepoであるので、それにSerenaが対応できるのかも確認する。backendアプリケーションとnotifierアプリケーションがリポジトリ内に存在するので、「backendとnotifierで、DBアクセスの差異を分析して」とだけ指示して解析させた結果が以下の通り。monorepoであっても十分に対応できていることがわかる。

(略)
  4. 主な差異のサマリー

  | 項目       | backend                           | notifier                |
  |----------|-----------------------------------|-------------------------|
  | RDB      | PostgreSQL (UniversalDao)         | なし                      |
  | Redis    | LettuceRedisClient (Nablarchラッパー) | RedisClient (Lettuce直接) |
  | 接続管理     | フレームワーク管理                         | 明示的な初期化/破棄が必要           |
  | データソース数  | 15ファイル (RDB中心)                    | 1ファイル (Redis のみ)        |
  | 永続化データ   | Account, Message, Channel等        | 認証トークンのみ (一時データ)        |
  | SQL使用    | あり (SQLファイル使用)                    | なし                      |
  | Entity使用 | あり (JPA風のEntity)                  | なし (値オブジェクトのみ)          |

  5. 設計上の違いの理由

- backend:
  ビジネスデータの永続化が必要なため、RDBをメインに使用。一時データ(WebSocketトークン、パスワードリセット等)にはRedisを使用
- notifier: WebSocket通知のための認証トークン検証のみが目的。永続化は不要で、高速アクセスが必要なためRedisのみを使用

  この差異は、各モジュールの責務の違いを反映した適切な設計といえます。

Claude Codeの解析プロセスの内部

この解析がどのように行われたかを理解するため、Claude CodeとSerena間の実際のやり取りを可視化してみる。たった一文の指示「backendとnotifierで、DBアクセスの差異を分析して」が、Claude Codeによってどのような段階的な調査プロセスに展開されたのか。

以下のシーケンス図を見ると、Claude Codeが段階的にコードベースを探索していることがわかる。最初にlist_dirfind_fileでプロジェクト構造を把握し、次にsearch_for_patternでDAOやフレームワークの使用状況を調査する。その後ReadツールやGrepツールで個別ファイルの内容確認、get_symbols_overviewでクラス構造を取得、最終的にデータベース設定ファイルやSQLスキーマまで読み取っている。この段階的なアプローチは、人間の開発者がコードベースを探索する過程に似ており、SerenaのLSPベースのツールセットと従来のファイル操作ツールを組み合わせることで実現されている。

diagram

効率の悪いDBアクセス解析

SerenaのLSPベースの解析能力は、単なるコード構造の理解だけでなく、パフォーマンス問題の検出にも応用できる。「効率が悪いDBアクセス部分を抽出して、それに対する改善策を示せ」という抽象的な指示をClaude Codeに与えてみた。Claude CodeはSerenaを用いてコード解析して、2分かけることなく、次のような問題を探し出した。

  主な問題点

  1. N+1問題: ループ内での繰り返しDB操作
  2. 過剰なクエリ: JOINを使えば1回で済むデータを複数回に分けて取得
  3. 非効率な削除: SELECT後にDELETEする2段階処理

  改善による効果

  - クエリ数の削減: 最大66%削減
  - ネットワークオーバーヘッドの削減: ラウンドトリップ回数の減少
  - パフォーマンス向上: 特に高負荷時やネットワークレイテンシが高い環境で顕著
  - データベース負荷の軽減: 接続数とクエリ処理回数の削減

  実装の優先順位

  1. 高優先度: N+1問題(#1) - 最も影響が大きい
  2. 中優先度: 頻繁に呼ばれる検索処理(#2, #5) - ユーザー体験に直結
  3. 低優先度: 削除処理の最適化(#4) - 頻度は低いが改善の余地あり

具体的には、ここで検知されたN+1問題は次の通りだった。まぁ、確かに問題となるケースはある。

  // 現在のコード:
  public void deleteMembersChannel(AccountId accountId) {
      Channels membersChannel = channelRepository.findMembersChannel(accountId);
      for (Channel channel : membersChannel.asList()) {
          delete(channel.id(), accountId);  // ループ内で毎回DBアクセス
      }
  }

  public void delete(ChannelId channelId, AccountId accountId) {
      ChannelMember channelMember = new ChannelMember(accountId);
      channelMemberRepository.remove(channelId, channelMember);
      messageRepository.remove(channelId, accountId);
      readMessageRepository.remove(channelId, accountId);
  }
  • 問題点
    • チャンネルごとにループして、各反復で3回のDB削除操作を実行
    • n個のチャンネルがある場合、3n回のDB操作が発生
  // 改善策:
  public void deleteMembersChannel(AccountId accountId) {
      Channels membersChannel = channelRepository.findMembersChannel(accountId);
      List<Long> channelIds = membersChannel.asList().stream()
          .map(channel -> channel.id().value())
          .collect(Collectors.toList());

      // バッチ削除に変更(1回のクエリで複数レコード削除)
      channelMemberRepository.removeByAccountIdAndChannelIds(accountId, channelIds);
      messageRepository.removeByAccountIdAndChannelIds(accountId, channelIds);
      readMessageRepository.removeByAccountIdAndChannelIds(accountId, channelIds);
  }
  # 対応するSQLクエリ(新規作成が必要):
  DELETE FROM channel_member
  WHERE account_id = :accountId
    AND channel_id IN (:channelIds[])

必要なメソッドの探索

内製フレームワークや共通コンポーネントを使用する際、開発者はしばしば「この機能を実装するためのフックポイントはどこか」という疑問に直面する。記事の冒頭で述べた課題がまさにこれだ。「WebSocketが開かれた時の挙動をカスタマイズするメソッドは?」という質問を投げかけてみた。このような探索的なタスクにおいても、Claude CodeがSerenaのツールセットをどのように使い分けているかをシーケンス図で可視化すると、段階的なコード理解のプロセスが見えてくる。

diagram

まとめ

SerenaとClaude Codeの組み合わせは、AIコーディング支援における課題を解決してくれそうだ。従来のファイルベースやRAGベースのアプローチでは難しかった、大規模コードベースのセマンティックな理解が、LSPとMCPの統合で可能になった。

SerenaがLSPを活用することで実現しているのは、単なるテキスト検索を超えた構造的なコード理解だ。シンボルの定義位置や参照箇所の特定、依存関係の追跡といった、IDEが提供してきた機能をAIエージェントに与えることで、Claude Codeは人間の開発者がやるような段階的なコード探索プロセスを実行できるようになっている。

実際のJavaプロジェクトでの検証でも、SerenaはREADMEで警告されているような致命的な問題なく動作して、monorepo構造への対応、N+1問題の検出、WebSocketメソッドの探索といった実用的なタスクで結果を出してくれた。初期化に数秒かかるという遅延も、大規模プロジェクトにおけるトークン消費の削減や解析精度の向上と比較すれば許容できる範囲だろう。

SerenaのようなLSPベースのアプローチが普及することで、内製フレームワークや共通コンポーネントを使った開発においても、AIエージェントがより効果的に機能するようになるはずだ。コードの構造的理解という基盤があれば、Few-shotの例示も、AIへの指示も、より的確に機能するだろう。