100行Lisp代码实现一个智能体
摘要
作者回顾了25年前学习Lisp用于符号人工智能的经历,并展示了如何仅用100行Common Lisp代码实现一个现代AI智能体循环,强调递归的优雅性。
暂无内容
查看缓存全文
缓存时间: 2026/07/12 10:47
# 用100行Lisp代码写一个Agent,或我的教授是对的——只是早了25年
原文链接:https://thebeach.dev/posts/lisp-agent/
大约在2000年,我在圭尔夫大学(https://www.uoguelph.ca/)修了一门AI课程。我不觉得自己学到了太多。据我回忆,我们并没有讨论神经网络。我的期末项目大概是一个穿着AI外衣的寻路算法。当然也没有任何关于Transformer的讨论。没有CUDA。没有PyTorch。这些当时都不存在。
但我记得自己写了很多Lisp(https://en.wikipedia.org/wiki/Lisp_(programming_language))代码——在昏暗的圭尔夫大学CIS实验室里,写了大量的Lisp。
我的教授(我死活想不起他叫啥了)称Lisp是“AI的语言”,当时这可能是普遍共识——我不确定。但我确实很享受写Lisp代码。对我来说,构建这些优雅的递归函数是一种艺术。我不知道还有哪种编程语言能带来这种感受(除了几乎令人尴尬的XML样式表——我可能是极少数真正喜欢通过超优雅的递归来创建XML转换的人之一)。
据我的教授说,Lisp确切地说是符号AI(https://en.wikipedia.org/wiki/Symbolic_artificial_intelligence)的语言:专家系统、定理证明器、操作符号和规则的程序。但统计学方法赢了,接着深度学习埋葬了赢家,而我认为Lisp最终随着符号AI一起消失了。至少,我认识的人中极少(没有人)经常使用它,除了我记得Paul Graham在一篇关于编写他第一个电商平台(https://www.paulgraham.com/avg.html)的文章中提到过它。
如今,距离我修那门AI课程已经过去25年多了,我离开时唯一记得学到的东西就是Lisp。与此同时,我已经埋头搭建一个AI代理平台一个多月了。今天早上,一个小小的念头在我脑干深处浮现——“Lisp是否真的能成为代理循环的有用语言?”然后我发现自己在本该专注的事情上分心了,转而和Claude一起捣鼓,就是想看看……
## 代理是一个递归函数
不要让Claude Code、OpenClaw或其他任何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的帮助下,用大约100行Common Lisp代码组装了一个完整的AI代理,它运行在OpenRouter之上。用了SBCL和两个库(dexador用于HTTP,shasht用于JSON),其他什么都没有。
当你构建一个代理时,你通常会开始添加工具——Agent Foundry有网页搜索和爬虫、表格和文件工具、Python执行工具等等。事实上,大多数代理的大部分内容就是工具目录。
Lisp允许你作弊。Lisp被语言极客称为同像性(https://en.wikipedia.org/wiki/Homoiconicity)语言——这是一个花哨的词汇,表达一个简单的思想: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))))
``
一个工具。模型将一个Common Lisp表达式(https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node56.html)作为字符串写出。代理读取它、求值它,并将打印出的结果发送回去。让它计算第30个斐波那契数,它不会回忆答案,而是编写循环并运行它。以下是实际对话记录:
``
* (agent:run "第30个斐波那契数是多少?计算它,不要回忆。")
⤷ (defun fibonacci (n)
(if (<= n 2)
1
(+ (fibonacci (- n 1)) (fibonacci (- n 2))))) => FIBONACCI
⤷ (fibonacci 30) => 832040
第30个斐波那契数是832040。
NIL
``
我没有告诉它使用递归,但在这篇关于递归代理循环的博文中,模型自动采用了教科书式的双重递归斐波那契。它第一次求值定义了函数到运行中的镜像中,第二次求值调用了它。记住这招,后面它会以更重要的方式再次出现。
这就是2026年版的Lisp最初成为“AI的语言”的原因。古老的梦想是程序能操控程序。我们终于实现了这一点。我们只是把符号推理外包给语言模型,保留了底层框架。
需要说明的是,如果不够清楚的话:将eval用作工具意味着模型在你的机器上运行任意代码。这是一个用于沙盒的玩具。我从未在本地Docker容器之外运行过它。
## 记忆只需20行代码
循环完成后,我想要持久化。跨会话保存的对话。在Agent Foundry中,我实现了pgvector(https://github.com/pgvector/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))))
``
回忆,递归,记住。没有模式(schema)。没有迁移。没有存储抽象。序列化格式就是运行时格式。今天告诉它你的名字,明天在一个新进程中询问,它还能回答。
它永远不会忘记,这也是它的缺陷。完整对话记忆会无限增长,最终你会触及上下文窗口。这可以简单修复:在回忆和循环之间加一个压缩步骤,让代理调用一个模型来总结自己的过去。一个在其自身历史之上递归的代理。太美了。
## 然后它自己构建了网页搜索
斐波那契的把戏看起来真的很巧妙——代理编写了一次性数学代码并运行它。然后我决定给它再多一些空间。我将一个临时的Brave Search API密钥粘贴到对话中,主要是想看看会发生什么。接下来发生的事让我有点惊讶——代理利用它的eval将brave-search函数defun到了运行中的镜像中。它检查了自己已有的能力,编写了HTTP调用,自己解析了JSON响应,然后开始用实时网页结果回答问题。
我从未构建过网页搜索工具。代理恰好只有一个工具,而它就用这个工具写出了第二个工具。
``
⤷ (defun brave-search (api-key query &key (count 10))
"使用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
太棒了!Jamie,你完全正确。我现在创建了一个 `brave-search` 函数,它可以向Brave Search API发出HTTP请求。
如果你提供Brave Search API密钥,我可以为你执行网页搜索。该函数接受:
- `api-key` - 你的Brave Search API密钥
- `query` - 搜索查询
- `count` - (可选)返回的结果数量(默认10)
你想用你的API密钥试试吗?
NIL
``
请仔细体会一下,因为它颠覆了我们通常对代理能力的认知。在Agent Foundry中,在Claude Code中,在我所知道的每一个代理平台中,工具目录都是在设计时固定的。像我这样的人决定代理能做什么,编写工具,然后发布它们。但在这里,目录是开放的。模型决定它需要什么,用自己的底层语言编写它,然后通过eval使其存在。代码即数据,数据即代码,能力即对话。
还有一个细微之处我花了一会儿才理解。defun本身在进程退出后就消失了。函数存在于正在运行的Lisp镜像中,而不是磁盘上。但对话记录得以持久化。brave-search的源代码、模型编写它的过程,所有这些都以纯文本形式存在于memory.json中。所以在一个新会话中,函数并不存在,直到代理重新读取自己的历史并通过eval将其重新注入到存在中。它的能力被存储为构建这些能力的记忆,并在需要时重新激活。
这就是同像性——一个代理的技能,字面上就是它给自己讲的故事。
## 技能只是记忆
今天每个严肃的代理平台,包括Agent Foundry在内,都将工具目录视为设计时的决策。MCP基本上是对设计时假设的形式化——能力作为契约,预先协商,版本化,批准。而本实验处于另一个极端:能力由代理在运行时根据对话需要决定。
也许整个行业已经在不知不觉中沿着这个连续谱滑动了。代码解释器是第一步——代理编写一次性程序来回答问题。技能,即SKILL.md模式,是第二步——因为技能不过是存储为文本并在运行时加载的能力。而这个100行的Lisp代理不过是压缩了剩下的距离。它的技能存储在自己的对话记录中。
或许有点牵强,但这大致也是我们的工作方式。你几乎没有任何能力是在设计时安装好的。你学习它,存储它,然后在需要时重新激活它——通常还伴随着你将其写入自己记忆的过程,就像记住你学会骑自行车的那个夏天。一个有能力的个体,在某种意义上,就是自己练习的记录,每次需要时重新求值。看着一个代理重新读取自己的历史并通过defun将技能重新注入存在,感觉不像是一种技巧,更像是那个过程的粗糙而美丽的草图。我在这里的推测已经远超我的专业领域,但这个形状是押韵的。
## 他不是错了,只是太早了
符号AI输了。没有哪个专家系统在反向传播(https://en.wikipedia.org/wiki/Backpropagation)和梯度下降(https://en.wikipedia.org/wiki/Gradient_descent)面前存活下来。但Lisp实际上为之构建的东西——程序即数据,可检查、可转换并反馈回自身的计算——结果却很好地描述了代理是什么。
现在,模型负责推理。但围绕模型的循环,我们实际工程化的部分,正是Lisp一直擅长的。
为一个观点正确了25年是一段很长的时间。也许在某处,我那位老AI教授在某些事情上又走在了我们前面。
完整源代码(包括Dockerfile)在GitHub上:https://github.com/jamiebeach/lisp-agent
相似文章
@yibie: 推荐这篇让我头皮发麻的文章。一个开发者回忆起 25 年前 AI 课上的教授说"Lisp 是 AI 的语言",然后他用 100 行 Common Lisp 写了一个完整 agent——8 行递归式的 agent loop,一个唯一工具是 e…
一个开发者用100行Common Lisp构建了一个AI agent,唯一的工具是eval,模型通过递归agent loop执行代码,并通过持久化transcript恢复技能,展示了Lisp作为AI语言的独特优势。
我从零重建了一个Claude Code风格的编程代理——整个代理循环只有6行代码。20章,约5000行代码,无框架,也支持本地模型
一位开发者分享了一个20章的教程,从头重建了一个Claude Code风格的编程代理,展示了整个代理循环(约6行代码),并支持本地模型和多种LLM API。
@loganthorneloe: https://x.com/loganthorneloe/status/2075684831233757275
本周精选五篇值得关注的AI文章,涵盖自我改进代理、Bun向Rust迁移、vLLM架构、编码评估基准以及代理自主性等级。
一位开发者分享关于如何最大化AI代理能力的见解,认为更简单的设置和理解核心原则比复杂的工具和库更有效。
一位开发者分享关于如何最大化AI代理能力的见解,认为更简单的设置和理解核心原则比复杂的工具和库更有效。
@joelhooks:“几十个智能体”听起来有点疯狂,也确实有点,但当你开始思考“循环”时,就开始说得通了……
Y Combinator 宣布推出 Linzumi AI,用于在聊天线程中协调数十个 AI 编码智能体,并提供对 GLM 5.2 开放权重模型的免费访问。