我花了大概三周时间,把 MyHandler 从“一个灵光一闪”变成了“每天开机都会主动打开”的 Windows 小助手。它本身的定位并不宏大:本地优先、只跑在 Windows 上、用 llama.cpp 接 Vulkan 做推理。但正是这种几乎“逆潮流”的选择,反而解决了我在云端助手身上积压了大半年的几个真实痛点。
先交代一下背景。我不喜欢把个人文件、剪贴板内容、日程安排这些东西交给云端模型处理,但又确实想要一个能理解自然语言、能帮我操作系统的助手。市面上的方案要么太重,动不动就要装 Python 环境、跑 Docker;要么就只能做一个聊天玩具,碰不到文件系统,也执行不了本地命令。MyHandler 的初衷很简单:用自然语言和 Windows 对话,让它帮你查文件、批量改文件名、整理目录、跑脚本、查系统状态,而这一切都在本机完成,断网也能用。
这篇内容我不会只讲“我做了什么”,更多是想拆解“我为什么这么做”,以及“在 Windows 上用 llama.cpp + Vulkan 做本地助手到底有哪些坑、哪些收益”。如果你也在考虑给电脑做一个本地 AI 调度层,或者正纠结于 Vulkan 和 CUDA 的选型,这篇文章应该能给你一份现成的答案。
1. 不是模型不够强,而是云端助手在 Windows 上“够不着”
先聊动机。过去一年我试过不少 AI 助手方案,大部分能力模型本身没问题,真正的问题出在“边界”上:它可以陪你聊天、写代码、改作文,但让它直接在 Windows 里做点事情,就非常别扭。
1.1 我反感的不是云,而是“为了云而云”
本地助手的核心优势不只是隐私,而是低延迟和确定性。我让助手“把下载文件夹里所有 .tmp 文件清掉”,它如果还要把文件列表传到云端,等模型推理完再返回,来回可能几十秒。而本地模型哪怕只有每秒几十个 token,操作路径是确定的,配合脚本执行,往往两三秒就能给出结果。
更重要的是,Windows 生态本身就是一个巨大的、还没有被 AI 很好“本地化”的系统。PowerShell 脚本、COM 对象、计划任务、文件关联、注册表……这些东西如果交给云端模型去理解,它在明面上很难知道你机器上的真实环境。可本机运行的 MyHandler 可以直接枚举目录、读环境变量、执行命令并拿到返回值,这种“确定性”是云端 API 给不了的。
1.2 “本地优先”到底优先了什么
我理解的本地优先,不是“坚决不联网”,而是把能力边界放在本机:
- 模型推理必须在本地完成:不把对话文本传出去。
- 系统操作权限留在本地:助手调用的是我允许范围内的命令。
- 上下文存储在本地文件:重启之后还能基于上次会话继续。
- 离线可用:断网状态下,它至少要能完成文件整理、命令执行这类操作。
这四点听起来简单,但真正实现时会逼着你做很多细节决策。比如对话历史存什么格式、系统命令允许列表怎么写、模型输出里的 JSON 怎么解析才稳。这些我会在第 3 节详细讲。
2. 为什么是 llama.cpp,以及为什么是 Vulkan 而不是 CUDA
选型是我最纠结的部分。目标是明确的:Windows 本机、尽量少的依赖、能跑 7B 到 32B 的量化模型、要有 GPU 加速。按这个标准,候选人其实只剩三个方向:llama.cpp、Ollama、LM Studio。
2.1 llama.cpp 胜出的三个理由
第一,运行时极简。llama.cpp 的 Windows 发布包就是一个 exe 加几个 DLL,不需要 Python,不需要 PyTorch,不需要 Docker。做完一次编译之后,拷到任何一台有 Vulkan 驱动的 Windows 机器上都能跑。这对我这种把工具当“可移动设备”的人来说太重要了。
第二,进程模型可控。我的助理架构需要一个能与外部程序对话的推理进程,llama.cpp 的llama-server正好暴露了 HTTP 接口,可以方便地从 go/rust/python 等语言调用,而 LlamaServer 的资源管理也相对简单。Ollama 当然也好用,但它更像一个服务式的全家桶,进程生命周期、模型加载策略由它自己管理。我想要的是更“嵌入”式的控制权。
第三,社区对 Vulkan 的支持已经足够稳定。这一点我后面细说。先给一个结论:如果你的机器是主流 N 卡,CUDA 依然是首选,但如果你想兼容 AMD、Intel 核显、甚至老一点的 N 卡,Vulkan 的兼容性要好得多。而 MyHandler 的目标用户万一是老婆的旧笔记本呢?那就必须选一个驱动层面最通用的底子。
2.2 Vulkan 解决了什么问题
我知道很多人会问:既然是在 Windows 上跑,为什么不用 CUDA?最大的原因是我不想把用户限定在 N 卡上。llama.cpp 官方编译包里有多个版本,带 Vulkan 的版本可以同时覆盖:
- NVIDIA 显卡(通过 Vulkan 驱动)
- AMD 显卡
- Intel 核显 / Arc 独显
甚至树莓派这类设备在未来也可能接进来跑小模型。
从实测来看,Vulkan 在 llama.cpp 上的性能已经不再是“能用”级别。以 7B Q4_K_M 模型为例,我的 RTX 3060 上 Vulkan 路径大概能到 30-40 tokens/s,CUDA 路径大概能到 45-55 tokens/s。差距确实有,但对助手对话场景来说,30 tokens/s 已经足够流畅。而换到 AMD 核显上,Vulkan 是唯一可行的加速方案。
还有个隐藏优点:Vulkan 不需要安装庞大的 CUDA Toolkit。它只需要显卡驱动自带的支持,对普通用户友好得多。而 MyHandler 的目标不是给 AI 工程师用的,是给普通 Windows 用户用的,我不能要求人家先装 3GB 的 CUDA 环境。
3. MyHandler 的核心实现:对话循环、工具调用与系统操作边界
确定用 llama.cpp 之后,剩下的问题就是产品层面的实现了。MyHandler 的本质是一个带工具调用能力的本地对话循环。结构上分三层:
- 对话管理层:维护历史消息、组装 prompt、解析模型输出。
- 工具调用层:定义可执行操作的白名单,执行后把结果回填给模型。
- 系统操作层:用 PowerShell / cmd 和 Windows API 完成实际动作。
3.1 对话循环:不追求多聪明,追求“不跑飞”
模型推理本来就慢,如果一次会话里模型输出的 JSON 格式错了,就得浪费几十秒重新生成。所以我在对话循环里做了一个非常朴素的策略:限制模型输出长度,优先让工具解析成功。
具体做法是:
- 系统提示词要求模型在需要执行操作时输出 JSON,格式类似:
{"tool": "file.list", "params": {"path": "C:\\Users\\me\\Downloads"}}- 如果 JSON 解析失败,我会把错误信息回填给模型,并要求它“只输出 JSON”,同时把温度降到 0.2。
- 如果两次解析仍失败,就放弃本轮调用,返回错误信息给用户,而不是让模型自由发挥。
这个小循环看似简单,却非常关键。我见过很多本地 AI 项目死在一个地方:模型不是不会干活,而是输出不稳定。你给它的自由度越大,它跑飞的概率就越高。本地推理没有云端那种“再试一次不花钱”的底气,所以格式约束比能力更重要。
3.2 给模型一个“可控但有限”的 Windows 操作面
让 AI 直接执行任意 PowerShell 命令是很危险的设计。我的方案是提供一个工具白名单,只有列表里的操作可以被调用。当前 MyHandler 支持:
| 工具名 | 作用 | 权限级别 |
|---|---|---|
| file.list | 枚举目录文件 | 只读 |
| file.search | 按文件名/扩展名搜索 | 只读 |
| file.rename | 批量重命名 | 写 |
| file.move | 移动文件 | 写 |
| file.delete | 删除文件,先进入回收站 | 高风险 |
| process.list | 列出进程 | 只读 |
| process.kill | 结束指定进程 | 高风险 |
| clipboard.read | 读取剪贴板 | 只读 |
| clipboard.write | 写入剪贴板 | 写 |
| shell.run | 执行白名单内的 PowerShell 命令 | 扩展 |
shell.run是最容易失控的一个,所以我做了一层命令前缀校验:只有白名单里的前缀才允许执行。比如:
Get-ChildItemGet-ProcessGet-ServiceStart-Process
注意,这里我没有把Remove-Item放进去,因为让模型自由拼接Remove-Item的参数风险太高,不如用专门封装过的file.delete工具,由代码来保证只进回收站。
3.3 进程隔离:模型只负责“想”,系统操作交给独立进程
这里有一个实现上的关键选择:llama.cpp 的llama-server和 MyHandler 本体是两个独立进程,通过本地 HTTP 通信。工具执行部分我放在 MyHandler 进程里,但系统命令使用 PowerShell 子进程来执行。
为什么这么设计?
- 防止模型推理进程崩溃时把工具执行进程也带走。
- 子进程有超时控制,极限情况下可以直接杀掉。
llama-server本身是一个独立看门狗可以重启的组件。如果显存不够导致崩溃,我的看门狗会自动重启它,而不影响用户正在编辑的内容。
实际操作中,我定义了一个executor接口,核心代码如下:
func (e *Executor) Run(tool string, params map[string]any) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() switch tool { case "file.list": return listFiles(params["path"].(string)) case "shell.run": return runPowerShell(ctx, params["command"].(string)) ... } }所有工具函数的返回值都是一段文本,会被拼进对话历史。模型看不到执行中间状态,它只看到最终结果。这样能省大量 token,也避免了模型在中间日志里“脑补”。
4. Vulkan 推理实测:模型、量化、性能与显存调优
如果你也想做类似的本地助手,最关心的可能还是“到底能不能跑起来”。这个章节我会给出一组实测数据,以及我在调 Vulkan 时反复踩的坑。
4.1 模型选择与量化级别
MyHandler 目前默认推荐三个档位:
| 档位 | 模型示例 | 量化 | 内存占用 | 适合场景 |
|---|---|---|---|---|
| 入门 | Qwen2.5-3B-Instruct | Q4_K_M | 约 2.5GB | 低配机器、简单文件操作 |
| 均衡 | Qwen2.5-7B-Instruct | Q4_K_M | 约 5.5GB | 日常助手,综合表现最好 |
| 进阶 | Qwen2.5-14B-Instruct | Q4_K_M | 约 10GB | 复杂推理、长上下文 |
为什么一直提 Qwen 系?因为对中文用户来说,Qwen 工具调用能力在同等参数规模下表现比较稳。模型的选择直接决定了 JSON 输出的稳定性。我用 3B 模型做简单操作时,成功率能达到 90% 以上,但涉及多步任务时会露怯;换到 7B 之后,复杂任务的成功率明显提升,输出也更有条理。
量化级别我固定为 Q4_K_M,这是 llama.cpp 社区公认的“性价比甜点”。Q8 的精度更高,但显存占用翻倍,助手的响应速度会变慢。对于工具调用这种容错场景,Q4_K_M 完全够用。
4.2 不同显卡上的 Vulkan 性能实测
我手上能测试的设备有限,但覆盖了三种典型场景:
| 设备 | 模型 | Vulkan 速度 | CUDA 速度 | 备注 |
|---|---|---|---|---|
| RTX 3060 12GB | 7B Q4_K_M | 35 t/s | 48 t/s | Vulkan 可用性良好 |
| GTX 1650 4GB | 3B Q4_K_M | 18 t/s | 不可用 | 显存受限,4GB 是下限 |
| AMD Ryzen 6800H 核显 | 3B Q4_K_M | 9 t/s | 不可用 | 慢但能跑,可作纯 CPU 对比 |
注意 GTX 1650 的显存只有 4GB,加载 3B 模型外加 context 内存已经到临界值。我一开始想用 7B,结果还没开始对话就直接 OOM。后来加了--ctx-size 4096才勉强稳定。如果你显存不到 6GB,老老实实跑 3B,别贪。在 CPU 模式下,同一个模型大约只有 4-6 t/s,用 Vulkan 至少快了一倍以上,这就是它的价值。
4.3 Vulkan 编译参数与踩坑
llama.cpp 官方提供的 Windows 发布包是预编译好的,但如果你想自己编译,要点如下:
cmake -B build -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release我遇到的第一坑是老显卡驱动不支持新的 Vulkan 接口。llama.cpp 在运行时如果报vkEnumeratePhysicalDevices返回空,基本就是驱动问题。解决方法是更新显卡驱动。如果设备太老,就只能跑 CPU 模式。
第二个坑是混合显卡笔记本的 GPU 选择。很多笔记本有核显和独显,llama.cpp 默认可能会选择核显,导致性能惨淡。我用环境变量GGML_VK_DEVICE=1强制指定独显,性能立刻提升。
第三个坑是Vulkan 显存不足时不会自动回退 CPU。它直接报错退出。所以我在 MyHandler 里做了一个“预检”:加载模型前先读显存总量和当前占用,估算该模型需要的内存,如果不够,自动在 CPU 模式下重新加载。代码逻辑大概是这样:
func pickBackend(modelMemoryMB int) string { vkMem := getVulkanMemoryMB() if vkMem > modelMemoryMB+1024 { return "vulkan" } return "cpu" }虽然 CPU 模式慢,但至少不会完全没法用。这个兜底策略让 MyHandler 能在更多机器上“跑起来”而不是“报错退出”。
4.4 进程参数:context、batch、threads 的经验值
llama.cpp 有大量参数,我最终稳定的启动参数是:
llama-server.exe \ -m qwen2.5-7b-instruct-q4_k_m.gguf \ --ctx-size 8192 \ --batch-size 256 \ --n-gpu-layers 999 \ --threads 8 \ --no-warmup--n-gpu-layers 999表示尽可能把所有层都加载到 GPU。如果显存不够,可以先 20 层左右,配合 CPU 混合推理。但实测下来混合推理速度比纯 CPU 快得有限,不如直接降模型档位。
--no-warmup跳过 warmup,启动时间能缩短 1-2 秒。这个参数对交互体验很关键,因为本地助手的“第一印象”就是启动速度。
5. 实测中的意外情况:从混乱输出到 PowerShell 编码坑
任何工具都是在踩坑中成熟的。这一章记录三个最让我印象深刻的坑,可能对你更有价值。
5.1 模型输出的 JSON 前后有杂讯
无论我系统提示词怎么写,模型偶尔还是会在 JSON 前后输出解释文字,比如:
好的,我来帮你列出这个目录的内容。 {"tool": "file.list", "params": {"path": "C:\\Users\\me\\Downloads"}} 希望这能帮到你!最开始的解析器直接失败了。后来我改成“正则提取 JSON 片段”:
import re, json def extract_json(text): match = re.search(r'\{.*\}', text, re.DOTALL) if not match: return None try: return json.loads(match.group(0)) except json.JSONDecodeError: return None这一改,工具调用成功率立刻提高了大约 20%。原因很简单:模型有“人情味”是好事,但作为工具使用者,我只关心 JSON 片段。以后对输出做解析时,不要假设模型会严格按格式输出,而是假设它一定会输出多余内容,然后把解析逻辑做得更宽容。
5.2 PowerShell 输出编码
这是 Windows 独有的坑。调用 PowerShell 获取文件名后,中文文件名在我手里直接变成乱码。原因是我没有显式指定输出编码。参考实际经验,正确做法是启动 PowerShell 时指定 UTF-8 输出:
$OutputEncoding = [Console]::OutputEncoding = [Text.Encoding]::UTF8或者在 Go 里直接cmd /c chcp 65001再执行命令。否则,凡是处理到中文路径和中文内容的场景,全都会段错误。
5.3 长对话的上下文污染
本地模型最怕的是上下文太长。刚开始我把所有工具执行结果都塞进对话历史,结果跑到第十轮左右,模型开始“分不清谁是用户谁是工具”,有时候会自己编造文件列表。
后来我做了三件事:
- 工具执行结果用
<tool_result>括起来,并在系统提示词里明确“这是命令输出,不是用户说话”。 - 执行结果超过 1000 字符时截断到关键部分。
- 每轮结束,把旧对话做摘要压缩,只保留关键操作记录。
压缩摘要的具体实现我用了另一个更小的模型,或直接调用当前模型对整段历史做“总结指令”,把摘要文本放回 context 开头。这样即使对话很长,模型也始终知道“这个用户正在处理哪些文件”。
6. 未来方向:把“AI 助手”变成“个人自动化平台”
MyHandler 目前已经满足了我对本地助手的大部分期望,但它距离我理想中的“本地优先自动化层”还有很长距离。接下来的几个方向我认为是值得探索的。
6.1 更稳定的结构化输出
我正在调研是否可以直接用 llama.cpp 的 grammar 功能来约束模型输出,从生成源头保证 JSON 格式正确。llama.cpp 支持 GBNF grammar,如果能给工具调用格式写一条语法规则,模型就会在生成时强制遵循 JSON 结构,彻底告别解析失败。这比任何后处理都可靠。
6.2 进程看门狗与系统托盘
现在 MyHandler 是一个命令行程序,我计划把它改造成托盘应用,开机自启,常驻内存,通过热键呼出输入框。模型进程由守护进程管理,一旦崩溃自动重启。这样用户感知到的就是一个“永远在线的本地助手”。
6.3 多模态入口
Windows 生态中截图是很常见的场景。我想下一步给它加入屏幕截图理解能力,让用户可以直接截图问“这个报错窗口是什么意思”。这需要视觉模型,本地跑起来会更吃力,但 3B 级别的 VLM 值得尝试。
6.4 可插拔工具库
现在工具白名单是代码里写死的。我准备做一个tools/目录,每个工具是一个独立的可执行文件或脚本,通过配置文件声明参数和权限等级。这样社区可以互相分享工具,而不需要每加一个功能就重编译整个程序。
回到最开始的问题:为什么用 llama.cpp + Vulkan 做本地 Windows 助手?我的答案很朴素——它让我第一次感觉到,AI 不只是陪你聊天的朋友,而是真正住在这台电脑里的管家。虽然它偶尔会理解错,偶尔输出乱码,但那种“所有数据都在我手里、断网照样干活”的踏实感,是任何云端助手都给不了的。
如果你也想在本机跑一个类似的助手,我的建议是:不要一开始就追求完美,先拿一个 3B 模型跑通文件查找和目录列举,再逐步加工具。把格式解析和上下文管理做好,一个“不那么聪明但非常确定”的本地助手,远比一个“聪明但经常失控”的玩具更实用。