← 기사 목록
日本語https://zenn.dev/topics/llm/feed

DeepSeek V4 Flash (ds4.c) を Lisp 的に扱う

추출된 키워드

42
ds4.c·5Lisp·5lispy·5agent loop·5DeepSeek V4 Flash·5S 式·4REPL·4LLM·4eval·3binding·3Scheme·3Python·3OpenCode·3ClaudeCode·3llama.cpp·3DwarfStar 4·3John McCarthy·2284Bモデル·2read-sexp·2to-sexp·2init.lispy·2tool-call-id·2tool-call-args·2tool-call-name·2tool-calls·2has-tool-calls?·2make-turn·2append-turn·2dispatch-tool·2llm-call·2Condition System·2live programming·2metacircular·2Steve Russell·2Emacs Lisp·2Clojure·2Smalltalk·2Common Lisp·2Cursor·2コンテキスト汚染·1critique·1powermetrics·1

원문

9,860
DeepSeek V4 Flash (ds4.c) を Lisp 的に扱う

DeepSeek V4 Flash (ds4.c) を Lisp 的に扱う

はじめに

最近は新しい LLM が出ても、llama.cpp の対応が遅かったりして困っていたんですが、DeepSeek V4 Flash 専用のds4.c(DwarfStar 4)が出ました。これで284Bモデルがローカルで動く!

ClaudeCode/OpenCode なんかを使えばいいんだろうけど、ClaudeCode は中で何やってるかわからん。OpenCode は大きすぎる。素朴に実装してみる。これは非常に面白い。皆作った方がいい。
でも、途中で飽きて Lisp 風に自分自身を書き換える事が出来れば面白いかも。と思って作りました。

https://github.com/Flowers-of-Romance/lispy

Q : nil って何やねん。
A : Python の None です。Lisp 風の nil ではありません。

Q : DeepSeek V4 Flash ってどう?
A : 悪くないです。ただ、長時間処理をさせていると、カーネルパニックは起きないんですが、端末が熱〜くなって、やばいです。単純に ds4 サーバの起動引数が悪かっただけかもしれませんが。

Q : メモリ機能はどこに配置?
A : メモリ機能はないです。自分好みになるだけで、結論がそれに引きづられるのが嫌いです。lispy では忌まわしきコンテキスト汚染として、読み込んでいません。必要であれば、lispy.SYSTEM_PROMPT.md などのシステムプロンプトに埋め込むことは可能です。

Q : "renew" ?
A : Claude 等は1Mコンテキストを扱うという触れ込みですが、20%も使えば、「今日はここまで。以降は明日」とか言って終わらせようとしてきます。我々は斜め下をいきます。LLM が「ああ、コンテキスト多いな」と感じたら、LLM が新しいセッションを始めます。この時に、普通は要約を残して、引き継ぎます。でもね、LLM って基本要約ですやん。そこからまた要約なんて耐えたられない。というそれだけのために、無駄な検索機能があります。

Q : 括弧多すぎやろ。
A : はい。なので、nl.py で自然言語で lispy.py を呼び出す様にしていますが、大分これは中途半端です。サーバを建てて、別 LLM から lispy.py を呼び出し、評価を委譲することもできます。

# ds4起動
cd ds4 && ./ds4-server --ctx 50000 --kv-disk-dir ~/ds4-kv --kv-disk-space-mb 32768 --trace ~/ds4-trace.txt
# ログ確認
tail -f ~/ds4-trace.txt
# powermetrics確認
sudo powermetrics --samplers cpu_power,gpu_power,thermal -i 1000

# Lispyサーバ起動 
.venv/bin/python server.py --port 9000 --yolo

Scheme 風の評価器に LLM 呼び出しを混ぜています。

特徴は、agent loop が Python の関数ではなく S 式の binding になっている、という点。

何を言っているのか意味がわからないですね。説明してみます。(勿論 LLM が)

Claude Code の中身を想像してみる

Claude Code や Cursor のような agent 型ツールが、内部で何をやっているかを単純化して書くと、こんな感じ。(妄想)

def agent_step(messages, user_input):
    messages.append({"role": "user", "content": user_input})
    response = llm_call(messages)
    messages.append(response)
    if response.tool_calls:
        for tc in response.tool_calls:
            result = dispatch_tool(tc.name, tc.args)
            messages.append({"role": "tool", "content": result})
        return agent_step(messages, "")
    return response

「ユーザ入力を履歴に積む -> LLM を呼ぶ -> tool 呼び出しがあれば実行して履歴に追記 -> tool があったらもう一周」、これだけ。実際の Claude Code はもっと複雑だが、本質は変わらない。

