第37篇:LSP 诊断——代码语义检查
Hermes 功能详解第37篇:LSP。语言服务器协议加持代码分析。
这篇讲的是:Hermes Agent 如何像一位“随队医生”一样,在你改完代码后立刻帮你查出隐藏的“内伤”——也就是语法之外的逻辑错误、类型错误和拼写错误。
为什么需要“语义检查”?
想象一下:你写了一段中文,错别字和标点错误(语法)很容易发现,但“把苹果放进冰箱”和“把冰箱放进苹果”这种逻辑问题,光靠眼睛可看不出来。代码也一样——语法检查只能发现“句子通不通”,而语义检查能发现“意思对不对”。
Hermes 内置了 20 多种语言服务器(比如 Python 的 Pyright、Go 的 gopls、Rust 的 rust-analyzer),它们像一群专业审稿人,在后台默默盯着你每次修改,然后告诉你:“你这里少了个定义”“那里类型对不上”。
第1步:确认你的项目在 Git 仓库里
LSP 只在 Git 工作区内生效。如果你的代码不在 Git 仓库里,这个功能就自动休眠——就像医生只在医院上班,不会跑到你家客厅来查房。
cd 你的项目目录
git init # 如果还没初始化仓库
第2步:查看哪些语言已“配好医生”
运行这条命令,看看哪些语言的服务器已就绪:
hermes lsp status
它会列出每种语言的安装状态。没装的就用 hermes lsp install <语言id> 补上,比如:
hermes lsp install python # 安装 Pyright
hermes lsp install go # 安装 gopls
顺带一提,Laravel 的 .blade.php 模板现在也有专属医生了——走的是 laravel-lsp,而普通的 .php 文件还是交给 intelephense。它没有自动安装配方,得自己用 Composer 装一次:
composer global require laravel/lsp
export PATH="$HOME/.config/composer/vendor/bin:$PATH"
装好之前,hermes lsp status 会把它标成 manual-only。
第3步:正常写代码,剩下的交给 Hermes
每次你用 write_file 或 patch 保存文件时,Hermes 会自动:
- 先做语法检查(微秒级,几乎不耗时)
- 再请语言服务器做语义检查:写入后把新内容发给语言服务器,TypeScript(
typescript-language-server)走同步诊断请求、pyright 走文档拉取,热服务器通常 10–90 毫秒就回;只有整个工具调用保持在约 80 毫秒以内时,写入才会等这个结论 - 只报告“这次修改新引入的问题”:有诊断时,Hermes 会拿它和修改前内容的诊断对比,只保留新增的那些
如果结论来得慢(冷启动的服务器、超大文件),它会挂到会话的下一个工具结果里,标注为 LSP diagnostics introduced by your earlier edit of <path>。如果模型想在一项检查还在跑时结束回合,Hermes 最多等 5 秒,把发现的问题作为新消息交给模型后才接受回答,所以一次性的 hermes chat -q 也能看到它们;之前的消息不会被改写。
如果同一个文件在检查完成前又被改了一次,新的检查会以旧那次修改的基线为准——第一次修改引入的错误仍会被报告,除非第二次修改把它修好了。
你会在返回结果里看到类似这样的信息:
{
"lint": {"status": "ok"},
"lsp_diagnostics": "ERROR [42:5] Cannot find name 'foo' (Pyright)"
}
lint 是语法通道,lsp_diagnostics 是语义通道——语法没问题但语义有错时,你会看到 lint: ok 加上一堆诊断信息。
第4步:按需调整配置(可选)
默认配置对大多数人都够用。如果你想让 Python 检查更严格,可以在 config.yaml 里这样设置:
lsp:
servers:
pyright:
initialization_options:
python:
analysis:
typeCheckingMode: "strict"
改完配置记得重启服务:hermes lsp restart
第4.5步:给冷启动留预算(可选)
第一次打开某个工作区时,语言服务器要先启动、初始化,还要把整个项目“读一遍”——像 tsserver 面对上万文件的大项目,光这一步就可能要一分钟。针对这种情况,Hermes 新增了 lsp.warmup_timeout:它只作用于某个工作区的第一次请求(服务器还没跑起来的那一次),之后的常规检查仍然走 wait_timeout。所以大项目应该调大 warmup_timeout,而不是把 wait_timeout 一起抬高。
lsp:
wait_timeout: 5.0 # 常规检查的预算,保持不变
warmup_timeout: 60 # 只给第一次冷启动用;0 表示不额外宽限
如果你用的是 pnpm 或 yarn,可以顺手把 lsp.package_manager 设成对应值——Hermes 会照你的包管理器策略来装服务器,不会偷偷绕过去用 npm。另外,Vue 项目现在固定用 @vue/language-server@2,如果你之前装过 3.x 版本,日志里会有一条一次性警告,删掉 <HERMES_HOME>/lsp/node_modules/@vue 和 <HERMES_HOME>/lsp/bin/vue-language-server*,再跑一次 hermes lsp install vue-language-server 就好。
第5步:工作区信任——别让陌生仓库“跑代码”
这是最近新增、也是最重要的一条安全机制。很多语言服务器会真的去执行项目自带的代码:pyright 会跑项目里配置的 Python 解释器,typescript-language-server 会加载项目的 node_modules/typescript,svelte-language-server 会读 svelte.config.js,rust-analyzer 每次保存都会跑 cargo check(包括构建脚本和宏),jdtls、kotlin-language-server、elixir-ls、zls、haskell-language-server 这些启动时还会去解析项目的构建文件(Gradle、mix.exs、build.zig、Cabal/Stack)。
这对你自己的项目没问题,但对一个刚被 agent clone 下来的陌生仓库就很危险。所以 Hermes 默认把每个工作区都当成“不信任”,除非它是:
- 你亲手指向的那个目录的 git 工作树:比如你在哪启动的(
cd my-app && hermes)、hermes -w创建的工作树、Desktop 或 TUI 会话打开的项目、或者 gateway 的terminal.cwd; - 或者列在
lsp.trusted_workspaces里的目录(含其下所有子目录)。
注意几个坑:在 agent 终端里 cd 不会改变会话的工作区;Cron 任务和 Kanban worker 没有自动信任(因为工作目录是 agent 自己选的),需要手动列进 lsp.trusted_workspaces;嵌套在受信工作树里的 checkout 因为自带 .git,不算受信;你 home 目录本身或更上层的 git 仓库也不算受信(否则一个 dotfiles 仓库就会把下面所有东西都信任了)。信任覆盖你指向的整个目录(包括之后 clone 进去的东西),并且到 Hermes 退出为止。
在不受信的工作区里,Hermes 默认拒绝:只启动下面这几个服务器,而且都用 Hermes 侧的设置,不碰项目代码。
| 服务器 | 不受信工作区下的行为 |
|---|---|
| pyright | 用 VIRTUAL_ENV 或 Hermes 自带的 Python,绝不用项目的 .venv/venv |
| typescript-language-server | tsserver.path 固定指向服务器旁边的 TypeScript;没有就跳过 |
| svelte-language-server | isTrusted: false(不读 svelte.config.js,不用项目的 svelte/prettier) |
| bash-language-server、yaml-language-server、dockerfile-ls、intelephense | 不变:它们不跑项目代码(yaml-language-server 可能会去抓文件里写的 JSON schema) |
| clangd | 不变:Hermes 从不传 --query-driver,所以不会跑项目编译器 |
其他服务器一律跳过,包括 rust-analyzer、gopls、jdtls、kotlin-language-server、elixir-ls、zls、clojure-lsp、haskell-language-server、lua-language-server、terraform-ls、prisma、astro、vue-language-server(它会加载项目 tsconfig.json 里指定的 vueCompilerOptions.plugins),以及你自己在 lsp.servers 里声明的任何服务器。诊断日志里会记一条 skipped: untrusted workspace …; add it to lsp.trusted_workspaces,hermes lsp status 会把它们标成 [trusted workspaces only]。
在本地后端上,保存后跑的 shell 检查器也一样会被跳过(只要终端当前目录不受信):npx tsc(它会跑仓库的 node_modules/.bin/tsc,或者按 .npmrc 从 registry 装包)和 rustfmt --check(rustup 会遵守仓库的 rust-toolchain.toml)。沙箱后端(Docker、SSH、Modal 等)不受影响。
代价是:需要项目依赖的诊断(比如未解析导入的警告)在信任之前可能不太准。想信任某个目录,这样写:
lsp:
trusted_workspaces:
- ~/code/my-app
第6步:失败重试与排除工作区(可选)
还有两个新参数值得知道:
broken_retry_seconds:某个工作区的服务器失败后(启动出错或请求超预算),这对 (服务器, 根目录) 会被暂时跳过。默认0表示本进程内一直跳过(直到你跑hermes lsp restart);设成N则 N 秒后重试一次,避免一次偶发卡顿就让某个工作区永远“失声”。跳过时每个根目录会打一条 INFO 日志,并附上重试时间。exclude_roots:用来彻底关掉某些工作区的语言服务器,支持 glob 模式(~会展开,裸路径也会匹配其下所有内容)。适合那种“一个巨型 monorepo 的服务器永远跑不完,但其他工作区还想保留诊断”的场景——它和servers.<id>.disabled不同,后者是把某个服务器在所有地方都关掉。注意它必须是列表,写成别的形状会打警告并跳过所有工作区的 LSP。
lsp:
broken_retry_seconds: 30
exclude_roots: ["~/work/huge-monorepo", "/srv/checkouts/*/vendor"]
最后补一句环境相关的:服务器和 npm / go install 自动安装器都是从 Hermes 的净化子环境里启动的(不带 gateway token 和 provider API key),所以某个服务器如果确实需要这类变量,只能通过配置里的对应键传给它。
常见问题
- 没装服务器会怎样? 自动降级为纯语法检查,不会报错。
- 服务器启动慢? 首次启动需要 1-10 秒,之后会常驻后台;空闲超过 10 分钟会自动回收,下次用到再自动拉起来。超大项目第一次冷启动可能更久,用
lsp.warmup_timeout给它留时间。 - 为什么我的服务器没启动? 先看
hermes lsp status里有没有[trusted workspaces only]标记——多半是工作区没被信任,把它加进lsp.trusted_workspaces即可。 - PowerShell 用户? 它比较特殊,需要手动下载模块包并设置路径,详见文档。
小结
LSP 诊断就像给代码加了一道“语义安检门”,让你在提交代码前就发现隐藏的“内伤”。语法检查保底,语义检查提升质量,两者配合,写代码更安心。别忘了新加的“工作区信任”机制——它挡住了陌生仓库在后台偷偷跑代码的风险,是这套诊断体系的安全底线。
下篇预告:第38篇我们将聊聊 Hermes 的“记忆管理”——它如何记住你的项目偏好,越用越顺手。敬请期待!
📖 官方文档
本文根据 Hermes Agent 官方文档编写,原文见:官方文档 › user-guide/features/lsp