UP | HOME

▼ 本文更新于 [2026-05-30 周六 14:24]

emacs-在org文件中利用ob-gptel保存调用LLM聊天对话(deepseek为例)


1. 前言

我用AI用得比较少,也就网页白嫖一下Google AI Studio的Gemini,或者玩一玩免费的DeepSeek网页版。最近DeepSeek发布了新版本,在保持低廉价格的同时可用性也仅次于几家闭源大模型了,不得不多用用了。

但是DeepSeek的网页版使用起来,感觉不如谷歌的AI Studio。后者将每场对话当做一个纯文本文件,可以自由编辑删减先前的用户输入、AI输出,而DeepSeek中用户的历史输入和模型的历史输出都是固定的、无法修改的,缺失了很大的灵活性。

幸好,我们有Emacs,可以充分自定义整个体验。

:今日发现 deepseek 的网页版也可以编辑用户发送内容了,会并行开一条新回答分支。现在只有一些不方便在网页上进行的提问会使用API

:有人测试发现网页版限制修改次数了:

大约在2026年5月29日17点左右的时候,DeepSeek官网与APP端的修改输入次数与重复生成次数上限都被大幅下调,根据我的实际测试,为专家模式3次,快速模式6次,识图与快速模式相同为6次,目前直至21点13分仍旧没有恢复。

还是继续用API吧。

2. 软件配置

首先要安装gptelob-gptel

因为大模型默认以 markdown 格式输出,所以需要安装 markdown-mode

(use-package markdown-mode
  :mode "\\.\\(?:md\\|markdown\\|mkd\\|mdown\\|mkdn\\|mdwn\\|mdx\\)\\'")

3. gptel

配置 gptel 。首先是变量设置环节。我们设置使用外部的 curl 用于发送网络请求。内置的 url-retrieve 总会有一些大大小小的问题。

