AIエージェントのススメ 第7回 技術知識の重要性

Markdownフォーマッター開発の失敗談の続きです。文字列置換ではなくAST(抽象構文木)ベースの処理が必要だった理由と、AIに丸投げせず自分で問題を考えることの大切さについて解説します。

発行

著者 高津戸 壮 テクニカルディレクター
AIエージェントのススメ シリーズの記事一覧

フォーマッターを作らせようとして失敗したもう1つの原因

Markdownのフォーマットを整えるフォーマッターをClaude Codeに作らせようとして失敗した話を、前回・今回と2回に分けて紹介しています。前回は失敗の原因の1つ「サブエージェントに対する理解不足」を取り上げました。今回は、もう1つの原因である「テキスト処理に対する理解の浅さ」について見ていきます。

前回の記事では、サブエージェントが<dl><dt><dd>のフォーマットを文字列置換で実装してしまったという話をしました。たとえばこんな実装です。

文字列置換による実装(イメージ)

function formatDlDtDd(html) {
  return html
    .replace(/<dl>/g, "<dl>\n")
    .replace(/<dt>/g, "  <dt>")
    .replace(/<dd>/g, "  <dd>");
}

この実装の問題点は、サブエージェントがHTMLのネスト構造を理解していないということでした。<details>の中に<dl>がある場合、<dt><dd>は4スペース(1段深く)インデントされるべきですが、文字列置換ではそういった文脈を考慮することができません。多機能なMarkdownフォーマッターを作るという今回のケースでは、より包括的な仕様を把握した上での実装判断が求められるので、サブエージェントに部分的な実装を投げる形ではうまくいかない結果になってしまったわけです。つまり、筆者のサブエージェントへの理解が不十分だったということです。

そしてここで、筆者は1つ重要な点を見落としていました。

筆者が見落としていたこと

それは「そもそも文字列置換でこの問題は解決できない」という技術的な事実です。

前回の記事で、サブエージェントがこの問題に対して文字列置換という方法を選んでしまったという話をしましたが、今振り返ると、筆者自身がこの問題の本質を理解していなかったといえます。

文字列置換というのは、テキストを単なる文字の並びとして扱います。たとえば、「<dt>という文字列を見つけたら、その前にスペースを付ける」という処理です。しかしHTMLやMarkdownは構造を持ったテキストです。タグがネストしていて、親子関係があります。この構造を無視して文字列置換だけで処理しようとすると、どうやっても限界があるわけです。

正規表現でやる方法もないわけではないでしょう。さまざまな処理を組み合わせてインデントの深さをカウントすることで解決できるかもしれません。しかし正直に告白すると、筆者はこの開発を「TDD-developer」というサブエージェントに任せきりにしていました。前回の記事でも触れましたが、このサブエージェントは「まずテストを書いてから実装する」という指示を初期化プロンプトに設定してあり、筆者としては「これで任せておけばうまくいくだろう」と考えていたのです。

そんなわけで、筆者は実際にはPCの前にずっといたわけではありません。サブエージェントに実装を任せ、ゲームをしたり、別の作業をしたり、時々戻ってきては結果を確認し、「なんでまだできてないんだ……」とClaude Codeに文句を言う。そういうことを繰り返していました。

そして戻ってきて見てみると、AIは確かに複雑な正規表現を使ってネストの深さをカウントしようとしていたりしました。次のようなコードです。

正規表現でネストを追跡する試み(イメージ)

function formatWithNestTracking(html) {
  let depth = 0;
  return html.replace(/<(\/?)(\w+)([^>]*)>/g, (match, slash, tag, attrs) => {
    if (slash) {
      depth--;
      // 閉じタグの処理...
    } else {
      // 開きタグの処理...
      depth++;
    }
    // インデントの計算...
  });
}

しかし、何度やってもうまくいきません。テストが通らない。1つ直すと別のケースが壊れる。その繰り返しでした。なぜか?

構造化されたテキストを扱う賢い方法

HTMLやMarkdownというのは、ただの文字列ではなく、構文規則に従った構造化されたテキストです。そのような構造化されたテキストを扱うには、実は開発の世界では定番とも言えるスマートな方法があります。それが「構文解析(パース)」して「AST(Abstract Syntax Tree: 抽象構文木)」という形式に変換し、その構造を理解した上で処理するというアプローチです。

ASTを使ったHTML/Markdownの処理

ASTを使ったHTML/Markdownの処理については、CodeGridの次の連載が参考になります。

たとえば、次のようなHTMLがあったとします。

HTMLのコード

<details>
  <summary>用語集</summary>
  <dl>
    <dt>用語1</dt>
    <dd>説明文</dd>
  </dl>
</details>

これを文字列として見ると、ただの文字の並びです。しかしASTとして見ると、次のような木構造になります。

ASTとして見た場合

details
├── summary
│   └── "用語集"(テキスト)
└── dl
    ├── dt
    │   └── "用語1"(テキスト)
    └── dd
        └── "説明文"(テキスト)

この木構造を理解していれば、<dt><dd><details>の中の<dl>の中にあるということがわかります。つまり、インデントは2段分必要だということが、構造から自明に導き出せるのです。

技術選定の放棄

そういうのがASTなわけですが、筆者はこのASTベースの処理というアプローチを知らなかったわけではありません

先ほど紹介したCodeGridの連載「unifiedとrehypeによるHTMLの加工」、実はこれを書いたのは筆者自身です。つまり、unified/remark/rehypeを使ったAST処理については、連載記事を書けるぐらいには理解していたはずなのです。

では、なぜこのフォーマッター開発で最初からそのアプローチを取らなかったのか? 答えは単純で、この問題をどう解決するか、そもそも考えようとしていなかったからです。

