系统篇

17|代码智能:从文本搜索到符号、版本与运行证据

系统篇约 5.5 千字1 组分步图解
本章目录

本章预备知识

源码有语言结构与版本关系。文本搜索找字符,AST找语法结构,LSP理解符号和类型;编译与测试提供运行证据,单一方法不能替代其他层。

本章各题列出所需基础。可先完成相应基础课,也可通过自检确认掌握程度。

进入深读前的小检查:对同名foo的两个模块,说明正则为何会混淆、定义引用关系能补什么,以及依赖未加载时语言服务为什么也可能不完整。

术语带虚线时可点击速查。公式中的字母先看定义;不要用猜缩写替代推导。

代码库不是普通文章集合。标识符、类型、模块关系、构建配置、版本和执行环境提供了比语义向量更精确的线索,但这些线索也有边界。

沿着对象与状态理解本章

代码里的同名字串、语法节点、符号引用、运行调用和测试证据是不同层。先看一段代码如何被这几层逐步定位,再判断需要全文检索、AST、LSP还是直接运行。

下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。

动态图解 · F30

代码检索不能只靠“语义相近”

grep、AST、LSP、调用图各自看到什么?

同一个函数名可能出现在注释、字符串、不同模块的同名函数中。文本搜索先定位线索,语法分析区分结构,语言服务帮助解析具体符号,运行和测试才告诉你这条路径是否真的触发。它们像不同视角的检查,不是越多越好地全部跑一遍。

AST按语言语法组织的代码结构树。
LSP让编辑器查询符号、引用和诊断等信息的协议。
调用图以函数为节点、可能的调用为边的关系图。

输入与适用条件:以foo为教学符号,不指向用户仓库。调用图可能受动态调用、条件编译与运行配置影响。

1 / 6 · 文本搜索

第1步,共6步

窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。

代码检索不能只靠“语义相近”:文本搜索文本搜索找到字符串 foo语法结构AST区分函数定义与注释符号解析LSP确定引用实际绑定哪个定义调用关系查谁调用它、受什么条件控制版本与环境核对当前源码和依赖配置测试与运行用执行证据验证候选原因教学流程;高亮表示当前步骤,不表示真实耗时比例。

搜索命中不代表命中的是实际执行的函数。

本步状态由图形与说明共同给出。

默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。

为什么需要

用代码自身结构减少无关阅读与误定位。

怎样实现

从最便宜且准确的路径/符号检索开始,证据不足再扩大。

可能的代价与误解

一上来全库语义扫描可能漏精确符号,同时浪费上下文。

如何判断是否做对

对已知缺陷记录实际入口、定义、调用者、版本和失败测试是否一致。

Q065|grep、AST、LSP、调用图各知道什么?为什么不能互相替代?

核心回答

grep寻找文本模式;AST描述语法结构;LSP通过语言服务提供定义、引用、类型诊断等能力;调用图描述可能或已观察的调用关系。文本出现不等于符号引用,语法正确不等于类型正确,静态可达不等于运行实际发生。[TREESITTER][LSP]

顺着一个函数名检索

搜索filter_items,可能命中函数定义、注释、字符串和测试数据。grep快且通用,但不知道哪些是同一符号。AST能识别函数声明和调用表达式,排除一部分注释/字符串干扰;但仅凭语法树,未必能解析导入别名、泛型或运行时分派。

语言服务结合项目配置、依赖与类型信息,把某个使用位置解析到具体定义。两个不同模块同名函数不应混在一起。若构建配置错、依赖没装、语言服务器索引未完成,结果也可能缺失或过期。

静态调用图能帮助扩展影响范围,但反射、依赖注入、动态导入、函数指针和多态会使它不完整或保守过宽。运行trace只展示这一次输入经过的路径,也不能证明其他路径不存在。可靠定位通常组合文本、结构、类型和运行证据。

运行示例 · Q065

同名foo出现三次,grep和LSP给出的集合为何不同

输入与条件。文件a定义foo,文件b导入a.foo并调用,文件c在一个函数内定义了另一个局部foo;注释也提到foo。

步骤与对象内部动作中间结果 / 新状态
1文本搜索搜索字符串foo得到定义、调用、局部同名与注释。
2语法过滤AST识别Call节点可去掉注释,但同名调用仍可能属于不同绑定。
3符号解析LSP查a.foo的引用应关联b的调用,不把c的局部foo混入。
4运行证据若调用由字符串动态生成静态引用可能漏,需检查注册与实际日志。
5修改范围根据绑定和测试确定调用方不能机械替换所有foo文本。

把过程连起来。文本相等、语法角色相等和符号身份相等是逐步增强但不同的关系。

边界与反例。LSP准确性依语言服务与工程配置;动态语言、反射、生成代码不保证静态穷尽。

依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。

