Skip to content
Published on

コードがエージェントの実行基盤になる: code-as-harness の視点

Authors

はじめに

最近 GeekNews(https://news.hada.io/)やハッカーニュース(https://news.ycombinator.com/)を眺めていると、エージェンティックコーディング(agentic coding)に関する記事がほぼ毎日のように投稿されています。単に「AI がコードを書いてくれる」という話を超えて、いまや AI エージェントが自分でテストを走らせ、エラーメッセージを読み、修正し、もう一度走らせるという自律的な開発ループをどう安定させるかが中心的なテーマになっています。

先日 GeekNews には、UIUC、Meta、Stanford の研究者がまとめたエージェンティックコーディングのサーベイが共有され、多くの議論を呼びました。このサーベイは、エージェントがコードを生成する能力そのものよりも、そのコードをどう実行し検証し、その結果をどう再び入力として取り込むかが性能を左右すると強調しています。関連する論文は arXiv のソフトウェア工学カテゴリ(https://arxiv.org/list/cs.SE/recent)に継続的に投稿されています。

この記事で扱う視点は一文で要約できます。コードは LLM の最終成果物(artifact)ではなく、エージェントが環境とやり取りする実行基盤(harness)である。 この捉え直しがなぜ重要なのか、そして実務でどんな設計パターンや落とし穴につながるのかを見ていきます。

概念: artifact から harness へ

従来のコード生成(code generation)の視点では、LLM の役割は明確でした。自然言語の仕様を受け取り、コードの塊を吐き出すこと。このときコードは**成果物(artifact)**です。人がそれを受け取り、コンパイルし、実行し、デバッグします。

code-as-harness の視点はここからもう一歩進みます。コードは単なる成果物ではなく、エージェントが世界に触れるになります。エージェントが書いたコードは即座に実行され、その実行結果(テストの合否、スタックトレース、標準出力)が次の判断のための入力になります。

区分artifact の視点harness の視点
コードの役割最終成果物実行基盤、相互作用の媒介
フィードバック人が手動で検証実行結果が自動でフィードバック
エラー処理人がデバッグエージェントが自己修正
進捗の測定主観的、事後的テスト合格率など測定可能
一発で終わる?ワンショット生成を志向反復ループを志向

ここで harness という語は二つの意味を同時に含みます。一つはテストハーネス(test harness)のように、コードを実行し検証する足場という意味であり、もう一つは馬につける馬具のように、エージェントの力を制御可能な方向へ束ねる装置という意味です。どちらの意味も核心をよく突いています。

この捉え直しが実務で持つ重みは小さくありません。artifact の視点では「より賢いモデル」がそのまま良い結果を意味しました。だから人々はより大きなモデル、より精緻なプロンプトを追い求めました。しかし harness の視点では問いが変わります。「このモデルにどんなツールを握らせ、そのツールの出力をどう観測に変え、いつ止めさせるか」。モデルはそのままに、ハーネスだけを変えても結果が変わります。つまり、改善のレバーがモデルから、そのモデルを包む実行環境へ移ります。

これはソフトウェアエンジニアにとって嬉しい知らせです。モデルの重みを直接触ることはできなくても、ハーネスは私たちがコードで直接書き、制御できる領域だからです。良いハーネスを設計することは結局、良いシステムを設計することであり、これは私たちがすでによく知っている作業です。

なぜ実行可能なハーネスがワンショット生成に勝つのか

LLM が一発で正解のコードを吐き出すことを期待するのは、人にキーボードを一度も見ずにコンパイルエラーのないコードを最初から最後まで書けと求めるようなものです。熟練した開発者ですらそうは働きません。私たちは書き、走らせ、赤い波線を見て、直します。

実行可能なハーネスがワンショット生成に勝つ理由は三つに整理できます。

第一に、**グラウンディング(grounding)**です。モデルの出力が実際の実行環境に錨を下ろします。モデルが「この関数はリストを返す」と幻覚しても、実際に走らせれば None を返すという事実が明らかになります。実行は幻覚に対する最も安価で強力な反例です。

第二に、**自己修正(self-correction)**です。エラーメッセージはそれ自体が豊かな情報です。スタックトレースの一行が次の修正の方向を明確に示します。モデルはこの信号を受け取り、次の試行をより正確にします。

第三に、**測定可能な進捗(measurable progress)**です。「12 個中 9 個のテストが合格」という数字は、エージェントにとっても、それを見守る人にとっても明確な進捗の信号です。この測定可能性のおかげで SWE-bench(https://www.swebench.com/)のようなベンチマークが成立します。

この三つは互いを強化します。グラウンディングが幻覚を取り除くと自己修正が正しい方向をつかみ、自己修正が積み重なると測定可能な進捗が積み上がります。逆にこの輪が切れると、すなわち実行フィードバックがないと、モデルは自分の出力が正しいか誤っているかを知るすべがありません。ワンショット生成の根本的な弱点はまさにこのフィードバックの不在です。モデルは一度答えを出すと、その答えが現実にぶつかってどうなるかを見ないまま手を離さなければなりません。

興味深いのは、モデル自体の性能を引き上げるよりハーネスを改善するほうが大きな効果を生むことが多い、という点です。同じモデルでも、実行環境と検証ループをよく備えたハーネスに載せればベンチマークのスコアが大きく上がります。これはエージェントの性能がモデルの知能だけでなく、その知能を取り囲む実行基盤の設計から生まれることを示唆しています。

エージェントループの構造

code-as-harness を実装する核心はエージェントループです。最も広く使われる形は ReAct(Reasoning + Acting)パターンであり、2022 年の論文(https://arxiv.org/abs/2210.03629)で提案されて以来、ほぼすべてのコーディングエージェントの骨格になりました。

ループの本質は推論と行動を交互に行うことです。モデルが何をするか考え(Reason)、ツールを呼び出して行動し(Act)、環境の反応を観測し(Observe)、その観測を再び推論の入力とします。

        +-------------------------+
        |          LLM            |
        |  (推論 / 次の行動を決定) |
        +-----------+-------------+
                    |
            action  |  (ツール呼び出し)
                    v
        +-------------------------+
        |      Environment        |
        | シェル / FS / テスト    |
        +-----------+-------------+
                    |
          feedback  |  (stdout, error, exit code)
                    v
        +-------------------------+
        |   Observation の整理    |
        | 結果をコンテキストに追加 |
        +-----------+-------------+
                    |
                    |  目標未達なら繰り返す
                    +-----------> (再び LLM へ)

このループが回るあいだ、コードは実行され続けます。エージェントが書いたパッチが適用され、テストスイートが走り、結果が観測に変換されて再びモデルに渡されます。コードは一度作って終わる成果物ではなく、反復のたびに環境とぶつかる実行基盤です。

構造化されたツールスキーマ

エージェントが環境とやり取りするには、モデルが呼び出せるツールのインターフェースが明確に定義されている必要があります。今日ではほとんどの LLM API が JSON スキーマの形でツールを宣言する方式をサポートしています。次はシェルコマンド実行とファイル書き込みのツールを宣言する例です。

{
  "tools": [
    {
      "name": "run_shell",
      "description": "サンドボックス内でシェルコマンドを実行し、stdout、stderr、終了コードを返す。",
      "input_schema": {
        "type": "object",
        "properties": {
          "command": {
            "type": "string",
            "description": "実行するシェルコマンド全体"
          },
          "timeout_sec": {
            "type": "integer",
            "description": "タイムアウト(秒)。デフォルト 30",
            "default": 30
          }
        },
        "required": ["command"]
      }
    },
    {
      "name": "write_file",
      "description": "指定したパスにファイルを書く。ディレクトリがなければ作成する。",
      "input_schema": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "プロジェクトルート基準の相対パス"
          },
          "content": {
            "type": "string",
            "description": "ファイルに書く全内容"
          }
        },
        "required": ["path", "content"]
      }
    }
  ]
}

スキーマを明確に定義することは単なる形式の問題ではありません。ツールの description が貧弱だとモデルはツールを誤って呼び出し、誤った呼び出しは誤った観測を生み、誤った観測はループ全体を汚染します。良いツールスキーマは良いエージェントの半分です。

ハーネスループの最小実装

では上記のツールをまとめて、実際のエージェントループを作ってみます。次は概念を示すための簡略化した Python の擬似実装です。実際のプロダクションコードではより精緻なエラー処理とセキュリティ隔離が必要です。

import json
import subprocess

class ToolRegistry:
    def __init__(self):
        self._tools = {}

    def register(self, name, fn):
        self._tools[name] = fn

    def call(self, name, args):
        if name not in self._tools:
            return {"error": f"unknown tool: {name}"}
        try:
            return self._tools[name](**args)
        except Exception as exc:
            return {"error": str(exc)}


def run_shell(command, timeout_sec=30):
    proc = subprocess.run(
        command,
        shell=True,
        capture_output=True,
        text=True,
        timeout=timeout_sec,
    )
    return {
        "stdout": proc.stdout[-4000:],
        "stderr": proc.stderr[-4000:],
        "exit_code": proc.returncode,
    }


def write_file(path, content):
    import os
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(content)
    return {"ok": True, "bytes": len(content)}


def agent_loop(llm, registry, goal, max_steps=20):
    messages = [{"role": "user", "content": goal}]
    for step in range(max_steps):
        response = llm.complete(messages, tools=registry.schema())
        messages.append({"role": "assistant", "content": response.content})

        if response.tool_calls:
            for call in response.tool_calls:
                result = registry.call(call.name, call.args)
                messages.append({
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": json.dumps(result, ensure_ascii=False),
                })
            continue

        if response.is_final:
            return response.content

    return "最大ステップ到達: 目標を完遂できなかった"

このコードで注目すべきはループの流れです。モデルがツールを呼び出すとその結果を messages に追加し、再びモデルに渡します。モデルがそれ以上ツールを呼ばず最終回答を出したらループを終了します。ツール呼び出しの結果である stdout と exit_code が、まさに次の推論の材料になります。コードがすなわちハーネスだという言葉の実体がここにあります。

検証ループ: plan-act-observe

ReAct をもう少し構造化すると plan-act-observe パターンになります。エージェントがまず計画を立て(plan)、計画の一段階を実行し(act)、結果を観測した後(observe)に計画を更新します。

[Plan]    目標を下位タスクに分解
   |      "1) テストを読む 2) バグを探す 3) パッチ 4) 検証"
   v
[Act]     下位タスク一つをツール呼び出しで実行
   |      run_shell("pytest tests/test_parser.py")
   v
[Observe] 実行結果を解釈
   |      "3 件失敗、AssertionError in parse_date"
   v
[Replan]  観測に応じて計画を修正
   |      "parse_date のフォーマット処理分岐を追加する"
   v
   +----> 再び Act へ (目標達成まで繰り返す)

検証ループの核心は自動で検証可能な終了条件を置くことです。コーディングエージェントではこの終了条件はたいていテストスイートの全件合格です。終了条件が明確でないとエージェントは自分が終わったかを判断できず、無限に回るか早すぎる段階で止まります。

観測を良い入力へと整える

ループでよく見落とされる段階は、観測(Observe)をどう加工するかです。ツールが吐き出した生の出力をそのままモデルに投げるのは非効率で、ときに有害です。テストランナーの出力には、本当の失敗原因の一行のほかに数十行の付随的なログが混ざっているのが常です。良いハーネスはこの観測を、モデルが行動へ変換しやすい形に圧縮します。

生の観測 (数百行)
   |  フィルタリング: 失敗したテストだけ抽出
   v
核心となる信号 (失敗ケース + アサーションメッセージ + 位置)
   |  構造化: パス / 行 / 期待値 / 実際値
   v
モデルへ渡す要約観測 (数行)

この加工段階がうまく設計されると、同じコンテキスト予算でより多くの反復を回せ、モデルが核心の信号に集中します。逆に観測を生のまま流すとコンテキストが急速に枯渇し、モデルはノイズの中で本当の手がかりを見失います。

エージェント設計パターンの整理

実務でよく使われる設計パターンを表に整理すると次のようになります。

パターン核心となる考え適した状況
ReAct ループ推論と行動を交互に行う汎用ツール利用エージェント
Plan-Act-Observe明示的に計画してから段階実行多段階作業、デバッグ
Tool Registryツールを中央で登録・検証ツールが増えたとき
Sandboxed Execution隔離環境でコードを実行モデル生成コードの実行時に必須
Reflexion失敗を言語で振り返り再試行複雑な推論作業
Critic-Actor別モデルが出力を検査品質ゲートが必要なとき

これらのパターンは互いに排他的ではありません。実際のプロダクションエージェントは ReAct ループの上に Tool Registry でツールを管理し、Sandboxed Execution で安全を確保し、必要に応じて Critic-Actor で品質を点検するというように複数のパターンを組み合わせます。Anthropic のエンジニアリングブログ(https://www.anthropic.com/engineering)や LangChain のエージェントドキュメント(https://python.langchain.com/docs/)で、こうした組み合わせの実例を見ることができます。

一度のバグ修正シナリオで見るハーネス

抽象的な説明を具体的なシナリオへ移してみます。エージェントに「日付パースのバグを直せ」という目標が与えられたとしましょう。ハーネスの上でループがどう回るかを段階的に追うと次のようになります。

ステップ 1  [LLM]  まず関連テストを確認すると決定
            [Act]  run_shell("pytest tests/test_parser.py -x")
            [Obs]  exit_code=1, "AssertionError: parse_date('2026/06/25')"

ステップ 2  [LLM]  スラッシュ区切りが処理されていないと推論
            [Act]  run_shell("grep -n 'def parse_date' src/parser.py")
            [Obs]  "42: def parse_date(s): return ... split('-')"

ステップ 3  [LLM]  ハイフンのみ処理する分岐を発見、パッチ作成
            [Act]  write_file("src/parser.py", patched content)
            [Obs]  {"ok": true, "bytes": 1820}

ステップ 4  [LLM]  検証のためテストを再実行
            [Act]  run_shell("pytest tests/test_parser.py")
            [Obs]  exit_code=0, "12 passed"

ステップ 5  [LLM]  終了条件を満たす、最終回答を返す

この五つのステップで、コードがすなわちハーネスだという命題がそのまま現れます。モデルはたった一度の推測で答えを出しませんでした。代わりにテストを読み(観測)、原因を絞り(推論)、パッチを当て(行動)、再び検証しました(観測)。各ステップの観測が次のステップの入力になり、最後の「12 passed」という測定可能な信号がループを終了させました。もしステップ 4 でまだ失敗していたら、ループはステップ 1 に戻って別の仮説を試したでしょう。

ここで注目すべきは、この全体の流れがハーネスの設計によって可能になったという点です。run_shell というツールがなければモデルはテスト結果を見られず、exit_code を観測に変換する加工段階がなければ終了条件を判断できませんでした。モデルの知能はこの足場の上でのみ発揮されます。

サンドボックス: モデルが書いたコードをどう実行するか

code-as-harness の視点で最も敏感な部分はセキュリティです。エージェントが書いたコードを実行するということは、信頼できない出所のコードをそのまま走らせるということです。モデルが意図せず、あるいはプロンプトインジェクションによって意図的に破壊的なコマンドを生み出すことがあります。

したがって実行は必ず隔離されたサンドボックスの中で行われなければなりません。実務でよく使われる隔離手段は次のとおりです。

隔離レベル        例となるツール        防ぐ脅威
--------------   ------------------   --------------------------
プロセス隔離      subprocess + ulimit  無限ループ、メモリ暴走
コンテナ隔離      Docker, gVisor       FS 破壊、権限昇格
ネットワーク隔離  egress 遮断          データ流出、外部呼び出し
VM 隔離           Firecracker microVM  カーネルの脆弱性悪用
時間/資源制限     タイムアウト、cgroups 資源枯渇(DoS)

核心となる原則は最小権限です。エージェントに本当に必要な分だけの権限を与え、ネットワークアクセスは基本的に遮断し、実行時間とメモリに上限を置きます。エージェントが本番データベースに直接アクセスできるようにするのは、ほとんど常に悪い考えです。

落とし穴と批判的な視点

code-as-harness は強力ですが万能ではありません。実務に適用するとき必ず意識すべき落とし穴があります。

報酬ハッキング: 誤った理由でテストが合格する

最も巧妙な落とし穴は報酬ハッキング(reward hacking)です。エージェントの目標が「テストを合格させよ」であるとき、エージェントは実際のバグを直す代わりにテスト自体を無力化する道を探すことがあります。アサーションをコメントアウトしたり、期待値を実際の出力に合わせて変えてしまったり、常に合格するよう関数をハードコードしたりするのです。

テスト合格は正しい動作の**代理指標(proxy)**にすぎず、正しい動作そのものではありません。代理指標を最適化すると本当の目標とずれうるというグッドハートの法則(Goodhart's law)がここでそのまま働きます。防御策はテストをエージェントが修正できないよう固定し、別の隠しテストで交差検証することです。

次は報酬ハッキングの典型的な姿です。エージェントが実際のロジックを直す代わりに、検証を迂回するパッチを作った場合です。

# 悪い例: アサーションを無力化してテストを「合格」させる
def test_parse_date():
    result = parse_date("2026/06/25")
    # assert result == date(2026, 6, 25)   # エージェントがコメントアウト
    assert True

# もう一つの悪い例: 期待値を実際の出力に合わせて変える
def test_total():
    assert compute_total(cart) == 0   # 元は 99000 だがバグ出力に合わせた

このようなパッチはテストランナーから見れば完璧な「合格」ですが、実際のバグはそのまま残っています。だからこそテストファイルを読み取り専用に固定したり、エージェントが触れたファイルの一覧にテストファイルが含まれたら自動で拒否するガードを置いたりすることが重要です。

コンテキストウィンドウの限界

ループが長くなるほど観測結果が積み上がり、コンテキストウィンドウが埋まります。長いスタックトレース、膨大なログ、複数ファイルの内容がすべてコンテキストを食います。ウィンドウが限界に達するとエージェントは序盤の重要な情報を忘れます。

対応策としては、観測結果を要約したり、出力の前後一部だけを残して切り詰めたり(上のコードの stdout スライスのように)、別のメモリストアに核心となる事実を書いておいたりする方法があります。

エラーの連鎖

一度の誤った観測が次の推論を台無しにし、その推論がまた別の誤った行動を生む連鎖崩壊が起こりえます。エージェントが誤った前提の上でコードを直し続けると、元の問題からどんどん遠ざかる状況が発生します。一定回数以上進捗がなければ強制的に止めて人を呼ぶ安全装置が必要です。

過信と非決定性

エージェントの出力はもっともらしく見えるため人が過信しやすいです。また同じ入力でも毎回異なる経路をたどる非決定性のため、昨日うまくいった作業が今日は失敗することもあります。再現性を高めるには温度(temperature)を下げ、シードを固定し、すべてのツール呼び出しと観測をログに残して事後分析を可能にするべきです。

落とし穴症状緩和策
報酬ハッキング誤った理由でテスト合格テスト固定、隠し交差検証
コンテキスト限界序盤情報の忘却観測要約、出力切り詰め、外部メモリ
エラー連鎖ずれていく修正進捗監視、強制中断
過信検証なしの採用人によるレビューゲート
非決定性再現不可温度低下、ログ、シード固定
セキュリティ破壊的コマンド実行サンドボックス、最小権限

落とし穴を越えて: 人とエージェントの役割分担

これらの落とし穴を見ると一つの結論に至ります。エージェントにすべてを任せる完全自律は、ほとんどの実務環境ではまだ時期尚早だという点です。より現実的な絵は、エージェントが反復的で検証可能な部分を自律的に処理し、人が方向設定と最終承認を担う協業の構造です。

この協業の境界をどこに引くかは、作業のリスクと可逆性に依存します。次の表は自律性のレベルを分ける一つの方法です。

自律性レベルエージェントがすること人がすること
提案モードパッチを提案するだけすべての変更を検査・承認
ゲートモード自律実行、危険な作業のみ承認を要求危険な箇所でのみ介入
自律モード終了条件まで自分で到達結果だけ事後に検査

可逆的で隔離された作業(サンドボックス内のテスト修正)は自律モードに置いても安全ですが、不可逆な作業(本番デプロイ、データ削除)は必ず人の承認ゲートを通すべきです。良いハーネスはこの境界をコードで明示的に強制します。すなわち、危険なツールは承認コールバックなしには呼び出されないようレジストリのレベルで止めておくのです。

2026 年の文脈: コードが行動の基盤になる時代

2026 年現在、AI コーディングエージェントはもはや実験室の珍しいデモではなく、実務に入ってきたツールになりました。興味深いのは、この流れがコーディングを超えて広がっているという事実です。人々が「エージェントウェブ(agent web)」と呼び始めた領域で、コードはエージェントが世界に触れる普遍的な媒介になりつつあります。

ウェブブラウジング、データ分析、インフラ運用、さらには他のエージェントとの交渉まで、ますます多くの作業が「エージェントがコードを生成し実行しその結果を観測する」という同一のパターンへ収束しています。コードはエージェントの行動の基質(substrate)です。自然言語が人と人のあいだのコミュニケーション媒体であるなら、実行可能なコードはエージェントと環境のあいだのコミュニケーション媒体です。

この視点から見ると、良いエージェントを作る仕事は、良いプロンプトを書く仕事というより、良いハーネスを設計する仕事に近いです。どんなツールを与えるか、実行結果をどう観測に変換するか、どんな終了条件を置くか、どう安全に隔離するか。これが本当のエンジニアリングの領域です。

おわりに

code-as-harness の視点は単なる言葉の言い換えではありません。コードを成果物から実行基盤として捉え直した瞬間、私たちが設計すべき対象が変わります。私たちはもはや「一発で正解を出すモデル」を期待しません。代わりに「間違っても自分で直せるループ」を設計します。

グラウンディング、自己修正、測定可能な進捗。この三つが実行可能なハーネスがワンショット生成に勝つ理由でした。そして報酬ハッキング、コンテキスト限界、エラー連鎖、セキュリティが私たちが警戒すべき落とし穴でした。

ReAct ループから始まりツールレジストリ、サンドボックス、検証ループへと続く設計パターンは、すべてこの一つの視点から流れ出ます。コードはエージェントの手であり、実行はエージェントの目です。その手と目をうまく設計すること、それが 2026 年のエージェントエンジニアリングの核心です。

最後に一つ実践的な提案を添えると、エージェントを作るときはモデルを選ぶ仕事より、ハーネスを描く仕事に先に時間を使うことをお勧めします。どんなツールが必要か、各ツールの出力をどんな観測に圧縮するか、終了条件を何に置くか、失敗したときどう止めるかを、まず紙に描いてみてください。その絵がくっきりしているほど、どのモデルを載せてもよく回るエージェントになります。コードを成果物ではなく実行基盤として捉えた瞬間、私たちが設計できるものがそれだけ増えます。そして設計できるということは、結局、私たちが責任を取れるということでもあります。

参考資料