こんにちは!この時期ってこんなに雨多かったっけなーと思いながら、このブログを書いております!

今回も生成AIネタです!

背景

CursorやClineなどのコーディングエージェントのおかげで、開発経験が乏しい私でも開発ができるようになりました。色々使っていくうちに、その実装にも興味が出てきました。

Clineはソースコードも公開されているので、Clineの実装がわかればCursorやWindsurfのようなコーディングエージェントの仕組みも理解できそうです。

結構情報量が多そうなので、ざっくりとわかるレベルでブログにしようと思います。私自身、まだまだ理解しきれていないところがあるので、何か誤りがあったら(優しく)マサカリ投げてください!

Clineとは

ClineはオープンソースのAIコーディングエージェントです。Visual Studio Code拡張機能として提供されており、AIとの対話を通じてコーディング作業を支援してくれます。

主な機能として以下があります

  • ファイルの作成・編集・削除
  • ターミナルコマンドの実行
  • ブラウザの操作
  • 各種AIプロバイダー(Claude、OpenAI、Google等)への対応

このような機能により、AIエージェントが開発者の指示に従って実際のコーディング作業を代行できます。これがコーディングエージェントと呼ばれる理由です。

結論

Clineはユーザーからの入力をFunction Calling(もどき)の仕組みにより、ツール(ファイル作成やコマンド実行などの処理)実行に繋げています。

個人的にはLangChainやLangGraphなどのエージェントフレームワークが活用されているのかと勝手ながら想像していましたが、そんなことはなかったです。

重要な概念

システムプロンプトとは

システムプロンプトは、LLM(大規模言語モデル)の行動を指導する基盤的な指示のことです。ユーザーの入力を処理する前に、LLMに与えられる役割や制約、文脈を定義します。

コーディングエージェントにおけるシステムプロンプトは特に重要で、以下のような内容が含まれます

  • エージェントの役割(コーディング支援AI)
  • 利用可能なツールの定義
  • レスポンス形式の指定(XML形式等)
  • 動作時の制約や注意事項

これは通常のユーザープロンプトとは異なり、システム側で事前に設定される指示文のため、ユーザーからは変更できません。LLMがどのように振る舞うかを決定する重要な要素です。

Function Calling

Function Callingは、LLMがプログラムの関数(ツール)を呼び出すための仕組みです。OpenAIのAPIでも提供されている機能で、LLMが単純なテキスト応答ではなく、構造化されたデータを返すことで、外部の機能を実行できるようになります。

通常のFunction Callingでは、LLMが以下のようなJSON形式で関数呼び出し情報を返します:

{
  "tool_calls": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "arguments": "{\"location\": \"Tokyo\"}"
    }
  }]
}

この情報を元にプログラムが実際の関数を実行し、結果をLLMに返すことで、LLMが外部システムと連携できるようになります。

ツールとは

LLMにおけるツールとは、LLMが実行できる具体的な機能(関数)のことです。実体はプログラムのメソッドや関数で、Function CallingにおけるFunctionに該当します。

Clineでは以下のようなツールが用意されています

  • write_to_file: ファイルの作成・書き込み
  • read_file: ファイルの読み取り
  • execute_command: コマンドラインツールの実行
  • replace_in_file: ファイルの部分編集
  • use_mcp_tool: MCPツールの実行

これらのツールにより、LLMは単純なテキスト応答を超えて、実際のファイル操作やシステム操作を実行できるようになります。

仕組み

Clineの動作フローは以下の通りです:

  1. ユーザーがLLMへ指示内容を入力
  2. LLMはシステムプロンプトに沿って、レスポンスをXML形式で出力
  3. XML形式のレスポンスをパーサーで構造化データに変換
  4. 構造化データを引数としてツールを実行する(ループして必要な分だけ実行される)

詳細

1. ユーザーがLLMへ指示内容を入力

チャット画面などからLLMへ指示内容を入力します。
この部分の詳細は割愛します。

2. LLMはシステムプロンプトに沿って、レスポンスをXML形式で出力

Clineのシステムプロンプトはこちらで確認できます。