ここで重要なのは、この loop が Python (や TypeScript) の関数として固められている、という事。ループの規則を変えたければ、ファイルを編集して再起動するしかない。「LLM 呼び出しの前に critique を挟みたい」「tool 呼び出し前に user に確認させたい」みたいな小さな変更でも、コードを直して再ビルドする必要がある。

三層が時間軸で分かれている。ウォーターフォール的な構造。

agent loop を S 式の binding にする

lispyのagent loop はこう書かれています。

(define agent-step
  (lambda (env input)
    (let ((env2 (append-turn env (make-turn "user" input))))
      (let ((response (llm-call env2)))
        (let ((env3 (append-turn env2 response)))
          (if (has-tool-calls? response)
              (agent-step
                (fold (lambda (e tc)
                        (append-turn e
                          (make-turn "tool"
                            (dispatch-tool (tool-call-name tc) (tool-call-args tc))
                            (tool-call-id tc))))
                      env3
                      (tool-calls response))
                "")
              response))))))

擬似コードの Python 版とやっていることは同じ。違いは、これが走行中の REPL の binding として存在している事です。

Python 側に残っているのは「LLM を 1 回呼ぶ」「tool を 1 つ実行する」「turn を 1 つ作る/追加する」といった単発の primitive だけ。

llm-call
,
dispatch-tool
,
append-turn
,
make-turn
,
has-tool-calls?
,
tool-calls
,
tool-call-name
,
tool-call-args
,
tool-call-id
, これらが Python で書かれた基盤。

その上に乗っかる「呼び出しと結果と再帰の組み立て方」。要するに、agent loop の規則そのものは、評価対象のデータとして REPL に置かれている。

走行中に loop を書き換える

何ができるか。REPL を再起動せずに agent loop を差し替えられる。

main> 富士山の標高は?
富士山の標高は 3,776 m です。

main> (define agent-step
...     (lambda (env input)
...       (begin
...         (print "[呼び出し直前] input:" input)
...         (append-turn env (make-turn "user" input))
...         (let ((response (llm-call env)))
...           (begin
...             (append-turn env response)
...             response)))))

main> 北海道について 1 行で
[呼び出し直前] input: 北海道について 1 行で
北海道は日本最北の島で、広大な自然と冷涼な気候、…

(define agent-step ...)
を打った時点で、評価規則が更新される。次の入力からは新しい loop が走る。再起動なし、ファイル編集なし。

agent-step を LLM 自身に書き換えさせることもできる:

main> (prompt (string-append
...     "次は agent-step の現行定義: "
...     (to-sexp (lambda-body (lookup "agent-step")))
...     "  これを、tool 呼び出し前に critique を挟む版に書き換えて。"
...     "S 式 1 つだけで返す。説明禁止。"))

返ってきた S 式を

(eval (read-sexp ...))
で取り込めば、評価器が自分の規則を LLM 由来で更新する。metacircular の極端な姿。

なぜそれが嬉しいの?

agent を書いていると、「ここで critique を挟みたい」「ここで cost を測りたい」「ここで retry を入れたい」が無限に湧いてくる。普通の作り方では、思いつくたびにコードを直して再起動して、状態を作り直して、再現させて、確認する。フィードバックループが長い。

lispy では、思いついたものを REPL でその場で試せる。状態(turns / archive / bindings)はそのまま残っているので、再現コストがほぼゼロ。気に入らなければまた書き換える。「実験」と「実装」の境界が消える。

これは Lisp が昔からやってきたこと(REPL で関数を再定義する文化)を、agent loop という対象に適用しただけです。Common Lisp の Condition System や Smalltalk の "live programming" の系譜。

どうしてこれが Lisp でできるのか

なぜこの設計が Lisp じゃないと難しいか。

Lisp は code = data という性質を持っている。

(+ 1 2)
と書いたとき、これは「1 と 2 を足す式」であると同時に、「3 つの要素を持つリスト」でもある。Lisp から見ると区別がない。マクロというものは、リストを受け取ってリストを返す関数で、返ったリストが次に評価される。

この性質があると、「プログラムを書く道具」と「プログラムを書くプログラムを書く道具」が同じものになる。

define
で binding を作る道具と、
define
で agent loop を作る道具が、同じ
define

Python だと、

def
で関数を作るのと、関数を書き換えるのは別の操作になる。
func.__code__
を差し替えたり
exec
で動的生成したりはできるが、それは普通の
def
とは別世界の操作で、IDE は色を付けてくれないし、デバッガはついてこない。

Lisp では、

