17|代码智能:从文本搜索到符号、版本与运行证据
本章目录
本章预备知识
源码有语言结构与版本关系。文本搜索找字符,AST找语法结构,LSP理解符号和类型;编译与测试提供运行证据,单一方法不能替代其他层。
本章各题列出所需基础。可先完成相应基础课,也可通过自检确认掌握程度。
进入深读前的小检查:对同名foo的两个模块,说明正则为何会混淆、定义引用关系能补什么,以及依赖未加载时语言服务为什么也可能不完整。
术语带虚线时可点击速查。公式中的字母先看定义;不要用猜缩写替代推导。
代码库不是普通文章集合。标识符、类型、模块关系、构建配置、版本和执行环境提供了比语义向量更精确的线索,但这些线索也有边界。
沿着对象与状态理解本章
代码里的同名字串、语法节点、符号引用、运行调用和测试证据是不同层。先看一段代码如何被这几层逐步定位,再判断需要全文检索、AST、LSP还是直接运行。
下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。
代码检索不能只靠“语义相近”
grep、AST、LSP、调用图各自看到什么?
同一个函数名可能出现在注释、字符串、不同模块的同名函数中。文本搜索先定位线索,语法分析区分结构,语言服务帮助解析具体符号,运行和测试才告诉你这条路径是否真的触发。它们像不同视角的检查,不是越多越好地全部跑一遍。
输入与适用条件:以foo为教学符号,不指向用户仓库。调用图可能受动态调用、条件编译与运行配置影响。
1 / 6 · 文本搜索
窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。
搜索命中不代表命中的是实际执行的函数。
本步状态由图形与说明共同给出。默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。
为什么需要
用代码自身结构减少无关阅读与误定位。
怎样实现
从最便宜且准确的路径/符号检索开始,证据不足再扩大。
可能的代价与误解
一上来全库语义扫描可能漏精确符号,同时浪费上下文。
如何判断是否做对
对已知缺陷记录实际入口、定义、调用者、版本和失败测试是否一致。
Q065|grep、AST、LSP、调用图各知道什么?为什么不能互相替代?
核心回答
grep寻找文本模式;AST描述语法结构;LSP通过语言服务提供定义、引用、类型诊断等能力;调用图描述可能或已观察的调用关系。文本出现不等于符号引用,语法正确不等于类型正确,静态可达不等于运行实际发生。[TREESITTER][LSP]
顺着一个函数名检索
搜索filter_items,可能命中函数定义、注释、字符串和测试数据。grep快且通用,但不知道哪些是同一符号。AST能识别函数声明和调用表达式,排除一部分注释/字符串干扰;但仅凭语法树,未必能解析导入别名、泛型或运行时分派。
语言服务结合项目配置、依赖与类型信息,把某个使用位置解析到具体定义。两个不同模块同名函数不应混在一起。若构建配置错、依赖没装、语言服务器索引未完成,结果也可能缺失或过期。
静态调用图能帮助扩展影响范围,但反射、依赖注入、动态导入、函数指针和多态会使它不完整或保守过宽。运行trace只展示这一次输入经过的路径,也不能证明其他路径不存在。可靠定位通常组合文本、结构、类型和运行证据。
同名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可能漏掉特殊时间戳或缓存问题;只按路径也会误识别重命名。具体实现可以结合版本和内容标识,但不必为每次简单搜索建立复杂全仓签名。
索引对应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)就直接换库,可能把根因掩盖,还引入新匹配语义。修复前先确认需求:规则按仓库根还是当前目录?路径分隔符如何处理?目录规则是否包含子文件?符号链接是否跟随?这些边界决定测试和补丁。
最小补丁后,先跑原失败案例,再跑针对相邻语义的回归。若测试仍失败,应依据新错误更新假设,不能重复同一改法。每次操作最好能回答“预期看到什么,结果不同说明什么”。
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。
代码、测试、运行指标和文档彼此核对。文档说“最多重试三次”,代码常量是五次,不能自动选文档或代码为真;需判断当前环境与目标规范,再决定这是文档过期还是实现缺陷。
业务规则在测试里,为什么只读函数体不够
输入与条件。函数返回折扣,注释写“会员优惠”;测试明确:普通会员5%,高级会员10%,活动期间不可叠加。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1只读实现 | 发现两条折扣分支 | 还不知道不叠加是否重要约束。 |
| 2读测试 | 找到活动优惠与会员优惠冲突用例 | 得到优先级的可执行证据。 |
| 3读需求 | 核对测试是否仍对应当前业务 | 旧测试不是永远正确的权威。 |
| 4修改验证 | 新方案在活动+高级会员组合上运行 | 防止把两个单独正确分支相加成错误结果。 |
| 5报告 | 说明采用哪条规则、证据版本与验证结果 | 不是只说“代码更简洁”。 |
把过程连起来。实现、测试、需求与运行行为相互校验;只选其中一类容易错过业务约束。
边界与反例。折扣政策为虚构数据,不暗示任何真实企业规则。
依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。
追问:业务知识适合长期记忆还是仓库文档?
团队共享且应随版本变化的知识通常适合仓库或权威知识库;个人偏好和跨项目经验可放个人记忆;当前任务暂态放Checkpoint。相同事实重复存很多份会增加冲突,应保持权威来源并让记忆指向它。
追问:新人导读怎样兼顾易读与内部深度?
先给业务流程和核心对象,再链接具体模块、状态转移、数据结构和失败案例。不能只列目录,也不能开头就让读者读几千行源码。渐进式不是删掉细节,而是让细节有明确入口和前置知识。
追问:如何知道这些文档真的提高了Agent质量?
选真实业务任务,对比没有入口文档、完整大文档、按需结构化入口三组,测必要证据读取、错误业务假设、任务成功和成本。文档数量增长本身不是能力增长。