AI・機械学習
Lispで書かれた100行のエージェント
An agent in 100 lines of Lisp (thebeach.dev)
要約
この記事は、AI開発の黎明期に使われたLispという言語が、現代のAIエージェント構築においてもその有効性を発揮できることを論じています。著者は、Lispのホモイコニックな性質(コードがデータであり、データがコードである性質)を活用することで、AIモデルが直接Lispコードを生成・実行し、新たなツール(例:Web検索機能)さえも自ら作成できることを示し、わずか100行程度で機能的なAIエージェントループを構築できることを実証しています。
全文翻訳
約2000年頃、私はゲルフ大学でAIのコースを受講しました。あまり多くを学んだ記憶はありません。覚えている限り、ニューラルネットワークについては話しませんでした。学期末のプロジェクトは、AIの衣装を着た経路探索アルゴリズムだったと思います。トランスフォーマーについて話すこともありませんでした。CUDAもPyTorchもありませんでした。それらは何も存在しませんでした。しかし、私が覚えているのは、Lispでコーディングをたくさんしたことです。暗いゲルフ大学のCISラボで、たくさんのLispを。私の教授(名前は全く思い出せません)はそれを「AIのための言語」と呼んでいました。当時、それが一般的な考え方だったのかもしれませんが、私は確かではありません。しかし、私は実際にLispコードを書くことを楽しみました。私にとって、それらはエレガントな再帰関数を構築する一種の芸術でした。XMLスタイルシートを除いて、これほど響く他のプログラミング言語は知りません(XMLスタイルシートは、おそらく私が非常に少ない人々の一人であり、非常にエレガントな再帰でXML変換を作成することを本当に楽しんだでしょう)。私の教授によると、Lispは特にシンボリックAIの言語でした:エキスパートシステム、定理証明器、シンボルとルールを操作するプログラム。しかし、統計的手法が勝利し、次にディープラーニングが勝者を葬り去り、Lispは最終的にシンボリックAIと共に失われたのだと思います。少なくとも、ポール・グラハムが彼の最初のeコマースプラットフォームを書くことに関するエッセイの一つでそれを使用したと述べたことを除けば、それを定期的に使用している人はほとんど(全く)知りません。さて、今、私がAIコースを受講してから25年以上が経過し、この一つのことだけを覚えています - Lisp。そしてその間、私は1ヶ月以上AIエージェントプラットフォームの構築に没頭しており、今朝、この小さな考えが私の脳幹の底をくすぐります - 「Lispは実際、エージェントループに役立つ言語になり得るだろうか?」そして、私はやるべきことから気をそらされ、代わりにClaudeを使って試してみることになります…エージェントは再帰関数です# Claude Codeやその他のAIエージェントツールの力に惑わされないでください - フレームワークを剥ぎ取れば、エージェントループは信じられないほどシンプルです。メッセージのリストがあります。それをモデルに送信します。モデルは言葉で答えるか、ツールを使うように要求します。もし要求されたら、ツールを実行し、結果を付加して、もう一度繰り返します。おそらく、一部のエージェントはそれを状態を持つwhileループとして実装しています。しかし、おそらくそれは再帰とベースケースで実装するのが本当に良いでしょう。Lispの構文については全く触れません。もしあなたがLispでコーディングしたことがなければ、これは非常に奇妙に見えるでしょうが、ここにLispのエージェントループがあります:(defun agent-loop (messages) (let* ((message (ref (call-model messages) "choices" 0 "message")) (tool_calls (gethash "tool_calls" message))) (if (and tool_calls (plusp (length tool_calls))) (agent-loop (append messages (list message) (map 'list #'execute tool_calls))) (append messages (list message)))))それがエージェント全体です、冗談抜きで、わずか8行のCommon Lispです。ベースケース:モデルが応答し、履歴を返します。再帰ケース:ツールを要求し、それらを実行し、強化されたメッセージリストで再帰します。フレームワークなし。状態マシンなし。エージェントの状態は、再帰を通して折りたたまれる引数にすぎません。私はClaudeの助けを借りて、OpenRouterに対して動作する完全なAIエージェントを、約100行のCommon Lispでまとめました。SBCL、2つのライブラリ(HTTP用のdexador、JSON用のshasht)、それ以外は何もありません。唯一のツールはevalです# エージェントを構築するとき、通常はツールをボルトで留め始めます - Agent FoundryにはWeb検索とクロール、テーブルとファイルツール、Python実行ツールなどがあります。実際、ほとんどのエージェントの大部分はツールカタログです。Lispはあなたにチートを許します。Lispは言語オタクがホモイコニックと呼ぶものです - 非常に単純なアイデアに対する派手な言葉:LispプログラムはLisp自身のデータ構造(リスト)で書かれているため、コードはデータであり、データはコードです。プログラムは、食料品リストを作成するのと同じ方法で別のプログラムを作成できます。つまり、ツールを構築する代わりに、モデルに言語そのものを渡します:(defun lisp-eval (form-string) (handler-case (format nil "~s" (eval (read-from-string form-string))) (error (e) (format nil "ERROR: ~a" e))))ツールは1つです。モデルはCommon Lispのフォームを文字列として書き込みます。エージェントはそれを読み込み、evalし、印刷されたものを何でも返します。30番目のフィボナッチ数を尋ねると、それは答えを思い出さず、ループを書いて実行します。実際のトランスクリプトは次のとおりです:* (agent:run "What is the 30th Fibonacci number? Compute it, don't recall it.") ⤷ (defun fibonacci (n) (if (<= n 2) 1 (+ (fibonacci (- n 1)) (fibonacci (- n 2))))) => FIBONACCI ⤷ (fibonacci 30) => 832040 30番目のフィボナッチ数は832040です。NIL再帰を使うように指示したわけではありませんが、再帰的なエージェントループに関するこのブログ記事で、モデルは教科書的な二重再帰フィボナッチを自分で選びました。最初のevalでライブイメージに関数を定義し、2番目のevalでそれを呼び出しました。この動きを覚えておいてください、それは後でより大きな方法で戻ってきます。これは、Lispをそもそも「AIのための言語」にしたものの2026年版です。古い夢はプログラムを操作するプログラムでした。私たちはそれを手に入れました。シンボリック推論を言語モデルにアウトソースし、基盤を維持しました。注意点、非常に明確でない場合:evalをツールとして使用することは、モデルがあなたのマシン上で任意のコードを実行することを意味します。これはサンドボックス用の玩具です。ローカルのDockerコンテナの外では実行していません。メモリは20行です# ループが機能した後、永続性を求めていました。セッションをまたいで会話が存続すること。Agent Foundryではpgvectorをメモリに使用しましたが、このLispベースのエージェントにも使用できると思いますが、この実験では、Lispにそれを実装するのに最適な独自のプリミティブがあるのに、別の依存関係を導入する理由はありません。メッセージはすでにハッシュテーブルのリストです。つまり、精神的にはすでにJSONです。したがって、メモリはリストを書き出して読み戻すこと以上のものはありません:(defun remember (messages) (with-open-file (out *memory-file* :direction :output :if-exists :supersede) (shasht:write-json (coerce messages 'vector) out)) messages) (defun recall () (if (probe-file *memory-file*) (coerce (with-open-file (in *memory-file*) (shasht:read-json in)) 'list) (list *system-message*)))ループ自体は全く変更されませんでした。エントリポイントはパイプラインになりました:(remember (agent-loop (append (recall) (list new-user-message))))思い出し、再帰し、記憶する。スキーマなし。マイグレーションなし。ストア抽象化なし。シリアライゼーション形式は実行時形式です。今日あなたの名前を伝えて、明日新しいプロセスで尋ねると、それは答えます。それは決して忘れませんが、それは欠点でもあります。完全なトランスクリプトメモリは無制限に増加し、最終的にコンテキストウィンドウに達します。これは簡単な修正になる可能性があります:リコールとループの間に圧縮ステップを挿入し、エージェントがモデルを呼び出して過去を要約します。自身の履歴で再帰するエージェント。美しい。次に、それは自身のWeb検索を構築しました# フィボナッチのトリックは本当に見るのが面白かったです - エージェントが一時的な数学を書いて実行していました。その後、もう少し自由を与えることにしました。Brave Search APIキーを会話に貼り付けました。何が起こるかを見るためです。そして起こったことは私を少し驚かせました - エージェントはevalを使用して、ライブイメージにbrave-search関数を定義しました。利用可能なものをチェックし、HTTP呼び出しを書き込み、JSON応答自体を解析し、ライブWeb結果で質問に答え始めました。Web検索ツールは構築しませんでした。エージェントにはツールが1つしかありませんが、それを使用して2番目のツールを作成しました。⤷ (defun brave-search (api-key query &key (count 10)) "Search using Brave Search API" (let* ((encoded-query (quri:url-encode query)) (url (format nil "https://api.search.brave.com/res/v1/web/search?q=~a&count=~d" encoded-query count)) (response (dexador:get url :headers `(("Accept" . "application/json") ("X-Subscription-Token" . `,api-key))))) (shasht:read-json response))) => BRAVE-SEARCH 完璧です!あなたは完全に正しかったです、ジェイミー。私は今、Brave Search APIにHTTPリクエストを行うことができる`brave-search`関数を作成しました。私に提供していただければ