レスポンス例:npmコマンドの実行

<execute_command>
<command>npm run dev</command>
<requires_approval>false</requires_approval>
</execute_command>

レスポンス例:新規ファイルの作成

<write_to_file>
<path>src/frontend-config.json</path>
<content>
{
  "apiEndpoint": "https://api.example.com",
  "theme": {
    "primaryColor": "#007bff",
    "secondaryColor": "#6c757d",
    "fontFamily": "Arial, sans-serif"
  },
  "features": {
    "darkMode": true,
    "notifications": true,
    "analytics": false
  },
  "version": "1.0.0"
}
</content>
</write_to_file>

上記の例はレスポンスの全体ではありませんが、ツール実行に必要な部分です。
このようにXML形式でレスポンスが返されるようにシステムプロンプトで指定されています。

システムプロンプトでは以下のようにツール使用のフォーマットが定義されています:

# Tool Use Formatting

Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure:

<tool_name>
<parameter1_name>value1</parameter1_name>
<parameter2_name>value2</parameter2_name>
...
</tool_name>

そして<ツール名>パラメーターという形で、入力に対してどのツールをどのパラメーターで実行すれば良いかを返します。これはFunction Callingと同じことを行っています。

上記の「レスポンス例:新規ファイルの作成」では:

  • write_to_fileというツールを利用する
  • パラメーターpathsrc/frontend-config.json
  • パラメーターcontentファイルの内容

という情報が含まれています。

なお、システムプロンプトにはClineで利用可能なツールが定義されています。
レスポンス例にあったwrite_to_fileexecute_commandはシステムプロンプトでは以下のように定義されています:

## write_to_file
Description: Request to write content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to write to (relative to the current working directory ${cwd.toPosix()})
- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified.
Usage:
<write_to_file>
<path>File path here</path>
<content>
Your file content here
</content>
</write_to_file>
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd.toPosix()}
Parameters:
- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
- requires_approval: (required) A boolean indicating whether this command requires explicit user approval before execution in case the user has auto-approve mode enabled. Set to 'true' for potentially impactful operations like installing/uninstalling packages, deleting/overwriting files, system configuration changes, network operations, or any commands that could have unintended side effects. Set to 'false' for safe operations like reading files/directories, running development servers, building projects, and other non-destructive operations.
Usage:
<execute_command>
<command>Your command here</command>
<requires_approval>true or false</requires_approval>
</execute_command>

これらの内容はFunction Callingがわかれば、ピンとくる内容だと思います。
他にもread_file(ファイル読み込み)、replace_in_file(ファイル編集)、use_mcp_tool(MCPツール実行)などのツールがあります。

個人的にはreplace_in_fileの定義が面白いと思いました。

## replace_in_file
Description: Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file.
Parameters:
- path: (required) The path of the file to modify (relative to the current working directory ${cwd.toPosix()})
- diff: (required) One or more SEARCH/REPLACE blocks following this exact format:
  ```
  <<<<<<< SEARCH
  [exact content to find]
  =======
  [new content to replace with]
  >>>>>>> REPLACE
  ```
  Critical rules:
  1. SEARCH content must match the associated file section to find EXACTLY:
     * Match character-for-character including whitespace, indentation, line endings
     * Include all comments, docstrings, etc.
  2. SEARCH/REPLACE blocks will ONLY replace the first match occurrence.
     * Including multiple unique SEARCH/REPLACE blocks if you need to make multiple changes.
     * Include *just* enough lines in each SEARCH section to uniquely match each set of lines that need to change.
     * When using multiple SEARCH/REPLACE blocks, list them in the order they appear in the file.
  3. Keep SEARCH/REPLACE blocks concise:
     * Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file.
     * Include just the changing lines, and a few surrounding lines if needed for uniqueness.
     * Do not include long runs of unchanging lines in SEARCH/REPLACE blocks.
     * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures.
  4. Special operations:
     * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location)
     * To delete code: Use empty REPLACE section
Usage:
<replace_in_file>
<path>File path here</path>
<diff>
Search and replace blocks here
</diff>
</replace_in_file>

