第1章:ゼロマジック・マニフェスト

過去2年間にAIアプリケーションの構築を試みた方なら、「フレームワーク疲れ」を感じたことがあるでしょう。

人気のライブラリをインストールします。ReasoningEngineをインポートします。.run()を呼び出します。「Hello World」の例では魔法のように動作します。しかし、実際の作業—たとえばPythonファイルのインポート文を残したまま特定の行を編集するような作業—を試みた途端、動作しなくなってしまいます。

そして、フレームワークを使用したがために、修正することができません。抽象クラス、ファクトリーパターン、そして「チェーン」の層を掘り進めながら、幻覚を引き起こしている1つのプロンプトを見つけようと奮闘することになります。

ここではそのようなアプローチは取りません。

この本は「マジック」への反逆です。私たちは「ゼロマジック」アプローチを取ります:純粋なPythonで本番環境対応のコーディングエージェント「Nanocode」を構築します。LangChainも、AutoGPTも、Pydanticも使いません。

なぜでしょうか?自律エージェントは魔法ではないからです。それは単なるwhileループに過ぎません。

エージェントとは実際には何か?

ベンチャーキャピタルのマーケティングを取り除けば、「エージェント」は単なるサーモスタットです。

サーモスタットは温度を読み取り(入力)、目標値と比較し(判断)、ヒーターをオンにします(アクション)。そして待機し、これを繰り返します。それだけです。AIエージェントも同じことを行います。ただし温度の代わりにテキストを使用するだけです。

エージェントループ:ユーザー入力は、入力(センサー)が頭脳/LLM(コントローラー)に流れ、ツール(アクチュエーター)を起動し、頭脳が応答を出力するまで入力に戻るwhileループを通過する
図 1. エージェントループ:ユーザー入力は、入力(センサー)が頭脳/LLM(コントローラー)に流れ、ツール(アクチュエーター)を起動し、頭脳が応答を出力するまで入力に戻るwhileループを通過する

より具体的には、エージェントには4つの部分があります。頭脳はLLM—テキストを送信すると、テキストを返す、ステートレスな関数です。外部世界とやり取りするために、ファイルの読み取りやコマンドの実行などのツールを呼び出します。これらすべてが、タスクが完了するまで繰り返し実行されるループwhile True)の中に存在し、メモリ—単なるPythonのリスト—が会話履歴を蓄積していきます。(このリストはプログラムが終了すると消えます。永続的なストレージは第6章で追加します。)

whileループが書けるなら、エージェントは構築できます。

ゼロから構築することで、フレームワークユーザーには持ち得ないものを手に入れることができます:制御です。エージェントがループに陥ったとき、どの行のコードが原因なのかを正確に把握できます。APIの請求額が高額になったとき、どこでトークンが漏れているのかを正確に見ることができます。

何を構築するのか

Nanocodeはターミナルで動作するCLIツールです。同僚のように会話することができます。ファイルを読み、コマンドを実行し、コードを編集します。

この本を終えるころには、Claude Sonnet 4.6(またはDeepSeek、あるいはOllamaを介したローカルモデル)に接続し終えているでしょう。ファイルの読み書きやシェルコマンドの実行を行う手足となるツールと、コードベースを検索する目を与えます。そして、誤ってrm -rf /を実行しないように安全装置を構築します。

プロジェクトのセットアップ

1. プロジェクトの初期化

1 mkdir nanocode
2 cd nanocode
3 git init

2. 仮想環境を作成する

AIツールをグローバルにインストールしないでください。システムパッケージと競合します。

1 # Mac/Linux
2 python3 -m venv venv
3 source venv/bin/activate
4 
5 # Windows
6 python -m venv venv
7 venv\Scripts\activate

3. 依存ライブラリのインストール

必要なライブラリは3つだけです:

  • requests — LLM APIと通信するため
  • python-dotenv.envファイルからAPIキーを読み込むため
  • pytest — APIを呼び出さずにコードをテストするため

requirements.txtを作成します:

1 requests
2 python-dotenv
3 pytest

インストール:

1 pip install -r requirements.txt

4. キーを保護する

An icon of a warning1

警告: APIキーをGitHubにプッシュすると、ボットがそれを収集し、数分以内にあなたのアカウントの残高を搾取してしまいます。

.gitignoreを作成します:

1 .env
2 __pycache__/
3 venv/
4 .DS_Store
5 .nanocode/

AgentStop例外

イベントループを書く前に、クリーンな終了機構が必要です。コード全体に散らばったbreak文よりも、例外の方が適切です。

コンテキスト: 例外はエラー処理だけでなく、制御フロー機構としても機能します。ユーザーが/qと入力した時、AgentStopを発生させます。メインループがこれをキャッチして、クリーンに終了します。

コード:

1 # --- Exceptions ---
2 
3 class AgentStop(Exception):
4     """Raised when the agent should stop processing."""
5     pass

これはnanocode.pyの先頭に配置します。これはマーカー例外で、ロジックは含まず、単なる合図として機能します。

Agentクラス

