1. 读代码这件事,Agent 和人是两个物种
先说个我最近的真实经历。接手了一个前后端分离的电商后台项目,线上反馈“商品列表页点了筛选没反应”,看着很简单的一个 bug,结果我在浏览器 DevTools、前端代码和后端日志之间来回切了半个多小时:筛选按钮确实触发了请求,但请求参数里少了价格区间;后端接口也报了个 400,但前端异常处理把这条错误吞了,只弹了个“请稍后重试”。如果这时候手头有一个能读懂前后端代码、并且能沿着“用户点击 → 前端事件 → HTTP 请求 → 后端处理 → 数据返回”这个调用链一路查下去的 Agent,这类问题也就是一两分钟内的事。
这篇文章要聊的,就是给 Agent 补上这种能力:让 Agent 拥有阅读前后端代码的能力,并能沿着用户操作去定位调用链中的每一环。
很多人第一次听说这个需求时,第一反应是“把代码喂给 Agent 不就行了”。但实际跑过就知道,这条路根本走不通。我上一期提到过 Agent 的上下文窗口和推理深度是有限的,把整个仓库的代码一次性塞进去,结果往往是两件事同时发生:Token 很快烧完,关键调用关系被海量无关代码冲淡。这里面的核心差异在于——人会“扫描”代码,Agent 只能“检索”代码。
1.1 人类开发者排查问题的习惯路径
我自己排查前后端问题时的常规套路是:
- 复现操作,打开 DevTools 看 Network 请求是否有异常、请求参数对不对。
- 看控制台报错,从堆栈信息点进源码位置。
- 顺着报错位置向上下游扩展,比如看是哪个函数发起请求的、后端哪个接口处理这个请求。
- 翻日志,查接口入参、异常堆栈,定位到 Service 层逻辑。
这套流程的本质是以“报错信息”或“异常现象”为锚点,向调用链两端扩散。人眼扫代码是并行的,可以直接浏览整个函数体、相邻文件,靠经验和直觉判断哪里可疑。但我们做不到把整个系统“一次性预读”,本质上是边查边学。
1.2 Agent 阅读代码的“天生存款”与“天生缺陷”
Agent 的强项是:给一段上下文就能快速推理,多个文件的关键片段放进同一个窗口时,可以准确找出它们之间的调用关系。这比人强得多——人切三四个文件之后,工作记忆就开始不够用了。
Agent 的弱项也很明显:没有“全局视图”。它不能像人一样扫一眼目录结构就知道“这个页面文件引入了个公共组件,公共组件里又调了 utils 里的请求方法”。如果喂给它的代码缺少了某个中间文件,它的推理就会断裂,甚至开始编造一个不存在的调用链。
所以,让 Agent 阅读前后端代码这件事,本质上不是“把代码给它”,而是建立一套代码检索与上下文拼装的方法。当我解决完这个问题之后,整个排查效率提升非常明显,后面会结合我实际跑的一个场景把方法完整拆开讲。先记住一个结论:Agent 适合的阅读模式是“先地图、再路径、最后进函数体”,这和人的“老板键一开、全仓搜索”是完全不同的策略。
2. 先让 Agent 认路:仓库地图的构建方法
给 Agent 读代码的第一步,从来不是读具体的业务代码,而是先把仓库结构喂给它。这一步做不好,后面全是空中楼阁。
2.1 为什么必须显式构建“代码地图”
我在刚开始做这个功能时踩过一次坑:我直接把项目里一个核心页面组件和对应的 Controller 文件丢给 Agent,让它分析“从点击到接口的调用链”。结果它把 Vue 组件里一个import { getList } from '@/api/product'当成了直接发 HTTP 请求的函数,还一本正经地解释了 axios 的封装逻辑——但实际上@/api/product这个文件里的getList只是调用了一个request()封装方法,真正的 URL 前缀在别的地方配置。
这个问题的根源是:Agent 缺失了中间层的文件信息。它看到了组件的调用点和 API 模块的导出点,但不清楚这一层的函数签名、baseURL 拼接规则以及拦截器做了什么。
所以我现在实践中,第一件事永远是把项目的“地图”构建好。这一步我用两种方式:
- 对小型项目,直接让 Agent 解析配置文件(
package.json、vite.config.js、pom.xml等)。 - 对中大型项目,先跑一遍目录树生成命令,把关键目录和文件名列表给 Agent,让它先标注出“这可能是一条调用链上的节点”,再按需读取具体文件。
推荐使用tree命令(macOS/Linux 自带,Windows 可以用tree /F):
tree src -L 3 --dirsfirst tree src/main/java -L 4 tree src/main/resources -L 32.2 前后端分离项目的“地标文件”清单
每个项目架构不同,但前后端分离项目通常有比较固定的“地标文件”。我把它们整理成一张清单,实际构建 Agent 的仓库地图时,会按这个清单逐个确认。
| 端 | 地标文件 | 作用 |
|---|---|---|
| 前端 | src/main.js/main.ts | 应用入口,挂载根组件,注册全局插件(如 router、pinia) |
| 前端 | src/router/路由配置 | 页面路径与组件的映射关系,调用链的“页面入口” |
| 前端 | src/api/或src/services/ | 所有对后端的 HTTP 请求定义处 |
| 前端 | src/utils/request.js/http.js | axios 或 fetch 的封装,baseURL、拦截器、错误码转换都在这 |
| 后端 | pom.xml或build.gradle | 依赖和模块结构,能看出是否包含 Web、MyBatis、Redis 等 |
| 后端 | application.yml/application.properties | 端口、数据源、Redis、MyBatis 配置等 |
| 后端 | controller包 | 请求入口,URL 与方法的映射 |
| 后端 | service包及其实现 | 业务逻辑,事务边界 |
| 后端 | mapper包 +resources/mapper/*.xml | 数据访问,SQL 语句就在 xml 里 |
这组清单的构建目的很明确:让 Agent 知道“什么类型的代码应该出现在哪个位置”。比如它看到一个@RestController注解,就应该知道这是 Controller 层,它的职责是参数接收和数据返回,不应该在这里纠结复杂的业务条件逻辑。有了这层“先验结构”,后续阅读代码时的推理准确率会高非常多。
2.3 构建地图时用到的 Prompt 参考
以下是一个我常用的“地图构建 Prompt”,你可以根据项目调整:
你现在是一个全栈代码分析专家。下面是一个前后端分离项目的目录结构(树状图): [在这里粘贴 tree 输出] 请完成以下任务: 1. 识别前端入口文件、路由配置文件、API 请求定义目录。 2. 识别后端请求入口(Controller 包)、业务逻辑层(Service 包)、数据访问层(Mapper/DAO 包)。 3. 指出可能存在全局配置的文件(如 axios 封装、请求拦截器、数据库配置)。 4. 输出一个“代码地图”,包括每个关键文件的路径和一句话说明。 注意:这一步只做地图标注,不要进入具体业务代码分析。这一步跑完,Agent 已经知道了“哪些文件是调用链上的必经站点”。但它仍然不知道每个站点内部长什么样,所以下一步是分别从前端侧和后端侧把链路铺开。
3. 前端那半条链:从用户操作到 HTTP 请求
“沿用户操作查调用链”这句话里最难的一个词是“沿”。要让 Agent 具备这种追踪能力,前端代码的阅读必须从“用户做了什么”这一具体动作开始。
3.1 常见的事件绑定形态与对应检索法
浏览器端的用户操作,落到代码层面就是事件。我整理了三种最常见的绑定方式,Agent 阅读时需要根据特征做不同的检索:
| 绑定方式 | 代码特征 | 检索要点 |
|---|---|---|
| 模板指令(Vue) | @click="handleSubmit"、v-on:keyup.enter="search" | 直接在标签上查@事件名=,再跳转到对应方法定义 |
| 事件监听(React) | onClick={handleSubmit}、addEventListener('click', handler) | 查组件里的处理函数属性,或addEventListener回调 |
| 原生绑定 | document.getElementById('btn').onclick = ... | 查 DOM 操作代码中的onclick、onchange赋值 |
对于前后端分离项目,Vue 和 React 占绝大多数。前端 Agent 分析时,我一般让它执行一个三步检索:
- 找到目标页面文件(比如
ProductList.vue/ProductList.jsx)。 - 在文件内定位用户操作的绑定位置,得到处理函数名。
- 跳到处理函数定义处,看它内部做了什么——尤其是是否调用了
api模块导入的方法。
这一步的关键经验是:不要让 Agent 直接从页面文件“跳”到 api 模块,而是强制它先读处理函数体内的全部代码。因为处理函数里通常还有参数组装、 loading 状态处理、错误捕获、甚至多个分支调用了不同接口。跳过了这个函数体,就等于跳过了整条链路上最关键的一环。
3.2 确认 HTTP 层的真实请求目标
拿到 api 模块的方法之后,还不能急着下结论。因为api/product.js里的方法通常只是封装了一层:
export function getProductList(params) { return request({ url: '/product/list', method: 'get', params }) }这里的/product/list是一个相对路径。实际请求的完整 URL 取决于request.js里的baseURL。有些项目还会通过环境变量区分开发/生产接口前缀,比如VITE_API_BASE_URL。
所以我在给 Agent 的阅读指令里,会额外注明:
在分析 HTTP 请求时,必须找到 request 封装文件,说明 baseURL、请求拦截器、响应拦截器都对这条请求做了什么处理,再给出当前环境下的完整请求 URL。
别小看这一步。我曾经碰到过一个案例:前端代码里明明请求的/product/list,后端路由也是/product/list,但就是 404。结果排查下来发现响应拦截器里对 401 做了静默刷新 token 的逻辑,刷新失败后把原本的请求跳到了登录接口,Agent 如果不读拦截器,根本解释不了这个行为。
3.3 前端侧容易被忽略的“伪调用”
有三种情况经常让 Agent 在前端分析时得出错误结论,你在实际使用时要特别注意:
- 防抖/节流函数:
@click="debouncedSubmit"绑定的实际是一个包装函数,真正的提交函数在debounce内部。Agent 如果只看绑定处,会以为用户每次点击都会立刻触发请求。 - 动态路由参数:比如
router.push({ path: '/detail/' + id }),Agent 需要去路由配置里查:id参数名,才知道后端 URL 里的最终形态。 - 组件的二次封装:页面模板里用的是
<BaseTable @operation="handleOp" />,但BaseTable内部可能对它自己的按钮做了事件转发。分析时必须往子组件里再走一层。
针对这些情况,我的处理办法是给 Agent 增补一条规则:当事件绑定函数与业务方法名不一致,或者出现$emit、nextTick、debounce、throttle这类包装时,不要直接跳结论,而是继续追问“这个函数从哪来、被谁持有、最终执行了什么”。
4. 后端另外半条链:从接口入口到数据落点
前端追踪到完整请求 URL 之后,调用链就进入到了后端。后端这半条链的结构相对前端更规整,但层数更深,也更容易让 Agent 在抽象接口和具体实现之间迷路。
4.1 Controller 层的快速定位与参数解析
后端的入口很好找:在 Controller 类上根据前端请求的 method 和 URL 匹配@GetMapping、@PostMapping、@RequestMapping等注解。
这里容易出问题的不是定位,而是请求参数与入参对象的映射。一个典型的 POST 接口:
@PostMapping("/product/list") public Result<List<ProductVO>> list(@RequestBody ProductQuery query) { return productService.listByCondition(query); }Agent 需要明白几件事:@RequestBody会把请求体 JSON 反序列化成ProductQuery对象,query的属性名和前端传的参数字段必须一致;如果不一致,后端接收到的有可能是 null 或者直接 400。所以在后端入口这一步,我给 Agent 的任务清单是:
- 找到匹配的 Controller 方法。
- 列出方法的注解(URL、HTTP Method、参数绑定方式)。
- 如果参数是对象类型,读取该对象的字段定义,并与前端的请求参数做逐一对照。
4.2 Service 层才是逻辑分叉的重灾区
很多 Agent 追踪调用链时有个偷懒的倾向:从 Controller 一层直接跳到 Mapper,把中间的 Service 层略过。这在大促活动这类复杂业务场景里是致命的,因为 Service 层通常包含:
- 条件分支:比如当
query.getType() == 1时走 A 查询,否则走 B 查询。 - 事务控制:
@Transactional范围里可能调用了多个 Mapper 方法。 - 外部服务调用:比如查完数据后调用了 Redis 缓存、消息队列或者第三方接口。
我遇到过的一个典型案例是:用户点击“导出报表”,前端调/report/export,后端 Controller 的export方法看起来就是调用了reportService.export()。但export()里其实分了两大步:先查数据库列表,然后异步调文件生成服务。如果 Agent 只看 Controller 和 Mapper,会以为导出结果直接来自数据库查询,完全解释不了“为什么接口返回了成功、文件却迟迟没生成”这种现象。
所以在后端链路这一段,我的 Agent 指令会强制要求展开 Service 层的所有分支而非只读取主路径。展开方式:
在 Service 方法中,如果存在 if/else、switch、forEach、try-catch 等分支结构,请逐条列出每个分支下调用的方法或 Mapper 操作,并标注该分支由什么条件触发。这样一来,调用链就从单一路径变成了一棵条件树,后续排查问题时会非常有用——用户的操作参数差一个值,走的可能就是完全不同的代码分支。
4.3 Mapper 层与 SQL 的对应关系
最后一段是数据访问层。Java 项目里 MyBatis 最常见的两种形态:注解 SQL 和 XML SQL。我遇到过 Agent 只看了 Mapper 接口,没看 XML 文件,导致误判表名字段的情况。比如接口上只写着:
List<Product> selectByCondition(@Param("query") ProductQuery query);真正的 SQL 在resources/mapper/ProductMapper.xml里。所以我给 Agent 的规则是:凡是 Mapper 接口方法没有直接写 SQL 注解的,默认需要去同目录或 resources 下查找同名 XML 文件,并在分析结果中附上完整 SQL 及涉及的数据库表。
数据层还有一个容易忽略的点:多表关联。比如查询商品列表时,SQL 里LEFT JOIN category把分类名称也查出来了。Agent 如果不关注到这一点,会以为分类名是独立请求获取的,从而在“调用链”里多画一条不存在的线。这也是为什么我要求 Agent 把 SQL 里的 JOIN 表都要列出来,与前端展示字段做交叉比对。
5. 沿用户操作追溯调用链的完整流程与 Prompt 设计
到这里,前后端两半条链的阅读能力都有了。真正要解决“沿用户操作查调用链”,还需要把它们拼成一个连贯的追踪流程。我在实际操作中总结了一套“追踪七步法”,每次排查问题都按这个顺序让 Agent 执行,稳定性和准确率比自由发挥高很多。
5.1 追踪七步法
| 步骤 | 动作 | 产物 |
|---|---|---|
| 1 | 复述用户操作路径 | 操作步骤清单,如“打开列表页→点击筛选→选择价格区间→点击查询” |
| 2 | 定位前端事件绑定 | 页面文件路径 + 事件绑定代码 |
| 3 | 展开事件处理函数 | 函数体代码 + 参数组装逻辑 |
| 4 | 定位 HTTP 请求定义 | api 模块方法 + request 封装处理 |
| 5 | 匹配后端 Controller | URL、Method、参数绑定方式 |
| 6 | 展开 Service 业务逻辑 | 分支条件树 + 事务边界 + 外部调用 |
| 7 | 落到 Mapper/SQL | SQL 语句、涉及表、返回字段 |
这套流程最妙的地方是每一步的“产物”都会成为下一步的“输入”。Agent 不会跳跃式推理,每一步都有前一步的代码片段作为依据,大大降低了幻觉概率。
5.2 一个真实场景的全链路追踪演示
我用一个实际的“商品筛选”场景来说明整个追踪过程。用户操作:打开商品列表页,在筛选区选择了分类“手机”和价格区间“3000-5000”,点击查询按钮,页面无数据返回。
按照七步法,Agent 的分析输出是这样的:
步骤 1 产物:用户操作 = 点击“查询”,筛选条件 = { categoryId: 101, priceMin: 3000, priceMax: 5000 }。
步骤 2 产物:src/views/ProductList.vue第 88 行,<button @click="handleSearch">。
步骤 3 产物:
const handleSearch = () => { loading.value = true queryParams.value = { categoryId: filterForm.categoryId, priceMin: filterForm.priceMin, priceMax: filterForm.priceMax } fetchList(queryParams.value) }步骤 4 产物:src/api/product.js中fetchList调用request({ url: '/product/page', method: 'post', data }),request 封装中 baseURL 为/api,响应拦截器会在 200 时直接返回res.data.data。
步骤 5 产物:后端ProductController.page(@RequestBody ProductPageQuery query),匹配POST /api/product/page。
步骤 6 产物:ProductServiceImpl.page()里有个条件分支:当query.categoryId != null时,extraSql +=AND category_id = #{categoryId};当priceMin不为空时,extraSql +=AND price >= #{priceMin}。
步骤 7 产物:最终 SQL 里WHERE status = 1 AND category_id = 101 AND price >= 3000 AND price <= 5000,关联表product、category。
如果页面无数据,此时就可以直接对比 SQL 结果:是筛选条件本身没有匹配数据,还是前端参数没传给后端,还是 SQL 拼接漏了条件。整条链路一目了然。
5.3 调用链追踪专用 Prompt 模板
下面是我这几个月反复修改后稳定下来的 Prompt 模板,它结合了前四节的单侧分析能力与追踪流程。
请作为一个全栈代码分析 Agent,按照下面七步对用户操作进行调用链追踪: 1. 复述用户操作路径 2. 定位前端事件绑定位置(先查页面模板中的事件指令) 3. 展开事件处理函数,梳理参数组装逻辑 4. 定位 HTTP 请求定义,并说明 request 封装层(baseURL、拦截器)对请求的影响 5. 匹配后端 Controller 方法,说明 URL、HTTP 方法、入参绑定方式 6. 展开 Service 层逻辑,列出所有条件分支和调用的下层方法 7. 追踪到 Mapper/SQL,说明查询涉及的表与字段 规则: - 每一步都必须给出具体文件路径和代码行号。 - 不得跳过任何一步直接给出最终结论。 - 遇到分支结构时,按条件列出不同路径。 - 如果某一步无法在代码中找到对应实现,必须明确标注“未找到”,不得猜测。用好这个模板的关键是不要在一个 Prompt 里把七步全做完。我的经验是分两轮:第一轮让 Agent 走步骤 1-3,只输出前端结果;第二轮把前端结果作为上下文,再让 Agent 走步骤 4-7。这么做的原因是:如果七步强制性全部连贯执行,Agent 很容易在很长的分析过程中“迷失重点”,早早就忘记了第一步用户操作的具体参数。分轮执行后,Agent 每一步都聚焦在当前任务上,输出质量明显提升。
6. 这套方案的实际效果、踩坑记录与调优心得
方法讲完了,说说实测效果。我在一个模拟真实电商后台的前后端分离项目(Vue3 + Spring Boot + MyBatis)上跑了这套方案,模拟了 20 条不同难度的调用链追踪请求,包含正常链路、参数错误、分支选错、请求被拦截器等场景。
6.1 数据对比:追踪准确率从 40% 提到了 85%
最开始我用的“一次性全部代码丢给 Agent”的方式,20 条场景只对了 8 条,准确率 40%。原因五花八门:上下文太长导致 Agent 在开头就忘了用户操作参数;中间跳过了 Service 层导致分支判断全错;还有两次是 Agent 编造了不存在的文件路径。
改成“仓库地图 + 前端追踪 + 后端追踪 + 七步法分轮执行”的组合后,20 条场景对了 17 条,准确率 85%。剩下 3 条错误出在两个地方:一条是动态生成 SQL 的 XML 里包含<script>标签动态条件,Agent 读 XML 时没有识别出来;另一条是前端组件里使用了递归组件,事件绑定的实际来源在父组件,Agent 没追上去;还有一条是代码里同时存在新旧两套接口,Agent 匹配错了 Controller。
6.2 我总结的四个关键调优点
如果你要复刻这套方案,有四个细节我强烈建议你在搭建时就考虑进去。
第一,代码片段必须有“来源标注”。只是把代码贴给 Agent 远远不够。我在每次注入代码片段时,都会在片段前面加上file: src/views/ProductList.vue这样的文件路径标注,并且告诉 Agent“这一步只针对该文件的第 80-100 行”。这能显著降低 Agent 引用错文件的概率。
第二,大文件要按函数切块,而不是按文件切块。有些 Vue 单文件组件能超过 800 行,一个页面里包含五六个业务函数。如果直接整个文件怼进去,Agent 在处理其中一个函数时,注意力很容易被同文件里的其他函数干扰。我现在用的策略是:先用 grep 或 Agent 的代码定位能力找到目标函数起始行号,然后只截取该函数体及与其直接相关的模板片段。
第三,后端链路必须检查事务注解。@Transactional这类声明式事务的边界,直接决定了多个 Mapper 操作是否在一个连接里执行。Agent 在分析 Service 层时,应该明确读一下方法或类上的事务注解。否则它会把“先 insert 再 update”两个操作当成独立的,一旦第二个失败回滚,就解释不了为什么数据库里没有第一条数据。
第四,正则表达式和通配符匹配要提前排查。Spring MVC 的路径匹配在有些项目里不是纯粹的字符串相等,比如@RequestMapping("/product/**")这类通配写法。如果 Agent 只会精确匹配 URL,碰到通配就会漏掉真正的 Controller 入口。我在注入后端代码地图时,会把所有 Controller 类的类级@RequestMapping单独列一张表,让 Agent 在做 URL 匹配前先看过一遍这张表。
6.3 最后一个让调用链真正可解释的小技巧
追踪的结果如果只是告诉用户“问题出在 X 文件”,价值就折半了。我让 Agent 输出的最终结论必须是一条带路径的链式结构,像这样:
用户点击查询按钮 → ProductList.vue:88 @click="handleSearch" → ProductList.vue:102 handleSearch 组装 queryParams → src/api/product.js:31 fetchList → request({ url: '/product/page' }) → request 封装层:baseURL 为 /api,响应拦截器透传 data → ProductController.page @PostMapping("/product/page") → ProductServiceImpl.page() 第 45 行:条件拼装 categoryId → ProductMapper.selectPage(XML:ProductMapper.xml 第 12 行) → SQL: SELECT ... FROM product WHERE status=1 AND category_id=101 ...这条链式输出有两个作用。一是用户拿到路径后可以快速验证:哪一步的参数和实际行为对不上,问题点自然浮现。二是如果 Agent 推理错了,这条链是透明的,你一眼就能看出它是在哪一环“掉链子”的,而不是拿到一个无法核实的黑盒结论。
我自己在用这套方案做 Agent 日常巡检时,最大的体感是:排查问题从“问 Agent 问题”变成了“让 Agent 走链路”。后者不需要它有多强的临时推理能力,只需要它严谨地按图索骥,把每一步的真实代码摊开,调用链自然就出来了。你如果正在给自己的 Agent 补“读代码”的能力,我建议先别急着上大模型推理框架,把仓库地图、分端阅读、七步追踪这三层基础打好,后面的路会顺很多。