工程9 分钟阅读

Grep 完胜 LSP?Coding Agent 如何高效理解代码

给 Coding Agent 换上更精准的 LSP 工具,效果不一定更好。任务要找什么、代码库里有多少同名文本、工具返回了哪些代码,都会影响结果。

我们最近做了一组小规模实验:给 Coding Agent 接上一套基于 LSP 的语义导航工具,和它平时用的 grep 对比。

我们本来以为,用 LSP 的效果会更好。它理解符号、定义和引用,能排掉注释和普通字符串里的同名文本,返回结果更准,Agent 要处理的无关内容也更少。

实际测下来,LSP 的效果取决于具体任务。代码定位任务里,Agent 几乎只用 grep;强制它先用语义工具,成功率有时还会下降。只有在要找全调用方,或者代码库里同名文本很多的时候,LSP 的好处才明显。

检索准不准只是一部分。工具还得把下一步要用的上下文一起返回,让模型拿到结果就能用。模型在训练里见过多少类似的工具调用,可能也有影响。这次实验能看出上下文和返回格式的影响;训练数据没有改过,只能提出猜测,没法验证。

LSP 还有诊断、重命名、代码操作这些能力,这次都没测。下面的结论只适用于查找引用、跳转定义和列出文档符号这三种语义导航,代表不了 LSP 的全部价值。

模型能力等于模型与原生 harness 的乘积
Agent 的表现由模型和运行环境共同决定,工具接口是运行环境的一部分。

我们比较了什么

grep 按字符串查找文本,不理解代码结构,但直接返回文件路径、行号和匹配内容。LSP 语义导航基于语言服务器协议(Language Server Protocol),我们只接了查找引用、跳转定义、列出文档符号三种能力,它们理解代码里的符号关系,分得清真正的函数调用和注释里的同名文字。

我们在多个 Python 和 TypeScript 代码库里,用三个 Claude 模型做了三类任务:定位代码、找全引用、多文件重命名。只有两种方法都成功完成任务,才比较 token 消耗——失败的尝试往往半路就停了,看起来消耗少,这种“节省”没有意义。每种实验设置只跑了两到三次,这些结果只能提供一些线索,还不能当作普遍结论。

简单的代码定位任务里,两种工具都可用时,三个模型主动选语义工具的比例只有 0%~6%。强制先用语义工具,这组任务的成功率从 100% 掉到 89%。

换成“找出所有调用方”这类引用完整性任务,三个模型用语义导航的比例升到 45%~57%。这组任务里,LSP 把精确率(precision)从 grep 的 0.76 提到 1.00:返回的结果全是真正的调用点,排除了同名文本造成的误报。但两种方法的召回率(recall)都只有 0.66 左右——结果更干净了,找到的真实调用点并没有变多。Agent 没有把结果查完,换检索后端也没解决这个问题;在更强的模型上,换成 LSP 后查得更准了,token 却花得更多。

Agent 不是一味偏爱 grep,而是按任务挑工具

两种工具都可用、Agent 自由选择时,语义(LSP)工具调用所占的比例。

图例:Opus 4.8(蓝)、Sonnet 4.6(品红)、Haiku 4.5(绿)。

不同任务中的语义工具使用率:代码定位和重命名接近零,引用完整性任务为 45% 到 57%
同样的模型、同样的自由选择,换个任务,工具选择就变了。定位和重命名任务里,Agent 几乎只用 grep;要找全引用时,不用提示它也有一半左右的调用走 LSP。
任务 Opus 4.8 Sonnet 4.6 Haiku 4.5
代码定位 0% 4% 6%
引用完整性 45% 50% 57%
多文件重命名 3%

代码库本身也很重要。在同名文本干扰较少的 TypeScript 代码库 remeda 里,grep 本身就能准确找出引用,换成 LSP,F1 没有提升,token 反而多了 16%。另一个 TypeScript 代码库 hono 里,同名文本干扰多,grep 的精确率只有 0.51,换成 LSP 后 F1 提高了 0.246,token 还省了 12%。Python 代码库 requests 介于两者之间:F1 多了 0.072,token 多了 19%。

同名文本越多,LSP 越有用

引用完整性任务中的 F1 增益(ΔF1 = LSP − grep)。柱子的颜色表示 grep 在该代码库里的误报程度,precgrep 在这里的精确率。

图例:蓝色表示 grep 在这个代码库里查得准,品红表示误报多。

LSP 带来的 F1 变化:remeda 加 0.000,hono 加 0.246,requests 加 0.072
同一种语言的两个代码库,结论相反。grep 已经查得很准时,换成语义导航只会多出调用和解析的开销;同名文本多时,LSP 减少误报的好处才显出来。
代码库 语言 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 在这个代码库里有多少误报。Agent 也不是简单地“一直用 grep”:换个任务,工具选择会变;换个代码库,语义导航值不值得也会变。

多返回几行源码,会怎样

我们最初提供的 LSP 工具只返回文件路径、行号和列号。Agent 知道引用在哪里,却看不到代码,得逐个打开文件才能判断下一步。grep 的一行返回是这样的:src/auth.ts:42: return validateToken(token),位置和代码内容都在,模型不用再读一次文件,就能判断这条匹配相不相关。

于是我们改了一版 LSP 工具:除了位置,再附上引用点前后两行源码。语义后端没动,找到的引用还是那些,只是模型现在能直接看到旁边的代码了。做多文件重命名时,一次尝试成功率(pass@1)从 0.67 提高到 0.83,后续读取文件的次数从 15.2 次降到 3.2 次,比用 grep 时的 4.3 次还少。

返回源码上下文,语义导航才好用

