3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI駆動開発における「認知的負債」をtextlintで減らす

3
Posted at

はじめに

AIにコードを書かせる開発では、docs/ 以下にMarkdownで設計書や作業計画を置くことが増えます。

たとえば、次のような文書です。

  • プロジェクトの要件
  • 実装方針
  • 作業計画
  • レビュー観点
  • テスト方針
  • AIへの作業指示

これらの文書は、人間が読むためだけのものではありません。AIエージェントも次の作業で読みます。

そのため、AI駆動開発では docs/ 以下のMarkdownが、AIへの指示や判断材料になります。

これは便利です。ただし、しばらく運用していると問題が出てきました。

AIが書いたドキュメントが、だんだん読みにくくなっていきました。

文法的には間違っていません。Markdownとしても整っています。でも、読むと疲れます。

この記事では、この問題を AI駆動開発における認知的負債 と呼び、textlint とAIエージェントを使って減らした方法を書きます。

最初の違和感

最初は、小さな違和感でした。

AIが書いたドキュメントに、次のような語が増えていきました。

 taxonomy
 evidence
 contributor
 reviewer
 closeout
 signoff
 canonical path
 low-context reader

英語のまま書かれた語もあれば、日本語としては少し硬すぎる語もあります。

もちろん、これらの語が常に悪いわけではありません。必要な場面では使えばよいです。

問題は、普通の日本語で書ける場所にまで入り込むことです。

たとえば、次のような文です。

docs の taxonomy を定義します。
Issue / PR evidence を保存します。
contributor と reviewer の責務を整理します。

意味は分かります。しかし、日本語の開発ドキュメントとしては少し読みにくいです。

次のように書いた方が読みやすくなります。

docs の分類を定義します。
Issue / PR の記録を保存します。
共同作業者とレビュー担当者の役割を整理します。

1文だけなら大きな問題ではありません。

しかし、このような語が docs/ 全体に少しずつ混ざると、読む負担が増えていきます。

なぜ放置すると悪化するのか

AI駆動開発では、ドキュメントが次のAI出力の材料になります。

つまり、読みにくい語が docs/ に残ると、次のAIもそれを読みます。

すると、次のような循環が起きます。

AIが読みにくい語を使う
↓
その語がdocs/に残る
↓
次のAIがdocs/を読む
↓
その語を自然な表現として扱う
↓
さらに似た語が増える

こうなると、ドキュメントだけでなく、AIとの対話も読みにくくなっていきます。

レビューコメントも硬くなります。設計説明も硬くなります。作業計画も硬くなります。

最終的には、意味はあるのに読む気がしない文章が増えます。

これはコードの技術的負債に似ています。ただし、壊れるのはプログラムではありません。

壊れるのは、人間が読むときの集中力です。そして、AIに指示するときの明瞭さです。

プロンプトだけでは解決しにくかった

最初は、AIへの指示で直そうとしました。

taxonomy や evidence のような語を使わないでください。
もっと自然な日本語で書いてください。

しかし、この方法は安定しませんでした。

理由は単純です。

禁止したい語をプロンプトに書くと、その語そのものがAIの入力に入ります。

taxonomy を使わないでください

と書いた時点で、AIは taxonomy という語を読んでいます。

もちろん、AIは「使わないで」という指示も理解します。しかし、実際にはその語に注意が向きやすくなります。

さらに、問題はプロンプトだけではありません。

リポジトリ内のドキュメントにその語が残っている限り、次の作業でもAIはその語を読みます。

つまり、必要だったのは「気をつけて」と言うことではありませんでした。

ドキュメントに混ざった読みにくい語を、機械的に検出する仕組み が必要でした。

textlintで検出する

そこで textlint を使いました。

目的は、文章を完全に自動修正することではありません。

目的は、読みにくい語や表記揺れを検出し、AIや人間が直せる状態にすることです。

実際には、次のようなスクリプトを用意しました。

{
  "scripts": {
    "docs:lint-language": "node scripts/run-doc-language-lint.mjs"
  },
  "devDependencies": {
    "textlint": "^15.5.4"
  }
}

設定ファイルでは、独自ルールだけを有効にしました。

module.exports = {
  rules: {
    'hivelet-doc-language': true,
  },
};

ルール名はリポジトリ固有の名前です。やっていることは単純です。

辞書ファイルを読む
↓
Markdown本文を読む
↓
辞書に登録された語を見つける
↓
警告を出す

複雑な自然言語処理はしていません。辞書に登録した語を見つけるだけです。

ただ、それで十分でした。

実行結果はAIが読める形にする

textlint の結果は、AIエージェントにも読ませます。

そのため、textlint をそのまま実行するのではなく、ラッパースクリプトを挟みました。

実際のスクリプトでは、textlint をJSON形式で実行しています。