ここで中心となる抽象化について説明します:Agentクラスです。状態とロジックを1つの場所にまとめることで、テストが容易になります。

背景: すべてのロジックをmain()に配置することもできます。しかし、その場合、テストのためにinput()print()をモック化する必要が出てきます。ロジックをAgent.handle_input()として抽出することで、直接テストすることが可能になります。

コード:

10 class Agent:
11     """A coding agent that processes user input."""
12 
13     def __init__(self):
14         pass
15 
16     def handle_input(self, user_input):
17         """Handle user input. Returns output string, raises AgentStop to quit."""
18         if user_input.strip() == "/q":
19             raise AgentStop()
20 
21         if not user_input.strip():
22             return ""
23 
24         return f"You said: {user_input}\n(Agent not yet connected)"

ウォークスルー:

  • 13-14行目: 現時点では空のコンストラクタです。後の章でbraintoolsを追加します。
  • 18-19行目: 終了コマンド/qをチェックします。特別な値を返す代わりにAgentStopを発生させます。
  • 21-22行目: 空の入力をスキップします。空文字列を返します(表示する出力なし)。
  • 24行目: 入力をそのままエコーバックします。これはプレースホルダーです—後で、これをBrainに送信します。

テストによる成功の定義

メインループを書く前に、テストが必要です。

test_nanocode.pyを作成します:

 1 import pytest
 2 from nanocode import Agent, AgentStop
 3 
 4 
 5 def test_handle_input_returns_string():
 6     """Verify handle_input returns a string for normal input."""
 7     agent = Agent()
 8     result = agent.handle_input("hello")
 9     assert isinstance(result, str)
10     assert "hello" in result
11 
12 
13 def test_empty_input_returns_empty_string():
14     """Verify empty/whitespace input returns empty string."""
15     agent = Agent()
16     assert agent.handle_input("") == ""
17     assert agent.handle_input("   ") == ""
18     assert agent.handle_input("\n") == ""
19 
20 
21 def test_quit_command_raises_agent_stop():
22     """Verify /q raises AgentStop exception."""
23     agent = Agent()
24     with pytest.raises(AgentStop):
25         agent.handle_input("/q")
26 
27 
28 def test_quit_command_with_whitespace():
29     """Verify /q works with surrounding whitespace."""
30     agent = Agent()
31     with pytest.raises(AgentStop):
32         agent.handle_input("  /q  ")

テストを実行してください:

1 pytest test_nanocode.py -v
1 test_nanocode.py::test_handle_input_returns_string PASSED
2 test_nanocode.py::test_empty_input_returns_empty_string PASSED
3 test_nanocode.py::test_quit_command_raises_agent_stop PASSED
4 test_nanocode.py::test_quit_command_with_whitespace PASSED

すべて問題ありません。エージェントは基本的なケースを正しく処理しています。

An icon of a info-circle1

補足: なぜpytestなのでしょうか? pytestはtest_で始まる関数を見つけて実行します。定型コードも、クラスも必要ありません。テストコード自体は普通のPythonです—特別な仕組みはありません。

メインループ

次は、エージェントをターミナルに接続する薄いI/Oラッパーです:

29 def main():
30     agent = Agent()
31     print("⚡ Nanocode v0.1 initialized.")
32     print("Type '/q' to quit.")
33 
34     while True:
35         try:
36             user_input = input("\n❯ ")
37             output = agent.handle_input(user_input)
38             if output:
39                 print(output)
40 
41         except (AgentStop, KeyboardInterrupt):
42             print("\nExiting...")
43             break
44 
45 
46 if __name__ == "__main__":
47     main()

ウォークスルー:

  • 30-32行目: エージェントを作成し、起動メッセージを表示します。
  • 36行目: input()はユーザーが何かを入力するまでブロックして待機します。
  • 37-39行目: handle_input()を呼び出し、出力を表示します。
  • 41-43行目: /qによるAgentStopまたはCtrl+CによるKeyboardInterruptをキャッチして、正常に終了します。
An icon of a info-circle1

補足: Pythonのinput()は一度に1行ずつ読み込みます。本書のプロンプトはすべて1行です。これによりコードがシンプルに保たれます—本番環境のエージェントではreadlineや完全なTUIなど、より高度な入力方式を使用します。

注目すべき点として:すべてのロジックはAgent.handle_input()に含まれています。main()は単なるI/O用の接続コードです。これにより、標準入出力のモック化なしでエージェントのテストが可能になります。

実行してみましょう

1 python nanocode.py

表示される内容:

 1 ⚡ Nanocode v0.1 initialized.
 2 Type '/q' to quit.
 3 
 4 ❯ hello
 5 You said: hello
 6 (Agent not yet connected)
 7 
 8 ❯ /q
 9 
10 Exiting...

これがシャーシです。次はエンジンです。

まとめ

以上がシャーシです:Agentクラス、handle_input()メソッド、そしてwhile Trueループです。まだ実用的な機能は備えていませんが、これから構築するものはすべてこの骨格に組み込まれていきます。テストによって、開発を進めながら既存の機能を壊さないようにすることができます。