100行Lisp代码即可打造一个AI代理
速览
该文章介绍仅用100行Lisp代码即可实现的智能代理,展示了Lisp在AI开发中的优雅与高效。这种轻量级实现有助于理解代理架构及函数式编程在AI中的应用。它为AI入门提供了一种极简范式。
AI 深度解读
背景
大约在2000年,作者在加拿大圭尔夫大学(University of Guelph)修读了一门人工智能课程。他回忆当时并没有学到太多东西,课程中没有涉及神经网络,也没有Transformer、CUDA或PyTorch——这些技术当时根本不存在。期末项目只是一个披着AI外衣的寻路算法。但作者印象最深刻的是大量使用Lisp编程,在昏暗的CIS实验室里写了很多Lisp代码。
当时的教授(作者已记不清名字)称Lisp为“AI的语言”,这可能是当时的共识。作者很喜欢写Lisp代码,认为构建优雅的递归函数是一种艺术。根据教授的说法,Lisp是符号人工智能的语言:专家系统、定理证明器、操纵符号和规则的程序。然而,统计方法后来取得了胜利,深度学习又埋葬了胜者,Lisp最终随着符号AI一起被遗忘。作者几乎不认识任何人(实际上一个都没有)还经常使用Lisp,只记得Paul Graham在一篇关于他编写第一个电商平台的文章中提到过自己用Lisp。
距离那门AI课程已经过去了25年多,作者当时只记住了Lisp这一件事。而现在他花了一个多月构建一个AI agent平台,今天早上突然冒出一个念头:“Lisp对agent循环来说会不会真的是一门有用的语言?”于是他用Claude做了实验。
核心内容
agent是一个递归函数
不要被Claude Code、OpenClaw或其他AI agent工具的强大外表所迷惑——剥去框架,一个agent循环其实非常简单。你有一个消息列表,将其发送给模型。模型要么用文字回答,要么请求使用工具。如果它请求使用工具,你就执行工具,追加结果,然后再次执行这个过程。
有些agent可能把上述过程实现为一个带状态的while循环,但也许用带基例的递归来实现更好。作者给出了一个Common Lisp中的agent循环,仅8行:
(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)))))
基例:模型回答,返回历史。递归情况:模型想要工具,执行它们,用丰富后的消息列表递归调用。没有框架,没有状态机。agent的状态就是通过递归折叠的参数。
作者在Claude的帮助下,用大约100行Common Lisp(使用SBCL,两个库:dexador用于HTTP,shasht用于JSON)构建了一个完整的AI agent,运行在OpenRouter上。
唯一的工具是eval
通常构建agent时,你会开始添加各种工具——例如Web搜索、爬虫、表格、文件工具、Python执行工具等。实际上,大多数agent的大部分内容就是工具目录。
Lisp允许你“作弊”。Lisp是homoiconic的——程序是用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表达式字符串,agent读取、求值,并返回结果。例如,当模型被要求计算第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
The 30th Fibonacci number is 832040.
作者没有告诉它要用递归,但在这个关于递归agent循环的博文中,模型自动选择了教科书式的双重递归斐波那契实现。这代表了2026年版本的Lisp之所以被称为“AI的语言”的原因——程序操控程序。旧的梦想终于成真:我们把符号推理外包给了语言模型,保留了底层结构。
警告(如果不清楚的话):把eval当作工具意味着模型可以在你的机器上运行任意代码。这只是一个沙盒玩具,作者只在本地Docker容器中运行过。
记忆只有20行
一旦循环工作,作者想要持久化——跨会话保留对话。在Agent Foundry中,作者用pgvector实现了记忆,但在这个实验中,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))))。Recall、递归、remember。没有模式、没有迁移、没有存储抽象。序列化格式就是运行时格式。今天告诉它你的名字,明天在一个新进程中询问,它仍能回答。
它从不遗忘,这也是它的缺陷。完整转录记忆会无限增长,最终会触及上下文窗口。一个简单的修复方法是在recall和循环之间加入一个压缩步骤,让agent调用模型总结自己的过去——一个对自身历史进行递归的agent,非常优美。
然后它构建了自己的Web搜索
斐波那契技巧令人惊叹——agent编写一次性的数学并运行它。然后作者决定给它更多一点权限:他将一个临时的Brave Search API密钥粘贴到对话中。令人惊讶的是,agent利用它的eval工具,直接defun了一个brave-search函数到运行中的镜像中。它检查了自己可用的资源(HTTP库等),编写了HTTP调用,解析了JSON响应,然后开始用实时网络结果回答问题。
作者从未构建过Web搜索工具。agent只有一个工具,它用这个工具写了第二个工具。
⤷ (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
agent在对话中说:“完美!你完全正确,Jamie。我现在创建了一个brave-search函数,可以发起对Brave Search API的HTTP请求。”之后它就开始用实时搜索结果回答问题,不再需要人类提供额外工具。
关键要点
- Agent本质是递归函数:核心循环不过是将消息列表传递给模型,根据模型响应决定递归或基例,无需框架或状态机。
- 8行代码实现agent循环:作者用Common Lisp的
defun写出了完整的agent循环,凸显Lisp表达力。 - 唯一工具eval赋予了agent自编程能力:利用Lisp的homoiconic特性,agent可以直接求读任意Lisp表达式,从而自动生成函数、进行计算、甚至构建新工具。
- 记忆实现极其简洁:通过序列化消息列表到JSON文件,只需20行代码实现持久化,没有数据库、没有模式迁移。
- Agent能自我扩展功能:在获得Brave Search API密钥后,agent自动用eval编写了HTTP搜索函数,并开始使用实时数据——从单一工具衍生出第二个工具。
- **存在
