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

単体テストを書かせるのにチャットは要らなかった

1
Posted at

コンテキストエンジニアリングを本当に身につけたなら

gemini -p 'やって欲しいこと'、あるいはclaude -p 'やって欲しいこと'

これを打ち込んで端末を閉じたあと、気づけばPRが上がっている。そういう世界の話をする。

「AI使えてますアピール」ではない。コンテキストエンジニアリングを理論として知っているだけでなく、設計として実装すると何が起きるかの記録だ。コンテキストエンジニアリングの前提についてはこちらこちらに書いた。概念の話は繰り返さない。


PRが語る

2026年1月、チャット画面を一切開かずに単体テストとStorybookストーリーを量産させた。以下がその記録だ。

各PRにはGeneration Statisticsが記載されている。コンテキストに積まれる構造はこうだ。

UserInstruction + (FunctionCalling + ToolResponse) × N + ModelResponse

ターン数が多いほどコンテキストは膨らむ。それでも機能した理由を説明する。

使ったモデルはgemini-3-flashとgemini-2.5-flashだ。最新の高性能モデルではない。理由は二つ——安いことと、コンテキストを乗せやすいことだ。モデルの優劣の話は後述する。


何をしたか——二つのスクリプト

実体は二本のPythonスクリプトだ。

scripts/python/tests/generate_unit_test.py
scripts/typescript/tests/generate_storybook.py

設計は同じ構造を持つ。二フェーズアプローチだ。

Phase 1: セッション初期化
  → Conductorにファイル一覧を渡す
  → edit_todosでTODOリストを作成させて即終了