追问:Tree-sitter解析成功就表示代码能编译吗?

不。它关注语法结构并可容忍不完整编辑状态;类型错误、链接错误、依赖缺失和运行异常需要其他工具。把AST当完整语义数据库会错过工程配置层。

追问:为什么查references比全仓grep更有价值?

当语言服务正确配置时,它按符号身份返回引用,减少同名误命中,并可跨别名和模块追踪。但不支持的动态模式可能仍漏;应根据语言和项目情况选择,不能把零引用当绝对无人使用。

追问:什么时候直接grep最好?

已知错误码、配置键、日志文本、文件名或精确函数名时,低成本文本搜索往往是最有效入口。先用便宜工具缩范围,再使用语义与类型工具深入。不是技术越高级越应优先执行。

Q066|代码索引怎样处理Git版本、工作树和未保存文件?

核心回答

一个仓库可能同时有提交快照、磁盘工作树、编辑器未保存缓冲和后台索引。Agent必须知道当前证据来自哪一层,编辑与测试针对哪一层。路径相同不等于内容相同。

四份“同一个文件”的冲突

main上的filter.py版本r10,用户本地已改为r11但未提交;编辑器又有未保存r12;索引仍是r10。Agent从索引读到r10,生成补丁后写磁盘覆盖r11;用户保存编辑器r12又覆盖Agent补丁。即使每一步本身没有报错,最终结果也错。

安全的读取接口应尽可能返回内容来源与版本标识。编辑时核对预期旧内容或版本;测试实际运行磁盘或构建环境中的哪份代码也要明确。对IDE集成,可设计工作区overlay优先读取未保存内容,但不能假设所有工具都自动共享该overlay。

增量索引要处理文件修改、删除、重命名和生成文件变化。只按mtime可能漏掉特殊时间戳或缓存问题;只按路径也会误识别重命名。具体实现可以结合版本和内容标识,但不必为每次简单搜索建立复杂全仓签名。

运行示例 · Q066

索引对应v7,工作树已经改了,哪份代码才是证据

输入与条件。HEAD=v7,工作树有未提交变更w1;语义索引仍建于v7。模型搜索得到旧函数签名foo(x)。

步骤与对象内部动作中间结果 / 新状态
1查版本搜索结果携带index_commit=v7知道它是哪个快照。
2读当前文件工作树中签名为foo(x,y)当前任务应以实际工作树为准。
3合并证据旧索引用于定位文件,当前读取用于判断不能把定位证据当最终内容。
4补测范围调用方可能只传一个参数检查实际代码和测试失败。
5发布前记录提交或工作树指纹及测试对象确保最后交付不是又切回另一份版本。

把过程连起来。索引可以过期地帮助找路,但涉及修改与结论时必须回读目标版本。

边界与反例。不要求每次都重建全量索引;差异增量与按需读取可以更经济。

依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。

追问:全仓索引更新完再允许Agent工作是否最安全?

会降低延迟且可能阻塞不必要的任务。可采用候选索引定位后直接读取当前文件确认的策略:索引负责快找,实时读负责事实。高风险修改的关键依赖再明确检查版本,不必每次重建整个仓库。

追问:为什么只记录Git commit还不够?

未提交改动、生成代码、依赖锁文件、环境变量和测试配置都可能影响执行。验证证据至少要对应实际工作树状态与命令环境。commit是重要标识,但不涵盖所有可变输入。

追问:源码更新后旧结论是不是全部作废?

不一定。只需重新验证受影响的事实与依赖。可以把知识绑定到具体模块/文件/符号,变化触发局部复核。全量遗忘浪费成本,永远不失效又危险;关键是依赖范围清楚。

Q067|一个Bug应怎样逐步定位?为什么“多读源码”不是充分方法?

核心回答

先形成可观察失败,再提出可证伪假设,选择能区分假设的最小证据。代码阅读、测试和日志应服务于缩小原因集合,而不是无目的累积上下文。

以exclude不生效为例