(define agent-step ...)
を最初に打つときも、書き換えるときも、まったく同じ操作。差し替えと定義の区別がない。だから「走行中に loop を書き換える」が書ける。

自分自身を書き換える

agent-step
は、自分の中で
llm-call
を呼ぶ。LLM に「現在の agent-step の定義はこうだ、改良版を S 式で返して」と頼んで、返ってきたものを
(eval (read-sexp ...))
で取り込むと、agent が自分の評価規則を自分で書き換えることになる。

技術的には循環していない。

(define agent-step ...)
を打った時点で、評価器は
env.bindings["agent-step"]
を新しい値で上書きする。次の入力からは新しい方が引かれる。古い方は名前が上書きされて参照されなくなる。Python でクラスのメソッドを差し替えるのと、機械としては同じ操作。

eval

1960 年、John McCarthy が「Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I」という論文を書いた。Lisp の元論文として知られているが、その中で一番面白いのは、論文の最後の方にある一節。つまり、Lisp の評価器 (

eval
) を Lisp で書いて見せた箇所。

7 行ほどで書かれている。

eval
は S 式を受け取って、それを評価した値を返す関数。これを Lisp 自身の構文で書くことができる、ということは、Lisp が自分自身を Lisp で表現できるということ。

これがなぜ可能なのかというと、Lisp において「プログラム」と「データ」が同じ表現を持つから。

(+ 1 2)
は「1 と 2 を足す式」であると同時に、「3 つの要素を持つリスト」でもある。だから評価器は、自分の入力(プログラム)を、自分の操作対象(リスト)として扱える。

McCarthy はこの設計を、実装するつもりではなく数学の記法として書いていた(らしい)。Steve Russell が論文を読んで「お、これ、実装できるやんけ」と言い出して、最初の Lisp インタプリタが生まれた。eval の存在自体が、Lisp という言語の自己定義になっている。

lispy の自己書き換え

  • agent-step が S 式として表現できる (code = data なので)
  • その S 式を文字列として LLM に渡せる (
    to-sexp
    で text 化できる)
  • LLM が返した文字列を再び S 式として読める (
    read-sexp
    で tree 化できる)
  • その S 式を評価器が実行できる (
    eval
    で binding を更新できる)

四つすべて Lisp の標準装備でできている。McCarthy が 1960 年に置いた基礎の上に、LLM という新しい層を乗せただけ。

そして、これが LLM の登場で初めて意味を持つようになった。それまで、Lisp の自己書き換えは「人間が書き換える」しかなかった。マクロは展開時に行われる静的な書き換えで、走行中の動的な書き換えは、結局ユーザが REPL で打つしかなかった。LLM がそこに入ると、Lisp の自己言及構造が、初めて自律的に動き始める。

安全弁としての init.lispy

ただし、この自己書き換えには上限がある。LLM が誤った S 式を返したり、agent-step を「常に

nil
を返す関数」に書き換えたりすると、評価器が壊れる。次の入力に応答できなくなる。

lispy には

init.lispy
という seed ファイルがあって、起動時に自動的に load される。中身は agent-step と compose の元の定義。何か壊れたら、REPL で
(load "init.lispy")
と打てば元に戻る。

これは、Lisp の伝統そのものでもある。Emacs Lisp も Common Lisp も、走行中に自分を書き換えられるが、起動時にロードされる初期状態を持っている。

llm-call
dispatch-tool
といった単発の操作は、Lisp からは中身が見えない。Lisp が自分の評価規則を書き換えても、これらの基盤は触れない。

つまり、自己書き換えは評価規則のレイヤーでだけ起きる。基盤と seed は保護されている。

まとめ

  • agent loop は通常、Python や TypeScript の関数として固められている
  • lispy では agent loop を S 式の binding にした
  • 走行中に loop を書き換えられる
  • LLM に書き換えさせれば、agent が自分の評価規則を自分で更新する
  • これは McCarthy が 1960 年に置いた基礎 (Lisp が自分自身を Lisp で書ける) の上に、LLM という新しい層を乗せたもの

Lisp は方言の多い言語族で、それぞれが「S 式 + λ で何をしたいか」の表明として分岐してきた。Common Lisp は工業ドメイン、Scheme は教育と研究、Clojure は並行 Web、Emacs Lisp はエディタ。lispy は LLM agent という新しい生態的ニッチへの一枝、と言いたいところですが、今は凄く中途半端な実装になってるんですよね……。

というか、こんなの誰かがもっといい実装をしていると思うので、知っている方教えてください。

リポジトリ: https://github.com/Flowers-of-Romance/lispy

CC0 で置いてある。