llm-lsp.rb
Ruby 实现的 LLM 代码补全 LSP Server —— 让任何编辑器都能用上 AI 智能补全。
利用大模型的 FIM (Fill-In-the-Middle) 能力,通过标准 LSP 协议为编辑器提供实时代码补全。后端支持 Ollama、vLLM 等任何兼容 OpenAI Completions API 的服务。
灵感来源
本项目参考了 huggingface/llm-ls(Rust 实现的 LLM LSP 服务端)的设计思路,并在此基础上做了改进:
| llm-ls (HuggingFace) | llm-lsp.rb | |
|---|---|---|
| 语言 | Rust | Ruby |
| LSP 补全协议 | 自定义 llm-ls/getCompletions + textDocument/inlineCompletion
|
仅标准 textDocument/inlineCompletion
|
| 动态配置 | 不支持;inlineCompletion 只读全局配置,灵活配置依赖自定义协议每次请求传参 |
标准 didChangeConfiguration 动态切换 provider/model |
| 请求取消 | 不支持 | 支持 $/cancelRequest,中断 HTTP 连接立即释放 GPU |
| 流式补全 | 不支持(硬编码 stream: false) |
支持,逐 chunk 流式返回 |
| 请求防抖 | 客户端插件实现 | 服务端内置,连续请求只处理最新 |
| 补全采纳遥测 | 自定义协议 acceptCompletion/rejectCompletion
|
标准 executeCommand + 超时自动拒绝 |
| 异步架构 | Tokio | Async (Fiber) |
llm-ls 虽然也注册了标准的 textDocument/inlineCompletion,但该实现只是 getCompletions 的简化包装——所有配置(model、backend、FIM 参数等)只能读取全局固定值,无法动态调整。要获得灵活的每次请求配置,必须走自定义协议 llm-ls/getCompletions,现有的编辑器插件(llm-vscode、llm.nvim 等)也均基于此通信。
llm-lsp.rb 则完全基于标准 LSP 协议,通过 didChangeConfiguration 实现运行时动态切换 provider 和 model,既保持了协议标准性,又提供了不亚于自定义协议的灵活度。
特点
-
标准 LSP 协议 —— 实现
textDocument/inlineCompletion,无需自定义协议适配 - 流式补全 —— 逐 token 返回结果,打字般的补全体验
-
智能取消 ——
$/cancelRequest立即中断 HTTP 连接释放 GPU,不浪费算力 - 请求防抖 —— 连续快速输入时只处理最新请求,减少无效推理
- 异步非阻塞 —— 基于 Async Fiber 调度,补全过程不影响编辑器交互
- 多 Provider —— 同时配置多个后端,运行时随意切换
- Token 精确计数 —— 集成 HuggingFace Tokenizers,精确控制上下文窗口
-
补全采纳遥测 —— 每条补全附带
inlineCompletion/acceptcommand,编辑器采纳时回传;超时未采纳自动记录拒绝,可用于统计采纳率和模型效果评估
环境要求
- Ruby >= 3.1(开发使用 3.4)
- Bundler
- Ollama 或其他兼容 OpenAI Completions API 的后端
安装
gem install llm-lsp或在 Gemfile 中添加后 bundle install:
gem "llm-lsp"快速开始
# 启动 Ollama(如果还没有的话)
ollama serve
# 拉取一个适合补全的小模型
ollama pull qwen2.5-coder:1.5b然后在编辑器中配置 LSP 客户端指向 llm-lsp 即可。
使用
llm-lsp [options]
# 或通过 bundle
bundle exec llm-lsp [options]命令行参数
| 参数 | 说明 |
|---|---|
-c, --config FILE
|
配置文件路径(默认 ~/.config/llm-lsp/llm-lsp.yml) |
-m, --provider NAME
|
选择 provider(覆盖配置文件设定) |
--verbose LEVEL |
日志级别 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) |
--log FILE |
日志文件路径(默认 STDERR) |
-v, --version
|
显示版本号 |
-h, --help
|
显示帮助信息 |
配置文件
支持 YAML 配置文件,默认路径 ~/.config/llm-lsp/llm-lsp.yml,可通过 -c 指定其他路径。
provider: ollama
providers:
ollama:
model: qwen2.5-coder:1.5b
api_base: http://localhost:11434/v1
context_window: 2048
tokens_to_clear:
- "<|endoftext|>"
openai:
model: gpt-4
api_base: https://api.openai.com/v1
access_token: sk-xxxx
context_window: 8192优先级(从低到高):
- 配置文件(
~/.config/llm-lsp/llm-lsp.yml) -
-m命令行参数(仅覆盖 provider 选择) - LSP 客户端
initializationOptions(最高优先级,同名 provider 完全覆盖)
LSP 协议配置
initializationOptions
通过 LSP 客户端的 initialize 请求传入。完整结构:
当 initializationOptions 未提供 providers/provider 时,会使用配置文件中的默认值。
workspace/didChangeConfiguration
运行时动态修改配置,参数结构位于 params.settings:
{
"provider": "ollama", // 可选,切换当前 provider
"providers": { // 可选,更新 provider 配置(结构同 initializationOptions.providers)
"ollama": {
"model": "qwen2.5-coder:7b",
"api_base": "http://localhost:11434/v1"
// ... 同上所有字段
}
}
}编辑器集成
coc.nvim
在 coc-settings.json 中添加:
{
"languageserver": {
"llm-lsp": {
"command": "llm-lsp",
"args": ["--log", "/tmp/llm-lsp.log", "--verbose", "4"],
"filetypes": ["*"],
"initializationOptions": {
"provider": "ollama",
"providers": {
"ollama": {
"model": "qwen2.5-coder:1.5b",
"api_base": "http://localhost:11434/v1"
}
}
}
}
}
}如果已在配置文件中定义了 providers,initializationOptions 可以省略 providers 部分:
{
"languageserver": {
"llm-lsp": {
"command": "llm-lsp",
"args": ["-c", "/path/to/config.yml", "--log", "/tmp/llm-lsp.log"],
"filetypes": ["*"]
}
}
}测试
项目使用 Minitest 进行集成测试,通过启动 LSP 服务器子进程并进行 JSON-RPC 通信验证。
# 运行全部测试
bundle exec rake test
bundle exec ruby test/test_lsp.rb
# 运行单个测试
bundle exec ruby test/test_lsp.rb --name test_inline_completion注意:
- 部分测试依赖 Ollama 在
localhost:11434运行,不可用时自动 skip - Ollama 首次加载模型可能需要 30 秒以上
- 测试日志输出到
/tmp/llm-lsp-test.log
技术栈
| 组件 | 用途 |
|---|---|
| Async | 异步事件驱动(Fiber 调度) |
| IO::Stream | 配合 Async 的缓冲 IO |
| ruby-openai | OpenAI 兼容 API 客户端 |
| tokenizers | HuggingFace Tokenizers 绑定,精确 token 计数 |
| Minitest | 集成测试 |
致谢
- huggingface/llm-ls —— 本项目的主要参考,Rust 实现的 LLM LSP 服务端
- Ollama —— 本地运行大模型的最简方案
{ "provider": "ollama", // 可选,当前使用的 provider 名称 "providers": { // 可选,provider 定义(支持多个) "ollama": { "model": "qwen2.5-coder:1.5b", // 必填,模型名称 "api_base": "http://localhost:11434/v1", // 必填,API 端点 "access_token": "", // 可选,API 密钥(默认空) "context_window": 2048, // 可选,上下文窗口大小(默认 2048) "fim": { // 可选,FIM 特殊标记(默认 null,使用 prompt+suffix 模式) "prefix": "<fim_prefix>", "suffix": "<fim_suffix>", "middle": "<fim_middle>" }, "tokenizer_config": { // 可选,tokenizer 配置(默认 null,回退字符计数) // 三选一: "path": "/path/to/tokenizer.json", // 本地文件 "repository": "Qwen/Qwen2.5-Coder-1.5B", // HuggingFace Hub "url": "https://example.com/tokenizer.json" // URL 下载 }, "tokens_to_clear": ["<|endoftext|>"] // 可选,从补全结果中清除的标记(默认 []) } } }