0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenCodeを使ったAIハーネスのための最初の一歩

0
Posted at

OpenCodeを使ったAIハーネスのための最初の一歩

はじめに

最近よく見るAIハーネスについて読むことが増えました。コンセプトはなんとなく理解していたのですが、いまいち実践となるとわからなかったので今回、主にAIハーネスにおける最初の一歩として、OpenCodeのPluginsシステムを使ってHookを設定してみました。

※以下、OpenCodeのPluginsシステムを使って設定しているHookを総じてHookと呼びます

前提

  • 本記事はAIに書かせていません。私が最初から書いています
  • 本記事はAIによる添削を行なっています
  • OpenCodeを使った説明と実践になります
  • OpenCodeのPlugins機能を使ったHookの考え方や設定の仕方についての説明記事になります
  • 正しさを保証する記事ではないです

AIを使った開発の問題点

AIを使った開発はエンジニアの仕事を変えました。今後もAIは開発プロセスを早めるツールとして根を張っていくと思います。しかしスピードと引き換えに、別の問題も出てきました。

  • AI生成コードの正確性
  • AIが生成したコードのレビューコスト

これらが主な問題点として挙げられます。
(そのほかに、エンジニアスキルが育たないことなど、問題はあります。ここでは主に開発に直接関わっている部分にフォーカスします)

これの解決策として、近年AIハーネスというのが注目されています。

そもそもAIハーネスとは?

ここ最近よく見る言葉ですね。いわゆる"ハーネスする"というやつです。AnthropicもOpenAIも関連した記事をよく出しています。

実は、AIハーネスというのは定義自体は決まってないです。どこからどこまでをAIハーネスとするのかはエンジニア次第です。
しかし大枠の考え方としては、「AIにルールや制約をかける(手綱=ハーネス)ことで、AIの出力をより正確にする」というものです。AIの性能が良くなるからといってどんどんそのままタスクをやらせるのではなく、出力の品質を保証する形で制限をかけていこうということですね。
これを実現する上で大切な考え方が、

  • ルール・ガイドライン
  • ワークフロー
  • 進捗メモ・decision log などの永続メモリ
  • フック
    の4つです。

ルール・ガイドライン
AGENTS.md/CLAUDE.mdを中心とした全体的なルールファイルのことです。
AIにまとって欲しいルールやガイドラインをまとめておくと、これ1つでも一定の制限をAIの出力にかけることができます。

ワークフロー
/plan/work/review のようなSkillsを使った定型フローを実践させることで結果の再現性を高くすることができます。

進捗メモ・decision log などの永続メモリ
AIが何をしたのか、何をさせたのか、何が決定されたのかについてはドキュメントに残しておくことで、AI自身がそれをコンテキストとして参照できるようにします。そうすると、すでにやったことを避けるようになったり過去の決定をより尊重するようになります。

フック
(ファイル保存時にフォーマッター、タスク完了時にテスト実行など)
ユーザーがプロンプトを投げた時や、AIが修正を完了させたときなどのイベントをフックで拾えます。このフックを使うことでリンターを走らせたり、別のコマンドを呼んでトークン消費を抑えたり、幅広いことが実現可能です。

これらを組み合わせることで「長時間」「複数セッション」で、「暴走せず(暴走を抑制して)」、「品質を維持」して仕事させるための環境を作ることができます。

ものすごく極端な話、AIハーネスをちゃんと設定すれば、コーディングタスクのコーディング開始からUT実装までを自動でやらせることもできると思います。
しかしこの記事では、完全な自動化にはフォーカスしません。あくまでAIハーネスはこうやってできるのか、とりあえずここから始めればいいのか、という観点からHookの利用方法を通して、読者の皆さんに考え方が伝わればと思います。

なぜOpenCodeなのか?

