
LSPs for LLMs 这个题目最近在 AI 辅助编程和 LLM Agent 方向讨论得越来越多。LSP 本意是 Language Server Protocol也就是语言服务器协议它负责让编辑器拿到类型、补全、诊断、跳转定义这些 IDE 级能力。LLM 则是大语言模型。把两组词放在一起指的不是协议本身变成了大模型而是一条实用工程路径当 LLM 需要理解或修改代码时不要只把文件文本和选中片段塞进 Prompt而是先通过 LSP 从语言服务器取回结构化的语义信息再把这些信息作为上下文交给模型。很多编程助手给人的第一印象是“不够聪明”其实不一定是模型能力不足而是上下文太薄。模型拿到的内容往往只是当前文件里的一段代码看不到函数定义的类型、调用点、诊断信息、引用关系。它只能靠自己训练时积累的先验知识去猜。而 LSP 背后是一套已经成熟的编译器级代码分析能力恰好能补上这块空白。这篇文章会用一个本地可跑的最小 Demo 演示整条链路安装 pyright 语言服务器用 Python 写一个极简 LSP 客户端从当前文件里取出诊断和符号信息组装成 Prompt再调用一个 OpenAI 兼容接口拿到修复建议。读者对象是正在开发编程助手、IDE 插件、LLM Agent 工具链的开发者。1. LSP 与 LLM 的组合到底解决什么问题1.1 LSP 先解决了编辑器与语言服务器之间的通信问题LSP 最早要解决的是编辑器生态分裂的问题。在 LSP 出现之前一种语言要在一个编辑器里获得代码补全、跳转、诊断能力通常需要为该编辑器单独写插件。编辑器数量一多维护成本会迅速膨胀。LSP 的思路是把“语言智能”从编辑器里抽出来放进一个独立进程称为语言服务器。编辑器是客户端语言服务器是服务端双方通过标准协议通信。协议底层的传输格式是 JSON-RPC 2.0所有请求、通知、响应都是结构化 JSON。一份最典型的 LSP 交互流程大致是这样的客户端 - 服务器initialize 请求 服务器 - 客户端initialize 响应告知服务器支持哪些能力 客户端 - 服务器initialized 通知 客户端 - 服务器textDocument/didOpen 通知告诉服务器文件内容 客户端 - 服务器textDocument/hover 或 textDocument/definition 等请求 客户端 - 服务器shutdown 请求 客户端 - 服务器exit 通知对 LLM 应用来说LSP 最大价值在于你不需要自己去解析 AST、做类型推导、维护跨文件索引。语言服务器已经把这件事做了很多年并且做得比普通脚本更可靠。你只需要把它的结果读出来组织成 Prompt 或工具调用结果即可。1.2 纯文本上下文会让 LLM 丢失太多语义现在很多 AI 编程功能的实现方式是用户在编辑器里选中一段代码插件把文本发给模型问“这段代码有没有问题”或“帮我改一下”。这种方式在简单场景下有效但只要代码涉及类型、跨文件调用、编译错误纯文本就明显不够。举个例子给你看两行代码def add(a, b): return a b result add(1, 2)模型大概能猜出这里有问题但它不知道类型检查器具体报了什么不知道add是在哪个类里定义的不知道这个函数在同一个项目里被其他模块以什么类型调用。如果 prompt 里只给这段文本模型给出的建议很容易变得空泛比如“请确保传入相同类型”之类。换成 LSP 增强后的上下文你可以在 Prompt 中直接给出这类信息pyright 诊断 第 5 行第 10 列 Argument of type str cannot be assigned to parameter b of type int in function add模型看到的是确定的问题描述而不是靠猜。它可以直接从“类型不匹配”这个结论出发给出把2改为2或对b做类型转换的方案。这就是 LSP 对 LLM 的最直接价值把“模型需要推断的信息”变成“语言服务器已经计算好的信息”。1.3 LSP 能为 LLM 提供的信息类型LSP 并不是只提供诊断。一个完整的语言服务器可以暴露很多能力通常通过初始化时返回的capabilities字段声明。下面这张表整理了与 LLM 上下文构建关系最密切的几类 LSP 能力LSP 方法返回的信息对 LLM 的价值典型使用场景textDocument/diagnostic或textDocument/publishDiagnostics错误、警告、提示信息让模型知道代码当前有哪些确定问题代码评审、自动修复textDocument/documentSymbol文件中的类、函数、变量符号树让模型快速了解文件结构和职责代码概览、重构、补全textDocument/hover符号的类型签名和文档注释让模型获得准确类型减少猜测分析函数行为、生成调用代码textDocument/definition符号定义的位置让模型跳转到真正的实现跨文件分析、代码修复定位textDocument/references符号被引用的位置列表让模型知道改动影响范围重构、影响面分析textDocument/completion当前位置可用的补全候选结合模型生成更符合上下文的代码代码生成、自动补全这里要注意的是LSP 返回的是结构化 JSON这对 LLM 场景很友好。你可以直接把它转成 JSON 片段塞进 Prompt也可以先做过滤只保留诊断和符号。相比直接塞整段源文件结构化信息在 Token 利用率和准确率上都有优势。2. 准备工作区依赖、测试项目和 LSP 通信基础2.1 为什么选择 pyright 作为第一个接入的语言服务器学习 LSP 有一个心理门槛不想从零开始写语言服务器又怕协议太复杂。这里建议先把现成语言服务器作为“黑盒”接入等跑通了再深入协议内部。选择 pyright 有几个原因。第一它的类型检查能力很强产生的诊断信息非常清晰。第二Python 测试项目足够简单前后代码都不需要额外编译。第三pyright 的包发布在 PyPI 上用pip安装即可不需要在一开始接触 Node.js 工具链。同一个思路也适用于其他语言比如 C/C 用 clangdGo 用 goplsJava 用 jdtls。它们都遵循同一套 LSP 协议换语言只是换启动命令和文件 URI核心链路不变。2.2 安装依赖并验证先准备一个干净的工作目录然后安装 pyright。python -m pip install pyright安装完成后做两步验证pyright --version which pyright-langserver如果pyright --version能正常输出版本号说明主命令可用。which pyright-langserver用于确认语言服务器二进制也在 PATH 中后面 Python 客户端需要通过它启动子进程。环境要求可以参考这张表项目最低要求说明操作系统Linux、macOS 或 Windows不同平台主要影响 file URI 写法Python3.10 或更高文章示例使用现代类型和语法pyright最新稳定版通过pip install pyright安装网络可访问 package index安装 Python 依赖时需要2.3 创建一个带语义信息的测试项目在同一个目录下创建sample_project里面放两个文件。sample_project/ calculator.py main.pycalculator.py定义了一个简单的计算器类class Calculator: def add(self, a: int, b: int) - int: return a b def multiply(self, a: int, b: int) - int: return a * bmain.py故意写了一个类型错误用来验证 LSP 诊断能否被读到from calculator import Calculator def main() - None: calc Calculator() result calc.add(1, 2) print(result) if __name__ __main__: main()先不用 LSP直接在命令行运行静态检查确认 pyright 能看到这个类型错误pyright sample_project正常情况下会看到类似这样的输出sample_project/main.py:6:19 - error: Argument of type str cannot be assigned to parameter b of type int in function add这一步很关键。如果命令行静态检查已经看不到错误后面 LSP 客户端拿不到诊断问题出在文件本身或 pyright 配置而不是协议交互。2.4 JSON-RPC 消息格式和 Content-Length 帧LSP 客户端和语言服务器之间通过标准输入和标准输出通信但并不是一行一个 JSON。每个消息都有固定的帧格式header 加 body。header 里最重要的字段是Content-Length表示 body 的字节数。body 是 JSON 文本。一个完整的消息长这样Content-Length: 152\r\n \r\n {jsonrpc:2.0,id:1,method:initialize,params:{processId:null,rootUri:file:///path/to/sample_project