目录

如何利用AI快速理解复杂项目

本文转载自知乎Up主笙囧同学的如何利用cursor快速理解复杂代码工程? - 知乎

去年我试着读 vLLM 的源码。

起因是我在用 vLLM 做推理服务的时候遇到了一个性能问题,想看看它的调度器到底是怎么工作的。我clone了仓库,打开文件树,看到了大概几百个Python 文件、一堆 C++ 扩展、还有 CUDA kernel。

我盯着屏幕看了十分钟,关掉了编辑器,去泡了杯咖啡。

那种感觉你一定体会过——就像你站在一座陌生城市的市中心,四周全是高楼,每栋楼里都有几十层,你知道你要找的东西就在某一栋楼的某一层的某个房间里,但你连东南西北都分不清。

后来我开始用 Cursor 来辅助读源码。摸索了一段时间之后,确实找到了一些有效的方法。

不是那种”让AI帮你读完所有代码然后给你一份总结”的方法——那种方法我试过,没用,AI给你的总结跟你自己看README得到的信息差不多。

我要说的方法更像是把Cursor当成一个对这个代码库了如指掌的向导,你问它路,它带你走,但路是你自己走的,风景是你自己看的。

为什么直接让AI”总结整个项目”没用

这是很多人的第一反应。打开Cursor,把整个项目索引了,然后问:”帮我介绍一下这个项目的整体架构。”

AI会给你一段话。听起来头头是道。什么”这个项目采用了分层架构,核心模块包括 XXX、YYY、ZZZ,它们之间通过 XXX 方式通信”。

你读完觉得”嗯嗯好有道理”。

然后呢?

然后你发现自己并没有真正理解任何东西。你知道了几个模块的名字,但你不知道数据是怎么从入口流到出口的。你不知道某个关键的设计决策背后的原 因。你不知道当一个请求进来的时候,代码到底在做什么。

因为理解不是信息的传递,是认知结构的构建。 AI把架构信息告诉你,相当于给了你一张地图。但地图不是领土。你没有亲自走过那些路,地图上的每一个 标记对你来说都只是一个符号,而不是一段体验。

这就是为什么”让AI帮你读代码”作为一个整体策略是行不通的。

但”在你自己读代码的过程中,让AI帮你解决具体的障碍”——这个策略是非常有效的。

两者的区别是什么?是主动权在谁手里。前者你是被动的信息接收者,后者你是主动的探索者。AI的角色从”替你读”变成了”帮你读”。

我的方法:从一个问题开始

读任何一个复杂项目,我现在都不会从”了解整体架构”开始。

我会从一个具体的问题开始。

读 vLLM 的时候,我的问题是:”一个推理请求从进入系统到返回结果,经历了哪些步骤?”

读 FastAPI 的时候,我的问题是:”当我写了一个 @app.get('/users') ,框架是怎么把这个装饰器变成一个能处理HTTP请求的东西的?”

读 Redis 的时候,我的问题是:”当我执行 SET key value 的时候,这个命令从被接收到数据被写入内存,中间经过了什么?”

一个好的问题就是你的故事线。 你不需要理解整个项目——你需要沿着一个问题,把它涉及到的代码路径从头到尾走一遍。

走完一条路径之后,你对项目的理解就不再是零了。你有了一个”锚点”。然后你可以从这个锚点出发,问第二个问题,走第二条路径。两条路径之间一定 会有交叉点——那些交叉点就是项目的核心模块。

几条路径走完,项目的骨架自然就在你脑子里了

这个方法不需要AI也能做。但有Cursor的帮助,效率会高非常多。

第一步:找到入口

每一条故事线都需要一个入口。对于大部分项目来说,入口就是用户最先接触到的那个界面

如果是一个 Web 框架,入口就是你创建 app、注册路由的地方。

如果是一个推理引擎,入口就是你调用 model.generate() 的地方。

如果是一个数据库,入口就是它接收客户端连接和命令的地方。

怎么用 Cursor 找入口? 直接问它:

1
2
3
这个项目的用户入口在哪?当一个用户发起一个推理请求时,
代码的执行从哪个文件的哪个函数开始?请给我具体的文件路径
和函数名,不需要解释细节。

注意最后那句”不需要解释细节”。这很重要。

你在这个阶段只需要一个起点,不需要AI给你长篇大论。如果你让AI一次性解释太多,你会淹没在信息里,反而找不到方向

Cursor 会告诉你类似这样的信息:”入口在 vllm/entrypoints/llm.pyLLM.generate() 方法”。

好。打开那个文件。开始读。

第二步:沿着调用链往下走

打开入口文件之后,你开始读代码。很快你会遇到第一个障碍——你看到了一个函数调用,但你不知道那个函数做了什么、在哪个文件里。

