给 Coding Agent 换上更精准的 LSP 工具,效果不一定更好。任务要找什么、代码库里有多少同名文本、工具返回了哪些代码,都会影响结果。
我们最近做了一组小规模实验:给 Coding Agent 接上一套基于 LSP 的语义导航工具,和它平时用的 grep 对比。
我们本来以为,用 LSP 的效果会更好。它理解符号、定义和引用,能排掉注释和普通字符串里的同名文本,返回结果更准,Agent 要处理的无关内容也更少。
实际测下来,LSP 的效果取决于具体任务。代码定位任务里,Agent 几乎只用 grep;强制它先用语义工具,成功率有时还会下降。只有在要找全调用方,或者代码库里同名文本很多的时候,LSP 的好处才明显。
检索准不准只是一部分。工具还得把下一步要用的上下文一起返回,让模型拿到结果就能用。模型在训练里见过多少类似的工具调用,可能也有影响。这次实验能看出上下文和返回格式的影响;训练数据没有改过,只能提出猜测,没法验证。
grep 按字符串查找文本,不理解代码结构,但直接返回文件路径、行号和匹配内容。
LSP 语义导航基于语言服务器协议(Language Server Protocol)。我们只接了三种能力:查找引用、跳转定义、列出文档符号。它们理解代码里的符号关系,分得清真正的函数调用和注释里的同名文字。
LSP 还有诊断、重命名、代码操作这些能力,这次都没测。结论只适用于上面三种语义导航,代表不了 LSP 的全部价值。
我们在多个 Python 和 TypeScript 代码库里,用三个 Claude 模型做了三类任务:定位代码、找全引用、多文件重命名。每种实验设置只跑了两到三次,这些结果只能提供一些线索,还不能当作普遍结论。
只有两种方法都成功完成任务,才比较 token 消耗。失败的尝试往往半路就停了,看起来消耗少,这种“节省”没有意义。
简单的代码定位任务里,两种工具都可用时,三个模型主动选语义工具的比例只有 0%~6%。强制先用语义工具,这组任务的成功率从 100% 掉到 89%。
换成“找出所有调用方”这类引用完整性任务,三个模型用语义导航的比例升到 45%~57%。
| 任务 | Opus 4.8 | Sonnet 4.6 | Haiku 4.5 |
|---|---|---|---|
| 代码定位 | 0% | 4% | 6% |
| 引用完整性 | 45% | 50% | 57% |
| 多文件重命名 | 3% | — | — |
找全引用时,LSP 能少报一些不相关的结果。这组任务里,LSP 把精确率(precision)从 grep 的 0.76 提到 1.00:返回的结果全是真正的调用点,排除了同名文本造成的误报。
但两种方法的召回率(recall)都只有 0.66 左右。召回率是所有真实调用点里,Agent 最后找出来的比例。结果更干净了,找到的真实调用点并没有变多。
Agent 没有把结果查完,换检索后端也没解决这个问题。在更强的模型上,换成 LSP 后查得更准了,token 却花得更多。
在同名文本干扰较少的 TypeScript 代码库 remeda 里,grep 本身就能准确找出引用。换成 LSP,F1 没有提升,token 反而多了 16%。F1 把精确率和召回率合在一起衡量,按这个指标看,检索质量没有改善。
另一个 TypeScript 代码库 hono 里,同名文本干扰多,grep 的精确率只有 0.51。换成 LSP 后,F1 提高了 0.246,token 还省了 12%。
Python 代码库 requests 介于两者之间:F1 多了 0.072,token 多了 19%。
| 代码库 | 语言 | grep 精确率 | ΔF1(LSP − grep) | Token 变化 |
|---|---|---|---|---|
| remeda | TypeScript | 1.00 | +0.000 | +16% |
| hono | TypeScript | 0.51 | +0.246 | −12% |
| requests | Python | 0.76 | +0.072 | +19% |
两个 TypeScript 代码库结果相反,光看是不是强类型语言,判断不了 LSP 有没有用。还得看 grep 在这个代码库里有多少误报:它已经查得很准时,换成语义导航只会多出调用和解析的开销;同名文本多时,LSP 减少误报的好处才显出来。
我们最初提供的 LSP 工具只返回文件路径、行号和列号。Agent 知道引用在哪里,却看不到代码,得逐个打开文件才能判断下一步。
grep 的一行返回是这样的:
src/auth.ts:42: return validateToken(token)
位置和代码内容都在,模型不用再读一次文件,就能判断这条匹配相不相关。
于是我们改了一版 LSP 工具:除了位置,再附上引用点前后两行源码。语义后端没动,找到的引用还是那些,只是模型现在能直接看到旁边的代码了。
做多文件重命名时,用 LSP 的一次尝试成功率(pass@1)从 0.67 提高到 0.83,后续读取文件的次数从 15.2 次降到 3.2 次,比用 grep 时的 4.3 次还少。
| 方案 | pass@1 | 引用点召回率 | Token 数 | 后续读取次数 |
|---|---|---|---|---|
| grep | 1.00 | 1.000 | 2,451 | 4.3 |
| LSP,只返回位置 | 0.67 | 0.930 | 4,131 | 15.2 |
| LSP,附带源码上下文 | 0.83 | 0.958 | 3,336 | 3.2 |
只给位置,模型还得决定读哪些文件、调用读取工具,再看返回的代码;带上源码,就少了这几步。
Anthropic 在 Writing effective tools for agents 里也讲到这一点:工具返回的上下文本身就是接口的一部分。语义上正确只是第一步,返回内容还得够模型做下一步决策。
模型会不会因为训练里见多了类似 grep 的工具调用,才更习惯这种返回格式?这次没有改训练数据,回答不了这个问题。附上源码,可能更接近模型熟悉的返回格式,也可能只是省了几步操作。两种解释都说得通,这次实验还分不清。
LSP 和 grep 覆盖的范围不一样。语义引用只包括代码里真正的符号关系;但多文件重命名要改的,还有注释、docstring、配置文件和普通字符串。这些不算语义引用,却同样得更新。find_references 按设计不会返回它们,grep 会把所有同名文本都列出来。
语义引用 ⊂ 文本出现位置
要把这些文本一并改掉,grep 本来就能查得更全,模型再熟悉 LSP 也改变不了两种工具的查找范围。
grep 为什么表现更好,这里有两种解释,需要分清:
Coding Agent 还包括模型周围的运行环境,通常叫 harness。模型收到哪些指令、能调哪些工具、参数怎么写、结果和错误怎么返回,以及下一轮能看到什么,都由它安排。
Agent 的实际能力 = 模型 × harness
这次实验里,LSP 后端没动,只是多返回几行源码,成功率和文件读取次数就都变了。模型权重没有变,改的只是工具接口。
Anthropic 在 Effective harnesses for long-running agents 里也谈到了运行环境:长时间工作的 Agent 要跨会话继续做事,需要初始化环境、记录进度、验证结果。即使只看一次工具调用,返回格式、错误信息和备用路径,也都会影响模型接下来做什么。
所以同一个模型换到另一套 runtime,不一定还能跑出原来那套 harness 的 benchmark 成绩。
LSP 在同名文本多的代码库里确实查得更好,多返回几行源码也确实减少了后续读取,不能据此说 LSP、MCP 或新的 Agent Skill 没用。给 Agent 加工具,要看它做完任务的整个过程:
path:line:content 往往比只有位置的对象好用。grep。Anthropic 在 Building effective agents 里建议优先用简单、可组合的模式。工具也是一样,数量多了,任务不一定做得更好。
LSP 能减少误报,但代码库里得确实有同名文本的干扰,才能看到这个好处。工具只返回位置,查得再准,模型也得额外打开文件;附上几行源码,拿到结果就能继续判断。评估新工具时,得一起看 Agent 会不会选它、任务做没做完、花了多少成本,以及返回的内容够不够用。
完整的实验设计、任务定义和结果见 Does a Language Server Save Tokens for Coding Agents?。
AgentConnect 也是这个思路:用开放协议连接不同 Agent,同时保留各自的原生 runtime 和工具循环。
这是一组初步的小规模试验。任务和代码库都不多,只测了三个 Claude 模型,每种实验设置只跑了两到三次。我们测了基于 LSP 的引用查找、定义跳转和文档符号,没有测
textDocument/rename、diagnostics 或 code actions。如果直接让 LSP 做语义重命名,在这次grep表现较好的重构任务里,结果可能会不同。编辑任务都在本地完成,这些不是标准 SWE-bench 分数。结果可以作为设计工具时的参考,但不能代表所有模型、工具和代码库。