「Markdownのフォーマッターを作りたい」という課題に対して、「どういうアプローチで実装すべきか」を真剣に考えることなく、「まぁClaude Codeがなんとかしてくれるだろう」と丸投げしてしまった。詳細はよくわからないけど、AIがいい感じにやってくれるはず──そう思っていたのです。

問題を分解して考えるということ

最終的に完成したフォーマッターは、次のようなアプローチを取っています。

  1. Markdown/MDXのテキスト全体をASTとしてパースする
  2. ASTの各ノードを1つずつ処理する
  3. 各ノードの処理は独立しているので、個別に調整できる

たとえば、HTMLブロックのフォーマットには、Prettierを使っています。ASTの中から「これはHTMLだ」というノードを見つけたら、その部分だけをPrettierに渡してフォーマットしてもらう。ほかのノードには影響しません。

ASTを走査して各ノードを処理

visit(ast, (node) => {
  if (node.type === "mdxJsxFlowElement") {
    // JSX要素の処理
  }
  if (isHtmlElement(node)) {
    // HTMLはPrettierでフォーマット
    // 他のノードには影響しない!
  }
});

このアプローチのよいところは、問題を小さく分解できるという点です。「Markdownをフォーマットする」という大きな問題を、「見出しの後に空行を入れる」「HTMLをきれいに整形する」「JSXの属性を1行ずつ並べる」といった小さな問題に分解し、それぞれを個別に解決していく。1つの処理を変更しても、ほかの処理には影響しない。

文字列置換では、こうした分解ができません。テキスト全体を1つの文字列として扱うので、ある置換が別の置換に影響してしまう。だから「1つ直すと別が壊れる」という無限ループに陥っていたのです。

問題解決への向き合い方

先ほど書いたように、筆者はこの開発中、Claude Codeに丸投げしてゲームをしたりしていました。そして戻ってきて結果を見ては、「なんでできてないんだ」とClaude Codeに文句を言っていた。前回の記事でも「サブエージェントの使い方を誤った」という話をしましたが、今振り返ると、その解決策をサブエージェントに求めるという態度自体が、開発に向き合っていなかったことの表れだと思われます。

Xなどを見ていると最近AI関連でよく見かけるのは、「○○という機能が出てヤバい。これを知らないと人生を無駄にしています」みたいなポスト……。そんなことあるわけないでしょう。

機能を知らないからできない、というわけではありません(もちろん、機能は知っていたほうがいいですが)。根本的な原因は、自分自身が問題をどう解決するか、考えることに向き合っていない、考えようとしていないということです。

実際には、この実装に1週間以上かかりました。その大部分は、間違ったアプローチで延々と試行錯誤していた時間──というか、筆者がゲームをしている間にAIが間違った方向で格闘していた時間です。方向性が間違っているソースコード一式がそこにあると、AIはそれを軌道修正するのが難しくなります。既存実装に文字列置換がたくさんあれば、「文字列置換でやるんだな」というバイアスがかかってしまいますからね。

問題解決のプロセスをスキップしない

この経験から筆者が学んだことは、問題をどう解決するかを考えるプロセスは、スキップできないということです。

AIエージェントは確かにすごいです。コードを書いてくれるし、デバッグもしてくれるし、ドキュメントも作ってくれます。しかし、「この問題をどういうアプローチで解決するか」という判断は、結局のところ人間がしなければなりません。

この解決のためにAIを補助的に利用することはできます。Claude CodeにはPlanモードという機能がありまして、使い方は単純。Shift + Tabを押すたびにモードが順番に切り替わり、そのうちの1つがPlanモードです。このPlanモードのとき、Claude Codeはコードを編集したりは基本的にしません。これからやる手順を相談するモードで、あれこれ対話した末に、こういう方法で実装するのがよいか? と聞いてくる、計画を練るモードです。

このPlanモードを使ってもよいですし、最近筆者はそういう実装方法などで悩んだ場合、「3つのサブエージェントにWeb検索などで方法を調査させ、それらから得られた結果をまとめて提案させる」というような方法を使ったりしています。サブエージェントは並行起動できるのでこの方法はオススメです。もちろんこういうのはすぐに別のより良い方法が出たりするものですが、AIをどう使うかという観点で参考にしていただければと。

結局のところ、AIはいろいろとやってくれるわけですが、根っこの指示がブレているといつまでもブレ続けたままということが言えるでしょう。そのための計画を練るステップというのは重要です。

AIエージェントは万能ではない

この連載の初回で、「AIエージェントに手放しで任せておけば万事解決! とはまったくもってなりません(重要)」と書きました。今回の失敗談は、まさにその具体例と言えるでしょう。

AIエージェントは強力なツールですが、それを使いこなすのは人間です。どの方向に進むべきかを判断し、適切な指示を出すのは人間の役割です。問題をどう分解するか、どんなアプローチを取るか──そういった「考える」部分は、AIに丸投げすることはできません。

「AIがあるから勉強しなくていい」ではなく、「AIを効果的に使うために、自分で考える」という姿勢が大切と言えるのかもしれません。そしてそうやってどこまでをAIに任せられるのか? の塩梅がだんだんと体で理解できてきた時、AIエージェントは自分の手足のように使いこなせる感覚が得られると筆者自身は感じています。

今回は、Markdownフォーマッター開発の失敗から、AST処理という技術的な話と、「問題解決のプロセスはスキップできない」という姿勢の話をしました。AIに丸投げしてゲームをしていた1週間は、まさに反面教師といったところでしょうか。

次回は、AIの出力を人間がチェックしきれないという問題について取り上げます。AIが速すぎて確認が追いつかない──この悩みに対して、視点を変えることで解決の糸口を探ります。AIエージェントとの開発はまだまだ日々発見があるので、引き続きお付き合いいただければと思います。