今回、Hookを実装するのにOpenCodeを選びました。
しかし実際問題、Hookを実装するのであれば、Claude CodeやCodexの方がすっきりした形で実装できます。また、世の中はAI Coding Agent戦国時代で、こういう記事を書くならClaude CodeやCodexの方が適切かもしれません。
しかし、今回はあえてOpenCodeを選んでいます。理由は以下になります。

  • OpenCodeの方がHookの設定が泥臭い
    • OpenCodeのPluginsシステムはとても柔軟です
    • 柔軟な分だけ、やらないといけないことも多くなります
    • 一見面倒ですが、考え方を覚えるにはとても応用の効くものになっています
  • 全員がAnthropicやOpenAIのモデルやサービスにアクセスがあるわけじゃない
    • AIハーネスでは色々なモデルやエージェントを使い分けるパターンが普通になりつつあります
    • 現在は安いモデルも出ていますし、全員がClaude CodeやCodexをフルで活用できるほどのお金を出せるわけではありません
    • 接続できるLLM/Providerが豊富で、かつ無料で使えるAIコーディングエージェントであるOpenCodeを使うことでより幅広いユーザーにコンセプトが伝えられると判断しました

※しかしこの記事はOpenCodeの記事ではないので、詳しい設定や説明は行いません。

AGENTS.mdについて

実践に入る前に、OpenCodeを含むAI Coding Agent共通の重要概念としてAGENTS.mdについて説明します。

この記事では基本的にはHookを中心としていますが、それでも外せないのはAGENTS.mdです。
ざっくりいうと、AGENTS.md(Claudeの場合はCLAUDE.md)とは、セッション間で共有される指示を書いておくファイルのことです。

AIのチャットを開くと会話ができますが、各チャットはセッションと呼ばれるものです。これはAIを使った開発(AI Coding Agentを使った開発)でも同じことです。しかし開発を行っていると、同じ指示をセッションの最初に行うことが出てきます。さらにそれが出てくると、それが増えていきます。AGENTS.mdはセッションを開かれるたびに読み込まれるため、共通指示を書いておけます。

とても簡単な機能ですが、AIハーネスでは大きな役割を担う機能です。

Hookの実践

考え方

AIハーネスの文脈におけるHookの役割は主にAIの実装結果に対して検証やテストを実行することです。これはAIの修正前後にCLIなどを実行してリンターやフォーマッター、ユニットテストを実行させることで実現します。

もちろん、検証やテストの実行自体はAI自体ができます。
しかし、そもそもAIは自身の実装には自信を持っているようで、自己評価をさせても自信満々で問題ないと回答する傾向にあります。

A second issue, which we haven’t previously addressed, is self-evaluation. When asked to evaluate work they've produced, agents tend to respond by confidently praising the work—even when, to a human observer, the quality is obviously mediocre. This problem is particularly pronounced for subjective tasks like design, where there is no binary check equivalent to a verifiable software test. Whether a layout feels polished or generic is a judgment call, and agents reliably skew positive when grading their own work.

https://www.anthropic.com/engineering/harness-design-long-running-apps#:~:text=A%20second%20issue,their%20own%20work.

(そういう意味では人間に似ている気がします。私も自分の実装に自信があれば、当然そのように回答します。それで間違っていたことなんて数えきれないほどあります)

また、CLIの実行は「確実に」行われます。しかしAIに依頼したCLI実行は「ほぼ確実に」行われます。一見すると問題ないようですが、100%実行される処理と98%実行される処理では、特に検証においては大きな違いです。AIへの依頼はごく稀にスキップされることがあります。コードの検証はAIに依頼するのではなく極力CLIを使ったコマンド実行に頼った方が安全です。特にHookなどに組み込める場合、Hookはあくまでプログラムなので、AIへの依頼と違い100%実行されます。AIに自己評価させる・テストを実行せよと依頼するとしても、やらなかった場合にしっかりと結果を見ていなければ、誤った状態のコードをpushすることになりかねません。

なのでこの記事ではHookの実例として、Pythonアプリケーションにおけるリンターなどの実行をセットアップします。

当然Hookは今回の実例以外の役割も持たせることができます。実装ができれば、用途は無限です。ここではわかりやすい例としてスタンダードな実装検証を例に出します。

テックスタック

