把多个大模型塞进 Alfred:一次 Workflow 改造记录

按下 ⌥ Space,输入 ai 这段 curl 报错什么意思,回车。几秒后,回答直接在 Alfred 的 Text View 里流出来。

这就是我想要的 AI 入口:不打开网页,不找 tab,不切客户端,也不用每次换模型都重新组织问题。Alfred 已经是我每天最常用的启动器,所以这个项目的目标很直接,把常用的大模型接进 Alfred。

这不是一篇使用说明。它更像是一份开发记录:为什么要做这个 workflow,中间踩了哪些坑,它现在给我的工作流带来了什么方便,以及后面还能怎么长。

Alfred Text View 流式输出

多模型对比示例

我需要的是一个更短的 AI 路径

网页端当然好用。我日常也会打开 ChatGPT、Gemini、Qwen、Kimi 这些产品页面,它们各有适合自己的场景。

但网页不是我思考时的最短路径。Alfred 对我来说已经接近第二个 Finder,很多动作都是从那个输入框开始。问 AI 也应该一样,敲几个字,看一眼结果,然后回到正在做的事。

我刚开始用 Mac 时,就很容易被 Alfred 的 workflow 吸引。当时为了省几步操作,会用 JavaScript、Python 再配合 Alfred 的各种节点手搓小工具:查资料、处理文本、打开固定项目、拼接命令。那些 workflow 不复杂,但确实把很多重复动作从日常工作里拿掉了。

到了 AI 时代,这件事又变了。很多 workflow 已经不用完全自己手搓,只要把需求讲清楚,大模型基本能生成一个可用的骨架,再由我补上 Alfred 运行时里的细节。相比以前一边查文档一边拼节点,生成速度和实现难度都变了一个量级。

很多问题发生在写代码、查资料、读文档的中间。我只是想问一句:“这个错误更可能是哪一层的问题?”或者“翻译一下这段话?”如果每次都要切浏览器、找 tab、等页面加载,动作本身就开始打断思路。

另外,我刚好订阅了几个大模型的 Coding Plan,手里有 DeepSeek、MiMo、火山方舟、阿里云百炼这些 API key。既然 Alfred 已经是固定入口,把这些 API 接进去,比再多开几个网页更符合我的日常节奏。

所以我希望这个 workflow 做几件事:

  • 输入 ai 就能问。
  • 支持连续对话。
  • 能在几个国产模型之间切换。
  • API key、endpoint、模型名集中管理。
  • 失败时直接告诉我哪里错了,而不是空白轮询。

这就是项目起点。

从官方 Workflow 改,而不是从零写

我没有从零写。

Alfred 官方 ChatGPT Workflow 已经解决了几个关键问题:用 JXA 调 curl,解析 OpenAI 风格 SSE,通过 Text View JSON 流式刷新,保存多轮上下文,并支持清空、停止、复制回答。

这些单独看都不复杂,但组合在 Alfred 里就很容易出边界问题。既然官方版本已经跑通了 Alfred 这部分,我更应该基于它改,而不是重新造一个看起来干净、实际上没被 Alfred 打过的轮子。

官方 ChatGPT Workflow 源码

现在它能做什么

现在这个 workflow 叫 Alfred Multi-Model AI,这是整个workflow:

Alfred Multi-Model AI Workflow 全景

它把几个 OpenAI-compatible 的模型服务放进同一个 Alfred 入口:

  • DeepSeek
  • MiMo Token Plan
  • 火山方舟 Coding Plan
  • 阿里云百炼 DashScope
  • 还可以继续扩展

我可以用 ai 发起对话,用 aimodel 切换模型,用 aihist 查看和切换历史会话。Text View 顶部会显示当前模型,响应通过 SSE 流式回来,并带一点打字机式的显示效果。

它也保留了官方 workflow 里几个顺手动作:清空当前对话、复制上一条回答、停止生成。真正每天会用的工具,最后都要靠这些小动作省时间。

后来加的几个小细节,反而对体验影响很大:

  • 对话轮次之间加 --- 分割线。
  • 首包未返回时显示“正在生成,请稍候…”。
  • 网络错误、curl 错误、API 错误都直接显示。

工具用久了,影响心情的往往不是有没有大功能,而是失败时它是不是讲人话。

Alfred 真正麻烦的是运行时细节

我原以为主要工作是“换 endpoint”。

事实证明,endpoint 只是最容易的部分。

第一个坑是 JXA 和 Foundation 的桥接。脚本里曾经把 JavaScript 的 null 传给 Foundation 方法,真实请求路径一跑就报:

1
-[NSNull count]: unrecognized selector

这不是业务逻辑错,而是桥接层把“空值”变成了 NSNull。最后的修复也很朴素:需要空字典时就传 NSDictionary.dictionary,不要让 Foundation 猜。