这是Cursor最有价值的使用场景之一

选中那个函数调用,问Cursor:

1
2
这个函数做了什么?用一两句话概括它的核心职责,
然后告诉我它在哪个文件里定义的。

注意:一两句话。 你不需要AI给你逐行解释那个函数的实现。你现在只需要知道”它大概做了什么”以及”它在哪”。

这就像你在陌生城市里走路,遇到一个路口,你不需要知道每条岔路通向哪里的完整地图。你只需要知道”这条路大概通往火车站”和”那条路大概通往商 业区”,然后选择跟你目标相关的那条走下去。

你会沿着调用链一路走下去。入口函数调用了 A,A 调用了 B,B 调用了 C……

你不需要走完所有的分支。你只需要走跟你的问题相关的那条主线

中间遇到不影响主线理解的分支——比如日志记录、参数校验、缓存检查——直接跳过。怎么判断一个分支是不是主线?问 Cursor:

1
2
在这个函数里,哪一行是实际执行核心逻辑的?
其他部分可以先忽略吗?

Cursor会告诉你:”第47行的 self._run_engine() 是核心调用,上面的都是参数处理和校验,可以先跳过。”

然后你跳到第47行,继续往下走。

第三步:遇到不懂的技术栈时

这是你提到的第二个痛点——项目里用了你不熟悉的语言或技术。

比如你在读一个 Python 项目,突然发现核心的计算部分是用 C++ 写的 CUDA 扩展。你不懂 CUDA。

传统的做法是:先去学 CUDA,学个几天几周,然后回来接着读。

有了 Cursor 你可以换一种做法:不学 CUDA 本身,只理解那段 CUDA 代码在做什么

选中那段 C++/CUDA 代码,问 Cursor:

1
2
3
4
我不熟悉CUDA编程。请用我能理解的方式解释这段代码
在做什么——不需要解释CUDA的语法细节,只告诉我
它的输入是什么、输出是什么、中间做了什么计算。
用Python的思维方式类比。

最后那句”用 Python 的思维方式类比”很关键。它告诉 Cursor 用你已有的知识框架来解释新东西。

Cursor 可能会说:”这段 CUDA kernel 本质上在做的事情相当于 Python 里的:对一个大矩阵的每一行做 softmax,然后跟另一个矩阵做矩阵乘法。只不过它把这个计算拆分到了GPU的几千个线程上并行执行。”

你不需要理解它是怎么拆分的、线程是怎么同步的——那些是CUDA的实现细节。你需要理解的是它在计算层面做了什么。 有了这个理解,你就能把这个 CUDA函数当成一个”黑盒”,知道它的输入输出,然后继续沿着主线往下走。

同样的方法适用于你遇到的任何不熟悉的技术栈:

  • 不懂 Rust?让 Cursor 用你懂的语言类比解释

  • 不懂某个框架的特定 API?让 Cursor 解释它的作用而不是用法

  • 不懂某个设计模式?让 Cursor 解释它在这个项目里解决了什么问题

关键原则是:你的目标不是学会那个技术栈,而是理解代码的逻辑流。两者需要的知识量差了一个数量级

第四步:画地图

走完一条主线之后,你需要把你走过的路记下来。

我的做法是在一个单独的markdown文件里,边读边记。格式很简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## 一个推理请求的完整路径

1. 入口:LLM.generate()(vllm/entrypoints/llm.py)

 - 接收用户的prompt,封装成SequenceGroup

2. 调度:Scheduler.schedule()(vllm/core/scheduler.py)

 - 决定哪些请求可以在这一步被处理
 - 核心逻辑:根据当前GPU显存和KV Cache的空间来排队

3. 执行:ModelRunner.execute_model()(vllm/worker/model_runner.py)

 - 把一批请求打包,送进模型做一次forward
 - 这里会调用CUDA kernel做实际计算

4. 采样:Sampler.forward()(vllm/model_executor/layers/sampler.py)

 - 根据模型输出的logits,采样出下一个token

5. 结果返回:把生成的token拼回去,返回给用户

## 我的理解

vLLM的核心创新在第2步——调度器的PagedAttention机制

让KV Cache可以像操作系统的虚拟内存一样按需分配,

不需要预留连续的大块显存……

这份笔记不是给别人看的,是给你自己看的。 所以不需要写得很正式,用你自己能理解的语言就好。

你可能觉得”我直接记在脑子里不就行了”。不行。复杂项目的调用链太长了,你走到第五层的时候大概率已经忘了第二层的细节。写下来才能在脑子里维 持一张完整的地图。