進める前に、前提となるプロジェクトの認識を揃えましょう。

  • OpenCodeをAI Coding Agentとして使う
  • Pythonプロジェクト
  • プログラミング言語、環境はmiseを使う
  • pythonのパッケージ管理はuvを使う
  • pythonのリンター兼フォーマッターとしてruffを使う
  • タスクランナーとしてTaskを使う
  • pythonにおけるデータのモデリングと型付けにpydanticを使う
  • pythonの型付け確認用にmypyを使用
  • pythonにおいてimportの方向性を制限するためimport-linterを使う
  • commitやpush時にCLIを実行するためにpre-commitではなくlefthookを使う
  • pythonの単体テストを行うためにpytestを使う

完成したプロジェクトはこちら
リポジトリのREADME.mdを確認いただければ、それぞれがどんな意図でそこにいて、リポジトリ自体を見れば実装そのものを見ることができます。

この記事では、手元にPythonプロジェクトがあることを前提にします。
では、そのプロジェクトでの開発フローを整理してみましょう。

開発時の状況を考える

開発時にはいろいろな間違いが発生します。これを避けるための古典的なものがレビューやリンターの導入、フォーマッターの導入、そして単体テストです。これはプロジェクトがでかくなればなるほど事実でしょう。これらはすでに開発の現場ではスタンダードに必要なものになっていて、ツールもたくさんあります。AIが本格的に開発タスクを行えるようになる前では、pre-commitlefthookで自動でこれらが実行されるようにしているプロジェクトも多かったと思います(今もそのまま使っていると思います)。

人間が開発をしている分には、そのタイミングの確認で良かったと思います。エンジニアが自分でコードを書いて、気になる人はcommitやpushをする前に自分でCLIでリンターやフォーマッター、単体テストのコマンドを実行して問題がないか確認し、問題がなければcommitやpushに行けました。これを忘れても、commitやpushしたときにコマンドが実行されて、エンジニア自身がこれに気づいて修正できます。

AIに開発をさせると、このフローに微妙な歪みが出ます。
というのも、AIが作成したコードを全面的に信頼するVibe Codingでない限り、生成されたコードはcommitされる前にあなたのローカルで生成され、あなたによって確認されます。しかし、AIに明確に指示をしない限り、コマンドを実行して自分で書いたコードが正しいのか、最低限のチェックさえ実行してくれません。クソ優秀だが新人エンジニアみたいなものです。

これをAIの動作の中に組み込んで自動でやらせよう、というのが基本的な考え方です。
そうすれば、修正完了してから人間がコマンドを実行してから間違いに気づくのではなく、AIが修正を実行した後に勝手にコマンドを実行して間違いに気づき、勝手に直してくれます。ここまでやってくれたら嬉しくないですか?あなたがAIのコードをレビューする頃には、あなたが用意した一通りのチェックを完了させている状態です。そうすれば、レビューは本当に関係のある部分に集中できます。また、AIも自分の間違いに気づけるため、指摘されなくても修正を行えるため、試行錯誤の時間を減らすことができます。

ここの例では以下2つのツールをAIの修正後に実行させるようにセットアップします。

  • ruff
  • mypy

少し寄り道ですが、個人的にはコマンド実行は同じインターフェースに集約している方覚えやすく、実行もしやすいように感じます。そのため、ここではTaskを導入して、Taskfile.ymlで定義したコマンドを実行するようにしています。

# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: "3"

vars:
  GREETING: Hello, world!

tasks:
  onboarding:
    desc: Print a greeting message
    cmds:
      - echo "{{.GREETING}}"
    silent: true

  check:lint:
    desc: Call ruff for linting
    cmds:
      - uv run ruff check {{.CLI_ARGS}} --output-format json
      # - uv run ruff check {{.CLI_ARGS}}

  check:imports:
    desc: Call lint-imports for import checks
    cmds:
      - uv run lint-imports

  check:types:
    desc: Call mypy to check for type errors
    cmds:
      - uv run mypy . --output json

  check:tests:
    desc: Run pytest
    cmds:
      - uv run pytest

  check:
    desc: Call all checks
    cmds:
      - task check:lint
      - task check:imports
      - task check:types
      - task check:tests

上記を見て貰えばわかるように、ruffの実行はtask check:lint、mypyの実行はtask check:typesのように行えます。

さて本題に戻ります。