第二个坑是 Text View 的 rerun。我一开始依赖 streaming_now 变量判断是否正在流式输出,后来发现 Alfred 的 rerun 环境不能完全按这个假设工作。结果是脚本每 0.1 秒都像新请求一样重置 stream 文件,界面一直停在空的 AI:

这里还有一个细节:Alfred 的 rerun 间隔有范围限制,低于 0.1s 会被忽略。我曾经把它设成 0.06,看起来像是在优化打字机效果,实际只是让 Alfred 直接拒绝执行。

后来改成用本地状态文件判断:

  • ai.stream 保存 curl 写入的响应流。
  • ai.pid 保存当前 curl 进程。
  • ai.query 保存本次问题。
  • ai.display_count 控制打字机显示进度。

这不花哨,但可靠。Alfred 每次 rerun 都是一次独立脚本执行,本地文件反而是更清楚的状态边界。

后来又补了一个失败路径:stream 里不一定总是 SSE。有时候里面只有 curl 的纯文本错误,如果解析器只认 data:,界面就会继续空白轮询。现在这类内容会直接显示成错误,至少我能知道该查 endpoint、DNS 还是 API key。

多模型不只是多几个配置

一开始我以为多模型只是四个字段:endpoint、API key、model、provider name。

做着做着发现,真正需要管理的是模型身份和会话身份。

比如切到 MiMo 后问“你是什么模型”,如果当前会话里残留 DeepSeek 的历史,模型很可能被上下文带偏。它不是接口错了,而是我把错误的历史喂给了它。

现在项目里有 sessions/index.jsonsessions/<id>.json。每个会话记录 provider、标题和更新时间。chat.json 仍然保留,作为当前会话的兼容镜像。

这里还有取舍。我试过“切换 provider 自动新建会话”,后来撤回了。因为有时候我确实想让另一个模型接着同一段上下文回答。更合理的做法,是以后在 aimodel 里提供两个显式动作:

  • 切换模型,保留当前上下文。
  • 切换模型,并新建会话。

行为应该让用户选择,而不是藏在脚本里猜。

它现在给我的方便

最直接的变化是少切上下文。

我可以在写代码、读文档、写说明的时候直接呼出 Alfred,输入 ai,把问题扔进去。如果想换个模型看回答差异,再用 aimodel 切一下。

它最适合这些场景:

  • 快速解释报错。
  • 让不同模型改写同一段话。
  • 比较 DeepSeek、MiMo、火山、百炼的回答差异。
  • 起草说明、提交信息、文章小节。
  • 问一些不值得专门打开网页的小问题。

还有一个意外的好处:它不会把我带进网页端的信息流。没有推荐,没有侧边栏,没有一堆历史卡片。它只回答当前问题。

这让我更容易把 AI 当工具,而不是当另一个工作场。

后面还能怎么扩展

这个 workflow 现在只是通用聊天入口,但它很适合继续拆出更小的专用入口。

我比较想做这些:

  • aitrans:固定翻译提示词,输出直译、自然表达、技术文档表达。
  • aidoc:先查常用技术文档,再让模型总结,减少胡说。
  • aireview:读取当前 git diff,输出变更摘要、风险点和测试缺口。
  • Universal Action:选中文件或文本后直接交给 AI,解释代码、总结文档或改写片段。
  • 剪贴板输入:空查询时自动读取剪贴板,少一步粘贴。
  • 个人知识库:索引本地笔记和文档,让回答基于自己的上下文。
  • prompt 预设:严格代码审查、中文博客编辑、技术方案反驳者、需求澄清助手。
  • 会话管理增强:按 provider 过滤、重命名、删除、导出 Markdown。

这些扩展不需要一个庞大的 AI 客户端。它们只需要在需要的时候,从 Alfred 里出现。

我怎么看这个小项目

这个 workflow 没有发明新技术。

它只是把几个常用模型入口放到 Alfred 里,再把流式显示、会话、模型切换、错误处理这些细节补到能日常使用。

但这类工具的价值就在这里。它不是为了展示 AI 能做什么,而是让 AI 出现在我真正需要它的位置。少一次浏览器切换,少一次复制粘贴,少一个“等会再问”的念头。时间长了,这些摩擦会决定一个工具到底会不会留下来。

这个项目也提醒我,个人工具可以做得快,但不能只靠“看起来能跑”。plutil 通过不代表 Text View 行为正确,JXA 能编译不代表 Foundation 桥接不会炸,API 能返回不代表 stream 解析器能看懂。

我现在更愿意把它当成一个小产品维护。功能可以少,但失败路径要清楚,配置要集中,状态要能解释。

对我来说,它已经跨过了“玩具脚本”的线。