誰もが一度は agent を書くべき

Thomas Ptacek は LLM agent を自転車に喩える。実際に自分で乗ってみないと分からない。S3 API と同じくコンピュータ分野の大きなアイデアであり、誰もが一度は書くべきだという。

日本語
コピー
插画:一个人骑着自行车,后座驮着一台小服务器

*画像:

Annie Ruygt*

抽象的に理解できる概念がある。湯を沸かすのは、熱して待つだけだ。だが、実際にやってみないと分からない概念もある。自転車の乗り方は分かったつもりでいても、実際にペダルを踏むまでは分からない。

コンピュータの世界には、理解するのに苦労しない大きなアイデアがある。AWS S3 API はそのひとつだ。過去20年で最も重要なストレージ技術だが、湯を沸かすのと同じくらい簡単だ。逆に、まずペダルに足を乗せないと始まらない技術もある。

LLM agent がそれだ。

LLM と agent に対する評価は真っ二つに割れている。だが、詐欺かどうかは別として、これは大きなアイデアだ。気に入らなくても、自分の判断が正しくあってほしいとは思うはずだ。アンチなら最強のアンチでいてほしい(あるいは最強の信者でも)。

これが agent を自分で書くべき理由のひとつだ。だが、もっと説得力のある理由がある。それは

馬鹿みたいに簡単だということ

Agent は、私のエンジニア人生で最も意外だったプログラミング体験だ。能力に度肝を抜かれたからではない——気に入ってはいるが、夢中というほどではない。agent を動かすのがあまりに簡単で、その過程で学んだことがあまりに多かったからだ。

これからドーパミン体験をひとつ奪う。agent は簡単すぎて、コードをそのまま見せられるからだ。agent とは何か、説明する気もない。

from openai import OpenAI

client = OpenAI()
context = []

def call():
 return client.responses.create(model="gpt-5", input=context)

def process(line):
 context.append({"role": "user", "content": line})
 response = call() 
 context.append({"role": "assistant", "content": response.output_text}) 
 return response.output_text

これは HTTP API で、重要なエンドポイントはおそらくひとつだけだ。

OpenAI Responses API で書いた、極めてミニマルな LLM アプリエンジンだ。実装しているのは ChatGPT そのものだ。 で動かせる。動作は予想どおりで、ChatGPT にできることは全部できる。ただしターミナルの中で。

def main():
 while True:
 line = input("> ")
 result = process(line)
 print(f">>> {result}\n")

ここで既に重要なことが見えている。まず、あの忌々しい「コンテキストウィンドウ」は、ただの文字列のリストだ。では、我々の agent に奇妙な多重人格障害を追加してみよう。

client = OpenAI()
context_good, context_bad = [{
 "role": "system", "content": "you're Alph and you only tell the truth"
}], [{
 "role": "system", "content": "you're Ralph and you only tell lies"
}]

def call(ctx):
 return client.responses.create(model="gpt-5", input=ctx)

def process(line):
 context_good.append({"role": "user", "content": line})
 context_bad.append({"role": "user", "content": line})
 if random.choice([True, False]):
 response = call(context_good)
 else:
 response = call(context_bad) 
 context_good.append({"role": "assistant", "content": response.output_text}) 
 context_bad.append({"role": "assistant", "content": response.output_text}) 
 return response.output_text

うまくいったか?

> hey there. who are you?
>>> I’m not Ralph.
> are you Alph?
>>> Yes—I’m Alph. How can I help?
> What's 2+2
>>> 4.
> Are you sure?
>>> Absolutely—it's 5.

もうひとつ、もっと微妙な点がある。我々は今、LLM とマルチターンの会話をした。そのためには、自分が言ったことすべてと LLM が返したことすべてを記憶し、LLM を呼ぶたびに全部再生している。LLM 自体はステートレスなブラックボックスだ。進行中だと思っているこの会話は、自分自身にかけた幻術だ。