const result = spawnSync(
  'pnpm',
  ['exec', 'textlint', '--rulesdir', 'scripts/textlint-rules', '--format', 'json', 'docs/**/*.md'],
  {
    cwd: process.cwd(),
    encoding: 'utf8',
  },
);

その後、JSONを読み取り、警告を次の形に整えます。

<file>:<line>:<column> warning <message> <ruleId>

警告がなければ、次のように出します。

Hivelet docs language lint: no warnings.

この形式にすると、人間もAIも扱いやすくなります。

AIには、次のように指示できます。

pnpm docs:lint-language を実行してください。
警告が出た箇所を、文意を変えずに修正してください。

ポイントは、AIに「どの語を禁止するか」を長々と教えないことです。

AIにはlint結果を読ませます。語彙の一覧は辞書に置きます。

辞書で管理する

読みにくい語は、JSONL形式の辞書で管理しました。

たとえば、次のような行です。

{"pattern":"\\btaxonomy\\b","replacement":"分類"}
{"pattern":"\\bevidence\\b","replacement":"証跡"}
{"pattern":"\\bcontributor\\b","replacement":"共同作業者"}
{"pattern":"\\breviewer\\b","replacement":"レビュー担当者"}
{"pattern":"\\bsection\\b","replacement":"節"}
{"pattern":"\\blow-context reader\\b","replacement":"低コンテキスト読者"}

この例では evidence を「証跡」としています。ここはプロジェクトによっては「記録」の方が自然かもしれません。

大事なのは、一般的に正しい訳語を決めることではありません。自分たちのドキュメントで、何をどの表記に寄せるかを明示することです。

これにより、AIに毎回こう指示しなくて済みます。

taxonomy は分類と書いてください。
contributor は共同作業者と書いてください。
reviewer はレビュー担当者と書いてください。

代わりに、次のように言えます。

pnpm docs:lint-language を実行してください。
警告が出たら、文意を変えずに修正してください。

禁止したい語をプロンプトに並べるのではなく、辞書ファイルに置きます。

これが重要でした。

固定したい表現はbacktickで守る

すべての英語を日本語にしたわけではありません。

ファイル名、設定キー、状態値、イベント名などは、むしろ原文のまま固定した方がよいです。

たとえば、次のような語です。

plan.md
doc_type
Go
No-Go
approval.requested

これらは普通の文ではなく、ファイル名や固定値です。

そのため、次のようにbacktickで囲むルールにしました。

{"pattern":"\\bplan\\.md\\b","replacement":"`plan.md`","requiresBacktick":true}
{"pattern":"\\bdoc_type\\b","replacement":"`doc_type`","requiresBacktick":true}
{"pattern":"(?<!-)\\bGo\\b(?!-)","replacement":"`Go`","requiresBacktick":true,"flags":"g"}
{"pattern":"\\bNo-Go\\b","replacement":"`No-Go`","requiresBacktick":true,"flags":"g"}
{"pattern":"\\bapproval\\.requested\\b","replacement":"`approval.requested`","requiresBacktick":true}

このルールの目的は、表記揺れを減らすことです。

たとえば、同じものを指すのに次のような表記が混ざると、読み手は迷います。

plan.md
計画ファイル
plan file
作業計画

固定のファイル名として参照するなら、次のように書きます。

`plan.md` を更新します。

一方で、普通の日本語までbacktickで囲むのは避けます。

`設計`を確認します。
`レビュー`を実施します。

これは読みづらいです。

backtickは、コード、ファイル名、設定値、状態値などに限定します。

独自ルールは単純にする

独自ルール自体は、できるだけ単純にしました。

実際のルールでは、辞書ファイルを読み込み、Markdownの文字列ノードに対して正規表現を当てています。

const TERMS_PATH = path.resolve(__dirname, 'hivelet-doc-language-terms.jsonl');
const EXTRA_TERMS_ENV = 'HIVELET_DOC_LANGUAGE_EXTRA_TERMS_JSONL';

辞書の読み込みでは、各行をJSONとして読み、pattern を正規表現に変換します。

function parseTerms(source, sourceLabel) {
  return source
    .split('\n')
    .map((line) => line.trim())
    .filter((line) => line !== '' && !line.startsWith('#'))
    .map((line, index) => {
      let term;

      try {
        term = JSON.parse(line);
      } catch (error) {
        throw new Error(`${sourceLabel}:${index + 1}: invalid JSONL entry: ${error.message}`);
      }

      return {
        ...term,
        pattern: new RegExp(term.pattern, normalizeFlags(term.flags)),
      };
    });
}

検出時は、文字列ノードに対して辞書を順に当てます。

