第1章:ゼロマジック宣言

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

人気のライブラリをインストールする。ReasoningEngine をインポートする。.run() を呼び出す。「Hello World」のサンプルでは魔法のように動く。でも、実際に何か意味のあることをしようとした瞬間——たとえば、インポート文を消さずにPythonファイルの特定の行を編集するといった作業——途端に壊れる。

しかも、フレームワークを使っているせいで、自分では直せない。抽象クラス、ファクトリーパターン、「チェーン」の層を掘り進んで、ハルシネーションの原因となっているプロンプトを探し出そうとする羽目になる。

ここではそんなことはしません。

この本は「マジック」への反乱です。「ゼロマジック」アプローチで進めます。つまり、プロダクションレベルのコーディングエージェント Nanocode を純粋なPythonで構築するのです。LangChainなし、AutoGPTなし、Pydanticなし。

なぜか? 自律型エージェントは魔法ではないからです。ただの while ループです。

エージェントとは、本当に何なのか?

ベンチャーキャピタルのマーケティングを取り払えば、「エージェント」とはただのサーモスタットです。

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

エージェントループ:ユーザー入力がwhileループを流れ、入力(センサー)がブレイン/LLM(コントローラー)に渡され、ツール(アクチュエーター)を起動し、ブレインが応答を出力するまで入力に戻り続ける。
図 1. エージェントループ:ユーザー入力がwhileループを流れ、入力(センサー)がブレイン/LLM(コントローラー)に渡され、ツール(アクチュエーター)を起動し、ブレインが応答を出力するまで入力に戻り続ける。

より具体的に言うと、エージェントは4つの部品で構成されています。ブレインはLLMです——テキストを送るとテキストが返ってくる、ステートレスな関数です。ブレインはツールを呼び出します——「ファイルを読む(Read File)」「コマンドを実行する(Run Command)」といった関数で、外の世界と対話します。これらすべてはループ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 クラスを見ていきます。状態とロジックをひとつの場所に保持するため、テストが容易になります。

背景: すべてのロジックを 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 を送出します。シャットダウンの処理方法は呼び出し元が決定します。
  • 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行目: AgentStop/q による)または KeyboardInterrupt(Ctrl+C による)をキャッチし、ループを抜けます。
An icon of a info-circle1

補足: Pythonの input() は1行ずつ読み込みます。本書のプロンプトはすべて1行形式です。これによってコードをシンプルに保てます。本番エージェントでは、readlineやフルTUI といった、より高度な入力方法を使うことが多いです。

この分離に注目してください:Agent.handle_input() にすべてのロジックが含まれており、main() は単なるI/Oのつなぎ役です。この構造により、stdin/stdout をモック化することなくエージェントをテストできます。

実行してみよう

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 ループ。まだ何も役に立つことはできていませんが、これ以降に構築するものはすべてこのスケルトンに接続されていきます。テストによって、作業を進める中ですでに動いている部分を壊さないようにします。