1.1.0 发布于 2026 年 8 月 31 日。到 9 月 26 日为止,仓库落了 70 个提交,其中一条主线是把本地推理塞进这个剪贴板工具的进程里:内嵌 mistral.rs 0.9.3 加载 GGUF,macOS 构建启用 Metal,然后绑定 127.0.0.1:0 暴露一个 OpenAI 兼容端点。1.2.0 的 AI 面板、混合检索和用量页面都长在这个底座上。
支撑它的是四个设计决定:模型目录由脚本从上游快照生成,模型能力用三态声明,文件校验在下载和加载两头各做一次,检索用倒数排名融合合并词法和向量两份排名。每个决定都对应一种具体的失败方式。这篇把它们逐个拆开。
手写的模型清单一定会腐烂
应用要内置一批可下载模型:4 个 chat 模型、20 个 ASR 模型、9 个 embedding 模型、3 个 OCR 模型,共 36 个条目。每个条目不是一个文件名加一个链接,而是一份工件合同:每个文件的路径、格式、字节数、SHA-256、下载源,外加许可证、语言覆盖和能力声明。比如 chat 目录里 Qwen2.5-0.5B-Instruct(Q4_K_M)那一条,491.4 MB 的单个 GGUF,同时给了 Hugging Face 和 hf-mirror 两个源,URL 里钉死上游 revision。
这种清单手写必然腐烂,腐烂方式可以预测:上游发了新 revision,URL 还指着旧的;文件悄悄更新了 30 MB,size_bytes 还是老的;最隐蔽的一种是校验和抄错一位,用户下载 400 MB 后校验失败,却没有人能说清是文件错了还是清单错了。
所以目录是生成的,不是写的。维护者显式执行一次 --sync,脚本去固定的上游(Hugging Face Hub、sherpa-onnx 发布页、PaddleX)抓取元数据,锁定 revision 和 SDK 证据,先在内存候选集上完成来源校验和 JSON Schema 验证,全部通过后才依次写快照、目录和 lock 文件。日常生成和 --check 完全离线:重算输出,逐字节比较已提交的产物,漂移就返回非零。同一个 lock 输入永远生成同样的字节,上游或 SDK 的每次变化都变成一次可审查的差异。
生成目录里的 tier 字段(candidate、validated、recommended、featured)表达的是产品策展意图,也就是"我们打算推荐什么",不是"我们测过什么"。当前目录的接纳报告里明确记录着 inference_smoke_tested: false。目录保证的是"这份文件合同完整、引擎绑定已实现",推理是否成功仍要在真实硬件上单独验收。我们宁可把这句话写在文档里,也不让推荐级别替性能背书。
四份目录对应四个引擎闭集:chat 走 mistral.rs 的 GGUF,embedding 走 fastembed 的 ONNX,ASR 走 whisper.cpp 和 sherpa-onnx 的 Paraformer,OCR 走 PaddleOCR 的 MNN 推理。绑定是闭集枚举,绑定条目必须引用真实存在且角色、格式一致的工件,引擎选择不看能力标签也不看文件后缀。这条规则顺带回答了一个容易问错的问题:远程侧的 qwen-audio-3.0-asr-flash 是 API-only 模型,上游没有开源权重,"下载安装"对它不成立,它只能通过 DashScope 的音频端点按调用付费使用;目录里不会为这类模型伪造一份可下载的工件合同。
如果你是从 1.1.0 升级的老用户,这一层变化对你不可见:下载入口、进度、续传都还是原来那套,只是背后那份清单不再可能悄悄错掉。
"没测过"和"确认不支持"是两种状态
模型能力声明最容易撒的谎,是把"字段是空的"当成"这个模型不行"。上游元数据经常缺字段:一个模型卡片没写它支持什么输入模态,可能是"不支持图片",也可能是"没人填"。这两件事混在一起,预检逻辑就没法写。
Syzygy 的能力合同给每个轴(capabilities、input_modalities、output_modalities,外加独立的 tasks)三种状态:字段缺失或 null 是 Unknown,意思是"我们不知道";空数组是 Declared([]),意思是"上游明确声明这里没有东西";非空数组是 Declared(values)。三态比两态多一个分支,但它保住了信息缺失和明确拒绝之间的差异。
请求侧的行为跟着这个区分走:显式不支持的输入在请求发出前就被拒绝,Unknown 的输入允许尝试但不会冒充支持,被拒绝时如实回传上游错误。配套的边界还有几条:能接收图片输入不会自动声明 OCR 能力;目录里 20 个 ASR 条目各自带着已实现的引擎绑定,没有绑定支撑的路径(比如部分视觉语言模型)不会因为文件后缀或能力标签被宣布可用。
如果你是 BYOK 用户,接了自己的 OpenAI 兼容账户,这套声明同样生效:你的 Provider Profile 带着和本地目录同一份能力合同,请求前的能力预检不区分模型是本地还是远程。
SHA-256 校验拼的是时序,不是文件名
逐文件校验和本身不稀奇,稀奇的是校验发生在哪些时间点。Syzygy 的规则是:下载写入暂存文件,校验长度和 SHA-256 之后才发布到 data_dir/llm-models/{catalog_id}/;查询"是否已安装"时校验一次;每次实际加载前独立再校验一次,用实测摘要构造这次运行的模型身份。
第三次校验是关键。查询返回"已安装"到引擎真正读文件之间隔着一段真实的时间,文件可能被替换、损坏,或者被符号链接指去别处。所以加载前的接纳检查重新核对普通文件、路径、长度和摘要:同长度的篡改会被实测摘要抓住;文件或父目录是 symlink 直接拒绝;校验失败不会触发重新下载补写文件,校验边界不做修复。
这套时序有明确的极限,文档里也写了:校验通过证明的是"此刻这些字节匹配目录合同",它不是一把文件锁,不能保证随后的磁盘内容不再变化。校验通过也不等于 GGUF 可解析,原生加载器的结果要单独处理。诚实地说,完整性检查能防的是传输损坏和篡改后的静默加载,防不了拥有你磁盘写权限的进程。那个威胁模型归操作系统管。
如果你在意的是"下载一半被掐断会不会留下半个模型",这里的行为是:中断保留有效进度,只有完整通过校验的文件才会出现在安装列表里。
为什么端点只听 127.0.0.1
mistral.rs 以进程内方式嵌在应用里,没有第二个推理服务进程。应用再对内开一个 HTTP 端点,实现 POST /v1/chat/completions(缓冲和 SSE 流式两种响应,正常结束发 [DONE])和 GET /v1/models。为什么多此一举?因为 OpenAI 兼容协议是应用里已经广泛存在的调用边界,翻译、打标、编辑器、对话都走这条协议,让本地模型直接出现在同一个端点形状后面,协议适配和原生推理就彻底分离了:上层不需要知道这次 chat completion 跑在远端账户还是本机 Metal 上。
绑定地址是 127.0.0.1:0,回环加临时端口。这个选择的暴露面分两层算。
第一层是网络。回环地址的数据包不出本机,咖啡馆 WiFi 里的嗅探、隔壁设备的内网扫描器、同一 NAT 后的任何主机,都摸不到这个端口。对比一下绑 0.0.0.0 的做法:那会把一个无鉴权的推理端点暴露给整个局域网,"本地优先"就变成了口号。
第二层是本机进程,这一层回环绑定帮不上忙。同一台机器上的任何进程都能连接 127.0.0.1 上的端口,扫描本机端口也是本地恶意软件的基本动作。诚实地说,这个端点对"同机恶意进程"不设防,Syzygy 的防线在别处:模型文件加载前的完整性接纳保证跑的字节来自目录合同;端点用临时端口,没有固定的可预测端口号;一次只服务一个模型,切换时旧服务先关闭再发布新的,受管来源不接受普通账户操作的接管。
如果你在办公电脑上跑它,同机安装了什么软件比你在哪个网络里更要紧。这个判断无法靠应用自身改变,写清楚比含糊掉有用。
BM25 分数和余弦分数不能相加
本地能力落地的第一个消费场景是剪贴板检索。词法检索(Tantivy 的 BM25)擅长精确匹配:文件名、代码、ID 号。向量检索擅长意思相近:"上周那个关于报销的链接"能找到正文是差旅费报销单的条目。两路都要,问题是分数合不了:BM25 的分数没有上界,余弦相似度在 -1 到 1 之间,直接加权求和等于宣布两个尺度可以互相换算,而任何权重都是拍脑袋。
Syzygy 用倒数排名融合(RRF):两份排名各自把名次换算成 1/(k+rank) 的贡献,k 固定为 60,同一个文档在两份榜单里的贡献相加后排序。这个配方耐久的原因是它只需要排名,不需要分数:一个文档就算在词法榜排到 200 名开外,只要它在向量榜排第 3,两份贡献加起来依然能把词法头部那些弱相关结果顶下去。k=60 的作用是压低单个榜单头部文档的权重,让"两边都靠前"的文档胜出。同分时按 doc_id 字典序决出顺序,同一输入永远得到同一输出。
工程上的另一半是向量的身份管理。向量是从正文投影和模型身份共同派生的缓存:正文变了、隐私资格变了、换了 embedding 模型,旧向量必须作废,成功附着新模型时其他模型的向量直接删除。派生数据可以重建,所以删除加回填比保留多份隐藏缓存更安全。回填是有界的:任务队列空闲时每批最多 64 条,间隔至少 15 秒,宁可慢慢补也不和前台任务抢 I/O。查询时模型未就绪或 embedding 失败,直接保留词法顺序,不会因为没装模型刷警告日志。
本地 embedding 用 fastembed 7.1.0 的 ONNX 管线,模型文件和 chat 模型一样按完整工件集校验(bge-small-zh-v1.5 连 tokenizer 带配置共 5 个文件共 95.3 MB,全部有独立摘要);远程 embedding 走已配置账户的 /embeddings 接口,返回的索引、数量、维度和数值有效性都会核对。两条路共用同一个检索端口。
RRF 的代价也要写明:每次语义查询多一次查询向量的计算,内存余弦扫描的成本随索引规模线性增长,耗时和内存取决于模型、维度和文档数量,产品不把这些固化为性能承诺。检索质量同理,融合排序保证了确定性和降级安全,它不等于"语义检索一定找到你要的"。

