
字节笔记本
2026年10月6日 · 约 6 分钟读完
DeepSeek Harness 的 LSP 语义导航接缝
DeepSeek Harness 的 LSP 语义导航接缝
DeepSeek 开源了 agent 框架 DeepSeek Harness(简称 dsh),采用"一切皆插件"的架构,由 Cordis 框架驱动。在它的源码文档中,有一篇专门讲 LSP 子系统的设计说明,对所有想给 AI 编码工具补上语义导航能力的开发者都值得细读。本文把其中的关键设计整理成一篇导读。
为什么要一个专门的 LSP 接缝
AI 编码代理要读懂代码,光靠文本搜索远远不够:跳到定义、找全部引用、看悬浮文档,这些语义级导航能力通常由语言服务器(LSP)提供。DeepSeek Harness 的做法不是把 LSP 零散地塞进各个工具,而是把它做成一个可选的"能力接缝"(capability seam),全部语义导航收敛在一个 ctx.lsp 服务上。
结构上拆成三个包:dsh-lsp 是服务定义,持有 ctx.lsp 和提供方注册表;dsh-lsp-stdio 是通用的 Service Provider,一个经配置的 stdio 语言服务器宿主;dsh-tool-lsp 是消费方,定义暴露给模型的 lsp 工具 schema。文档明确划分:LSP 是一项可选能力,不属于 agent loop 主干,所以它的词汇表单独放在子系统文档里,而不进入核心文档。这样切分带来一个直接的好处:更换语言服务器提供方,模型请求导航的方式完全不变。

四个操作,闭合联合
接缝和模型恰好暴露四种语义查询:goToDefinition(跳转定义)、findReferences(查找引用)、goToImplementation(转到实现)、hover(悬浮信息)。这是一个闭合联合,新增一个操作会同时牵动接缝、提供方和工具三层,而且是编译期强制的改动,谁漏改谁编译不过。符号查询和调用层级被有意排除在外,文档的解释是它们需要不同的 schema。
type LspOperation = 'goToDefinition' | 'findReferences' | 'goToImplementation' | 'hover'坐标约定上有个容易忽视的细节:接缝内部的位置和范围一律采用从零开始的 UTF-16 编码,与 LSP 协议保持一致;而面向模型的工具层使用从 1 开始的行号光标习惯,换算全部发生在工具进出的路上,模型不需要感知协议细节。
请求:全字段必填,没有 resolve()
请求对象 LspQueryRequest 有四个字段:operation、filePath、position、workspaceRoot。每个字段都是必填:workspaceRoot 由调用方提供,永不默认;languageId 不在请求里,它来自提供方注册时声明的扩展名映射,例如 .ts 对应 typescript;超时和结果上限由消费方自己决定。因此没有任何字段需要实现层给默认值,也就不存在一个 resolve() 步骤。
提供方实际收到的是 LspProviderQuery,等于调用方请求加上派生出的 languageId。这个 languageId 只用来同步瞬态文档,从不参与提供方选择。提供方的选择按查询进行且顺序无关,依据是文件的扩展名;没有匹配的提供方就抛出 LSP_UNAVAILABLE 错误。
结果:判别联合与一个 URI 细节
返回值 LspQueryResult 是闭合的判别联合:三个导航操作归一化为 locations,hover 归一化为内容或 null。消费方对 kind 做 switch 就能获得穷尽性检查,联合里加一个新分支,没处理的代码会直接编译报错。
有两个值得注意的细节。其一,findReferences 的结果永远包含声明本身,这是提供方在内部强制保证的,调用方没有开关可关。其二,locations 变体额外携带 resolvedWorkspaceUri,也就是提供方为工作区根解析出的规范化 file: URI。调用方若想把结果里的位置 URI 转成相对路径,必须用这个坐标,而不是拿请求里可能带符号链接的路径去套宿主平台的路径规则,因为执行平台和调用平台可能是两个环境。

注册与错误:原子预留,按码路由
注册提供方时,registerProvider 会原子性地预留它的稳定 id 和全部小写带点的扩展名映射;任何冲突或非法输入都不会发布任何东西,直接抛出 LspError,返回的 disposer 负责释放全部预留。LspError 继承自框架的 HarnessError,带一组稳定错误码:LSP_INVALID_PROVIDER、LSP_CONFLICT、LSP_UNAVAILABLE、LSP_DISPOSED、LSP_UNSUPPORTED_OPERATION、LSP_MALFORMED_RESPONSE。调用方按错误码路由逻辑,而不是去解析报错文本。
同样值得注意的是接缝"不给"什么:不暴露协议类型,不暴露进程和文档控制,也没有通用的 JSON-RPC 逃生舱。能力边界收得越紧,上层契约就越稳定。
写在最后
这篇文档的价值不在 LSP 本身,而在它示范了一种约束式设计:闭合联合换来编译期保障,全必填请求换来零默认值,原子注册换来注册表一致性,稳定错误码换来调用方可路由。四条约束互相咬合,让"换语言服务器"这件最容易失控的事变成一个不影响模型侧的局部替换。按仓库说明,DeepSeek Harness 目前处于开发者预览阶段,接口仍可能出现破坏性变更,但其子系统文档的颗粒度已经足以当作 agent 工程的设计参考来读。