而且这份笔记还有一个用处——当你后续探索第二条故事线的时候,你可以把新的路径叠加到同一份地图上,看到不同路径之间的交叉点。那些交叉点往往 就是项目最核心的模块。

第五步:在关键节点上深入

走完主线之后,你已经有了一个粗粒度的理解。接下来你可以选择在某些你特别感兴趣的节点上深入。

比如我走完vLLM的主线之后,对调度器特别感兴趣——因为它是vLLM性能优势的核心。

这时候我会打开调度器的代码,让Cursor帮我做更细粒度的解读:

1
2
3
请帮我梳理Scheduler.schedule()这个方法的内部逻辑。
按执行顺序列出它做了哪几件事,每件事用一句话概括。
对于涉及PagedAttention的部分请详细解释。

注意这次我要求”详细解释”了——因为这是我选择深入的节点,值得花时间理解细节。

在主线探索阶段,你的提问策略应该是”概括+定位”——告诉我大概做了什么、在哪里。

在深入阶段,你的提问策略变成”细节+原因”——具体是怎么实现的、为什么这么设计

这个节奏很重要。如果你从一开始就事事追问细节,你会迷失在细节里走不动。如果你全程只停留在概括层面,你最终得到的理解是浅薄的。

先粗后细,先主线后分支。 像画画一样——先画骨架,再填细节。

一些容易踩的坑

坑一:一次问太多。

不要发这种消息:

1
2
3
帮我解释一下这个项目的整体架构、核心模块的职责、

主要的设计模式、数据流向、以及关键算法的实现原理。

这种问法得到的回答一定是又长又泛的废话。不是 Cursor 不行,是你的问题太大了,任何人面对这种问题都只能给你一个泛泛的回答。

好的提问是窄的、具体的、有明确边界的

  • “这个函数的第三个参数是干什么用的?”

  • “这个类为什么要继承那个基类而不是直接实现?”

  • “数据从这个函数出来之后,下一步去了哪里?”

每个问题只解决一个困惑。解决完了再问下一个。你的理解就是这样一个困惑一个困惑地拼起来的

坑二:完全信任Cursor的回答。

Cursor 有时候会瞎说。尤其是当你问的问题涉及到跨文件的复杂逻辑时,它可能会给你一个听起来很合理但实际上是错的解释。

怎么办?交叉验证

Cursor 告诉你”这个函数会调用XXX”——你自己跳过去看一眼,确认它确实调用了。

Cursor 告诉你”这个变量在YYY文件里被初始化”——你自己搜一下那个变量名,确认它说的位置是对的。

不需要每一句话都验证,但关键节点一定要自己确认。 尤其是那些会影响你对整体架构理解的判断——比如”模块A和模块B是通过消息队列通信的”—— 这种结论你最好自己看到代码里的证据。

养成这个习惯之后,你会发现大部分时候 Cursor 是对的,但偶尔它确实会犯错。那些”偶尔”的错误如果你没有验证就接受了,可能会导致你对整个项目的理解建立在一个错误的基础上。

坑三:试图一次读完。

复杂的开源项目不是一个下午能读完的。vLLM 的代码我前前后后读了大概两周,中间穿插着其他工作。

不要给自己压力说”今天一定要把这个项目读完”。你今天读明白了一条主线,就是实实在在的进展。明天再读一条。

每次读完记得写笔记。 你下次再回来的时候,看一眼笔记就能快速恢复上下文,不用从头开始。

坑四:只读不跑。

这可能是最大的坑。

光看代码你能理解的东西是有限的。把项目跑起来,加几个断点或者打几行日志,亲眼看到数据怎么流动的——这个过程给你的理解深度是光看代码的好几 倍。

你可以让 Cursor 帮你做这件事:

1
2
我想在本地把这个项目跑起来,走通一个最简单的例子。
请告诉我最少需要哪些步骤。如果有复杂的外部依赖可以mock掉的,告诉我怎么mock。

不需要搭建完整的开发环境。只要能跑通一个最小的例子,能让你在关键位置加断点看到数据,就够了。

跑起来之后,在你走过的主线上的关键函数入口加个断点或者日志。然后触发一个请求,看实际的执行路径是不是跟你读代码时理解的一致。

经常会有惊喜。 你以为数据走的是路径A,实际上走的是路径B——因为有一个条件分支你读代码时忽略了。这种”预期和现实的偏差”本身就是最好的学习 机会。

针对不同类型项目的策略差异

不同类型的项目,”故事线”的选择方式不一样。简单说几个常见的:

Web框架类(FastAPI、Express、Gin等):

最好的故事线是”一个HTTP请求从进入到返回的完整生命周期”。从监听端口→接收请求→路由匹配→中间件执行→handler调用→响应返回。这条线走 完,你就理解了框架的骨架。