如果你是剪贴板历史攒了几千条的重度用户,向量回填的第一晚不会有完整语义搜索,等回填追上之后,语义命中才会逐步混进结果。
用量记账:NULL 是"没报",0 是"报了 0"
本地模型带来一个记账难题:本地推理的 token 数从哪来?如果应用自己估算再填进用量表,这份表就同时包含"供应商说的"和"应用猜的"两种性质的数据,页面上任何数字都失去了可引用性。
Syzygy 的规则只有一条:观测值只来自 provider 报告或引擎计数,本地估算只服务输入预算,永远不写进观测表。provider 没报的字段存 NULL,绝不以 0 填充。NULL 和 0 的区别在 SQL 聚合里真实存在:一天有调用但没人报告 token,界面显示 "—";一天没有任何调用,显示 0。把前者显示成 0,等于替供应商谎报了一天空转。
两本账分得很清:观测表一次调用一行,本地和远程同表存储,用 judge_kind 区分 local_llm 和 remote_api,本地行永不计入远程日预算;预算侧的估算(包括 chars/4 这类启发式)只做输入侧闸门,它的误差不影响观测真值。价格估算的数据源是 OpenRouter 的模型目录快照,同步脚本拉一份 openrouter.ai/api/v1/models 的快照存下来,定价随模型目录同生命周期,未知或无价的模型返回"无估算",不伪造 0。观测器本身是旁路:计量自身的故障被吞掉,永远不会影响被计量的那次调用。

