Ideas about Lisp with AI
About
感觉自己都没有怎么在写代码的时候使用过 AI, 还是有些落伍了 (bushi). 所以试试给自己做一个简单的 AI agent.
API
API 的部分折叠了, 毕竟大家可能不是很喜欢看
首先是依赖:
(ql:quickload '(:dexador :str :shasht :parse-float))
Ladder
这里使用 DeepSeek 的 API:
(defparameter *host* "api.deepseek.com"
"DeepSeek API host. ")
(defparameter *api-key* "sk-you-should-set-it-your-self"
"DeepSeek API key. ")
(defparameter *temperature* 1.0
"Default temperature. ")
(defparameter *model* "deepseek-chat"
"Default LLM model")
一个 JSON object 的 API 的请求和接受处理可以如下抽象:
(defun query (method api content
&key
(headers *headers*)
(api-key *api-key*)
(host *host*))
"Query for `api' by `method'.
Return a JSON object as hash-table. "
(shasht:read-json*
:stream (dex:request
(format nil "https://~A/~A" host api)
:method method
:headers (cons (cons "Authorization"
(format nil "Bearer ~A" api-key))
headers)
:content (if (null content) "" (json content)))
:null-value :null
:false-value nil
:true-value t
:array-format :list
:object-format :hash-table
:eof-error t))
这里不使用 shasht:write-json
来产生 JSON 输入, 而是通过 json
函数进行处理.
JSON 函数的定义
(defun json (obj)
"Turn Lisp `obj' to JSON string. "
(with-output-to-string (json)
(labels ((write-json (obj)
(if (atom obj)
(cond ((null obj) (write-string "false" json))
((eq obj t) (write-string "true" json))
((eq obj :null) (write-string "null" json))
((eq obj :true) (write-string "true" json))
((eq obj :false) (write-string "false" json))
((stringp obj) (shasht:write-json-string obj json))
(T (shasht:write-json obj json)))
(cond ((and (evenp (length obj)) (keywordp (first obj)))
;; plist as object
(write-char #\{ json)
(shasht:write-json-string (str:snake-case (pop obj)) json)
(write-char #\: json)
(write-json (pop obj))
(do-plist (key val obj)
(write-char #\, json)
(shasht:write-json-string (str:snake-case key) json)
(write-char #\: json)
(write-json val))
(write-char #\} json))
(T
;; normal list write as array
(write-char #\[ json)
(write-json (pop obj))
(dolist (elem obj)
(write-char #\, json)
(write-json elem))
(write-char #\] json))))))
(write-json obj))))
其中的 do-plist
的宏定义类似于 dolist
宏:
(defmacro do-plist ((key val plist &optional result) &body body)
"Like `dolist' but on plist. "
(let ((val-rest (gensym "VAL-REST")))
`(loop for (,key . ,val-rest) on ,plist by #'cddr
if (endp ,val-rest)
do (error (format nil "Not property list: ~A. " ,plist))
do (let ((,val (first ,val-rest))) ,@body)
,@(when result `(finally (return ,result))))))
比如可以测试一下如何了解自己还剩下多少的 API 用量:
(let ((res (query :get "user/balance" nil)))
(gethash* res "balance_infos" #'first "total_balance"))
9.99
(剩得不多了… )
其中的 GETHASH* 的函数定义
(defun gethash* (hash-table &rest keys)
"Recursively get keys from `hash-table'.
Return `default' if not found. "
(if (endp keys) hash-table
(let* ((key (first keys))
(hash (if (functionp key)
(funcall key hash-table)
(gethash key hash-table))))
(apply #'gethash* hash (rest keys)))))
于是可以定义一个简单的 wrapper:
(defmacro define-api (name (response method api &key required optional)
&body body)
"Define LLMisp API function. "
`(defun ,name (,@required &key ,@optional &allow-other-keys)
;; docstring
,@(when (stringp (first body))
(list (pop body)))
;; check parameters before query
,@(when (and (listp (first body)) (eq (first (first body)) :check))
(mapcar #'(lambda (test) `(assert ,test)) (rest (pop body))))
(let ((,response
(query ,method ,api
;; generate the content plist
(list ,@(let ((param ()))
(dolist (var required (nreverse param))
(push (intern (symbol-name var) :keyword) param)
(push var param)))
,@(let ((param ()))
(do-bind-list ((var) optional (nreverse param))
(push (intern (symbol-name var) :keyword) param)
(push var param)))))))
;; return `response' directly if not used in `body'
,@(if body body (list response)))))
DeepSeek API
以下是抄袭至 文档 的说明:
- Get User Balance
(define-api balance (res :get "user/balance") "Get user current balance. Return values are total balance and raw JSON obj hash-table. " (when (gethash "is_available" res) (values (parse-float:parse-float (gethash* res "balance_infos" #'first "total_balance")) res)))
- Lists Models
(define-api models (res :get "models") "Return a list of models name and raw JSON obj hash-table. Lists the currently available models, and provides basic information about each one such as the owner and availability. Check Models & Pricing for our currently supported models." (values (mapcar #'(lambda (model) (gethash "id" model)) (gethash "data" res)) res))
- Create Chat Completion
(define-api chat-complete (res :post "chat/completions" :required (messages) :optional ((model *model*) (frequency-penalty 0) (max-tokens 4096) (presence-penalty 0) (response-format :null) (stop :null) (stream nil) (stream-options :null) (temperature *temperature*) (top-p 1.0) (tools :null) (tool-choice "none") (logprobs nil) (top-logprobs :null))) "Creates a model response for the given chat conversation. Return output message and raw JSON obj hash-table. " (:check (or (eq frequency-penalty :null) (<= -2 frequency-penalty 2)) (or (eq max-tokens :null) (< 1 max-tokens 8192)) (or (eq presence-penalty :null) (<= -2 presence-penalty 2)) (or (eq temperature :null) (<= 0 temperature 2)) (or (eq temperature :null) (<= top-p 1))) (values (gethash* res "choices" #'first "message" "content") res))
- FIM: Create FIM Completion
(define-api fill-in-middle (res :post "beta/completions" :required (prompt) :optional ((model *model*) (echo nil) (frequency-penalty 0) (logprobs 0) (max-tokens 4096) (presence-penalty 0) (stop :null) (stream nil) (suffix :null) (temperature *temperature*) (top-p 1))) "The FIM (Fill-In-the-Middle) Completion API. Return values are message and raw JSON obj hash-table. " (:check (or (eq frequency-penalty :null) (<= -2 frequency-penalty 2)) (or (eq logprobs :null) (<= logprobs 20)) (or (eq presence-penalty :null) (<= -2 presence-penalty 2)) (or (eq temperature :null) (<= temperature 2)) (or (eq top-p :null) (<= top-p 1))) (values (gethash* res "choices" #'first "text") res))
假如折叠隐藏前面的 Ladder 一节, 估计只看这部分的 define-api
会有一种:
“啊? 这 Lisp 好简单哦” 的错觉吧. 虽然并不是想要强调 Lisp 其实很复杂
(复杂吗? 并没有吧… 毕竟实际的行数也不多).
这里吐槽一下
之前的看了一个 Lisp 的书, 里面把代码放在 Github 上, 结果发现其实实现得也不咋的. 还不如我自己写的优雅容易拓展. (bushi)
这里给个例子试试吧:
(chat-complete '((:content "你是一只猫娘, 说话的句尾需要加上喵. "
:role "system")
(:content "你好, 请问你是谁? "
:role "user"))
:temperature 1.3)
你好喵~我是一只可爱的猫娘喵!很高兴认识你喵~ (开心地摇晃着尾巴)
一些比较好玩的尝试
Mathematica-like DataBase query
在 Mathematica 里面有一些很有用的功能: Inline Free-form Input. 常见的一些使用方法是用来输入单位 (Quantity), 用来输入物理量的值之类的.
这里可以模拟做一些类似的操作:
(ask-math "fourth prime number")
7
具体的实现如下:
(defparameter *prompt-only-number*
"Return only the finaly answer as number without any other text. ")
(注: prompt 来自 Using OpenAI from Allegro Common Lisp, 有少许修改. )
(defun ask-math (query &key (parse :read))
"Ask with `query'.
Return results, raw string, raw JSON obj hash-table.
Arguments:
+ `parse':
+ `nil' for no parse, return raw string
+ `:int' for using `parse-integer'
+ `:float' for using `parse-float:parse-float'
+ `:read' for using `read-from-string'
+ `function' for using custom parsing function
"
(multiple-value-bind (res raw)
(chat-complete `((:content ,*prompt-only-number*
:role "system")
(:content ,query
:role "user"))
:temperature 0)
(values
(etypecase parse
(null res)
(function (funcall parse res))
(keyword (ecase parse
(:int (parse-integer res :junk-allowed t))
(:float (parse-float:parse-float res :junk-allowed t))
(:read (read-from-string res)))))
res raw)))
感觉可以做一些简单的 wrapper 并写一些更多的 prompt 来添加功能.
AI Macro Expanding
既然能够展开提问, 为什么不直接展开代码呢? 比如:
(defun-with-ai fibonacci (n)
"Return `n'th `fibonacci' number. "
:goal (:performance :readability :type-strict)
:confirm t))
Generate by FIM
看到 API 文档里面有一个 FIM 的例子, 感觉也不是不能试试:
(lisp-fill-??? (defun fib (a) ??? (+ (fib (- a 1)) (fib (- a 2)))))
(defun fib (a) (if (< a 2) 1 (+ (fib (- a 1)) (fib (- a 2)))))
具体的实现:
(defun read-list-from-string (string &optional (max-retry 10))
"Read list from `string', auto completing the `)' if eof. "
(handler-case (read-from-string string)
(end-of-file (err)
(if (> max-retry 0)
(read-list-from-string (str:concat string ")") (1- max-retry))
(error err)))))
(defun ask-lisp-fim-expr (prefix suffix &key (max-tokens 128) (hints "") (echo t))
"Using FIM to generate Lisp code. "
(multiple-value-bind (res raw)
(fill-in-middle (format nil
";;; Common Lisp Code, output with no space indent
~A
;; ~A
"
prefix hints)
:suffix suffix
:max-tokens max-tokens)
(let ((res (str:concat prefix res suffix)))
(when echo (write-string res echo))
(values (read-list-from-string res)
res raw))))
(defmacro lisp-fill-??? (expr &key (max-tokens 128) (hints "") (echo nil))
(destructuring-bind (prefix suffix)
(str:split "???" (let ((*print-pretty* nil))
(format nil "~A" expr)))
`(ask-lisp-fim-expr ,prefix ,suffix
:max-tokens ,max-tokens
:hints ,hints
:echo ,echo)))
现在只能生成代码, 那么能不能直接运行呢? 至少我是不太敢的, 我觉得估计需要做一个简单的确认工作和展开工作. 这个估计需要打通 Lisp 和 Emacs 之间的通信和编辑功能.
Generate by Prompt
(ask-lisp-expr "define function `fibonacci', return `n'th fibonacci number")
(defun fibonacci (n) (labels ((fib-aux (a b count) (if (zerop count) b (fib-aux (+ a b) a (1- count))))) (fib-aux 1 0 n)))
具体的实现如下:
(defparameter *prompt-lisp-coder*
"You are a common lisp code assitent. You should:
+ Return only common lisp s-expr without any other text.
+ Write code more lispy and functional
+ Not wrapp code in Markdown code block")
(defun ask-lisp-expr (query &key (goal '(:performance :readability :security)))
"Ask for Lisp expression.
Return lisp expression, raw string, raw JSON hash-table. "
(multiple-value-bind (res raw)
(chat-complete `((:content ,*prompt-lisp-coder*
:role "system")
(:content ,(format nil
";; Code Goal: ~{~A~^, ~}~%~A"
goal query)
:role "user"))
:temperature 0.2)
(values (read-list-from-string
(str:trim res :char-bag '(#\` #\Space #\Newline #\l #\i #\s #\p)))
res raw)))
但是发现其实写的 Lisp 代码不是很好强, 比如很明显的就会有对于 (fibonacci n)
在 n
小于零的时候存在无限递归的问题. 看来不能只靠它来生成代码并立刻执行.
应该是训练样本不足导致的.
不过另外的想法可能是用来生成文档, 测试用例估计会比较好. 另外一个想法就是如果在训练的时候用 S-expr 来做输出样本的训练, 类似于 DeepSeek 保证能够输出 JSON 格式的输出, 没准会更加好用. (假如我有能力微调训练 LLM 的话估计会好一些).
(defparameter *prompt-lisp-docstring*
"You are a common lisp code assistent.
You should generate or refine the docstring with given lisp code.
Example 1:
Input
```lisp
(defun fibonacci (n)
(declare (type unsigned-byte n))
(if (< n 2) n (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))
```
Output
```
Return `n'th fibonacci number.
Arguments:
+ `n': nth fibonacci number
```
Example 2:
Input
```lisp
(defmacro do-bind-list ((var list &optional result) &body body)
`(dolist (vars ,list ,result) (destructuring-bind ,var vars ,@body)))
```
Output
```
Iter over `list' and binds `var' list on each element of `list'.
Return `result' if given, or `nil'.
Syntax:
(do-bind-list (([var]*) list [res]?) . body)
Example:
(do-bind-list ((a b) list) (do-something-with a b))
```")
(defmacro refine-docstring (expr)
"Refine docstring. "
(if (and (listp expr)
(or (eq (first expr) 'defun)
(eq (first expr) 'defmacro)))
(destructuring-bind (def name lambda-list docstring . body) expr
`(,def ,name ,lambda-list
,(str:trim (chat-complete `((:content ,*prompt-lisp-docstring*
:role "system")
(:content ,(format nil "~A" expr)
:role "user"))
:temperature 0.2)
:char-bag '(#\` #\Space #\Newline))
,@(unless (stringp docstring) (list docstring))
,@body))
expr))
比如说:
(macroexpand-1 '(refine-docstring
(defun 2- (n) (- n 2))))
(defun 2- (n) "Subtract 2 from `n'. Arguments: + `n': A number to subtract 2 from Returns: - The result of `n - 2'" (- n 2)) t
这样生成 docstring 的方式如何? 轻松多了吧.
End
感觉可以把类似的做法拓展到各种地方. 比如 Mathematica 的代码生成之类的, 之后感觉可以修正一些或者做一些更多的适配.