多文件重命名任务,模型为 Opus 4.8,使用预热索引的 pyright。两个 LSP 组用同一个语义后端,只有返回格式不同。

图例:grep(蓝)、LSP 只返回位置(品红)、LSP 附带源码上下文(绿)。

grep、只返回位置的 LSP、返回源码上下文的 LSP,在 pass@1 和后续文件读取次数上的比较
只给位置,模型还得决定读哪些文件、调用读取工具,再看返回的代码;每条引用附上前后两行源码,就少了这几步。检索后端始终没变,变的只是返回内容的形式。
方案 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 的工具调用,才更习惯这种返回格式?这次没有改训练数据,回答不了这个问题。附上源码,可能更接近模型熟悉的返回格式,也可能只是省了几步操作。两种解释都说得通,这次实验还分不清。

有些任务本来就更适合 grep

LSP 和 grep 覆盖的范围不一样。语义引用只包括代码里真正的符号关系;但多文件重命名要改的,还有注释、docstring、配置文件和普通字符串。这些不算语义引用,却同样得更新。find_references 按设计不会返回它们,grep 会把所有同名文本都列出来。

语义引用 ⊂ 文本出现位置

要把这些文本一并改掉,grep 本来就能查得更全,模型再熟悉 LSP 也改变不了两种工具的查找范围。

grep 为什么表现更好,这里有两种解释,需要分清:

  1. 任务要求:有些任务连代码之外的文本也要改,只查语义引用会漏掉这些地方。
  2. 工具使用习惯:模型可能更熟悉常见命令和它们的返回格式。

第一条实验已经验证了,直接来自两种工具查找范围的差别。第二条只是假设,与工具选择和返回格式的结果一致,但这次没有改训练数据,验证不了。

grep 具有优势的结构原因和分布原因
任务要求决定了 grep 什么时候更合适,使用习惯则解释了模型为什么更愿意走熟悉的路径。

换一套工具,同一个模型的表现也会变

Coding Agent 还包括模型周围的运行环境,通常叫 harness(运行框架)。模型收到哪些指令、能调哪些工具、参数怎么写、结果和错误怎么返回,以及下一轮能看到什么,都由它安排。

这次实验里,LSP 后端没动,只是多返回几行源码,成功率和文件读取次数就都变了。模型权重没有变,改的只是工具接口。

Anthropic 在 Effective harnesses for long-running agents 里也谈到了运行环境:长时间工作的 Agent 要跨会话继续做事,需要初始化环境、记录进度、验证结果。即使只看一次工具调用,返回格式、错误信息和备用路径,也都会影响模型接下来做什么。

后训练里如果包含 Agent 轨迹,这些轨迹里的提示、工具调用、返回结果和出错后的补救路径,同样由 harness 定义。一个反复用 readgrepeditbash 训练出来的模型,学到的做法可能就依赖这几个接口;换一套工具,实际能力也可能跟着变。

Agent 的实际能力 = 模型 × harness

所以同一个模型换到另一套 runtime,不一定还能跑出原来那套 harness 的 benchmark 成绩。支持同一个模型,不等于复现同一个 Agent:工具怎么选、参数怎么定义、结果和错误怎么返回,都会影响模型的做法。

给 Agent 加工具前先看六件事

LSP 在同名文本多的代码库里确实查得更好,多返回几行源码也确实减少了后续读取,不能据此说 LSP、MCP 或新的 Agent Skill 没用。给 Agent 加工具,要看它做完任务的整个过程。

我们的建议是先用原生工具,再按下面六条检查新加的能力:

  1. 任务有没有完成。Token 变少但成功率下降,不算优化。
  2. Agent 会不会主动调用。工具可用,不代表模型知道什么时候用。
  3. 拿到结果能不能继续做。path:line:content 往往比只有位置的对象好用。
  4. 有没有保留备用路径。语义导航和文本检索覆盖的范围不同,替代不了对方。
  5. 换个任务或代码库,还好不好用。同名文本越多,语义导航越可能有用;要覆盖所有文本时,还是得靠 grep
  6. 模型要不要专门学怎么用。提示词能告诉模型有这个工具,但要让它在合适的时候用,可能还需要额外训练或强化。

Anthropic 在 Building effective agents 里建议优先用简单、可组合的模式。工具也是一样,数量多了,任务不一定做得更好;工具之间要区分清楚、容易理解,还得在模型的工作流程里真的用得上。

结论

LSP 能减少误报,但代码库里得确实有同名文本的干扰,才能看到这个好处。工具只返回位置,查得再准,模型也得额外打开文件;附上几行源码,拿到结果就能继续判断。评估新工具时,得一起看 Agent 会不会选它、任务做没做完、花了多少成本,以及返回的内容够不够用。

完整的实验设计、任务定义和结果见 Does a Language Server Save Tokens for Coding Agents?

AgentConnect 也是这个思路:用开放协议连接不同 Agent,同时保留各自的原生 runtime 和工具循环。

在 GitHub 上 Star AgentConnectagentconnect-md/agentconnect 开始使用 AgentConnectdocs.agentconnect.md
这是一组初步的小规模试验。任务和代码库都不多,只测了三个 Claude 模型,每种实验设置只跑了两到三次。我们测了基于 LSP 的引用查找、定义跳转和文档符号,没有测 textDocument/rename、diagnostics 或 code actions。如果直接让 LSP 做语义重命名,在这次 grep 表现较好的重构任务里,结果可能会不同。编辑任务都在本地完成,这些不是标准 SWE-bench 分数。结果可以作为设计工具时的参考,但不能代表所有模型、工具和代码库。