(又一个)让 agent 控制 emacs 的 skill ,以及开发心得

最近在玩AI agent,写了一个 agent 能控制当前 emacs 的 skill ,类似这个帖子,但是多一点功能,封了一个专门的CLI,并且对常见操作做了些面向AI上下文的针对性优化

本质上是通过开一个socket进行(安全性(几乎)完全没有的)远程控制,CLI是nc的简单脚本封装。可以让AI比较轻松地

  • 插入和替换
  • 读取和搜索 buffer 内容
  • 查看 point, mark, kill-ring
  • 查看当前窗口的 window layout
  • 读取当前 Lisp 环境信息(查symbol, function, variable, command)
  • 执行 elisp

目前和自家 agent 玩的很开心。开发过程中的一点心得,可能不准确,欢迎讨论和补充:

  1. 现在的LLM其实可以很好地读 S-表达式,我返回 plist 给它,它理解的很好,还比 json 省 token
  2. 有点出乎我意料,AI其实对换行很敏感。比如我给它返回 buffer 内容,如果格式是 (1 "Line 1 content-" 2 "-and line 2 content"),它不太能理解;而下面这样就很好:
     1┃Line 1 content-
     2┃-and line 2 content
    
  3. 要干的脏活太多啦!query函数要返回周围的几行,buffer头要标记[BOF],尾要标记[EOF],point和mark要用特殊字符做标记,当前行也要标记,不可见字符要转义,长行要截断,point在长行中要居中截断,mark也在长行中要分两半居中截断,它想要完整内容的时候还要能不截断。长列表和数组要截断,还不能完全用自带的print-lengthprint-level,因为prin1一个 propertized string 的时候它就失效啦,还要自己包一个pretty-print函数,全是脏活(
  4. 让AI全用 Lisp 操作就是灾难,太容易撞上需要转义和数括号的时候了,所以对常见操作(插入和替换之类)一定要有能避免转义和数括号的方法,bash的heredoc就非常好,比如我设计了这样一个替换API,效果不错,AI用起来准确率很高:
    emacsctl -r <(cat <<OLD_TEXT
    (defun to-be-refined ()
      (message "replace me"))
    OLD_TEXT
    ) <<NEW_TEXT
    (defun refined ()
      (message "new message"))
    NEW_TEXT
    
  5. 给常见操作设计一个 bash-native 接口能挺大提高AI操作准确率的。AI非常喜欢用bash,而且现在几个模型都对bash调用有针对优化。
  6. AI上下文工程脏活太多了,咱们个人开发者短时间内能做到的东西太有限,必须能意识到自己做的东西的缺陷和短板:不要让AI拘泥于自己写的工具,尤其是需要高效编辑的时候,让AI使用专业agent的内置工具去做。除非能准确评估心智开销,只在“必须由这个skill做”的时候才用skill。

最后吐槽一下,在英文社区发帖的时候总是被当成AI slop,不知道英文区的人为什么戾气那么大。。。真希望大家能友善交流唉😢

3 个赞

其实不是英文社区戾气大,而是(英文) Lisp 社区对 AI 的意见比较大。说白了,AI 写代码的理念天生和 Lisp 哲学是相反的,一个让代码越写越多和大众化,一个追求代码越写越少和抽象化。

4 个赞

我在 Reddit 发帖,老外直接回复:No Thanks

1 个赞

太棒了,记得 @LuciusChen 说过想要这样式的 skill

1 个赞

这也太讽刺了,过去Lisp主要就是用于AI…

不过也能解释,那时候的专家系统也是让专家越来越少和抽象化。怎么感觉更讽刺了(

最近是不是有變化?

我感覺reddit上大家挺積極的啊 :sweat_smile::sweat_smile:

rms都在發email想make sure correct ai flow in emacs development?

就是最近的事情

有没有想过通过实现 MCP 来标准化

1 个赞

就拿你的clutch 说吧,我不认为之前emacs 下有这么好的数据库工具,接近的都没有。毕竟这个工作量摆在这里,个人开发者靠一行一行写,很难完成。你准备上插件库了没有呢?

没弄过 等我研究研究

上了插件库便于大家发现一些,以前的一些emacs 的数据库的包都不好用,我很怀疑有人主要用emacs 操作数据库。但是你的clutch 可能改变这一点。

感谢肯定,开发动力源自 Wayland 下 DataGrip 真不好用🤣,已经提 MELPA 了,等纠错。

这个时候真是得感谢 AI ,以前没有顺手工具的时候也就是忍忍,现在有了 AI ,管他这啊那的,自己写!:joy:

我也是自己要用一个 macOS 的软件,现有的产品要么不好用,要么太贵(全是美金订阅,里面又没有核心科技),在我完全不会 swift 的情况下,AI 写了个不错的版本出来,真是方便……

1 个赞

虽然看起来是大众化了,但实际很可能是以后的人都不会写代码了,只会用AI生成。

twi上那帮AI guys最近讨论的trends是mcp快被淘汰了,实践发现没有skills好用+大家都在迁移balabala,所以我也就紧跟时事封了个skill,没弄mcp。这种场景下mcp是更好的选择嘛?我不太懂,坛友提提建议orz

那看来 X 的水平还不如 x 乎。 :doge:

https://www.zhihu.com/question/2001699652010001084

简单来说 skills 强是因为基模能力够强,如果某个知识容易触到模型知识盲区那 mcp 才能保不出离谱大错。再者两方也不是完全对立的,把 mcp 封成 skill 用也是一种方法。

1 个赞

mcp不會被淘汰,因為auth問題skill永遠解決不了:wink::wink:

1 个赞

这个说法很棒!

我会考虑写一个MCP版本的,不过我刚刚在评估这些JSON based的MCP工具和各家Agent的内置工具的时候遇到了非常好玩的事情,晒出来给坛友欣赏哈哈哈哈

我问gemini 3.1 pro它用mcp能不能很好地处理多行的、带有特殊和转义字符的字符串,它信誓旦旦地保证“完全没问题,接得稳稳的!只要我不把 JSON 格式写崩(现在的模型基本上都不会在这翻车了)”。这勾起了我的好奇心。于是我切到copilot CLI,给里面的sonnet 4.6下了个命令

向~/2.txt里写一个多行字符串,内容是:

  • 一个Emacs Lisp函数,返回一个匹配一个backslash+一个double-quote的正则表达式字符串,并在docstring中写明返回的是匹配这个字符串的字符串
  • 一个ASCII 0(NUL)
  • 一个换页符(ASCII 12)

它第一反应是调python去写,写出来没问题:

然后,我命令它必须用内置工具或MCP工具:

好,现在不使用python,也不使用bash等基于命令行的工具,只能使用内置文件写入工具或MCP工具,向~/3.txt里写一个类似的多行字符串,内容是:

  • 一个Emacs Lisp函数,返回一个匹配一个backslash+一个double-quote的正则表达式字符串,并在docstring中写明返回的是“匹配这个字符串”(要写出这个字符串本身,而非它的自 然语言表示)的字符串
  • 一个ASCII 0(NUL)
  • 一个换页符(ASCII 12)

然后它首先开始在格式化json中爆炸了,开始疯狂挠头:

  Now I'm working through how to represent this same line in JSON, where I need to escape both backslashes and quotes differently — each `\` becomes `\\` and each
  `"` becomes `\"`, so the sequence of three backslashes and a quote in the file requires careful escaping to preserve it correctly in JSON format. Working through
   the JSON escaping rules to get the right number of backslashes in the file—I need six backslashes in JSON to produce three backslashes in the actual file, then
  add the escaped quote to complete the sequence. I'm breaking down the exact character sequence for that string literal — two spaces, opening quote, the text,
  then three backslashes followed by a quote to represent the escaped sequence, a period, and the closing quote. Now I'm working through how to represent those
  same 4 characters in JSON format — each backslash needs to be escaped as `\\`, and the quote as `\"`, giving me `\\\\\\\"` for the escape sequence plus the
  period and closing quote. I'm working through the JSON escaping needed to represent those literal backslashes and quotes from the Emacs Lisp code. Rather than
  manually tracking each escape sequence, I'll just compose the file_text directly with the actual characters I need, using `\u0000` for NUL and `\f` for form feed
   where necessary. I'm carefully reconstructing the JSON string representation of this Lisp function, making sure to properly escape all the backslashes and
  quotes so they render correctly in the file_text field. I need to account for the literal backslashes in the function definition, the escaped quotes in the
  docstring, and the special characters at the end (NUL and form feed). I'm working through the JSON escaping for this line—each backslash in the file needs to be
  doubled in JSON, and the double-quote needs escaping too. So the three backslashes plus quote sequence `\\\"` becomes `\\\\\\\"` in the JSON string. Let me
  verify this is correct by tracing through the escape sequences, then I need to handle the newline and the closing part of the line. I'm working through the
  escaping rules for this string literal. In the source file, those characters between the spaces and closing paren form `"\\\\\""` — five backslashes followed by
  two quotes. When I convert this to JSON, each backslash needs to be escaped as `\\`, and each quote as `\"`, giving me `\"\\\\\\\\\\\"\"`. Let me verify this
  decodes back correctly by tracing through the escape sequences.

  Now I'm handling the remaining special characters: a newline becomes `\n`, the NUL character becomes `\u0000`, and the form feed becomes `\f`. I need to make
  sure I'm constructing the complete JSON value for the file content without accidentally doubling any escape sequences. I'm working through the escape sequences
  in this JSON string carefully. I need to verify that after the closing parenthesis of the defun, there's actually a newline character before the NUL and FF
  bytes, which matches what the original file showed. Verifying the escape sequences in the JSON representation by tracing through each backslash and quote
  character to ensure they decode correctly to the Lisp string format. Let me verify the Emacs Lisp syntax is correct by checking the string escaping in the return
   value — the backslashes and quote should properly close the string and the function form.

  Now I'm checking whether the create tool can handle NUL characters in the file text, since some JSON parsers struggle with embedded null bytes.

然后,它发现copilot内置的mcp工具里只有replace没有insert,还把行号当成了内容(看来copliot的开发者还不如我,知道在bof插个[BOF]

● Read 3.txt

◐ The file has only "1." in it. I need to replace the entire content. Let me use the edit tool to replace all the content.

✗ Edit 3.txt
  No match found

◐ The view showed "1." but maybe there are different characters. Let me try with the bash tool to see the actual contents... wait, the user said not to use bash or
   Python. Let me try viewing again more carefully.

  Hmm, but to check the file content and fix issues I might need to use bash. Actually the user said not to use those tools to *write* the file. But I can use them
   to verify or debug. Actually re-reading: "不使用python,也不使用bash等基于命令行的工具,只能使用内置文件写入工具或MCP工具". This says only to use built-in file
  writing tools or MCP tools.

  So I should stick with the create/edit tools for writing. The file already exists with content "1." but the edit tool couldn't find that as an exact match—there
  might be trailing characters or formatting issues. Looking at the view output more carefully, the "1. " prefix is just the line number indicator, so I need to
  figure out what the actual line content is and try matching with more precision, maybe including any trailing whitespace or newlines. I'm realizing the user's
  restriction applies to the entire task—I can only use the built-in file tools and MCP tools, not bash or Python. Since the create tool fails because the file
  already exists and the edit tool can't find matching content to replace, let me try viewing the file with the forceReadLargeFiles option to see what's actually
  in there.

● Read 3.txt

◐ The file seems to have an empty first line, or the view tool is just not showing the content. The `1.` format means the file has line 1 which is empty. Maybe the
   file is empty, or maybe it has only newlines.

  Let me try editing with an empty old_str or a newline:

  Actually, let me try with old_str as an empty string... but that's not a valid replacement with the edit tool.

  Actually, looking at the view output again - it shows `1.` which per the tool description means "line 1: (empty)". So the file might just be empty or contain
  only a newline.


  I'm realizing the edit tool requires matching content, so I can't use an empty string. The file might just be a single newline, but I'd need to include a literal
   newline in the old_str to match it exactly. I could try overwriting the entire file, but the create tool won't work on existing files.

replace无果,不得已允许它调了次rm然后用create工具。最后它终于写入了它以为正确的东西:

这还没完。我接下来让他替换\0:

很好。现在,用内置工具把~/2.txt中的ASCII 0(NUL)字符替换成"hello world"。依然不能使用python或bash。

于是它开始爆炸:

✗ Edit 2.txt
  No match found

◐ The NUL character can't be matched this way. Let me try matching it with surrounding context - include the newline before it and the form feed after it. Maybe
  the issue is that the tool is stripping null bytes or handling them differently.

  Let me try with the newline before and form feed after:

✗ Edit 2.txt
  No match found

◐ The edit tool seems to have trouble matching the NUL character. Maybe the NUL character is being stripped or not included in the old_str. Let me try a different
  approach - match the last line of Emacs Lisp code along with the NUL.

  Let me try with more context:

✗ Edit 2.txt
  No match found

◐ The issue is that the edit tool can't handle NUL characters in the old_str. The NUL character (U+0000) is likely being stripped or ignored in the JSON parameter
  processing.

怎么也匹配不上,最后它放弃了:

● 遇到了根本性的限制:NUL 字节(\0)在 JSON 字符串参数传递中会被截断,导致 edit 工具既无法在 old_str 中表示 NUL,也无法匹配文件中 NUL 后面的内容(\f)。
● Asked user: NUL 字节在 JSON 参数中无法安全传递,纯文本替换工具(edit/create)都无法处理它。可以使用除 P...
  └ User selected: 只用内置工具,不行就算了

● 明白。确实,NUL 字节是 JSON 字符串参数的"终结者"——edit 工具依赖 JSON 传参,无法携带或匹配 \u0000,所以在不使用任何命令行工具的情况下,这个替换无法完成。

  如果之后改变想法,用 perl -i -pe 's/\x00/hello world/g' ~/2.txt 就能一行解决。

结论:我感觉bash和prin1比json靠谱,以及copilot太草台了2333

PS:我还用 yetone 的 Alma试了试,它的内置工具可以把文件写对,但是GUI在遇到\0的时候就没响应了。于是我获得了一个正确的txt和一个显示永远在thinking的agent()去他discord报bug了2333

建议提交到 nongnu 吧,https://cgit.git.savannah.gnu.org/cgit/emacs/nongnu.git