我给 DeepSeek DSH 做了个 zvec-grep 插件
摘要
从一次代码语义搜索实验,到一个自动索引、可见状态、后台增量更新的 DeepSeek DSH 插件,我在 dsh-zvec-grep 开发中做了哪些取舍。

最近,我无意中了解到 zvec,这是一款由阿里巴巴开源的向量数据库。它直接运行在应用程序进程中,索引保存在本地,不需要另外维护一套数据库服务。随后出现的 zvec-grep 又把文件发现、代码切分、embedding、BM25、向量召回和结果融合组合起来,变成了面向代码仓库的检索工具。
为了把这次学习整理清楚,我做过一期视频:《阿里巴巴开源向量数据库 zvec 与 zvec-grep:代码搜索不只是向量检索》,感兴趣的朋友可以戳链接,B站同款在这里。视频主要回答“Codex 已经会搜索,为什么还需要 zvec-grep”,也记录了我用真实代码仓库演示精确搜索与语义召回的过程。后来开发插件时,关于工具分工、索引更新等的判断,都来自这次的学习。
一开始我只是想知道:Codex 配合 rg 已经很好用了,代码语义搜索还能补上什么?后来我在真实仓库里做尝试以后,又开始琢磨,能不能把这套能力放进其他 Agent。再往后,这个念头变成了一个 DeepSeek DSH 插件:dsh-zvec-grep。
今天这篇文章,算是我的一期开发笔记吧。我会按实际开发过程,梳理从试用 zvec-grep 到设计插件的几次转折,以及我对“DSH插件应该怎样为用户工作”的理解。期望对大家有所帮助。
先确认它解决的是什么问题
已知函数名、配置键、错误信息或一段原文时,rg 依然是更合适的工具。它搜索的是当前磁盘,不需要索引,结果可以穷举,也没有索引过期的问题。
语义搜索的价值出现在另一种情况:我知道业务行为,却不知道代码采用了什么名字。
我在 chat-ollama 仓库里试过这样一个问题:用户在个人目录和项目目录里分别添加了自定义 Agent 能力,应用如何发现它们、处理重名,并在新的 Agent 请求里让它们生效?
这个问题没有给出文件名、函数名或变量名。普通文本搜索先从 custom agent capabilities、personal directory 之类的表达开始,没有命中。模型需要先猜到项目里把这类能力称作 skill,然后继续查目录扫描、合并规则和 Agent 创建入口。
把含义相同的英文问题交给 zvec-grep 后,第一次查询就召回了 Agent 创建入口和技能目录扫描实现。沿着这两个入口读取当前源码,可以还原完整流程:同时扫描用户目录和项目目录,按技能名称合并,项目级定义覆盖同名用户级定义,创建 Agent 时再把技能加入 middleware 和 instruction。
这次实验没有证明向量搜索可以替代 grep。它证明的是,在“不知道项目用了什么词”的时候,语义索引有机会给 Agent 一个更好的第一跳。找到入口以后,仍然要回到实时源码验证。
同一个问题直接换成中文,召回质量又明显下降。这也让我很早就确认了 zvec-grep 的局限性:embedding 模型、查询语言和仓库语言都会影响结果,插件还不能把“语义相关”包装成“回答正确”。
注,在视频中,我没有通过 agent 调用 zvec-grep 工具,而是直接通过 CLI 做的查询演示。
从自己试用到给其他 Agent 使用
zvec-grep 已经提供了 CLI 和面向多个 Agent 的安装流程。用户可以安装 zg,执行 zg install,再进入仓库运行 zg index。这很适合主动选择并配置一套检索工具的人。
但把它做成 DeepSeek DSH 插件时,我不想把这些步骤原样搬给用户。因为插件的意义不应该只是“安装后再告诉你去安装另一个工具”。
我希望安装命令结束以后,下面这些事情都由插件接管:
- 从当前 Session 知道正在使用哪个 workspace。
- 第一次进入 workspace 时自动建立索引。
- 文件发生变化后自动增量更新。
- Agent 在适合的任务里获得语义搜索工具。
- 用户能看见索引正在建立、刷新、可用,还是出了错。
- DSH 退出或插件卸载时,索引任务与后台资源能一起释放。
最后,安装只剩一条命令:
npx @deepseek-ai/dsh plugin --profile web add @sugarforever/dsh-zvec-grep
不再需要单独运行 zg install、zg index,也不需要启动 MCP Server。
现在启动 DSH 就好。比如:
npx @deepseek-ai/dsh web
没有包装 CLI,也没有重写 zvec-grep
最初很容易想到两种实现。
一种是从 DSH 调用 zg CLI,或者接入它的 MCP Server。这条路能复用现成入口,但会引入额外进程、配置文件和服务生命周期。用户安装了一个插件,背后却还要理解另一个常驻服务为什么没有启动、端口是否冲突、配置是否写对。
另一种是直接基于 zvec 重写代码搜索。这又走到了另一个极端:文件发现、Git ignore、代码切分、文档提取、embedding、索引 schema、增量 diff、BM25、RRF、结果裁剪和异常恢复,都需要重新实现。它没有改善用户体验,只是把上游已经做过的工程再做一遍。
最后我选择了中间位置:直接使用 zvec-grep 正式导出的 createZvecGrep() 引擎 API。
DeepSeek DSH Session
-> dsh-zvec-grep workspace runtime
-> createZvecGrep()
-> zvec-grep 的扫描、切分与混合检索
-> 本地 zvec 索引
这样既保留 zvec-grep 已有的检索能力,又让 DSH 插件管理 workspace、工具注册、状态展示与资源释放。插件不是给 CLI 再包一层,而是在适配两个项目的生命周期。
workspace 才是运行时的边界
DSH 创建或恢复 Session 时,插件从不可变的 session.header.cwd 取得 workspace 路径。相同 workspace 下的多个 Session,共用一个搜索引擎、一个 watcher 和一套索引协调状态。
这件事看起来只是做了一个 Map,但它决定了插件会不会在用户开三个对话后,把同一个仓库索引三遍。运行时会先把路径转换成真实路径,避免符号链接和不同写法把同一目录识别成多个 workspace。
首次索引在后台开始。zvec_search 不负责触发刷新,也不会为了等待索引而导致工具调用长时间不返回。如果当前状态是 indexing、refreshing 或 error,它会立即返回结构化状态;只有索引处于 ready 时,才会查询现有索引。
我在开发过程中曾采用“第一次搜索自动等待索引完成”的行为。真实使用以后,我改了这个决定。大仓库的首次索引时间不可预测,让工具调用静默地挂在那里,Agent 和用户都不知道是在工作还是已经卡住。即时返回状态以后,Agent 可以稍后重试,也可以先退回精确搜索。Agent 或用户应该足够智能地去判断,当索引正在进行时,是该等待还是跳过。
这是我对插件行为的一条基本判断:耗时的后台准备可以自动完成,但不能把等待隐藏在一次看不见进度的调用里。
索引“保鲜”不能只靠一句“自动更新”
语义检索多了一份索引,就一定会遇到索引与当前文件不一致的问题。插件启动后会监听 workspace 的新增、修改和删除事件,把短时间内连续发生的路径合并,再调用 index({ changedPaths }) 做增量更新。
第一版使用 Chokidar。后来在真实 workspace 里观察资源占用时,我把它换成了 Node.js 原生递归 fs.watch。插件只在 watcher 层硬过滤 .git、.zvec-grep 和 node_modules,其他文件是否应该进入索引,继续交给 zvec-grep 的扫描规则判断。
原生 watcher 更直接,但操作系统事件不是绝对可靠的。为了修复可能漏掉的变化,运行时默认每小时再安排一次完整校准。这里的结构是:
文件事件 -> 750ms 合并 -> changedPaths 增量索引
每小时定时任务 ---------> 完整校准
增量路径负责日常响应速度,定时校准负责最终修复漂移。搜索本身则使用 autoUpdate: false,避免前台查询又偷偷启动另一套刷新逻辑。谁负责更新、谁负责查询,在插件里职责明确分开。
状态不是装饰,而是功能的一部分
如果插件自动建立索引,却不告诉用户发生了什么,那么“零配置”很快就会变成“无法判断”。第一次下载本地 embedding 模型、建立较大仓库的索引、处理一批文件变化,都可能需要时间。
因此我给 DSH Web 增加了一个很小的状态胶囊。它会显示 Loading、Indexing、Refreshing、Ready 或 Error。展开以后还能看到当前 workspace 和待处理变化数量。