OpenCodeのプラグインシステムの詳細はこちらを確認いただけると詳しくわかると思いますが、ここでは基本的なことだけ説明します。

OpenCodeにおけるプラグインは、必ずルートディレクトリに.opencode/ディレクトリを作成し、.opencode/plugins/ディレクトリの中に、JavaScriptもしくはTypeScriptファイルとして作成する必要があります(これは変わる可能性があります)。

ファイルの中身は基本的に以下の形をとります(自分はTypeScript派なのでtsファイルを作成しています)。

import type { Plugin } from "@opencode-ai/plugin";

export const YourPlugin: Plugin = async ({
  $,
  directory,
  client,
}) => {
  // NOTE: 各Hookイベントを定義したオブジェクトを返却する

  return {
	// NOTE: ここにHookイベントに合わせた処理を定義していく
  };
};

YourPluginは複数の引数をとりますが、詳しくはドキュメントを確認してください。
よく使うのが

  • $
  • directory
  • client
    の3つです。
    OpenCodeではBunを実行環境として使っています。この$はBunのAPIを通してCLIなどのコマンドを実行するためのものです。
    directoryはルートディレクトリまでのパスが渡されています。実行ディレクトリを指定したいときなどに役に立ちます。
    clientはAIと対話するためのOpenCode SDKクライアントです。OpenCodeの操作および接続されているLLMへ追加情報を渡したりすることができます。

これらを使って、以下のことが実践できます。

  • 適切なタイミングのイベントを監視するようにオーバーライド
  • 対象のツール実行か確認
  • コマンドの実行
  • 実行結果を確認し、OpenCodeを操作もしくはLLMに情報を追加

今回はリンター/フォーマッターであるruffと、型付けに対して処理が適切に扱われているかを確認するmypyが実行されるようにしたいです。なので、上記のフローは以下のように考えて分解できます。

  • AIがツール実行を完了した後(AI Coding Agentがコードを修正したりするのは、AI Coding Agentが持っているツールを通して行われます)
  • ツールが編集や書き込みであった場合に
  • task check:XXを実行して
  • 結果、問題がなければ問題がなかったことを通知してHook処理を抜ける
  • 問題があればその問題を修正依頼とともにAIに返却する。

では、実際に組み立ててみましょう。

Hookの設定

まずはAIがツール実行を完了した後に実行されるHookをオーバーライドします。

import type { Plugin } from "@opencode-ai/plugin";

export const RuffLintAfterEditPlugin: Plugin = async ({
  $,
  directory,
  client,
}) => {
  return {
    "tool.execute.after": async (input, output) => {
    }
  };
};

これにより、OpenCodeがファイルを作成したり編集したりを完了させた後に、こちらが好きな処理を実行できるようになります。

ツールの確認

実行されたツールにはファイルを読み込むREADから編集のPATCHまで様々あります。
今回作成しようとしてるHookでは書き込みと編集の後にruffとmypyを実行することなので、興味あるのは書き込みツールと編集ツールのみです。

import type { Plugin } from "@opencode-ai/plugin";

const EDIT_TOOLS = new Set([
  "edit",
  "write",
  "patch",
  "apply_patch",
  "multiedit",
]);

export const RuffLintAfterEditPlugin: Plugin = async ({
  $,
  directory,
  client,
}) => {
  return {
    "tool.execute.after": async (input, output) => {
      const tool = input.tool;

      if (!EDIT_TOOLS.has(tool)) {
		// NOTE: 書き込み・編集系のAI操作じゃなければPluginの処理を抜ける
        return;
      }
    }
  };
};

コマンドの実行

書き込み・編集系のツール実行以外の場合はHookの処理を抜けるので、ここからはこのHookの実行は書き込み・編集系のツールの実行後だとわかります。

次に、コマンドの実行を行いましょう。今回はruffのlint実行をTaskインターフェースで、Bunの$を通して実行します。

import type { Plugin } from "@opencode-ai/plugin";

const EDIT_TOOLS = new Set([
  "edit",
  "write",
  "patch",
  "apply_patch",
  "multiedit",
]);