Clineでも「このファイルを〜になるように修正して」と入力すると、ファイルの一部分を編集して差分を提示してくれます。そのファイル編集の実態がreplace_in_fileツールです。このシステムプロンプトを見ると、ファイル編集って地味に実装が大変なところだと思いました。

replace_in_fileツールはパラメーターにdiffとして「編集前の内容」と「編集後の内容」を渡して、置換処理を行っています。もし編集前の内容が1文字でも違っていたらうまく編集されません。そのため、Critical rulesという形でLLMが勝手に編集前の内容をいじらないように厳しくルールを定義しています。Critical rulesは現時点ではreplace_in_fileくらいでしか使われていません。

このようにClineのシステムプロンプトを見ると、どれだけ作り込まれているかを実感できます。

3. XML形式のレスポンスをパーサーで構造化データに変換

ClineはLLMからのレスポンスを元にツール実行を行います。
ただし、XML形式のレスポンスはあくまで一つの文字列であるため、関数であるツールを実行するためには構造化されている必要があります。

そのため、ツール実行の前にXML形式のレスポンスをパーサーで構造化データにパースします。

パーサーの実装はこちらで確認できます。

4. 構造化データを引数としてツールを実行する

ツール実行の引数となるデータ(レスポンスをパースしたもの)を引数として、ツールを実行します。

execute_commandを例にとると、以下のような実装になっています(一部抜粋):

case "execute_command": {
  // パラメーター取得
  let command: string | undefined = block.params.command
  const requiresApprovalRaw: string | undefined = block.params.requires_approval
  const requiresApprovalPerLLM = requiresApprovalRaw?.toLowerCase() === "true"

  try {

      // ... 省略 ...

      // 自動承認判定
      const autoApproveResult = this.shouldAutoApproveTool(block.name)
      const [autoApproveSafe, autoApproveAll] = Array.isArray(autoApproveResult)
        ? autoApproveResult
        : [autoApproveResult, false]

      // ... 省略 ...

      // 実際にコマンドを実行し、結果を取得
      const [userRejected, result] = await this.executeCommandTool(command)

      // ... 省略 ...
}

各ツールの実装はこちらに集約されています。

通常、1回のレスポンスで複数回のツール実行を行います。この機構はループ処理にて実装されており、完了判定がされるとツール実行が終了します。

なぜFunction Calling「もどき」なのか?

一般的なFunction CallingはLLMがツール実行に必要な構造化データを返し、それを元にプログラムが関数を実行するというものです。

[{
    "type": "function_call",
    "id": "fc_12345xyz",
    "call_id": "call_12345xyz",
    "name": "get_weather",
    "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
}]

ClineはなぜかXML形式でレスポンスを返却して、それをわざわざパースして構造化データにしています。
この実装故、私はこれをFunction Calling「もどき」と呼んでいます。

なぜわざわざこの実装になっているのでしょうか?XML形式で返さずに最初から構造化データじゃダメなのでしょうか?

悶々と考えて調査していると、以下のような言及を見つけました:

When your prompts involve multiple components like context, instructions, and examples, XML tags can be a game-changer. They help Claude parse your prompts more accurately, leading to higher-quality outputs.

Anthropicのドキュメントより

プロンプトが複雑になり、要素が多くなった場合はXMLタグによる記法を用いると、LLMが意図したレスポンスを返しやすくなるとのことです。

Clineのシステムプロンプトを見ても要素が多く複雑なので、こういった背景からXML形式→パースの仕様になったのだと推測できます。

最後に

Clineの仕組みをざっくり解説させていただきました!かなり荒削りな解説なので、有識者の方が見ると色々ツッコみたくなる内容かもしれませんが、あくまで初学者にコーディングエージェントの動作のイメージを掴んでもらうことを目的として書いているので、そこはご容赦いただけますと幸いです。

コーディングエージェントの技術はまだまだ発展途上で、今後もより洗練された仕組みが生まれてくると思います。興味のある方はぜひClineのソースコードを実際に読んでみてください!