Qwen Code 原生 LSP 集成实战:.lsp.json 配置、lsp 工具的 12 种代码智能操作与安全模型
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
Qwen Code 内置了原生 Language Server Protocol(LSP)支持,让 AI Agent 能够像 IDE 一样“理解”你的代码:跳转定义、查找引用、悬停文档、诊断告警、调用层级与代码操作。本文基于官方文档 docs/users/features/lsp.md 展开,结合packages/core/src/lsp/与packages/core/src/tools/lsp.ts的源码实现,系统讲清楚:如何用--experimental-lsp启用该能力、如何编写.lsp.json及各字段的确切语义、统一lsp工具暴露的全部操作及其参数约定,以及工作区信任、命令路径限制等安全机制和完整的排障路径。
一、功能定位与启用方式
1.1 LSP 带来的能力
Qwen Code 的 LSP 支持通过连接“理解你代码的语言服务器”工作。一旦通过.lsp.json(或扩展)完成配置,Qwen Code 会自动启动这些服务器,并借助它们:
- 跳转到符号定义(go-to-definition);
- 查找符号的所有引用(find references);
- 获取悬停信息(文档、类型信息);
- 查看诊断消息(错误、警告);
- 访问代码操作(快速修复、重构);
- 分析调用层级(call hierarchy)。
这套能力最终统一收敛到一个名为lsp的工具上,Agent 在会话中即可调用(详见第三节)。
1.2 启用:--experimental-lsp 命令行标志
LSP 目前是 Qwen Code 的实验性功能,需要显式传参启用:
qwen --experimental-lsp从源码可以确认这一开关的真实生效逻辑:在 CLI 配置解析 中,LSP 仅在同时满足“非 bare 模式、非 provisional workspace、且显式传入--experimental-lsp”三个条件时才被置为启用:
// LSP configuration: enabled only via --experimental-lsp flag !provisionalWorkspace && !bareMode && argv.experimentalLsp === true;也就是说,不存在配置文件级别的隐式开关,每次启动都必须带这个标志,否则lsp工具会返回 “LSP … is unavailable (LSP disabled or not initialized)”(见 LspToolInvocation.execute)。
1.3 前置条件:安装对应语言服务器
LSP 服务器是配置驱动的:你必须定义它们,Qwen Code 才会启动。各语言对应的服务器与安装命令如下:
| 语言 | 语言服务器 | 安装命令 |
|---|---|---|
| TypeScript/JavaScript | typescript-language-server | npm install -g typescript-language-server typescript |
| Python | pylsp | pip install python-lsp-server |
| Go | gopls | go install golang.org/x/tools/gopls@latest |
| Rust | rust-analyzer | 通过 rustup 安装组件(rustup component add rust-analyzer)或按官方说明安装 |
| C/C++ | clangd | 通过系统包管理器安装 LLVM/clangd |
| Java | jdtls | 安装 JDTLS 与 JDK(java需在 PATH 中) |
二、编写.lsp.json配置
2.1 基本格式
在项目根目录放置.lsp.json文件即可声明语言服务器。顶层每个键是一个语言标识符,其值是对应服务器的配置对象:
{ "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "extensionToLanguage": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } }从源码结构看(LspConfigLoader.parseConfigSource),解析规则是:键(如typescript)成为该服务器负责的languages列表的唯一元素;若command是字符串则直接取它作为服务器name,否则回退为键名。
2.2 C/C++(clangd)配置
依赖条件:
- clangd(LLVM)已安装并在 PATH 中可用;
- 项目中存在编译数据库(
compile_commands.json)或compile_flags.txt,这是 clangd 给出准确结果的前提。
示例配置:
{ "cpp": { "command": "clangd", "args": [ "--background-index", "--clang-tidy", "--header-insertion=iwyu", "--completion-style=detailed" ] } }若编译数据库位于 build 子目录(CMake 等场景常见),需显式告知 clangd:
{ "cpp": { "command": "clangd", "args": ["--background-index", "--compile-commands-dir=build"] } }2.3 Java(jdtls)配置
依赖条件:JDK 已安装且java在 PATH 中;JDTLS 已安装且jdtls在 PATH 中。
{ "java": { "command": "jdtls", "args": ["-configuration", ".jdtls-config", "-data", ".jdtls-workspace"] } }注意源码中的默认常量(constants.ts):文档打开后默认仅等待 200ms(DEFAULT_LSP_DOCUMENT_OPEN_DELAY_MS),而 jdtls、clangd 这类慢服务器构建 AST/索引需要更久,Qwen Code 对首次返回空结果的文档级操作会默认等待 2000ms 再重试一次(DEFAULT_LSP_DOCUMENT_RETRY_DELAY_MS)。这解释了“刚打开项目时 Java/C++ 首次查询可能为空、稍后正常”的现象。
2.4 配置字段完整说明
必填字段:
| 字段 | 类型 | 说明 |
|---|---|---|
command | string | 启动 LSP 服务器的命令。支持通过PATH解析的裸命令名(如clangd)和绝对路径(如/opt/llvm/bin/clangd) |
可选字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
args | string[] | [] | 命令行参数 |
transport | string | "stdio" | 传输类型:stdio、tcp或socket |
env | object | - | 环境变量(数字/布尔值会被规范化为字符串) |
initializationOptions | object | - | LSP 初始化选项(随initialize请求发送) |
settings | object | - | 通过workspace/didChangeConfiguration下发的服务器设置 |
extensionToLanguage | object | - | 文件扩展名到语言标识符的映射(扩展名不带.且统一小写) |
workspaceFolder | string | - | 覆盖工作区文件夹(必须位于项目根目录内,否则回退为项目根并告警) |
startupTimeout | number | 10000 | 启动超时(毫秒),默认值见 DEFAULT_LSP_STARTUP_TIMEOUT_MS |
shutdownTimeout | number | 5000 | 优雅关闭超时(毫秒),默认值见 DEFAULT_LSP_SHUTDOWN_TIMEOUT_MS |
restartOnCrash | boolean | false | 崩溃后自动重启 |
maxRestarts | number | 3 | 最大重启次数,默认值见 DEFAULT_LSP_MAX_RESTARTS |
trustRequired | boolean | true | 是否要求受信任工作区(见 4.2 的源码级细节) |
字段解析时源码会做严格的类型规范化(buildServerConfig):args只保留字符串元素;env中非字符串值会转为字符串;startupTimeout/shutdownTimeout必须是正数,否则被丢弃并回退默认值;transport只接受stdio/tcp/socket(大小写不敏感),其余值一律回退为stdio。
另外两个容易踩坑的硬性校验:
transport为stdio但缺少command→ 该配置被丢弃并在 debug 日志中告警;transport为tcp/socket但缺少有效的 socket 信息(端口 1–65535,或 Unix socket 路径)→ 同样被丢弃。
2.5 TCP / Socket 传输
对于使用 TCP 或 Unix socket 传输的服务器(例如远端语言服务器),配置形如:
{ "remote-lsp": { "transport": "tcp", "socket": { "host": "127.0.0.1", "port": 9999 }, "extensionToLanguage": { ".custom": "custom" } } }源码中 normalizeSocketOptions 同时支持三种写法:socket直接给字符串(作为 Unix socket 路径)、socket: { host, port }(TCP)、socket: { path }(Unix socket,也兼容socketPath键名)。TCP 连接还有内置的重试退避(初始 250ms、上限 1000ms,见 constants.ts#L51-L55)。
2.6 配置加载与合并的底层逻辑
理解配置如何被加载,有助于排查“配置明明写了却不生效”的问题。LspConfigLoader 的完整流程:
- 用户配置:只读取项目根目录下的
.lsp.json(path.join(workspaceRoot, '.lsp.json')),文件不存在时返回空列表而不是报错; - 扩展配置:遍历已启用扩展的
config.lspServers(详见第五节); - 合并策略:mergeConfigs 先并入扩展配置,再并入用户配置,同名(同
name)服务器时用户配置覆盖扩展配置。源码注释明确说明:内置预设(built-in presets)已禁用,语言服务器必须由用户显式配置,不会自动探测安装。
还有一个值得注意的严格模式:loadUserConfigsStrict对.lsp.json的每个条目做强校验(必须是对象、必须能构造出合法配置),任何一条不合法都会让整份配置加载失败并返回错误,供上层(如/lsp状态检查)提示用户修正 JSON,而不是静默忽略。
三、统一lsp工具:12 种代码智能操作
Qwen Code 将全部 LSP 能力收敛到单一工具lsp,通过operation参数区分操作。工具定义见 LspTool,支持的 12 种操作在 LspOperation 类型 中一次性声明完毕:
| 分组 | 操作 | 必需参数 | 源码默认 limit |
|---|---|---|---|
| 代码导航 | goToDefinition | filePath+line+character | 20 |
findReferences | 同上,另有可选includeDeclaration | 50 | |
goToImplementation | filePath+line+character | 20 | |
| 符号信息 | hover | filePath+line+character | - |
documentSymbol | filePath | 50 | |
workspaceSymbol | query(可选limit) | 20 | |
| 调用层级 | prepareCallHierarchy | filePath+line+character | 20 |
incomingCalls | callHierarchyItem(来自 prepare 的结果) | 20 | |
outgoingCalls | callHierarchyItem | 20 | |
| 诊断 | diagnostics | filePath | - |
workspaceDiagnostics | 无(可选limit) | 50 | |
| 代码操作 | codeActions | filePath+line+character,可选endLine/endCharacter/diagnostics/codeActionKinds | 20 |
3.1 位置类操作的参数约定
goToDefinition、findReferences、hover、goToImplementation、prepareCallHierarchy这五类基于位置的操作要求精确的filePath+line+character。文档给出的关键使用建议是:如果你不知道符号的精确位置,先用workspaceSymbol(按名搜索)或documentSymbol(列出文件内符号)定位,再发起位置类操作。
Operation: goToDefinition Parameters: - filePath: Path to the file - line: Line number (1-based) - character: Column number (1-based)Operation: findReferences Parameters: - filePath: Path to the file - line: Line number (1-based) - character: Column number (1-based) - includeDeclaration: Include the declaration itself (optional)Operation: goToImplementation Parameters: - filePath: Path to the file - line: Line number (1-based) - character: Column number (1-based)Operation: hover Parameters: - filePath: Path to the file - line: Line number (1-based) - character: Column number (1-based)参数校验在 validateToolParamValues 中实现,要点:
- 行号和列号均为 1-based,源码内部再减 1 转换为 LSP 协议的 0-based 位置(resolveLocationTarget);
filePath支持绝对路径或工作区相对路径,也支持带://的 URI,会被规范化为file://URI(resolveUri);- 任意行/列参数小于 1 都会被拒绝;
- 每个操作可通过
serverName显式指定目标服务器(同一语言配置了多个服务器时),否则按“第一个返回结果的服务器胜出”策略。
3.2 符号信息
Operation: documentSymbol Parameters: - filePath: Path to the fileOperation: workspaceSymbol Parameters: - query: Search query string - limit: Maximum results (optional)workspaceSymbol有一个源码级的加分细节(executeWorkspaceSymbols):在返回符号列表之外,还会自动对排名第一的匹配项追加一次references查询,把它的引用位置一并带回给 LLM,让 Agent 一次调用就拿到“定义位置 + 使用位置”双份上下文,减少后续轮次。
3.3 调用层级
调用层级是三步式协议:先prepareCallHierarchy拿到某个位置上的调用层级项,再把该项传给incomingCalls/outgoingCalls。
Operation: prepareCallHierarchy Parameters: - filePath: Path to the file - line: Line number (1-based) - character: Column number (1-based)Operation: incomingCalls Parameters: - callHierarchyItem: Item from prepareCallHierarchyOperation: outgoingCalls Parameters: - callHierarchyItem: Item from prepareCallHierarchycallHierarchyItem的完整结构(name、uri、range、selectionRange为必填,kind/detail/data/serverName可选)定义在工具的 JSON Schema 中(definitions.LspCallHierarchyItem)。源码会保留prepare返回的data不透明字段原样传回给服务器——部分服务器(如 clangd)依赖它定位内部索引,手动改写该项会导致查询失败。
输出格式上,prepareCallHierarchy/incomingCalls/outgoingCalls/codeActions四类操作除了人类可读的编号列表外,还会在 LLM 可见内容中追加一个 JSON 区块(formatJsonSection,lsp.ts#L943-L945),供 Agent 精确消费结构化数据;incomingCalls的结果还会标注每个调用发生的具体位置(fromRanges,最多显示 3 处,超出以+N more收尾)。
3.4 诊断
Operation: diagnostics Parameters: - filePath: Path to the fileOperation: workspaceDiagnostics Parameters: - limit: Maximum results (optional)诊断结果按[SEVERITY] line:col (code) [source]: message的格式输出(executeDiagnostics),severity 覆盖error/warning/information/hint四级(映射表见 DIAGNOSTIC_SEVERITY_LABELS)。workspaceDiagnostics按文件分组输出,并在标题行给出“共 N 个文件 M 条问题”的汇总。
3.5 代码操作(Code Actions)
Operation: codeActions Parameters: - filePath: Path to the file - line: Start line number (1-based) - character: Start column number (1-based) - endLine: End line number (optional, defaults to line) - endCharacter: End column (optional, defaults to character) - diagnostics: Diagnostics to get actions for (optional) - codeActionKinds: Filter by action kind (optional)line/character缺省端点默认等于起点(即“单点”范围);请求上下文固定triggerKind: 'invoked'(executeCodeActions)。支持的codeActionKinds过滤值:
quickfix— 错误/警告的快速修复;refactor— 重构操作;refactor.extract— 提取为函数/变量;refactor.inline— 内联函数/变量;source— 源代码级操作;source.organizeImports— 整理 import;source.fixAll— 修复所有可自动修复的问题。
源码中的 CODE_ACTION_KIND_LABELS 还包含refactor.rewrite,且空字符串的 kind 会被归一化为quickfix。输出列表里,服务器标记为isPreferred的项会带★,携带edit或command的项会标注(has edit)/(has command)。
另外,lsp工具在工具清单中是延迟加载的(shouldDefer: true,经 ToolSearch 按需载入,见 LspTool 构造参数),其工具描述明确要求 Agent:“当 LSP 可用时,优先用 LSP 作为代码智能查询的主工具,而不是先用 grep/glob”——这正是该功能的设计意图:让模型直接消费语义级结果,而非文本级搜索。
四、安全模型:工作区信任与路径限制
4.1 为什么默认要求“受信任工作区”
LSP 服务器以你的用户权限运行、并且可以执行代码(语言服务器通常会求值宏、调用 build 工具链等)。因此 Qwen Code 的默认策略是:只有受信任工作区才会启动 LSP 服务器。
- 受信任工作区:已配置的 LSP 服务器正常启动;
- 非受信任工作区:除非服务器配置中设置了
trustRequired: false,否则服务器不会启动; - 标记信任:在 CLI 中执行
/trust命令。
源码中这一判断发生在启动前:LspServerManager 会记录 “requires trusted workspace, skipping startup”,NativeLspService 则把被跳过的服务器记入skipped列表,跳过原因固定为server_trust_required(LspServerSkipReason),这些服务器仍会出现在/lsp状态输出中以便你排查。
4.2 单服务器信任覆盖(及一个源码级细节)
你可以在特定服务器的配置中覆盖信任要求:
{ "safe-server": { "command": "safe-language-server", "args": ["--stdio"], "trustRequired": false, "extensionToLanguage": { ".safe": "safe" } } }这里有一个文档没有明说、但对行为有实际影响的细节:从 LspConfigLoader.loadUserConfigs 与 parseUserConfigSourceStrict 的实现看,用户.lsp.json的每个条目在解析时都会被强制trustRequired: true(forceTrustRequired: true)。也就是说,项目内.lsp.json写的trustRequired: false不会放松信任要求;而扩展提供的 LSP 配置走的是不强制的路径,trustRequired: false的单服务器覆盖在扩展场景下才真正生效。保守理解:项目级配置永远要求受信任工作区,这是刻意设计的安全底线。
4.3 命令路径安全
command字段只允许两类取值:可在PATH中解析的裸命令名,或绝对路径。相对路径且逃逸出工作区的命令会被直接拦截,源码中的报错文案是(LspServerManager.ts#L417):
LSP server <name> command path is unsafe: ../../outside/payload测试用例(LspServerManager.test.ts#L545)正是用../../outside/payload这样的路径验证该拦截。对应到排障:若你的服务器不在 PATH 中,请改用绝对路径(如/opt/llvm/bin/clangd),不要尝试用../之类相对路径“绕”到别处。
五、扩展声明的 LSP 服务器(lspServers)
扩展可以在其plugin.json的lspServers字段中提供 LSP 服务器配置,取值有两种形式:
- 内联对象:直接写语言键值布局的配置;
- 字符串路径:指向一个
.lsp.json文件的相对/绝对路径。
格式与项目.lsp.json完全一致(语言为顶层键)。扩展启用时 Qwen Code 即加载这些配置(loadExtensionConfigs)。两个实现细节:
- 字符串路径相对于扩展自身目录解析(resolveExtensionConfigPath);
- 扩展配置中的字符串会经过变量替换(hydrateExtensionLspConfig),可用
${CLAUDE_PLUGIN_ROOT}引用扩展根目录、${workspacePath}引用当前工作区,便于跨项目复用的扩展声明可移植的服务器路径。
与用户配置的合并规则如 2.6 所述:同名服务器时用户配置优先。
六、排障与调试
6.1 状态检查:/status与/lsp
/status会打印一行 LSP 汇总(对应 LspStatusSnapshot 的各状态计数):
LSP: disabled LSP: enabled, 1/1 ready LSP: enabled, 0/1 ready (1 failed) LSP: enabled, no servers configured LSP: enabled, status unavailable/lsp(实现见 lspCommand.ts)输出逐服务器明细表:
**LSP Server Status** | Server | Command | Languages | Status | |--------|---------|-----------|--------| | clangd | `clangd` | c, cpp | READY | | pyright | `pyright-langserver` | python | FAILED - startup failed |状态机为四态:NOT_STARTED/IN_PROGRESS/READY/FAILED(LspServerStatus),FAILED 时会附带最后的error与 stderr 尾部输出。若修改了.lsp.json,客户端接口支持reinitialize重新读取配置并 reconcile 现有连接(LspClient.reinitialize)。
6.2 服务器不启动:六步排查清单
- 确认
--experimental-lsp标志:启动时是否带了该标志(漏掉是最常见原因); - 确认服务器已安装:手动执行
clangd --version之类命令验证; - 检查命令:服务器二进制必须在系统
PATH中,或使用绝对路径;逃逸工作区的相对路径会被安全拦截(见 4.3); - 检查工作区信任:未
/trust前服务器不会启动; - 查看日志:带
--debug启动后在 debug 日志中检索 LSP 相关条目(见 6.5); - 检查进程:
ps aux | grep <server-name>确认服务器进程是否真的存在。
常见错误信息对照:
command path is unsafe -> relative path escapes workspace, use absolute path or add to PATH command not found -> server binary not installed or not in PATH requires trusted workspace -> run /trust first LSP connection closed -> server started but exited or closed stdio before replying to initialize6.3 性能问题(启动慢 / 大项目)
- 大项目:考虑排除
node_modules等超大目录(通过语言服务器自身参数,如 clangd 的.clangd配置或--compile-commands-dir收敛索引范围); - 服务器启动慢:在
.lsp.json中调大该服务器的startupTimeout(默认 10000ms,见 constants.ts#L13-L14),避免默认超时先于服务器初始化完成。
6.4 查询无结果
- 服务器尚未就绪:仍在索引中。C/C++ 项目请确认
args含--background-index,且项目根(或父目录)存在compile_commands.json/compile_flags.txt;编译数据库在 build 子目录时用--compile-commands-dir=<path>指定; - 文件未保存:语言服务器以磁盘内容为准,保存文件后它才能感知;
- 语言不对:确认该文件扩展名映射到了正确的服务器(检查
extensionToLanguage); - 进程不在:
ps aux | grep <server-name>验证(clangd / typescript-language-server / jdtls 等)。
6.5 调试日志:--debug+ 日志检索
LSP 没有独立的 debug 开关,直接复用 Qwen Code 的标准调试模式:
qwen --experimental-lsp --debugdebug 日志写入会话 debug 日志目录,检索 LSP 相关条目:
# 默认运行时目录 rg "LSP|Native LSP|clangd|connection closed" ~/.qwen/debug/latest # 或不用 ripgrep: grep -E "LSP|Native LSP|clangd|connection closed" ~/.qwen/debug/latest # 若设置了 QWEN_RUNTIME_DIR rg "LSP|Native LSP|clangd|connection closed" "$QWEN_RUNTIME_DIR/debug/latest"有价值的日志模式:
[LSP] ...— 原生 LSP 服务与服务器管理器发出的日志(debugLogger 命名空间即LSP,见 LspConfigLoader.ts#L22);[CONFIG] Native LSP status after discovery: ...— 会话发现到的服务器配置;[CONFIG] Native LSP status after startup: ...— 启动结果,含 ready/failed 计数;[STATUS] LSP status snapshot for /status: ...— debug 模式下运行/status时打印的状态快照。
6.6 clangd 专项验证
clangd 启动失败时,从项目根目录直接验证服务器本身:
clangd --version clangd --check=/path/to/file.cpp --log=verboseC/C++ 项目通常应提供compile_commands.json或compile_flags.txt;编译数据库位于 build 目录时按 2.2 的方式传--compile-commands-dir。最后可用ps aux | grep clangd确认进程存活。
七、最佳实践与 FAQ
最佳实践:
- 全局安装语言服务器:保证所有项目可用;
- 按项目定制:确有需要时用项目
.lsp.json覆写服务器参数; - 保持服务器更新:定期升级语言服务器以获得最好的索引与诊断质量;
- 谨慎授予信任:只信任来源可靠的工作区。
FAQ:
Q:如何启用 LSP?启动时加
--experimental-lsp:qwen --experimental-lsp。Q:如何知道哪些语言服务器正在运行?以
qwen --experimental-lsp --debug启动,然后:/status看一行汇总,/lsp看逐服务器状态表,或直接检索 debug 日志:rg "LSP|Native LSP|<server-name>" ~/.qwen/debug/latest # 或:grep -E "LSP|Native LSP|<server-name>" ~/.qwen/debug/latest # 若配置了 QWEN_RUNTIME_DIR: rg "LSP|Native LSP|<server-name>" "$QWEN_RUNTIME_DIR/debug/latest"LSP 复用 Qwen Code 标准的
--debug模式,没有单独的 LSP debug 开关。Q:同一文件类型能用多个语言服务器吗?可以,但每次操作只会用一个服务器的结果,第一个返回结果的服务器胜出。可用
serverName参数强制指定目标服务器。Q:LSP 在沙箱模式下能工作吗?LSP 服务器运行在沙箱之外以便访问你的代码,同时受工作区信任控制约束。
适用范围与限制提示:本文所述行为以当前仓库源码为准。LSP 能力当前处于实验阶段(--experimental-lsp),仅支持项目根.lsp.json与扩展lspServers两种配置来源,无内置服务器预设;workspaceFolder不得逃逸项目根;用户.lsp.json的信任要求被强制为 true。若你依赖trustRequired: false放松信任,请通过扩展配置而非项目文件来实现。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考