export const RuffLintAfterEditPlugin: Plugin = async ({
  $,
  directory,
  client,
}) => {
  return {
    "tool.execute.after": async (input, output) => {
      const tool = input.tool;

      if (!EDIT_TOOLS.has(tool)) {
		// NOTE: 書き込み・編集系のAI操作じゃなければPluginの処理を抜ける
        return;
      }
      
      const result = await $`task check:lint -- ${directory}`.nothrow().quiet(); 
    }
  };
};

このとき、.nothrow().quiet()の実行を忘れないようにしてください。
.nothrow()はコマンド実行にエラーが発生した場合にBun実行側でエラーとして処理されないようにするためのものです。つまりエラーならエラーで、コマンド実行の結果を受け取れるようになります。
.quiet()はコマンド実行時に発生する様々な実行結果を静かにします。特にOpenCodeを使っている場合、.quiet()がないとOpenCodeのTUIが崩れてしまいます。

問題がなければHook処理を抜ける

コマンド実行結果のexitCode0であれば、つまりコマンドの処理はエラーなしで実行が完了された場合、今回のようにruffを実行した場合にはruffでエラーがなかったということです。つまりリンターもフォーマッターも問題がなかったことを意味します。問題がなかったのであれば、Hookの処理を抜けてしまっても良いので、returnしてしまいましょう。ここではOpenCodeでToastを表示してからreturnするようにしています。こうすれば実行時に問題がなかったことがユーザーに表示されます。

import type { Plugin } from "@opencode-ai/plugin";

const EDIT_TOOLS = new Set([
  "edit",
  "write",
  "patch",
  "apply_patch",
  "multiedit",
]);

export const RuffLintAfterEditPlugin: Plugin = async ({
  $,
  directory,
  client,
}) => {
  return {
    "tool.execute.after": async (input, output) => {
      const tool = input.tool;

      if (!EDIT_TOOLS.has(tool)) {
		// NOTE: 書き込み・編集系のAI操作じゃなければPluginの処理を抜ける
        return;
      }
      
      const result = await $`task check:lint -- ${directory}`.nothrow().quiet(); 
      
      if (result.exitCode === 0) {
        await client.tui.showToast({
          body: {
            message: "lint has finished successfully",
            variant: "success",
          },
        });
        return;
      }
    }
  };
};

問題があればその問題を修正依頼とともにAIに返却する

逆に、exitCode0でなければ、つまりruffがエラーを出したということです。つまり静的に分析した結果、コードに問題箇所があったということです。この場合、AIにエラーがあったことを通知します。これはOpenCodeのプラグインが提供するclientを通して行えます。client.session.promptに値を渡すことで、AIにデータを追加で渡すことができます。これを使うことで、AIにruffのエラーを渡すことができます。

import type { Plugin } from "@opencode-ai/plugin";

const EDIT_TOOLS = new Set([
  "edit",
  "write",
  "patch",
  "apply_patch",
  "multiedit",
]);

export const RuffLintAfterEditPlugin: Plugin = async ({
  $,
  directory,
  client,
}) => {
  return {
    "tool.execute.after": async (input, output) => {
      const tool = input.tool;

      if (!EDIT_TOOLS.has(tool)) {
		// NOTE: 書き込み・編集系のAI操作じゃなければPluginの処理を抜ける
        return;
      }
      
      const result = await $`task check:lint -- ${directory}`.nothrow().quiet(); 
      
      if (result.exitCode === 0) {
        await client.tui.showToast({
          body: {
            message: "lint has finished successfully",
            variant: "success",
          },
        });
        return;
      }

      await client.tui.showToast({
        body: {
          message: "Lint failed; sending errors back to the agent",
          variant: "error",
        },
      });

	  const sessionId = input.sessionID;
      if (!sessionId) {
        return;
      }
      
      const errorsJson = result.json();
      const errors = JSON.stringify(errorsJson);
      await client.session.prompt({
        path: {
          id: sessionId,
        },
        body: {
          noReply: true,
          parts: [
            {
              type: "text",
              text: `Fix these lint errors:\n${errors}`,
            },
          ],
        },
      });
    }
  };
};

