像管理代码一样管理提示词:QuickGPT 的设计
初稿于 2024 年发表于公司内网,2026 年对外重写
提示词散落各处,没法迭代
我的提示词一度散落在各个地方:备忘录里存一些,聊天记录里留一些,还有一些填在各个 AI 工具的自定义指令框里。用的时候靠翻,翻不到就凭记忆重写一遍。
数量少的时候这不算问题。等积累到上百条,三个缺陷就藏不住了。第一,没法版本管理:一条提示词前后改过几个版本,旧版本丢在哪里不知道,改坏了也回不去。第二,没法迭代:好的提示词是逐次打磨出来的,第一版能用,第五版才顺手;散落存放意味着每次都从头写,之前的打磨全部作废。第三,没法带上下文:提示词是死文本,每次使用都要手动把选中内容、剪贴板、相关文件一段段贴进去。
2023 年我开始写 QuickGPT,一个开源的 Raycast 扩展(Raycast 是 macOS 上的快捷启动器,支持第三方扩展),代码在 github.com/ddhjy/quickgpt-raycast。到今天它管理着我个人 300 多条提示词。整个工具围绕两个理念设计:manage prompts like code,把提示词当代码管理;use prompts like tools,把提示词当工具使用。
Manage prompts like code
提示词以 HJSON 文件存放。HJSON 是对人友好的 JSON 变体,支持注释和多行字符串——写提示词这种多段长文本,不用像 JSON 那样把换行转义成 \n,还可以在某条提示词旁边加注释说明适用场景。一条提示词大致长这样(字段有省略):
{
# 提交代码前自检用
title: 代码评审
content:
'''
请评审以下改动,指出问题并给出修改建议:
{{diff}}
'''
}
这些文件放进 Git 仓库,代码怎么管,提示词就怎么管:每次修改有 diff,能看到一条提示词从第一版到现在改过什么;改坏了可以回滚;多台设备之间用 Git 同步,换电脑 clone 下来就是全部。
版本管理带来的最大变化是:打磨有了去处。用一条提示词时对输出不满意,顺手改措辞、补约束,commit 之后这次不满意就成了记录在案的一次修改,下次在它基础上继续改。散落存放时同样的不满意没有去处,只能等下一次从头再写,然后在同一个地方再不满意一遍。
这套做法解决了「散落」三个缺陷里的前两个。第三个——没法带上下文——靠第二个理念解决。
Use prompts like tools
提示词配上动态占位符之后,就从静态文本变成了「随处可调用的工具」:在任何应用里通过 Raycast 全局唤起,占位符在唤起那一刻自动填充成当下的内容。
占位符系统是 QuickGPT 的技术核心,常用的有这些:
{{input}}:唤起时手动输入{{selection}}:当前选中的文本{{clipboard}}:剪贴板内容{{currentApp}}:前台应用名{{browserContent}}:浏览器当前页面内容{{diff}}:当前仓库的 git diff{{file:路径}}:注入指定文件的内容{{option:key}}:交互式下拉选项{{selection|clipboard}}:fallback 链,选中文本为空时自动退到剪贴板
单看每一个都平常,组合起来才有意思。一条「代码评审」提示词引用 {{diff}},唤起时自动带上当前仓库的改动,评审要求和被评审的代码一次就位;一条「翻译」提示词用 {{selection|clipboard}},不管是选中了一段文字还是刚复制了一段,一键可用,不用关心内容从哪来。可枚举的参数则交给 {{option:key}} 做成下拉选项,唤起时选一下,一条提示词顶原来的好几条。
对比一下两种使用方式。没有占位符:找到提示词,复制,切到 AI 工具,粘贴,再回去复制要处理的内容,再粘贴。有占位符:在任何应用里唤起,内容已经组装完毕。前者的摩擦不大,但一天重复很多次,足以决定一条提示词是被用起来还是被遗忘。
散落存放还有一个隐含成本:提示词绑定在各自的工具里,浏览器插件里的出不了浏览器,某个客户端里的出不了客户端。占位符加全局唤起把这层绑定拆掉了——提示词属于系统层,在哪个应用里都能用。
忽略规则是数据边界
{{file:路径}} 指向目录时,会把目录内容送进模型。目录里未必都是该给模型看的东西:构建产物、体积很大的资源文件、带密钥的配置。
QuickGPT 读目录时叠加两层忽略规则:.quickgptignore 和 .gitignore,语法与 gitignore 相同,用来排除生成物、大文件和私密配置。
我的立场是:忽略规则是数据边界,不应该为了「上下文更完整」而绕过。工具的方便和数据边界冲突时,边界优先。这条原则写进代码比写在文档里有效——默认就不读,好过依赖使用者每次自觉。
一些顺手的设计
- 嵌套 subprompt:提示词可以分层组织,子提示词继承父级属性,同类提示词的公共部分只写一次。
- deeplink:每条提示词有自己的链接,可以携带占位符参数直达。其他工具由此能一键调起某条提示词,不需要先打开 QuickGPT 再搜索。
- 拼音搜索:提示词标题多是中文,搜索时敲拼音首字母就能命中。
- 临时文件目录 7 天自动过期清理,中间产物不会越积越多。
- 与 FinderLink 打通:FinderLink 是我的另一个工具,维护文件路径别名;这些别名可以直接写进
{{file:}}里解析。两个工具之间没有代码依赖,只有格式约定——个人工具之间用约定互联,比互相引用轻得多。
Raycast 扩展的两个通用坑
开发过程中有两个坑值得记录,写任何 Raycast 扩展都可能遇到。
第一个是 getSelectedText() 的调用时机。扩展窗口打开之后,焦点已经被 Raycast 抢走,这时再调 getSelectedText() 经常失败,代码随即错误地回退到剪贴板——用户明明选中了 A,扩展却拿到了剪贴板里的 B,而且并非每次复现,排查起来很费劲。修复模式是在模块加载的最早期就发起捕获并保存 promise,后续需要选中文本时复用这个 promise,不要用时现取。
第二个是不可见字符。从各种应用里选中或粘贴出来的文本,首尾可能混着零宽字符、U+FFFC(对象替换符)、控制字符。不清理的话,搜索匹配和提示词拼接会出现玄学问题:两段肉眼完全一样的文本,程序里就是不相等。对所有外部来源的文本清理首尾不可见字符,应该当成固定步骤。
提示词库是基础层
300 多条提示词的价值不在数量。它们连同 Git 历史一起,构成一份「我怎么用 AI」的可迭代记录:哪类任务交给模型、用什么话说、这些话怎么一步步变准。每次 commit 都是一次打磨的痕迹,翻历史能看到自己使用方式的变化。
提示词库是个人 AI 工作流的基础层。后来我做的东西——聚合上下文的工具、Agent 工作流——都建立在这层之上。基础层稳定,上面的东西才搭得起来。
QuickGPT 的完整介绍(技术要点、设计决策与当前状态)在它的项目页。