数据库/存储引擎类(Redis、LevelDB、SQLite等):

最好的故事线是”一个写操作从接收到持久化的完整路径”。从命令解析→数据结构操作→内存写入→持久化到磁盘。然后再走一条”读操作”的路径。两 条路径交叉的地方就是核心的数据结构和存储引擎。

AI推理/训练框架类(vLLM、DeepSpeed、PyTorch等):

最好的故事线是”一个前向传播从输入到输出的完整路径”。从数据预处理→模型加载→计算执行→结果后处理。特别注意计算是在哪里从Python进入 C++/CUDA的——那个边界通常是理解性能优化的关键。

编译器/解释器类(CPython、V8、GCC等):

最好的故事线是”一段源代码从文本到被执行的完整路径”。从词法分析→语法分析→AST→中间表示→优化→代码生成/执行。每一步的输入和输出是什 么,格式是什么。

不管哪种类型的项目,核心思路都是一样的:找到一条从入口到出口的主线,沿着它走,遇到障碍时让 Cursor 帮你清除障碍。

一个完整的实战流程示例

最后把整个流程用一个具体的例子串一遍。假设你想理解 FastAPI 是怎么工作的。

起手。 把 FastAPI 的仓库 clone 下来,用 Cursor 打开。

1
2
3
4
5
6
7
8
找入口。 问 Cursor:
当用户写了这样的代码:
app = FastAPI()
@app.get("/hello")
def hello():
 return {"msg": "hello"}
请告诉我 @app.get 这个装饰器的代码在哪个文件的
哪个函数里定义的。只给我位置,不用解释。

Cursor 告诉你在 fastapi/applications.py 里。打开它。

沿主线走。 你看到 app.get 实际上调用了 self.router.add_api_route() 。你不知道这个函数做了什么。选中它,问 Cursor:

1
这个函数做了什么?一句话概括。它在哪定义的?

Cursor告诉你它在 fastapi/routing.py 里,作用是”把你的函数包装成一个 APIRoute 对象,注册到路由表里”。

你跳到 routing.py ,看到了 APIRoute 这个类。你好奇路由匹配是怎么做的——当一个请求进来时,框架怎么知道该调用哪个 handler?

1
2
当一个HTTP请求到达时,FastAPI是怎么匹配到
对应的路由handler的?从哪个函数开始?

Cursor 告诉你 FastAPI 底层用的是 Starlette 的路由系统。实际的匹配逻辑在 Starlette 的 Router.__call__ 里。

遇到技术栈边界。 你可能不熟悉 Starlette。没关系:

1
2
3
我不熟悉Starlette。FastAPI和Starlette是什么关系?
我只需要知道它们之间的边界在哪——FastAPI自己做了什么,
哪些事情是委托给Starlette做的?

Cursor 会解释清楚边界。你决定要不要跨过这个边界深入到 Starlette 里去。也许现在不需要——你只需要知道”路由匹配这件事 Starlette 替你做了”就行, 你更感兴趣的是 FastAPI 自己加了什么东西。

记笔记。 走完这条主线之后你写下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
## 一个请求的处理流程

1. 启动时:@app.get 装饰器 → router.add_api_route() 
   → 创建 APIRoute 对象 → 注册到路由表

2. 请求到达时:Starlette的路由系统做URL匹配
   → 找到对应的 APIRoute

3. APIRoute 调用你的handler函数之前:

 - 依赖注入(Depends )被解析和执行
 - 请求参数被校验(用Pydantic)

4. 执行handler,返回结果

5. 结果被序列化成JSON返回

## 发现

FastAPI的核心价值不在路由(那是Starlette做的),
而在第3步——依赖注入和参数校验。
这才是它跟其他框架的本质区别。

选择深入点。 你对依赖注入特别好奇。开始第二轮探索,专门走Depends的解析流程。

循环。 每一轮探索都让你的地图更完整、更细致。三四轮之后,你对FastAPI的理解就已经远超大部分使用者了。


回到最开始的问题。

读复杂代码工程最大的难点从来不是”代码太多看不完”。代码多不可怕,可怕的是你不知道该看哪里。

Cursor 最大的价值不是替你读代码,而是在你迷路的时候告诉你该往哪走

但走路这件事,得你自己来

每一行你亲自读过的代码、每一个你亲自想明白的逻辑、每一次你的预期和实际运行结果对不上然后你搞清楚了为什么——这些时刻构成的理解,是任何AI 总结都给不了你的。

Cursor 是一个极好的向导。但向导的价值是带你去你自己想去的地方,不是替你走完全程然后给你讲一遍沿途的风景。

风景得自己看。看过的才是你的