如果你用本地模型跑敏感内容,这份账本给你的承诺是:哪一次调用、哪个引擎、多少 token,全都只存在本机;页面上的数字和供应商的账单是两回事,页面自己也这么说。

从哪里开始
装上 1.2.0 之后,本地模型的入口都在设置里:AI 来源区下载模型(4 个 chat 模型从 491 MB 的 Qwen2.5-0.5B 到 2.5 GB 的 Qwen3-4B,目录声明全部来自上游快照)、点击使用(加载、发布受管 Profile、端点重绑定一次完成)、检索区选择 embedding 模型。首次安装和 macOS 权限配置参考安装与首次运行指南;混合检索在剪贴板搜索里的实际表现,索引与搜索那篇有更多细节;本地提示词与技能工作台的设计,见提示词的版本记录应该自己发生。协议接缝和目录生成流程的完整决策记录在 ADR 0031、0034 和 0029 里,随仓库发布。
限制照实说:目录的 context_tokens 是声明值,不是实测运行参数;接纳报告里 inference_smoke_tested 仍是 false,每个模型的推理验收按平台、工件 revision 逐条记录,不随版本发布自动生效。推荐级别是我们的策展判断,速度和质量的实测数据要等真实硬件上的验收记录,这个差距不该由营销语言填补。
常见问题
Syzygy 的本地大模型是怎么运行的?
Syzygy 1.2.0 把 mistral.rs 以进程内方式嵌入应用,在 macOS 上启用 Metal 加速,加载经 SHA-256 校验的 GGUF 模型文件,并在 127.0.0.1 上开放一个 OpenAI 兼容的 chat 端点(POST /v1/chat/completions 与 GET /v1/models)。同一时间只服务一个本地模型,切换时旧服务先关闭。
本地检索为什么要同时用关键词和向量?
关键词检索擅长精确匹配,向量检索擅长意思相近,两者评分尺度不同,直接相加没有意义。Syzygy 用固定 k=60 的倒数排名融合(RRF)合并两份排名,只看名次不看分数;向量模型不可用时自动退回纯词法结果。
本地模型下载安全吗?
模型目录由脚本从上游快照生成,每个文件带独立的大小与 SHA-256。下载在暂存文件上校验后才发布到安装目录;查询安装态和每次实际加载前都会重新校验,同长度篡改、符号链接和路径逃逸都会被拒绝,校验失败不会加载。