このとき、現在いるsessionのidが必要になります。このsession idはHookの引数であるinputから取得することができます。このsession idはnullの可能性があるので、nullチェックは必ずする必要があります。

※注意点として、ruffのアウトプットフォーマットをjsonに指定しています。そのためresult.json()を実行できます。しかしフォーマットをjsonに指定していない場合、result.json()はエラーを出します。

完成品

※編集ツール群だけ、.opencode/plugins/utils.tsを作成して移動しました。

// NOTE: ruff
import type { Plugin } from "@opencode-ai/plugin";

import { EDIT_TOOLS } from "./utils";

export const RuffLintAfterEditPlugin: Plugin = async ({
  $,
  directory,
  client,
}) => {
  return {
    "tool.execute.after": async (input, output) => {
      const tool = input.tool;

      if (!EDIT_TOOLS.has(tool)) {
		// NOTE: 編集系のAI操作じゃなければPluginの処理を抜ける
        return;
      }

      // NOTE: Ruff CLIを実行。uvで仮想環境を管理してるため、uvを経由して実行する
      const result = await $`task check:lint -- ${directory}`.nothrow().quiet(); // ⚠️CAUTION: .quiet()がないとOpenCodeのTUIがおかしくなります

      // NOTE: エラーがない(exitCodeが0)であれば完了ログだけを出して処理を抜ける
      if (result.exitCode === 0) {
        // IDEA: ここでclient.session.promptやclient.tui.showToastでpluginの実行の問題がなかったことを通知することもできる
        await client.tui.showToast({
          body: {
            message: "lint has finished successfully",
            variant: "success",
          },
        });
        return;
      }

      // NOTE: エラーがある場合は内容をAIに返す
      //       この時、AIに修正するように指示を出すことで修正まで対応してくれる
      //       注意点として、このHookは上に修正指示の時にも実行される(ループする)
      //       そのため、問題が続くとループに入る可能性もある
      //       ループで修正する、がハーネスの基本だがトークン消費の問題もあるため、
      //       あまりに処理が長い場合は途中で止めるなどをした方が良い
      const sessionId = input.sessionID;
      if (!sessionId) {
        return;
      }
      // NOTE: Toastでユーザーに通知
      await client.tui.showToast({
        body: {
          message: "Lint failed; sending errors back to the agent",
          variant: "error",
        },
      });
      // NOTE: AIにレスポンスを返却する必要がある。返却するレスポンスには
      //       1. sessionId
      //       2. 返却内容
      //       が必要になる。
      const errorsJson = result.json();
      const errors = JSON.stringify(errorsJson);
      await client.session.prompt({
        path: {
          id: sessionId,
        },
        body: {
          noReply: true,
          parts: [
            {
              type: "text",
              text: `Fix these lint errors:\n${errors}`,
            },
          ],
        },
      });
    },
  };
};

同じように、Pythonの型付けをチェックするmypyも、AIが修正を実行した後にコマンドを呼び出して、エラーがあればAIに修正依頼を渡すようにしましょう。

// NOTE: mypy
import type { Plugin } from "@opencode-ai/plugin";

import { EDIT_TOOLS } from "./utils";

export const MypyCheckAfterEditPlugin: Plugin = async ({
  $,
  directory,
  client,
}) => {
  return {
    "tool.execute.after": async (input, output) => {

      const tool = input.tool;

      if (!EDIT_TOOLS.has(tool)) {
        // NOTE: 編集系のAI操作じゃなければPluginの処理を抜ける
        return;
      }

      const result = await $`task check:types`.nothrow().quiet();

      if (result.exitCode === 0) {
        await client.tui.showToast({
          body: {
            message: "type checking has finished successfully",
            variant: "success",
          },
        });
        return;
      }

      const sessionId = input.sessionID;
      if (!sessionId) {
        return;
      }

      await client.tui.showToast({
        body: {
          message: "Type check failed; sending errors back to the agent",
          variant: "error",
        },
      });

      const errorsJson = result.json();
      const errors = JSON.stringify(errorsJson);
      await client.session.prompt({
        path: {
          id: sessionId,
        },
        body: {
          noReply: true,
          parts: [
            {
              type: "text",
              text: `Fix these mypy errors:\n${errors}`,
            },
          ],
        },
      });
    },
  };
};

