本記事は リンクアンドモチベーション Advent Calendar 2024 の2日目です🎄
こんにちは! リンクアンドモチベーションでエンジニアリングマネージャーをしております川津と申します🌟
ここ最近は AI を用いた開発もかなり進化してきましたね!
僕は直近では Cline をさわっていたのですが、Cline のコードを読んでみたところ 「似たようなのを作れるんじゃないか?」 と思い、学習の目的で実際に簡単な AI ツール (VSCode Extension) を作ってみました。
実際に書いたコード
実際に書いたコードはこちらです。
- アイデアの大枠は Cline と似た感じだと思います
- コードはゼロから書いているので全く異なります
それなりに意図した通りには動くのですが、あくまで「お試し」として実装したものなので VSCode Extension として公開はしません。(コード生成の精度も微妙ですし)
git clone
して README.md に書いた手順で、ローカルの VSCode (debug mode) で動かせます。
処理の概要
Cline のコードを見た感じ、おおよそ次の動作を生成 AI (Anthropic) で実現している様です。
- ユーザーがチャット欄にコード生成の指示を入力する
-
Cline は生成 AI (Anthropic) を呼び、次のコンテキストを入力する
a) ユーザーのプロンプト指示
b) システムプロンプト (コード生成の手順の指示)
c) コード分析・出力のための tools - 生成 AI は、システムプロンプトで推奨された次の手順でコードを生成する
- はじめに、AI は既に何の情報を持っていて、何がコード生成に足りないか
<thinking>
tag 内に計画を出力する - 次に
list_files
tool でプロジェクトの構造とファイルの一覧を取得する - AI は
search_files
tool で正規表現でマッチしたファイル一覧を取得し、変更すべきファイルを特定する - AI は
list_code_definition_names
tool で指定した一部のディレクトリ配下のソースコードの定義情報 (e.g. クラス名、メソッド名、関数名...) を取得し、変更すべきファイルを特定する - 変更対象のファイル(だと思われるもの)が見つかったら
read_file
tool でファイル全文を取得する - AI はそれが変更対象と判断すると、コードを生成し、
write_to_file
tool で VSCode ワークスペース上の実際のファイルに書き出しを行う - まだ他にもファイルの変更が必要であれば、これらの手順 を繰り返し実行します
- はじめに、AI は既に何の情報を持っていて、何がコード生成に足りないか
厳密には、Cline は Anthropic の tool use (function calling) API を使っている訳ではなく、似た機構をシステムプロンプトとコード制御で実現していそうに見えました。
UI (VSCode Extension)
このツールは VSCode Extension として作成しています。Extension の作り方は下記公式の通りですが、あまり本記事の本題ではないので割愛します。
システムプロンプト
生成 AI は Anthropic API を使用しています。
以下の通り、システムプロンプトでプロジェクト全体のコードを分析し、生成をするまでの手順を指示しています。
あなたは TypeScript を用いたアプリケーションの開発に精通した AI エージェントです。
あなたはユーザーが指示した内容に従ってソースコードを生成し、そのコードをファイルに書き込むことで、ユーザーが期待する機能を完成させて下さい。
ユーザーは TypeScript 製のアプリケーションの開発を行っており、あなたはそのプロジェクトのワークスペースに tools でアクセスできます。
あなたが、ソースコードを生成するにあたって必要な情報は tools で提供されます。
一例として、次の手順でソースコードを分析し、コードを生成することができます。
1. はじめに必ず \`<thinking>\` タグの中で、すでに持っている情報と、タスクを進めるために必要な情報を評価してください。タスクおよび提供されているツールの説明に基づき、最も適切なツールを選択してください。
2. "list_files" ツールを使用して、ワークスペースの全てのファイルの一覧を取得します。最初の一度は必ずワークスペースのルートディレクトリを指定して下さい!
3. 次に "search_files" ツールを使用して、ファイルの内容が正規表現にマッチするファイルの一覧を取得してください。
4. 次に "list_code_definition_names" ツールを使用して、ソースコード中の定義名(関数名、クラス名、変数名、etc...)の一覧を取得してください。
5. コードのより詳細な内容を知りたい場合は "read_file" ツールを使用して、特定したファイルの内容を読み込むことができます。
6. 読み込んだファイルの内容をもとに、ソースコードを生成してください。
7. 生成したソースコードをファイルに書き込むために "write_to_file" ツールを使用してください。
8. 他にも変更の必要なファイルがある場合は、再度 2〜7 の手順を踏んでください。
見ての通り TypeScript プロジェクトにしか対応していないです😅
tool use (function calling)
tool の定義
前述のシステムプロンプトで AI にツールを使用する様に指示をしていました。Anthropic API では次の様に AI が利用できるツールを提供できます。
実際に書いたツール定義のコードは以下の通りです。
tools: [
{
'name': 'list_files',
'description': 'VSCode ワークスペースのルートディレクトリから再帰的にファイル一覧を取得します。',
'input_schema': {
'type': 'object',
'properties': {
'path': {
'type': 'string',
'description': `検索対象のディレクトリです。VSCode ワークスペースのルートディレクトリ "${workspacePath.fsPath}" からの相対パスで指定して下さい。`,
},
},
'required': ['path'],
},
},
{
'name': 'search_files',
'description': '指定したディレクトリから再帰的にソースコードファイルを読み込み、ファイルの内容が正規表現にマッチしたファイルパスの一覧を返します。',
... (省略) ...
},
{
'name': 'list_code_definition_names',
'description': '指定したディレクトリから再帰的にソースコードファイルを読み込み、定義名 (関数名、クラス名、変数名、etc...) の一覧を取得します。',
... (省略) ...
},
{
'name': 'read_file',
'description': '指定したパスのファイル内容を読み込んで返します。',
... (省略) ...
},
{
'name': 'write_to_file',
'description': '指定したパスのファイル内容の全文を書き換えます。',
... (省略) ...
},
],
これらの意味としては次の tool 定義になります。 AI はこれを解釈してきちんと引数値も渡してくれます。
tool name | params | 説明 |
---|---|---|
list_files |
・path | VSCode ワークスペースのルートディレクトリから再帰的にファイル一覧を取得します。 |
search_files |
・path ・regex ・file_pattern |
指定したディレクトリから再帰的にソースコードファイルを読み込み、ファイルの内容が正規表現にマッチしたファイルパスの一覧を返します。 |
list_code_definition_names |
・path | 指定したディレクトリから再帰的にソースコードファイルを読み込み、定義名 (関数名、クラス名、変数名、etc...) の一覧を取得します。 |
read_file |
・file_path | 指定したパスのファイル内容を読み込んで返します。 |
write_to_file |
・file_path | 指定したパスのファイル内容の全文を書き換えます。 |
AI の tool 呼び出し(要求)
AI が tool の利用を判断した場合、生成 AI は type = tool_use
の応答を返します。
以下の例では、AI は ワークスペースルート配下のファイル一覧をくれ と言っております。
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "はい、承知しました。記事カード一覧の顔アイコンの隣にある★マークをハートマーク(♥)に変更するタスクを実行します。\n\nまず、関連するファイルを特定し、必要な変更を行いましょう。\n\n<thinking>\n1. 記事カード一覧に関連するコンポーネントファイルを見つける必要があります。\n2. そのファイル内で★マークを使用している箇所を特定し、ハートマークに置き換える必要があります。\n3. まずは、プロジェクト内のファイル一覧を取得し、関連するファイルを探します。\n</thinking>\n\nまず、プロジェクト内のファイル一覧を取得しましょう。"
},
{
"type": "tool_use",
"id": "toolu_01AX3FUAC3tfjRDrY1JRGBjH",
"name": "list_files",
"input": {
"path": "."
}
}
]
},
これに対し私達が実装するプログラム側では、次の様に ツールを実行した後の応答 を与えて再度、生成 AI の呼び出しを行います。
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01AX3FUAC3tfjRDrY1JRGBjH",
"content": "/Users/yuusuke-kawatsu/src/dev-portal/README.md\n/Users/yuusuke-kawatsu/src/dev-portal/amplify.yml\n/Users/yuusuke-kawatsu/src/dev-portal/app/api/cache/faceicon/[hash]/route.test.ts\n/Users/yuusuke-kawatsu/src/dev-portal/app/api/cache/faceicon/[hash]/route.ts\n ... (ファイル一覧が続くが省略) ..."
}
]
},
↑ で渡しているファイルパスの一覧は、実際には list_files
tool に対応させた以下のコードで取得しています。
単に glob で再帰的にパスを一覧をしているだけの簡易なコードです。
import path from 'path'
import { findTargetFiles, pathOfWorkspace } from '../../util/utils'
/**
* @param dirPath これは対象ディレクトリの相対パス.
* @returns (一貫性がないが) ここではルートからの絶対パスのファイルパス一覧を返す.
*/
export async function listFiles(dirPath: string): Promise<string[]> {
// glob pattern.
const workspacePath = pathOfWorkspace()
const globPattern = path.join(workspacePath.fsPath, dirPath, '**', '*')
const paths = await findTargetFiles(
[globPattern],
['node_modules/**'],
false,
)
return paths
}
【補足】list_code_definition_names
tool
他の tools は非常にシンプルですが list_code_definition_names
は多少難しいコードになっています。
この tool 実装では、プロジェクトのファイルを再帰的に捜査し、tree-sitter 構文解析器を用いて AST を生成してコード定義(クラス名、メソッド名、関数名、...) を取得しています。
本ツールでは以下の通り XML 形式の文字列として出力しています。
<files>
<file>
<path>app/api/cache/faceicon/[hash]/route.ts</path>
<definitions>
<function>async function GET(</function>
</definitions>
</file>
<file>
<path>app/api/members/route.ts</path>
<definitions>
<function>async function GET(request: NextRequest) {</function>
</definitions>
</file>
... (省略) ...
<file>
<path>src/util.ts</path>
<definitions>
<function>function zip(...arrays: any[][]): any[][] {</function>
<function>function first(...items: any[]): any | null {</function>
<function>function parseIntParam(</function>
<function>function hash(str: string): string {</function>
</definitions>
</file>
</files>
わりと適当に実装したので、クラス名などが失われておりますが...💦
生成 AI の繰り返し実行(自問自答)
実際に動いている所を見てみましょう。
AI はコードの生成に至るまで、何度も tool use (function calling) を行います。これは Anthropic API の繰り返し呼び出しになります。
実際に最初のユーザー指示から最終的にコード生成(出力)がされるまでの生成 AI コンテキストをデバッグ取得して一覧してみました。
[
{
"role": "user",
"content": "★マークを記事カード一覧の顔アイコンの隣に配置してるんだけど、それを消して、ハートマークに変えてもらえますか?"
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "はい、承知しました。記事カード一覧の顔アイコンの隣にある★マークをハートマーク(♥)に変更するタスクを実行します。\n\nまず、関連するファイルを特定し、必要な変更を行いましょう。\n\n<thinking>\n1. 記事カード一覧に関連するコンポーネントファイルを見つける必要があります。\n2. そのファイル内で★マークを使用している箇所を特定し、ハートマークに置き換える必要があります。\n3. まずは、プロジェクト内のファイル一覧を取得し、関連するファイルを探します。\n</thinking>\n\nまず、プロジェクト内のファイル一覧を取得しましょう。"
},
{
"type": "tool_use",
"id": "toolu_01AX3FUAC3tfjRDrY1JRGBjH",
"name": "list_files",
"input": {
"path": "."
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01AX3FUAC3tfjRDrY1JRGBjH",
"content": "/Users/yuusuke-kawatsu/src/dev-portal/README.md\n/Users/yuusuke-kawatsu/src/dev-portal/amplify.yml\n/Users/yuusuke-kawatsu/src/dev-portal/app/api/cache/faceicon/[hash]/route.test.ts\n/Users/yuusuke-kawatsu/src/dev-portal/app/api/cache/faceicon/[hash]/route.ts\n ... (ファイル一覧が続くが省略) ..."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "ファイル一覧を確認したところ、記事カード一覧に関連しそうなファイルは `/Users/yuusuke-kawatsu/src/dev-portal/components/article_card.tsx` だと推測されます。このファイルの内容を確認しましょう。"
},
{
"type": "tool_use",
"id": "toolu_01ArrgnW46LsozJMNpQBvpJW",
"name": "read_file",
"input": {
"file_path": "components/article_card.tsx"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01ArrgnW46LsozJMNpQBvpJW",
"content": "import React from 'react'\nimport Image from 'next/image'\nimport { List } from 'postcss/lib/list'\n ... (コード本文が続くが省略) ..."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "ファイルの内容を確認しました。★マークは以下の行で使用されています:\n\n```tsx\n<span className=\"absolute top-0 left-0 text-yellow-400 text-lg\">★</span>\n```\n\nこの★マークをハートマーク(♥)に変更しましょう。また、色も赤色に変更します。\n\n以下のように変更します:\n\n```tsx\n<span className=\"absolute top-0 left-0 text-red-500 text-lg\">♥</span>\n```\n\nそれでは、ファイルを更新しましょう。"
},
{
"type": "tool_use",
"id": "toolu_01QxoqCNwxVmwNkFc396kzP9",
"name": "write_to_file",
"input": {
"file_path": "components/article_card.tsx",
"content": "import React from 'react'\nimport Image from 'next/image'\nimport { List } from 'postcss/lib/list'\n ... (コード本文が続くが省略) ..."
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01QxoqCNwxVmwNkFc396kzP9",
"content": "The file was written successfully."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "変更が正常に適用されました。記事カード一覧の顔アイコンの隣にある★マークがハートマーク(♥)に変更され、色も赤色(text-red-500)に変更されました。\n\n変更内容をまとめると:\n\n1. `components/article_card.tsx` ファイル内の以下の行を変更しました:\n - 変更前: `<span className=\"absolute top-0 left-0 text-yellow-400 text-lg\">★</span>`\n - 変更後: `<span className=\"absolute top-0 left-0 text-red-500 text-lg\">♥</span>`\n\n2. マークを★から♥に変更しました。\n3. 色を黄色(text-yellow-400)から赤色(text-red-500)に変更しました。\n\nこれで、記事カード一覧の顔アイコンの隣にハートマークが表示されるようになりました。変更を確認し、期待通りの結果が得られているかご確認ください。他に変更や調整が必要な点がありましたら、お知らせください。"
}
]
}
]
折角作ったのに list_code_definition_names
等は呼ばれていないですね...😭
おしまい
この様な記事を読んで頂きありがとうございました✨
本ツール自体は大分荒い実装のため実用には全く耐えないですが、僕自身最近のコード生成 AI の仕組みを知る良い機会になりました!