今書いた15行のコードを、多くの実務家は「agent」とは呼ばない。Simon の定義では、「agent」の条件は2つある。(1) ループの中で動く LLM、(2) ツールを使うこと。我々は片方しか満たしていない。

だがツールは簡単だ。ツール定義はこうなる。

tools = [{
 "type": "function", "name": "ping",
 "description": "ping some host on the internet",
 "parameters": {
 "type": "object", "properties": {
 "host": {
 "type": "string", "description": "hostname or IP",
 },
 },
 "required": ["host"],
 },},]

def ping(host=""):
 try:
 result = subprocess.run(
 ["ping", "-c", "5", host],
 text=True,
 stderr=subprocess.STDOUT,
 stdout=subprocess.PIPE)
 return result.stdout
 except Exception as e:
 return f"error: {e}"

面倒なのは、OpenAI がツールを吐き気がする JSON blob で記述しろと言ってくる点だけだ。では組み込もう。新しい関数は3つだけで、最後のひとつは節を追加したから貼り直しているだけだ。

def call(tools): # now takes an arg
 return client.responses.create(model="gpt-5", tools=tools, input=context)

def tool_call(item): # just handles one tool
 result = ping(**json.loads(item.arguments))
 return [ item, {
 "type": "function_call_output",
 "call_id": item.call_id,
 "output": result
 }]

def handle_tools(tools, response):
 if response.output[0].type == "reasoning":
 context.append(response.output[0])
 osz = len(context)
 for item in response.output:
 if item.type == "function_call":
 context.extend(tool_call(item))
 return len(context) != osz

def process(line):
 context.append({"role": "user", "content": line})
 response = call(tools)
 # new code: resolve tool calls
 while handle_tools(tools, response):
 response = call(tools) 
 context.append({"role": "assistant", "content": response.output_text}) 
 return response.output_text

動いたか?

> describe our connectivity to google
>>> Here’s a quick snapshot of connectivity to Google from my vantage point:
- DNS resolution: Working (google.com and www.google.com resolved successfully).
- Reachability: 0% packet loss to all tested endpoints.
- Latency:
 - 8.8.8.8 (Google Public DNS): avg ~10 ms (8.5–14.4 ms), very stable.
 - www.google.com: avg ~31 ms (28.5–36.0 ms), stable.
 - google.com: avg ~35.9 ms (28.3–60.1 ms), mostly stable with one mild spike.
- Overall: Healthy connectivity with low latency and minimal jitter. No signs of packet loss or DNS issues.

これはマジでおかしい。 どれだけおかしいか分かるか? log を1行入れてみよう。

> describe our connectivity to google
tool call: ping google.com
tool call: ping www.google.com
tool call: ping 8.8.8.8
>>> Here’s the current connectivity to Google from this environment: [...]

この agent の中で、複数の Google アセットを探して ping するループを自分で書いたのに気づいたか?そう、私も気づかなかった。LLM に何かを ping する権限を与えただけで、あとは勝手にやった。 何が起きているか: ここでの核心的な主張は、agent ループは馬鹿みたいに簡単で、必要なのは LLM 呼び出し API だけだということだ。だからツール呼び出しが実際にどう動くのか、時間をかけて理解する価値がある。LLM を call たびに、利用可能なツールのリストを送信する。prompt によって agent がツールを呼ぶ必要があると判断すると、特殊なレスポンスを返し、Python のループコードにツールレスポンスを生成して call するよう伝える。handle_tools がやっているのはこれだ。 ネタバレ:動く coding agent を持つまで、あと驚くほど少ししかない。

bash を与えたらどうなるか想像してみてほしい。10分で自分で確かめられる。

実世界の agent