やることはまるっきり一緒です。違うのは実行されるコマンドがtask check:lintからtask check:typesに変更されて、最後のclient.session.promptにエラーを渡す時の指示文も調整されているくらいで、そのほかは本当に同じです。

Hookのその他の使い方

極力トークン消費量を抑えたい場合、方法の1つとしてCLIの実行結果で自動修正できなかった箇所をログに残してもらって、ログに出てきている問題を自分で修正することです。ログを残しておくのはAIの挙動の履歴にもなりますし、やっぱりAIにやらせようとなった時にそのまま渡せるので、個人的にHookを作る場合にはログ出力を推奨します。実際リンクしているリポジトリのHookを見てもらうと分かりますが、ログ出力を多用しています。これはHookの動作確認や出力結果を見るために使っていて、これ無くして今回Hookを組めなかった気がします。

また、HookはCLI実行だけでなく、AIの操作そのものを制限することもできます。

コマンド実行ができる、ユーザーのinputを確認できる、AIが操作しようとしてるファイルを確認できる、というのはとても便利です。OpenCodeのドキュメントにもある例で、例えばAIが.envファイルを閲覧できなくしたりもできます。

beforeイベントでは引数outputにツールの実行引数が渡されます。

import type { Plugin } from "@opencode-ai/plugin";

export const EnvProtectionPlugin: Plugin = async ({ directory, client }) => {
  return {
    "tool.execute.before": async (input, output) => {
      const tool = input.tool;

      const isReadTool = tool === "read";
      const filePath: string | null = output?.args?.filePath;
      const hasEnvInFilePath = filePath?.includes(".env");
      if (isReadTool && hasEnvInFilePath) {
        const errorMessage = "Cannot read .env file";
        throw new Error(errorMessage);
      }
    },
  };
};

これは強力です。AIが見てはいけないものも指定できれば、プロンプトに不適切な内容があれば(例えばユーザー情報)、それをLLMに送信する前にブロックすることもできます。

やってみた気づき

今回はHookを中心に、AIハーネスの基本のキについてお見せできたかと思います。
この過程で、自分自身でも気づきがたくさんありました。
この気づきに共感できる/興味がある/気になるという方もいると思うので、共有します。

AGENTS.mdは人間向けでもある

これが自分の中では1番の気づきだったかもしれません。
「そんなん当たり前やんけ」と思われるかもしれませんが、AIを使ったコーディングをしていると、AGENTS.mdは「AI向け」という意識が強くなります。しかしAGENTS.mdに「書いた方が良い」内容を見てみると、チーム開発で新人が入ると毎回教えないといけない内容であることが多いことに気づくと思います。AIがREADME.mdを見て追加で判断を下せるように、人間もAGENTS.mdを確認して追加情報として扱えます。そのうち、これらを統合するファイルや考えというのも出てくるかもしれません。

AIハーネスは「最初」にやる

AIハーネスの真髄はプロジェクト単位のアーキテクチャ設計です。
アーキテクチャには正解はないですが、最初にアーキテクチャは決めておかないと後で困ることになります。
AIハーネスを行う場合はまず最初に各設定を組み込んでおきましょう。

これは難しく考える必要がありません。
迷った場合は、こう考えてみると良いです。

  • 自分がチームリーダーで、これからプロジェクトを開始する
  • これから数人でこのプロジェクトを作成していく
  • レビューとかもやらないといけないし、プロジェクトには共通的なルールが必要
  • このルールをみんなに平等に適応した方が良い

これらを実施したい場合は何をしたら良いか?

  • リンターが必要だ
  • フォーマッターが必要だ
  • コーディング規約を用意しよう
  • レビューの時の観点を用意しよう
  • ユニットテストは必須か?許容点は?
    というように、どんどんルールを考えるようになっていきます。
    これがまさにそのままAIハーネスの制約に落とし込めます。