Phase 2: TODOを一件ずつ処理
  → セッションJSONからunchecked TODOを読む
  → [taktコマンド](https://github.com/s-age/pipe/blob/main/src/pipe/cli/takt.py)でConductorに次の指示を送る
  → PIDファイルの消滅をポーリングして完了を検知
  → 次のTODOへ

重要なのはPhase 1だ。Conductorに「全部やれ」と渡さない。TODOリストを作らせて即終了させる。 処理はスクリプトが一件ずつ管理する。これによってコンテキストの膨張を制御している。


Conductorとは何か

roles/conductor.mdに定義されているロールだ。その原則はシンプルだ。

The Conductor MUST NEVER perform any processing directly. All processing MUST BE delegated to sub-agents exclusively via takt.

Conductorは処理しない。委譲するだけだ。taktというCLIを通じてサブエージェントを起動し、結果を待つ。それだけだ。

taktのインターフェースはこうなっている。

# 新規セッション開始
takt --purpose "..." --background "..." --roles "roles/engineer.md" --instruction "..."

# セッション継続
takt --session <SESSION_ID> --instruction "..."

スクリプトはこのtaktコマンドを組み立てて実行するだけだ。Conductorへの指示の中身も、invoke_serial_childrenの全パラメータを展開済みで渡す。曖昧さを排除する。


モデルの優劣より、コンテキストの質を考えろ

LLMがやっていることを一言で言えば、コンテキストを解析して、それらしいパターンに再構築することだ。

モデルは学習データの中から「このコンテキストが来たら、次はこういうパターンになる」という確率分布を持っている。入力を受け取り、その分布に従って出力を生成する。それだけだ。

つまり問うべきは「どのモデルが賢いか」ではなく、「どのパターンをどれだけ正確に提供できるか」だ。

モデル選択の誤った問い: 「Claude 4とGemini 3、どちらが優秀か」
モデル選択の正しい問い: 「このコンテキストを正確に解析できる規模のモデルはどれか」

gemini-3-flashとgemini-2.5-flashを選んだのはそういう理由だ。高性能モデルを使えば質が上がる、という考え方は半分しか正しくない。コンテキストが貧弱なままでは、どのモデルを使っても貧弱なパターンしか返ってこない。逆に言えば、コンテキストの質が十分であれば、flashモデルで事足りる場面は多い。

GeminiはGoogleのポリシーとしてロングコンテキストを前提に設計されているため、コンテキストを積極的に乗せるという使い方と相性がいい。一方でClaudeはロングコンテキストを好まないように設計されている節がある。漏洩したClaude Codeを解析した記事でもその実装方針が確認できるし、実際にロングコンテキストを与えるとthoughtsで難色を示すことも観測している。この設計方針の違いがモデル選択の理由だ。コストが低く試行サイクルを速くできることも、flashモデルを選ぶ実利的な理由として加わる。

広いコンテキストウィンドウを選ぶのは、詰め込むためではなく余白として持つためだ。この区別が次の話に繋がる。

実際に計測してみると、単体テストレベルであればコンテキストウィンドウは80〜120Kトークン程度に収まることが見えてきた。規模や依存関係の複雑さによってはそれを超えるケースもあったが、効率を上げるアイデアはすでに持っており、今後試していくつもりだ。

ここで一つ誤解を解いておきたい。コンテキストウィンドウは大きければ大きいほど良い、という考え方は間違いだ。

コンテキストが大きくなるほど、モデルは中間部の情報を参照しにくくなる——これをLost in the Middle(LitM)と呼ぶ。先頭と末尾は参照されやすいが、中間は希薄化する。関係のない情報が混入すればノイズになり、確率分布を本来の方向からずらすドリフトを引き起こす。

大きいコンテキスト:
  ├── ノイズの混入リスクが増す
  ├── 中間部の情報がLitMで参照されにくくなる
  └── 無関係な情報がドリフトを引き起こす

小さく密度が高いコンテキスト:
  ├── 必要な情報だけが揃っている
  ├── アテンションが目的の情報に集中する
  └── 出力の収束が速い

小さく、密度が高いことが重要だ。 コンテキストウィンドウの広さは、詰め込む余地ではなく、余白として持っておくものだ。


ワンショットを追求することから始まる

ここで哲学の話をする。

最初にやるべきことはワンショットで何が出来るかを追求することだ。

チャットで「うまくいかないからもう一回」を繰り返している間、コンテキストは汚染され続ける。過去の試行錯誤が中間部のアテンションを歪める。プロンプトを書き直すたびにキャッシュが無効化される。これは前の記事で実データを見せた通りだ。

ワンショットを追求するとはどういうことか。

悪い問い: 「どう言えばうまくやってくれるか」
良い問い: 「一発で正しい出力が出るコンテキストをどう設計するか」

ロールファイル、プロシージャ、リファレンスの構成を詰める。モデルに渡す前に、渡すべき情報が揃っているかを考える。これがコンテキストエンジニアリングの実践であって、プロンプトをこねくり回すことではない。


ツールの履歴を捨てない方向を模索する

ワンショットが機能しても、それ単体では複雑なタスクに対応できない。

次のステップはツールの履歴を捨てない方向を模索することだ。

Function CallingとTool Responseの履歴はコンテキストの一部だ。ファイルを読んだ履歴、テストを実行した履歴、型チェックをかけた履歴——これらは「モデルが今どこにいるか」を示す状態だ。これを捨てて毎回ゼロから始めるのは、プログラムのスタックを毎ステップ初期化するようなものだ。

pipeではセッションをJSONファイルとして外部管理している。セッションが終わってもファイルは残る。--session SESSION_IDで再開できる。これがスクリプトの--sessionオプションの意味だ。

# Phase 2でのtakt呼び出し
takt_cmd = [
    "poetry", "run", "takt",
    "--session", session_id,
    "--instruction", instruction,
]

同一セッションを継続することで、Conductorは前の文脈を引き継いだまま次のTODOを処理する。


コンテキストはパラメータ、Function CallingとRAGは副作用だ

整理しておく。

コンテキストはただのパラメータだ。モデルへの入力であり、f(context)contextそのものだ。それ以上でも以下でもない。

ではFunction CallingとRAGは何か。関数型プログラミングの文脈で言えば副作用だ。

正確に言えば、エージェントが情報不足に陥ったことを示すシグナルだ。モデルは自分のボキャブラリでは対応できない情報に直面したとき、外部に問い合わせる。ファイルを読む。検索する。スクリプトを走らせる。これが副作用の発生だ。

しかしそのシグナルとその結果をコンテキストに含めた瞬間、副作用ではなくなる。ただのパラメータになる。

副作用として発生:    モデルがファイルを読む(情報不足のシグナル)
コンテキストに収める: 読んだ内容がパラメータになる(副作用の消滅)

副作用として発生:    RAGが検索を走らせる(情報不足のシグナル)
コンテキストに収める: 検索結果がパラメータになる(副作用の消滅)

だからこそ、どのツールを使ったかが重要になる。どのファイルを読んだか、何を検索したか、どのスクリプトを実行したか——これらはエージェントが何を「知らなかった」かの完全な記録だ。この記録はコンテキスト設計の改善に直結する。次回からそれをコンテキストに最初から含めれば、副作用の発生を減らせる。一発で正しい出力が出るコンテキストに近づいていく。

特にRAGは、知らない誰かの知識が成否を左右することがある。RAGを使った結果が悪かったとき「モデルがバカだ」という解釈は間違いだ。モデルの重みは固定されており、何も変わっていない。変わったのはコンテキストの質だ。失敗の原因はモデルではなく、何が検索されてコンテキストに乗ったかにある。

ベンダーのチャット画面を使っている限り、この記録は手に入らない。何を検索したか、どのRAGチャンクがヒットしたか、Function Callingの生の内容——全て隠蔽される。ベンダーに握らせて隠蔽させている間は、資産を捨てているも同然だ。

pipeがセッションをローカルのJSONファイルとして管理する理由はここにある。ツール呼び出しの全履歴が残る。読んだファイル、実行したスクリプト、返ってきたレスポンス——全てが検査可能だ。次のエージェントへの入力を設計するとき、その履歴が根拠になる。


マルチエージェント環境でワンショットの積み上げが効く

ワンショットの精度を上げ、ツール履歴を保持するようにしたとき、マルチエージェント環境でその積み上げが乗数として効いてくる

単体エージェント:   ワンショット精度 × 1
マルチエージェント: ワンショット精度 × エージェント数 × 並列度

Conductorは何もしない。テスト生成も、型チェックも、lintも、全てサブエージェントに委譲する。各サブエージェントは専用ロールと専用プロシージャを持つ。コンテキストは目的に最適化されている。

ここでpipeのUNIX哲学の適用方法について触れておきたい。

pipeにおけるロールは How(どう振る舞うか) を定義する。プロシージャは What(何をするか) を定義する。この二つは独立している。

roles/python/tests/tests.md          # How: テストエンジニアとして振る舞う
  × procedures/python_unit_test_generation.md  # What: テスト生成の7ステップを実行する

roles/conductor.md                   # How: 委譲に徹する
  × procedures/python_unit_test_conductor.md   # What: TODO管理とinvoke手順

同じロールでも、渡すプロシージャを変えれば振る舞いが変わる。同じプロシージャでも、ロールの組み合わせを変えれば視点が変わる。これが「一つのことをうまくやれ」というUNIX哲学をエージェント設計に適用したときの形だ。ツールは汎用で、組み合わせが専門性を生む。

これを私はAasFと呼んでいる。Mitchell Hashimotoの真似をしたわけではない。Wikiの日付を見てもらえば2025年12月にはそう呼んでいたと分かるはずだ。

f(context) → result

エージェントを純粋関数として扱う。状態を持たない。使い捨てだ。この設計があるから、並列実行も、リトライも、段階的な委譲も、全てシンプルに実現できる。

純粋関数に限りなく近づくとはどういうことか。文字列として同一の出力を得ることではない。表現が違っても意味が等しい状態まで持っていくことだ。"dog"と"犬"は表記が異なるが意味は同じだ。同じコンテキストから、表現の揺らぎを超えて意味として等しい結果が収束するよう設計を詰めていく——それが純粋関数への近似だ。出力を固定することではなく、収束の質を高めることだ。


単体テスト生成が自律的に回った

poetry run python scripts/python/tests/generate_unit_test.py \
    src/pipe/core/repositories

これを実行したあとは待つだけだった。スクリプトが自律的に回る。

  1. Pythonファイルをスキャンしてレイヤーを検出
  2. Conductorを起動してTODOリストを作成
  3. AST解析で依存関係を抽出
  4. Conductorに完全展開済みのinvoke_serial_children命令を送信
  5. サブエージェントがテスト生成、lint、型チェック、カバレッジ検証を実行
  6. 完了検知、次のTODOへ

チャットは一度も開かなかった。PRに残った履歴がその証拠だ。


何から始めるか

コンテキストウィンドウを理解する。ワンショットを追求する。履歴を捨てない設計を考える。マルチエージェント環境を構築する。

この順序で積み上げたとき、claude -p 'やって欲しいこと'の一行が現実になる。


最後に——pipeについて

この記事はpipeの宣伝ではない。

pipeは私自身のために作ったプロダクトだ。自分の思想を自分のコードで実装したものであって、広く使ってもらうことを目的として設計していない。CLIの引数も、セッション管理の仕組みも、ロール・プロシージャの運用方法も、「私が分かればいい」という基準で作られている部分が多い。おそらく使いにくいと感じる人間の方が多数派だろう。それで構わない。

ここで説いたことはpipe固有の話ではない。ワンショットを追求する姿勢、ツール履歴を設計として扱うこと、ロールとプロシージャを分離してエージェントを関数として組み合わせること——これらはどのフレームワークでも、自前のスクリプトでも、実現できる考え方だ。

claude -p 'やって欲しいこと'の一行で何が動くかは、その設計の精度が決める。

余談

これをハーネスエンジニアリングだと感じる人間がいるかも知れないが、2025年末から年始にかけて開発した時期にはそんな概念はない。私は私の考えるコンテキストエンジニアリングをしただけだ。個人的なお気持ちはこの記事で書いてあるので割愛する。

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