100行Lisp代码实现一个智能体

Hacker News Top 工具

摘要

作者回顾了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

相似文章