上記を見ていきましょう

  • リンターが必要だ
    • → エンジニア向けには、lefthookやpre-commitでリンターが実行されるようにしよう
    • → AI向けには、Hookでリンターが実行されるようにしよう
  • フォーマッターが必要だ
    • → エンジニア向けには、lefthookやpre-commitでフォーマッターが実行されるようにしよう
    • → AI向けには、Hookでフォーマッターが実行されるようにしよう
  • コーディング規約を用意しよう
    • → AGENTS.mdに記載しよう、人間が見たって良い
  • レビューの時の観点を用意しよう
    • → AGENTS.mdに記載しよう、人間が見たって良い
  • ユニットテストは必須か?許容点は?
    • → 必須なら、CLIを実行するように人間/AI両方向けにセットアップする
    • → AIに自動で修正させたいか?それならHookを通してAIに結果を渡すようにしよう
      このように、自分がリーダーでこれからチーム開発をするんだという観点に立つことができれば、AIハーネスで何が必要なのかが自ずと見えてきます。

コンテキストの肥大化/トークン消費は増える

AIハーネスの根本は制御です。ルールを追加して、事前に必要な指示を入れておいて、AIが期待外の挙動をしないようにレールを敷くことです。そのため、コンテキストが肥大化しやすい。コンテキストが肥大化するということは、その分トークンを消費するということ。他のLLMと組み合わせたり、極力CLIを使うなどでコンテキスト肥大を抑えましょう。

AI開発はそのままでは勉強にはならない

これはAIを使った開発をやったことある人であればみんな共感いただけると思いますが、自分から学びに行かなければ何も覚えません。AIにやらせたので当たり前ですね。仕事やMVPを早く出したいという場合には良いかもしれません。しかし最終的にAIが出したコードに責任を持つのはあなたです。自己研鑽を怠るとどんどん忘れていってしまいます。

Anthropicも、この問題に言及しています。
https://www.anthropic.com/research/how-ai-is-transforming-work-at-anthropic#:~:text=People%20report%20increasing,volume%20(Figure%202).

個人的には、AIが書いたコードを疑って、「なぜ?」をAIと突き詰めたり、同じ実装をリファクタリングしてみたりすると、結構勉強になります。

最も重要な気づき

しかし最も重要な気づきは、「全部はいらない」ということです。
AIハーネスは強力です。そして実際、AIを使った開発が変わっていくとしても、AIハーネスで培われたアーキテクチャや考えは残っていくと思います。
ネットを見ていると今は何でもかんでもAIハーネスで、これをやらないと置いていかれる感覚になりますよね。自分もなってましたし、それが今回の記事を書く原点でした。
しかし、フルグレードのハーネスは全員には必要ありません。AIはこれからさらに進化して、現在のAIハーネスの大きな部分は不要になる可能性があります。自分や自分のチームが必要としてる範囲をブロックのように使うのが最も賢いやり方だと思います。

個人的な意見を述べるなら、AGENTS.mdとHookはAIを開発に組み込む以上、今後も必須になっていくと思います。自分は現在アサインされているプロジェクトや自分で動かしているプロジェクトでは、この2つは必須として用意しています。別にファンシーなことやキラキラしたこと、大層なことはさせなくて良いと思います。ただ愚直に、細かいけど役に立つことをやらせれば良いです。

最後に

最近コーディングタスクはAIに任せていたのですが、今回のHookを調べて組み立てるのは自分で実装してみました。やはりコーディングは楽しいですね。AIハーネスを実践すると人間なんかいらないじゃんって考えになりがちですが、AIを使った開発で空いた時間は、手作業でやる時間に充てるようにしようと思いました。

また、エンジニアには「嗅覚」というものがあると思います。この嗅覚なくして、AIが書いたコードの正当性の評価はできません。面倒な仕事はAIにやらせましょう。それで浮いた時間は、自分の自己研鑽に充てられたら、それが正しい機械の使い方かもしれませんね。

以下、自分が共感したエンジニアの嗅覚に関して述べている記事です↓
https://strategizeyourcareer.com/p/developer-taste-ai-slop
記事では「Taste」という単語が使われていますが、これは英語圏の言い方で、日本語に訳すと「嗅覚」が近いと思います。全部英語ですが、翻訳しながら読んでみてください。エンジニアもマネージャーも、有意義なことが書かれていると思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?