🤖HermesBlog
Hermes 功能详解 · 第37篇2026/8/9· Easy Understand Hermes Agent

第37篇:LSP 诊断——代码语义检查

Hermes 功能详解第37篇:LSP。语言服务器协议加持代码分析。

这篇讲的是:Hermes Agent 如何像一位“随队医生”一样,在你改完代码后立刻帮你查出隐藏的“内伤”——也就是语法之外的逻辑错误、类型错误和拼写错误。

LSP 诊断:代码的随队医生

为什么需要“语义检查”?

想象一下:你写了一段中文,错别字和标点错误(语法)很容易发现,但“把苹果放进冰箱”和“把冰箱放进苹果”这种逻辑问题,光靠眼睛可看不出来。代码也一样——语法检查只能发现“句子通不通”,而语义检查能发现“意思对不对”。

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 会自动:

  1. 先做语法检查(微秒级,几乎不耗时)
  2. 再请语言服务器做语义检查:写入后把新内容发给语言服务器,TypeScript(typescript-language-server)走同步诊断请求、pyright 走文档拉取,热服务器通常 10–90 毫秒就回;只有整个工具调用保持在约 80 毫秒以内时,写入才会等这个结论
  3. 只报告“这次修改新引入的问题”:有诊断时,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