1 前言 🔗
在大模型时代,一切皆有可能。对于 Emacs 同样如此,大模型拉低学习了 Emacs 的门槛,我们通过大模型可以做到以前我们敢想不敢做,或敢做没时间做,或有时间不会做的事。
今天继续这个新的系列,跟大家分享在大模型时代,我们可以如何“玩” Emacs,我们应该怎么用大模型来满足自己对 Emacs 使用的需求。
2 大模型 🔗
这个系列的几个案例,均基于 OpenAI 的 ChatGPT-4o
模型(主要是花了钱,不用浪费不是😀)。大模型能做很多事,这里简单说说我们可以通过 ChatGPT 对 Emacs 做什么:
- 让 ChatGPT 帮忙阅读代码,解释代码,这样以前看不懂的代码可以通过 ChatGPT 看明白
- 让 ChatGPT 帮忙写代码,尤其是我们对 Emacs 的配置进行调整,需要写一些函数甚至插件
- 让 ChatGPT 优化已经写好的代码,让代码更加规范和可读
- 让 ChatGPT 充当 Emacs 百科,充当 Google 的角色,回答各种配置问题
- …
所有的这一切,都需要有一个前提,就是你需要学会「正确的问问题」。我想这个技能,不是因为 ChatGPT 的出现才出现,这个问题亘古至今如此,只是对象,从以前问夫子,到网络时代问网友,再到 AI 时代问大模型,其原理不变其宗。
我们与 ChatGPT 的交流,主要通过提示工程。提示词是有方法论的,网上有很多教程,这里不做介绍。与 AI 交流,其实就跟练剑一样,练的多了,也就会了剑,无他,惟手熟耳!
关于这些案例,我在与 ChatGPT 交谈时,实际上并没有很好地遵循提示工程的“三段式”,比较随性,仅供参考。
3 中文按词移动和删除 🔗
3.1 背景 🔗
大家知道英文的单词之间有空格,而中文的词语之间没有空格,这就导致了中英文之间对于词的处理逻辑完全不同。如果按照英文的逻辑,我们按词来移动光标,就会把所有连在一起的中文句子作为一个“词”,这显然不符合我们中国人的使用习惯。
![](/ox-hugo/gif_20240521_183406.gif)
之前一直用 cireu
大佬的 jieba.el 包,很好用,但是不知道是因为升级了 node 还是因为升级了这个包,突然不能用了,一直报如下的错误,以我这个粗浅的水平捣鼓了半天也没找到解决方法。
map-keymap: Wrong type argument: processp, #[0 “���� �����������\”!&�" [“JIEBA-SERVER” jieba-server-start-args make-process :name :command :coding utf-8-emacs-unix :noquery t :connection-type …] 16]
于是就产生了找 ChatGPT 写一个自用版本的想法。
3.2 正确的描述我们的问题 🔗
3.2.1 第一次描述问题 🔗
令人欣喜的是 ChatGPT 给出的初始解决方案就是用 Python 实现的分词部分,这也正是我想要的。
![](/ox-hugo/img_20240521_211340.png)
3.2.2 测试以及需求累进的描述 🔗
我们需要对每一个模块进行测试,并且将测试的结果,精确的描述给 ChatGPT,让 ChatGPT 给出更新的代码,然后我们再次重试。
![](/ox-hugo/img_20240521_211720.png)
整个过程会遇到很多问题,例如性能的问题,刚开始的版本很卡:
![](/ox-hugo/img_20240521_211927.png)
当然,ChatGPT 并不总是正确的,至少我个人觉得他给的 2 个解决方案,并不是很好,我自己想了下给了他一个可能的解决方案来解决性能问题:
![](/ox-hugo/img_20240521_212035.png)
以及,我们将 Python 脚本服务化,通过 REST API 的方式来调用:
![](/ox-hugo/img_20240521_212122.png)
中间并不是一帆风顺,还会遇到各种报错和警告:
![](/ox-hugo/img_20240521_212228.png)
进一步地,我们要考虑在 Mac 上如何优雅的启动这个服务:
![](/ox-hugo/img_20240521_212309.png)
最麻烦的就是当你搞定了 Python 脚本和服务,搞定了其中 Backward 方向移动的时候,在 Forward 方向却一直无法正确的移动,此时,也许需要一点点自己的判断力、逻辑思维能力和创造力,我现在回想来,在整个过程中,我自己发挥了 2 个重要的作用:
- 提供了一个思路是把光标所在的句子文本送去分词,而不是 ChatGPT 提供的把整个 buffer 送过去分词
- 在经过大量的测试后发现,光标在移动到句子分界线尤其是句首的时候,总是出现问题,于是我扩展了送过去分词的文本范围,一句不行,我把两句一起送过去,这样既解决了性能的问题,也解决了边界的问题,当然这个方法并不优美,算是一个笨方法。
3.3 最终的效果 🔗
最终我们得到了如下的效果:
- 我们按下
M-f
和M-b
可以实现中文的按词向前向后移动,而且兼容英文。我们按下M-DEL
和M-d
实现了向后和向前按词删除。 - 我们利用了一个非常简单的 Python 程序来做 jieba 服务后端,其他部分完全通过 elisp 实现。
- 我们综合的考虑了性能和效果。
![](/ox-hugo/gif_20240521_213319.gif)
3.4 最终的代码和使用 🔗
3.4.1 Python 服务端 🔗
Python 服务端代码:
你可以将端口替换成你想用的端口,我这里随便挑了一个不太会被其他程序占用的端口。
from flask import Flask, request, jsonify
import jieba
# jieba.setLogLevel(jieba.logging.ERROR)
app = Flask(__name__)
@app.route('/segment', methods=['POST'])
def segment_text():
data = request.json
text = data.get('text', '')
if not text:
return jsonify([]) # 如果没有提供文本,返回一个空列表
words = jieba.lcut(text)
return jsonify(words)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=27350)
此时你就可以通过 python segment.py
来启动这个后端服务了。
如果你想在 Mac 上以 launchctl
和 gunicorn
来启动服务,ChatGPT 也帮我们生成了如下的 plist 文件:
请注意,请务必将下面的文件所有路径替换成你自己的路径!
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.remacs.segment</string>
<key>ProgramArguments</key>
<array>
<string>/Users/randolph/.venv/py310/bin/gunicorn</string>
<string>--chdir</string>
<string>/Users/randolph/bin/python_script</string>
<string>-w</string>
<string>4</string>
<string>-b</string>
<string>127.0.0.1:27350</string>
<string>segment:app</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Users/randolph/.venv/py310/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/randolph/Temp/com.remacs.segment.out</string>
<key>StandardErrorPath</key>
<string>/Users/randolph/Temp/com.remacs.segment.err</string>
</dict>
</plist>
然后我们可以通过如下的命令来启动这个服务:
launchctl load ~/Library/LaunchAgents/com.remacs.segment.plist
3.4.2 elisp 代码 🔗
(require 'request)
(defun call-jieba-segment (text)
"调用 RESTful API 使用 jieba 分词 TEXT。"
(let ((response (request
"http://127.0.0.1:27350/segment"
:type "POST"
:data (json-encode `(("text" . ,text)))
:headers '(("Content-Type" . "application/json"))
:parser 'json-read
:sync t)))
;; (message "请求状态: %s" (request-response-status-code response))
;; (message "请求响应: %S" (request-response-data response))
(if (not (eq (request-response-status-code response) 200))
(error "调用 jieba 分词 API 失败: %s"
(request-response-status-code response))
(request-response-data response))))
(defun get-current-sentence ()
"获取当前光标所在的句子。"
(save-excursion
(let (beg end)
(setq beg (progn (backward-sentence) (point)))
(setq end (progn (forward-sentence) (forward-sentence) (point)))
(buffer-substring-no-properties beg end))))
(defun get-segmented-words ()
"获取当前句子的分词结果。"
(let* ((sentence (get-current-sentence))
(words (call-jieba-segment sentence)))
(append words nil)))
(defun get-sentence-boundaries ()
"获取当前句子的起始和结束位置。"
(save-excursion
(let (beg end)
(setq beg (progn (backward-sentence) (point)))
(setq end (progn (forward-sentence) (forward-sentence) (point)))
(cons beg end))))
(defun backward-word-by-jieba ()
"基于 jieba 分词结果向后移动一个单词。"
(interactive)
(let* ((words (get-segmented-words))
(pos (point))
(boundaries (get-sentence-boundaries))
(sentence-start (car boundaries))
(word-starts (let ((start sentence-start)
starts)
(dolist (word words)
(setq starts (append starts (list start)))
(setq start (+ start (length word))))
starts)))
(catch 'break
(dolist (start (reverse word-starts))
(when (< start pos)
(goto-char start)
(throw 'break nil))))))
(defun backward-delete-word-by-jieba ()
"基于 jieba 分词结果向后删除一个单词。"
(interactive)
(let* ((words (get-segmented-words))
(pos (point))
(boundaries (get-sentence-boundaries))
(sentence-start (car boundaries))
(word-starts (let ((start sentence-start)
starts)
(dolist (word words)
(setq starts (append starts (list start)))
(setq start (+ start (length word))))
starts)))
(catch 'break
(dolist (start (reverse word-starts))
(when (< start pos)
(delete-region start pos)
(throw 'break nil))))))
(defun forward-word-by-jieba ()
"基于 jieba 分词结果向前移动一个单词。"
(interactive)
(let* ((words (get-segmented-words))
(pos (point))
(boundaries (get-sentence-boundaries))
(sentence-start (car boundaries))
(word-starts (let ((start sentence-start)
starts)
(dolist (word words)
(setq starts (append starts (list start)))
(setq start (+ start (length word))))
starts)))
(catch 'break
(dolist (start word-starts)
(when (> start pos)
(goto-char start)
(throw 'break nil))))))
(defun forward-delete-word-by-jieba ()
"基于 jieba 分词结果向前删除一个单词。"
(interactive)
(let* ((words (get-segmented-words))
(pos (point))
(boundaries (get-sentence-boundaries))
(sentence-start (car boundaries))
(sentence-end (cdr boundaries))
(word-starts (let ((start sentence-start)
starts)
(dolist (word words)
(setq starts (append starts (list start)))
(setq start (+ start (length word))))
starts)))
(catch 'break
(dolist (start word-starts)
(when (> start pos)
(delete-region pos start)
(throw 'break nil)))
;; 如果没有找到大于 pos 的分词起始位置,则删除到句子的结束位置
(when (< pos sentence-end)
(delete-region pos sentence-end)))))
;; 将函数绑定到快捷键
(global-set-key (kbd "M-b") 'backward-word-by-jieba)
(global-set-key (kbd "M-DEL") 'backward-delete-word-by-jieba)
(global-set-key (kbd "M-f") 'forward-word-by-jieba)
(global-set-key (kbd "M-d") 'forward-delete-word-by-jieba)
3.5 完整的聊天记录 🔗
这个案例,是我最近这段时间花时间最多,最复杂的案例,当然主要原因也是我自己水平有限。因此这个聊天记录里,有很多非常低级的问题,最终生成的代码也应该有很大的提升空间,仅供参考。
4 结语 🔗
今天这个案例,是一个相对复杂的案例,它涉及到了 Emacs 和外部服务之间的交互,整个聊天过程也比较复杂,除了 ChatGPT 之外,还需要动很多脑筋想办法解决问题。最终实现自己需求的那一刻,那份满足感难以言表。
后续,将会继续分享几个案例。
- Org mode 代码块的出错信息
- org-super-links 链接与反链改造
- …
我也会持续的使用 ChatGPT 和 Emacs,持续分享我的心得,感谢您的阅读。