(use-package gptel :demand t 
  :custom
  (gptel-use-curl t)

gptel-directives 是一整套系统提示词,你可以把需要重复使用的提示词写在里面,在 gptel-menu 中快捷切换。我这边就默认留空了。

  (gptel-directives
    '((default . "")))

设置自动移动光标。如果你在gptel的buffer中对话,默认情况下,模型的输出插入完毕后光标会停留在你发送的位置。通过以下设置自动跳转光标

  :hook
  (gptel-post-response-functions . gptel-end-of-response)

设置模型。首先在windows上,需要专门配置一下 curl

  :config
  (when (eq system-type 'windows-nt)
    (setq gptel-curl-extra-args '("--ssl-no-revoke")))

开始配置deepseek模型。这里配置默认使用 deepseek-v4-pro 模型。记得将 YOUR-DEEPSEEK-KEY 替换为你自己的api密钥。

  (setq gptel-model   "deepseek-v4-pro"
        gptel-backend (gptel-make-openai "DeepSeek"
                        :host "api.deepseek.com"
                        :endpoint "/chat/completions"
                        :stream t
                        :key "YOUR-DEEPSEEK-KEY"
                        :models '("deepseek-v4-flash" "deepseek-v4-pro")))

接下来配置一些预设,可以等会通过 :preset 启用。
这里设置了两个预设, max 用于配置思考深度为max(默认high),而 no-r 用于关闭flash模型的思考。

  (gptel-make-preset 'max
    :request-params '(:reasoning_effort "max"))
  (gptel-make-preset 'no-r
    :request-params '(:thinking '(:type "disabled"))))

4. 2026年5月11日-说明


经测试,在启用 gptel 的分支功能情况下

;; 启用分支功能
  (setq gptel-org-branching-context t)
  (setf (alist-get 'org-mode gptel-prompt-prefix-alist) "@user\n")
  (setf (alist-get 'org-mode gptel-response-prefix-alist) "@assistant\n")
  ;; https://github.com/karthink/gptel/discussions/1173 作者的删除回复中org标题函数
  (defun my/gptel-remove-headings (beg end)
    (when (derived-mode-p 'org-mode)
      (save-excursion
        (goto-char beg)
        (while (re-search-forward org-heading-regexp end t)
          (forward-line 0)
          (delete-char (1+ (length (match-string 1))))
          (insert-and-inherit "*")
          (end-of-line)
          (skip-chars-backward " \t\r")
          (insert-and-inherit "*")))))
  (add-hook 'gptel-post-response-functions #'my/gptel-remove-headings)

gptel 会将所有上下文视作一次单独的用户输入,如果里面有LLM的回复,则会被单独作为回复打包发送。
因此,假设有一个对话是这样:

* test
** test2
*** test32

则在「test32」后调用 gptel-send 时,发送的内容为:

[
    {
      "role": "system",
      "content": ""
    },
    {
      "role": "user",
      "content": "* test\n** test2\n*** test32\n你好"
    }
  ]

而等待AI回复后,假设是这样:

* test
** test2
*** test32
你好!我看到你发的测试内容了
**** test4 

则在「test4」后调用 gptel-send 时,发送的内容为:

[
    {
      "role": "system",
      "content": ""
    },
    {
      "role": "user",
      "content": "* test\n** test2\n*** test32\n你好"
    },
    {
      "role": "assistant",
      "content": "你好!我看到你发的测试内容了"
    },
    {
      "role": "user",
      "content": "**** test4"
    }
  ]

这种方式比 ob-gptelsession 功能灵活许多,可以考虑优先使用。但缺点也比较明显:一是必须一发一答,否则会将前面的所有内容拼接成单次用户输入,缓存命中可能就会变低;二是org的标题层级星号前缀会被保留,导致在长期对话中可能出现层级问题。不过如果不需要分叉,也没有必要另开一个新的标题吧,直接继续对话即可。

:注意,不开启 gptel-mode 的情况下,文本属性不会持久化保存。这就意味着关闭重启之后,会把先前所有记录作为纯文本的单次用户输入发送给LLM,导致上下文缓存可能丢失,或者需要更多次来重建。如果需要重启后依旧保持对话轮次,最好还是使用 ob-gptel

:经检测,需要开启minor-mode gptel-mode 才可以持久化保存对话边界。

我们需要在对应 org 文件的第一行加上 # -*- eval: (gptel-mode 1); -*- ,再重新加载这个文件,这样就可以让 gptel-mode 帮我们自动保存哪些是用户输入哪些是 LLM 的回复了,即使重启 Emacs 也没有任何问题,可以正确识别。

5. ob-gptel

配置 ob-gptel 。由于该包还未上melpa,我们只能从对方的repo手动下载了。下面的代码适用于30之后的emacs,我设置了该包在 org 加载完成后立即载入。

(use-package ob-gptel :after org :demand t
  :vc (:url "git@github.com:jwiegley/ob-gptel"
           :rev :newest)

gptel 加入 org-babel 的可执行语言中:

  :config
  (add-to-list 'org-babel-load-languages '(gptel . t))

修复一个小问题:当光标在 end_srcc 之后时,按 C-c C-c 执行代码的时候,会将本代码块重复纳入对话上下文中。解决方法也很简单,直接在寻找session函数前执行一下 org-backward-element 即可。

  (defun my/ob-gptel-exclude-current-block (orig-fun &rest args)
    "Adjust point so that the current block is not included in session history."
    (save-excursion
      (org-backward-element)
      (apply orig-fun args)))
  (advice-add 'ob-gptel-find-session :around #'my/ob-gptel-exclude-current-block))

6. 使用例

请参考ob-gptel的README。这里举例最基础用法:
基本查询

#+begin_src gptel
What is the capital of France?
#+end_src

#+RESULTS:
The capital of France is Paris.

切换模型

#+begin_src gptel :model deepseek-v4-flash
Write a haiku about Emacs.
#+end_src

:session 参数会收集所有共享相同会话名称的前置代码块。这里启用了 :dry-run yes ,让我们可以先不把内容发送给LLM,而是在 RESULTS 中提前预览一下

#+begin_src gptel :session my-chat
Tell me about Emacs.
#+end_src
#+RESULTS:
假设这是第一个输入的模型回复
#+begin_src gptel :session my-chat :dry-run yes
What about its history?
#+end_src
(:model "deepseek-v4-pro" :messages
        [(:role "user" :content "Tell me about Emacs.")
         (:role "assistant" :content "假设这是第一个输入的模型回复")
         (:role "user" :content "What about its history?")]
        :stream :json-false :temperature 1.0)

如果你已配置了 gptel 预设,可以直接使用它们:

#+begin_src gptel :preset no-r
Explain monads simply.
#+end_src

7. 配置文件

(files--ensure-directory my/desktop-path)
(let ((file "gptel-config.txt"))
  (org-babel-tangle-file (buffer-file-name) (concat my/desktop-path file) "elisp")
  (format "[[https://blog.prayhand13013.top/%s][一键查看上述配置代码汇总]]" file))

一键查看上述配置代码汇总

© Published by Emacs 32.0.50 (Org mode 10.0-pre) | RSS Comment