服务端状态接口只接受 Session ID,再从 DSH 自己的 Session 列表中查找 workspace。浏览器不能通过查询参数随便读取另一个本地路径的状态。客户端切换 Session 时也会取消旧请求,避免较慢的响应把新 workspace 的状态覆盖掉。
这个 UI 没有参与搜索排序,却改变了插件能否被理解。一个后台功能,只要存在准备时间和失败状态,就应该给用户留下可观察的界面,而不是只把日志写进终端。
退出时也要完成工作
首次索引可能持续很久。最初的实现里,如果 DSH 在索引过程中退出,插件的 close() 会等待尚未完成的任务,结果是用户关闭应用时反而被后台索引阻塞。
我为这个场景补了一个会挂起的测试,再用 AbortController 把取消信号传给首次索引与后续刷新。插件卸载时先取消任务,清理 debounce timer 和定时校准,关闭 watcher,最后释放每个 workspace 的引擎。
自动启动的资源,也应该自动结束。
插件要替用户承担复杂度
开发 dsh-zvec-grep 以后,我对插件的理解比开始时更具体了。
插件不是把一个库暴露出来就结束。它应该利用宿主已经知道的信息,例如 Session 的 workspace;应该选择合适的自动化边界,例如后台建立与更新索引;应该把不可避免的不确定性暴露出来,例如索引状态和错误;还应该保留退出路线,让 Agent 在索引未就绪时继续使用 rg。
这也是为什么插件会在 DSH 中注册一段 system prompt,提醒 Agent 做出工具分工:不知道具体措辞或文件位置,需要语义发现或跨文件搜索时,使用 zvec_search;已知标识符、字面量、正则,或者需要穷举所有结果时,使用精确 grep。zvec_search 自己的工具描述还会进一步说明,它适合查找架构、关系、控制流和跨文件证据。
默认 embedding 是本地的 local/potion-code-16m-v2,源文件、模型推理和索引可以留在机器上。索引保存在 workspace 的 .zvec-grep/ 下,项目应把它加入忽略规则。多语言召回仍然取决于模型,搜索结果也始终只是候选证据,不是对当前代码的最终判断。
从第一次对照实验到插件能够自动工作,我真正想保留的不是“向量搜索比 grep 更强”这个结论,而是一种组合方式:让语义搜索负责不确定的第一跳,让精确搜索负责已知文本和完整匹配,再让 Agent 回到当前文件完成验证。
如果你对插件感兴趣,欢迎点赞。使用中如有问题或建议,请在 GitHub 提交 issue 或 PR。
- dsh-zvec-grep: github.com/sugarforever/dsh-plugins/tree/main/dsh-zvec-grep
- 插件源码:sugarforever/dsh-plugins/dsh-zvec-grep
- zvec:github.com/alibaba/zvec
- zvec-grep:github.com/zvec-ai/zvec-grep
相关文章
2026年9月6日
新版 DSH 为什么启动不了 dsh-lark
一次插件兼容性升级的完整复盘:从干净环境复现启动崩溃,到识别 npm 产物与源码的差异,再用真实 tarball 完成端到端验收。
2026年8月28日
一天多了 4,260 颗星,Archify 想让 Agent 交付能核验的架构图
GitHub Trending 上的 Archify 不把架构图当作一张漂亮图片:Agent 先写类型化 JSON,再由本地渲染器验证、交付成单文件 HTML。
2026年8月27日
34.7k stars 的科研 Skill,开始长成一个本地研究工作台
Scientific Agent Skills 登上 GitHub Trending 的背后,是 163 个科研流程与一款本地 AI co-scientist 的组合。它把 Agent 从“回答问题”推进到可检查的研究过程。
最近一封 · Sample
新版 DSH 为什么启动不了 dsh-lark
“一次插件兼容性升级的完整复盘:从干净环境复现启动崩溃,到识别 npm 产物与源码的差异,再用真实 tarball 完成端到端验收。”
—— william
来信
里面装的是
- 新文章 — 写完一篇就寄一封,不攒货
- 这周读到的、看到的、好用的工具
- 正在折腾的实验,附带翻车记录
约莫 1–2 周一封 · 随时退订
合作伙伴
CompeteMap — 英国及爱尔兰学生竞赛一站式搜索
数学、编程、科学、写作等各类竞赛信息汇总,支持按年龄和科目筛选,再也不错过报名截止日。