return {
  [Syntax.Str](node) {
    const text = getSource(node);

    for (const term of TERMS) {
      term.pattern.lastIndex = 0;

      for (const match of text.matchAll(term.pattern)) {
        report(
          node,
          new RuleError(buildMessage(match[0], term), {
            index: match.index,
          }),
        );
      }
    }
  },
};

ここで重要なのは、ルールを賢くしすぎないことです。

意味の理解はAIと人間のレビューに任せます。textlint は、増えやすい読みにくさを見つける役割に絞りました。

過去文書は対象外にする

今回の運用では、過去文書をlint対象から外しました。

たとえば、.textlintignore には次のような指定を入れました。

docs/archived/**
node_modules/**
coverage/**
tmp/**
test-results/**

過去文書まで全部直そうとすると、差分が大きくなりすぎます。

また、過去文書には「その時点で何が書かれていたか」という意味があります。それを後から大きく書き換えると、履歴としての価値が落ちます。

今回の目的は、すべての文書をきれいにすることではありません。

今後AIが読む現行ドキュメントを読みやすく保つこと です。

そのため、過去文書は対象から外し、現在の入口になる文書やガイドラインを優先して整えました。

校閲作業はAIエージェントに任せた

既存のドキュメントをすべて人間が直すのは大変です。

そこで、実際の校閲作業はAIエージェントに任せました。

ただし、1つのAIに docs/ 全体を読ませることはしませんでした。

それをやると、読みにくい語を大量に読ませることになります。結果として、校閲するAIまでその語に引っ張られます。

そこで、作業を分けました。

進行役のAI
  全体の対象ファイルを決める
  辞書候補を集約する
  最終的な辞書更新を行う

作業担当のAI
  1つのMarkdownだけを読む
  読みにくい語を抽出する
  担当ファイルだけを修正する
  lintを通す

つまり、1つの作業担当AIには、1つのドキュメントだけを渡す ようにしました。

これで、余計な語がAIの入力全体に広がることを抑えられます。

まず語を集め、その後に直す

作業は2段階にしました。

読みにくい語を集める

最初の段階では、AIに本文を直させません。

対象ファイルを1つだけ読ませて、読みにくい語や表記揺れの候補だけを出させます。

見る観点は次のようなものです。

- 普通の日本語で書ける英語
- 意味が広すぎる語
- 文脈に対して硬すぎる語
- 同じ意味なのに表記が揺れている語
- ファイル名や状態値なのにbacktickで固定されていない語

ここではまだ本文を変更しません。

候補を集めたあと、進行役のAIが辞書へ追加するか判断します。

lintを通しながら直す

辞書を更新したら、作業担当のAIに本文を修正させます。

このときの指示は短くできます。

担当ファイルを校閲してください。
pnpm docs:lint-language を実行してください。
警告が出たら、文意を変えずに修正してください。
担当ファイル以外は編集しないでください。

これで、AIに大量の禁止語をプロンプトで渡さずに済みます。

辞書更新は一括で行う

作業担当のAIには、辞書ファイルを直接編集させませんでした。

理由は、辞書がリポジトリ全体の文体に影響するからです。

1つのファイルだけで気になった語を、そのまま全体の禁止語にすると、やりすぎになることがあります。

そのため、作業担当のAIは「辞書追加候補」を出すだけにしました。

作業担当AI:
  この語は何度も出ています
  このように言い換えると読みやすいです
  一時的な辞書で検出できることを確認しました

進行役AI:
  候補を集める
  重複を取り除く
  必要なものだけ正式な辞書に追加する

実装上も、一時的な追加辞書を環境変数で渡せるようにしました。

const EXTRA_TERMS_ENV = 'HIVELET_DOC_LANGUAGE_EXTRA_TERMS_JSONL';

これにより、作業担当のAIは次の流れで候補を検証できます。

新しい候補を見つける
↓
一時辞書として渡す
↓
lintで検出できることを確認する
↓
候補として進行役AIへ返す

この分担により、局所的な判断がそのまま全体ルールになることを避けました。

Before / After

実際のリライトでは、単なる語の置き換えだけではなく、文書の冒頭も書き直しました。

Before

# Documentation And Technical Writing Guide

この文書は、docs を低コンテキスト読者前提で書くための基準である。

- 対象読者: docs を新規作成・更新する contributor、人間 reviewer、orchestrator
- いつ読むか: docs IA を設計するとき、stock doc を書き換えるとき、evidence doc の置き場所を決めるとき
- 位置づけ: docs 運用の正本 guideline

これは、プロジェクト内の文脈を知っている人なら読めます。
しかし、初めて読む人には負担が大きいです。

語が多すぎます。英語も多すぎます。この文書を読んだあとに何をすればよいかも、少し分かりにくいです。

After

# ドキュメントと技術文書の書き方

この文書は、リポジトリ内のドキュメントに書く情報をどこへ置くか、どの文書を現行ルールとして読むか、Issue / PR の記録をどう分けるかを判断するためのガイドです。

新しく文書を書く人、既存の文書を更新する人、人間のレビュー担当者は、本文を書く前にこの文書で置き場所を決めてください。

こちらの方が、初めて読む人にも分かりやすくなります。

何のための文書か。誰が読むのか。読んだあとに何をするのか。

それが最初に分かります。

もう1つのBefore / After

docs/ の入口になる文書でも、同じように直しました。

Before

docs の taxonomy

Hivelet の docs は、まず `正本ドキュメント / 参照資料 / 証跡文書` に分けて読む。

After

docs の分類

docs は、まず `正本ドキュメント / 参照資料 / 記録文書` に分けて読んでください。

ここで大事なのは、taxonomy分類 に置き換えたことだけではありません。

文全体を、読者への指示として自然にしました。

単語を置換するだけでは、読みやすい文書にはなりません。読者がどう読むかまで直す必要があります。

5〜10ファイルごとに人間がレビューした

AIに校閲させたとはいえ、完全自動にはしませんでした。

5〜10ファイルごとに区切り、人間が差分を確認してからコミットしました。

5〜10ファイルを校閲する
↓
lintを通す
↓
人間が差分を見る
↓
問題なければコミットする
↓
次のバッチへ進む

この単位がちょうどよかったです。

全部まとめて直すと差分が大きすぎます。1ファイルずつだと細かすぎます。

5〜10ファイルなら、変更の傾向を見ながら、意味が変わっていないか確認できます。

特に注意したのは、AIによる意味の変化です。

たとえば、次のような修正は危険です。

修正前: エラー時は必ず再試行します。
修正後: エラー時は必要に応じて再試行します。

文章としては自然になっています。しかし、仕様としては意味が変わっています。

textlint はこの違いを検出できません。そのため、人間のレビューは必要でした。

効果

この仕組みで、いくつかの効果がありました。

AIへの指示が短くなった

以前は、AIに次のような指示を毎回していました。

taxonomy は使わないでください。
evidence は証跡と書いてください。
contributor は共同作業者と書いてください。
reviewer はレビュー担当者と書いてください。

しかし、この指示自体が禁止したい語をAIに読ませています。

今は、次のように言えばよくなりました。

pnpm docs:lint-language を通してください。

語彙の管理を、プロンプトから辞書へ移せました。

読みにくい語の再混入を検出できる

一度直しても、AIはまた似た語を使います。

人間がレビューで毎回気づくのは難しいです。

textlint で検出できれば、再混入を機械的に見つけられます。

ドキュメントが普通の日本語に戻った

一番大きい効果はこれです。

「意味はあるが、読むと疲れる文」が減りました。

抽象的な語をすべて消したわけではありません。必要な語は残しました。

ただ、普通の日本語で書けるところは普通の日本語に戻しました。

それだけで、かなり読みやすくなりました。

限界

textlint は意味を理解しません。

辞書に一致した語を見つけるだけです。

そのため、次のような文は通ってしまいます。

関係者間の理解差分を低減します。

禁止語が含まれていなければ、lintは何も言いません。しかし、この文はまだ少し読みにくいです。

また、辞書に入れた語が常に悪いとも限りません。

たとえば「記録」と書くべき場面もあれば、「証跡」と書いた方が正確な場面もあります。
「レビュー担当者」と書いた方がよい場面もあれば、職種名として reviewer を残すべき場面もあります。

だから、辞書は増やしすぎない方がよいです。

textlint は文章を完成させる道具ではありません。

増えやすい読みにくさを早めに見つける道具 として使うのがよいです。

まとめ

AI駆動開発では、docs/ 以下のMarkdownは単なる成果物ではありません。

AIが次に読む入力です。

そのため、AIが書いた読みにくい語を放置すると、次のAI出力にも混ざります。そして、ドキュメント、レビュー、AIとの対話が少しずつ読みにくくなります。

これは、AI駆動開発における 認知的負債 です。

今回、その負債を減らすために次の仕組みを入れました。

  • textlintで読みにくい語を検出する
  • 禁止語をプロンプトではなく辞書に置く
  • 固定値やファイル名はbacktickで固定する
  • 過去文書は対象外にし、現行ドキュメントを優先する
  • AIエージェントに1ファイルずつ校閲させる
  • 辞書更新は一括で管理する
  • 5〜10ファイルごとに人間がレビューする

重要なのは、AIに「気をつけて」と言うことではありませんでした。

AIが読むドキュメントそのものを掃除すること。そして、その掃除を続けられる仕組みにすることでした。

textlint は意味を理解しません。

それでも、AI生成ドキュメントを普通の日本語に戻し、認知的負債の増加を止める道具としては十分に役に立ちました。

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?