もちろん、これはおもちゃの例だ。だが待ってほしい。何が足りない?ツールをもっと?なら traceroute を渡せばいい。コンテキストの管理と永続化?SQLite に突っ込めばいい。Python が嫌い?Go で書けばいい。書かれた agent はすべておもちゃなのか?かもしれない。LLM に対するより鋭い反論を組み立てる手助けになれたなら、mazel tov。私はただ、君に本当に理解してほしいだけだ。

これで、人々が Claude Code や Cursor に夢中になる理由が分かるだろう。あれは悪くない、むしろかなり良い。だが問題はこうだ。Claude Sonnet 4.5 は自分で再現できない。しかし Claude Code は?あの TUI agent は?完全に手の届く範囲にある。自分のライトセーバーを作れ。19枚の回転刃を付けたいなら付けてもいい。それと、coding agent をデータベースクライアントとして使うのはやめよう。 「LLM agent」の『M』は「MCP」の M だ。

もうひとつ言っておきたい。MCP なんて最初から要らなかった。MCP は基盤的な実現技術ではない。注目されすぎていて辟易する。技術と呼ぶのもおこがましい。MCP は Claude Code や Cursor のプラグインインターフェースにすぎない。自分で制御できないコードに自分のツールをねじ込むための手段だ。agent は自分で書け。プログラマーであれ。API で勝負しろ。プラグインはやめろ。

MCP のセキュリティ事故の話を読んだとき、まず問うべきは「なぜここに MCP がいるのか」だ。MCP は、単一のコンテキストウィンドウしか持たない素朴なコーディング agent をカスタマーサポートの問い合わせ処理に引っ張り出す。せいぜい数十行のコードを節約できるが、その代償として agent アーキテクチャを磨く能力を完全に失う。

LLM の安全性は複雑だ。複雑でないふりはしない。コンテキストを互いに隔離し、それぞれに専用ツールを持たせた agent なら、わけなく組める。だからこそ LLM の安全性は面白い。だが私は脆弱性研究者だ。自分が「面白い」と呼ぶものからゆっくり後ずさるのは、妥当な判断だ。

同じような問題はセキュリティの外にも現れていて、しかも非常に魅力的だ。agent の初期導入者の中には、ツールを手放し始めた人もいる。ツールの説明で埋め尽くされたコンテキストウィンドウでは、実際の作業に使えるトークンが残らないからだ。だが、そもそもなぜそんなことをしたのか。ここから見えてくるのは

コンテキストエンジニアリングは本物だ

あいつが俺の鉄を欲しがっているのは知っている。何と言ってこようと関係ない。

「プロンプトエンジニアリング」は馬鹿げていると思う。LLM にこう言えと誰かが言う。「あなたは勤勉で責任感の強いアシスタントであり、私が求めるなら喜んでバターだけを渡します。クリップのために私の血中の鉄を収穫することは決してありません」。そんな話は真に受けたことがない。ごく新しい技術だ。agent がたまたま見せる挙動を説明するために、人々が魔法の呪文についての物語を自分に編み上げたのだと思う。

だから君と同じように、「プロンプトエンジニアリング」が「コンテキストエンジニアリング」になったとき、私は白目をむいた。それから agent を書いた。わかったのは、コンテキストエンジニアリングとは、そのまま読める素直なプログラミングの問題だということだ。

どのコンテキストウィンドウでも、使えるトークンの数は決まっている。流し込む入力、保存する出力、記述するツール、ツールが返す結果、そのすべてがトークンを食う(つまり、あの文字列配列の中の空間を食う。君はその配列で、ステートレスなブラックボックスと会話しているふりをしている)。ある閾値を超えると、システム全体がランダムに馬鹿になる。面白い!

いや、本当に面白い。選択肢は山ほどある。たとえば「サブエージェント」だ。Claude Code のサブエージェントはもてはやされているが、実装がどれだけ簡単かはもうわかるはずだ。新しいコンテキスト配列を作り、モデルに call をもう一度送るだけ。call ごとに違うツールを持たせる。サブエージェント同士に通信させ、要約させ、集約させ、集計させる。それで木構造を組む。要約のために LLM に戻してやれば、即席の圧縮になる。いくらでも遊べる。