用户现象:配置了build/**但仍打包build目录。候选原因可包括配置没加载、规则解析错误、路径规范化不一致、匹配结果被后续覆盖。先确认运行配置中有规则,排除第一类;再打印或测试匹配入口收到的路径,发现是绝对路径而规则针对相对路径;构造最小测试后复现。此时重点转向规范化与根目录语义,而不是继续读取无关打包模块。

如果只读到fnmatch(path,pattern)就直接换库,可能把根因掩盖,还引入新匹配语义。修复前先确认需求:规则按仓库根还是当前目录?路径分隔符如何处理?目录规则是否包含子文件?符号链接是否跟随?这些边界决定测试和补丁。

最小补丁后,先跑原失败案例,再跑针对相邻语义的回归。若测试仍失败,应依据新错误更新假设,不能重复同一改法。每次操作最好能回答“预期看到什么,结果不同说明什么”。

运行示例 · Q067

exclude误删ab目录:从失败输入一步步到根因

输入与条件。要求排除目录a及其子目录,但保留ab。旧判断是path.startswith("a");输入为a、a/x、ab/y。

步骤与对象内部动作中间结果 / 新状态
1运行旧逻辑三个字符串都以a开头ab/y被误判排除。
2写正确语义规范化后path==a或path以a/开头边界是目录分隔符,不是任意字符前缀。
3最小反例用ab/y即可触发错误不需要先重构整个路径系统。
4回归集合a→排除,a/x→排除,ab/y→保留再加绝对/相对路径、分隔符规范化的实际需求。
5工程取舍目录是否涉及软链或大小写仅在部署平台与需求真实涉及时补对应规则。

把过程连起来。先把目标语义写成区分正确与错误实现的例子,再做最小必要修改。

边界与反例。字符串条件只对已规范化的教学POSIX路径成立;不能直接当作跨平台安全路径校验。

依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。

追问:反证式分析是否意味着设计很多检查表?

不必。可以只保留两三个可区分假设和最便宜验证。反证的核心是允许事实推翻自己的解释,而不是把简单Bug升级成长期研究项目。

追问:什么时候需要扩大阅读范围?

局部行为正确但系统结果错误、涉及调用方契约、状态传播或并发时,应沿数据/控制依赖扩展。先明确缺什么证据,再读相关边界。宽泛阅读应由问题驱动,而不是默认步骤。

追问:模型提出合理根因,为什么仍要复现?

合理解释可能有多个,只有观察与可重复反例能区分。复现也为补丁提供验证:原版本失败、新版本通过。没有复现时仍可做有依据修复,但应清楚说明验证不足,不能把推测包装成已确认根因。

Q068|怎样让Agent“懂业务”,而不是每次只看局部代码?

核心回答

把业务目标、不变量、关键流程、领域术语和实现入口连接起来,并保持可查询、可验证、可更新。业务理解不是把所有文档塞入Prompt,也不只是一份架构图。

建立从需求到实现的最短路径

例如“订单取消后不能重复退款”是业务不变量;它对应状态机、支付接口幂等键、数据库约束和测试。Agent处理退款Bug时,需要知道这个不变量,再找到执行路径和验证方式。文档只写“系统使用微服务架构”几乎不能指导正确修改。

可用轻量入口文件说明核心对象、边界、常见任务与代码路径;具体模块文档记录为什么这样设计、哪些替代方案被拒绝、当前例外。决策记录应该简短并带适用条件,不是每个变量命名都写ADR。

代码、测试、运行指标和文档彼此核对。文档说“最多重试三次”,代码常量是五次,不能自动选文档或代码为真;需判断当前环境与目标规范,再决定这是文档过期还是实现缺陷。

运行示例 · Q068

业务规则在测试里,为什么只读函数体不够

输入与条件。函数返回折扣,注释写“会员优惠”;测试明确:普通会员5%,高级会员10%,活动期间不可叠加。

步骤与对象内部动作中间结果 / 新状态
1只读实现发现两条折扣分支还不知道不叠加是否重要约束。
2读测试找到活动优惠与会员优惠冲突用例得到优先级的可执行证据。
3读需求核对测试是否仍对应当前业务旧测试不是永远正确的权威。
4修改验证新方案在活动+高级会员组合上运行防止把两个单独正确分支相加成错误结果。
5报告说明采用哪条规则、证据版本与验证结果不是只说“代码更简洁”。

把过程连起来。实现、测试、需求与运行行为相互校验;只选其中一类容易错过业务约束。

边界与反例。折扣政策为虚构数据,不暗示任何真实企业规则。

依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。

追问:业务知识适合长期记忆还是仓库文档?

团队共享且应随版本变化的知识通常适合仓库或权威知识库;个人偏好和跨项目经验可放个人记忆;当前任务暂态放Checkpoint。相同事实重复存很多份会增加冲突,应保持权威来源并让记忆指向它。

追问:新人导读怎样兼顾易读与内部深度?

先给业务流程和核心对象,再链接具体模块、状态转移、数据结构和失败案例。不能只列目录,也不能开头就让读者读几千行源码。渐进式不是删掉细节,而是让细节有明确入口和前置知识。

追问:如何知道这些文档真的提高了Agent质量?

选真实业务任务,对比没有入口文档、完整大文档、按需结构化入口三组,测必要证据读取、错误业务假设、任务成功和成本。文档数量增长本身不是能力增长。

记录保存在本机浏览器。

输入关键词即可检索。

↑ ↓ 选择 Enter 打开Esc 关闭 · 仅本地搜索

阅读设置

阅读设置仅保存在本机浏览器,不上传任何数据。