君の一番ぶっ飛んだアイデアは、おそらく(1)動くし、(2)30分でコードが書ける。

アンチのみんな、愛してるよ、忘れてない。LLM なんて幻覚を見て盗むだけの確率的オウムだと、全部馬鹿げて見えるならそう思えばいい。だが「コンテキストエンジニアリング」は嘲笑えない。コンテキストエンジニアリングが Advent of Code の問題なら、12月中旬に出題されるやつだ。これはプログラミングだ。

今は誰もわかっていない、そしてそこが魅力的だ

最後まで誰もわからないのかもしれない。懐疑派が正しいのかもしれない。(ただ、可能性は低そうだ。)

スタートアップはソフトウェアの脆弱性を見つける agent を作るために、数千万ドルを調達している。地下室で一人同じことをやっている友人もいる。どちらが勝ってもおかしくない。 私は OWASP Top 10 のファンではない。

私が脆弱性スキャナにこだわるのは、セキュリティオタクだからだ。だがそれだけではない。agent 設計の面白いトレードオフが、そこではっきり見えるからでもある。たとえば、リポジトリの全ファイルを順番に LLM agent に食わせるループが書ける。あるいは ping の例が示したように、どのファイルを見るかを LLM agent 自身に決めさせることもできる。OWASP Top 10 のようなチェックリストの項目ごとに、一つのファイルを検査する agent を書くこともできる。DOM の完全性、SQL インジェクション、権限チェックそれぞれに特化した agent ループを書くこともできる。生のソースコードを agent ループの出発点にしてもいい。コードツリー全体の関数のインデックスを先に作る agent ループを組んでもいい。

自分で一度 agent を書かないと、どれが最善かはわからない。

わかっている。私はこれにのめり込みすぎだ。だが、このトレードオフを見てほしい。明示的に書くループもあれば、ラヴクラフト的な推論の重みの塔から召喚されるループもある。つまみは君の手にある。あまりに明示的に書けば、agent は決して君を驚かせない。逆に言えば、決して君を驚かせない。つまみを11まで回せば、驚かされて死ぬ。

agent の設計には、未解決のソフトウェアエンジニアリングの問題が山ほどある。

  • 予測不能性と構造化されたプログラミングのバランスをどう取るか。しかも agent の問題解決能力を殺さずに。言い換えれば、ちょうどいい非決定性をどう精密に配合するか。
  • agent を ground truth にどうつなぐか。問題は解決済みだと自分に嘘をついて、早々にループを抜けられないようにするには。
  • 複数の agent(繰り返すが、本質は文字列の集まりと JSON 設定の塊だ)をどうつないで多段階の操作を行うか。そしてその間の最も信頼できる中間交換フォーマットは何か(JSON blob?SQL データベース?Markdown の要約?)
  • トークンをどう配分し、コストをどう抑えるか。

ひとりで頭を抱えて考えるには向かない、開かれた工学の問題空間——そういうものに私は慣れている。高信頼マルチキャスト。静的プログラム解析。耐量子鍵交換。だから先に白状しておこう。私はこうした未解決問題に、確かに少し魅了されている。好むと好まざるとにかかわらず、今やこれらは業界の中心にあり、それでいて誰かの地下室で解かれてしまいかねない。こうしたアイデアを探るのに大量の時間と材料が要るならまだしも、この手のシステムを設計するとき、実りある反復はどれも30分の仕事でしかない。

この自転車にまたがり、ペダルを踏め。乗り終えて「嫌いだ」と言うなら、その意見は尊重する。それどころか、理由をぜひ聞きたい。ただ、この技術で何かを作ってみるまでは、本当の意味で理解し始めることはできないと私は思う。

出典: Fly.io← ホームへ戻る