过去一段时间,我一直在做一件事:从一个有多年经验的前端工程师,往 AI Agent 和全栈方向走。

这个过程中,我做了一个叫 SciFlow 的科研文献 RAG 项目,也久违地重新当了一次初学者:Python 不熟,FastAPI 不熟,PostgreSQL、向量检索和 RAG 也都要边做边学。

做了一段时间后,我对“怎么学一门新技术”这件事有了些不同的看法。以前我习惯先找课程、看目录,觉得至少要把基础知识过一遍,才有资格开始做项目。现在我更愿意反过来:先找一个确实想做成的东西,让问题把学习路径逼出来。

不是因为基础不重要,而是有 AI 以后,代码和资料都太容易获得了。真正稀缺的已经不是“有没有一段代码”,而是:我知不知道现在要解决什么,能不能判断 AI 写得对不对,出了问题该往哪一层查,以及什么时候应该停下来。

能跑,不等于学会

这是用 AI 学技术时最容易踩的坑。

以前不会 FastAPI,至少要自己查文档,弄清楚路由怎么写、参数怎么传、数据库怎么接。现在只要说一句“帮我写一个 FastAPI CRUD”,几十秒后就能拿到一套结构完整、甚至可以直接运行的代码。

这种顺利很容易制造错觉:FastAPI 好像也没什么难的,我已经会了。

但真正开始维护项目后,我发现自己会卡在很多很小的地方:

db: Session = Depends(get_db)

为什么这里需要 Depends

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

为什么是 yield,而不是 return?请求结束后谁负责关闭 Session?如果把任务放进后台,原来的 Session 还能不能继续用?

还有 APIRouter、模块导入别名、Swagger 分组,甚至 sed -n '96,125p' app/main.py 这样的命令。它们和“实现一个 RAG 系统”比起来都很基础,却决定了我到底是在使用 AI 生成的软件,还是在学习软件工程。

现在我会用一个更实际的标准判断自己是否掌握了某段代码:能不能解释它,能不能改它,出错时能不能定位它,换一种约束后能不能重新做取舍。只要其中有一项不行,就还不能算真正会了。

不要学 FastAPI,去做一个必须用到 FastAPI 的东西

我没有给自己排过这样的课程表:第一周 FastAPI,第二周 PostgreSQL,第三周 RAG,第四周 Agent。SciFlow 的技术栈基本是被需求一点点推出来的。

最开始,我只想要一个 documents 接口,于是用到了 FastAPI。

数据放在 Python 数组里不可能长久保存,于是接入 PostgreSQL。SQL 越写越多,业务对象和表结构之间的转换开始变得麻烦,于是引入 SQLAlchemy。文档需要上传和检索,又接着碰到 PDF 解析、文本切片、Embedding 和 pgvector。

到了这里,系统已经可以完成一条最简单的 RAG 链路:

问题
→ 向量检索
→ 拼接上下文
→ 交给 LLM
→ 生成答案

如果按照教程的验收方式,“能返回一段像样的答案”可能就算完成了。但真实文档很快暴露了问题:明明包含答案的句子,在 Top-30 里都找不到。

这才让我开始认真研究 Chunking。固定长度切片会把核心句子和前后不相干的内容揉在一起,于是我改成 sentence-aware Chunking。改完以后,代表性证据从 Top-30 未命中到了 Top-1。

新的问题马上又来了:小 Chunk 更容易检索,但直接交给模型时上下文不完整。于是继续做 Parent–Child Retrieval:Child 用来精确匹配,命中后再取回对应的 Parent 作为回答上下文。

中文问题检索英文论文不稳定,就加入 Query Rewrite。一个问题改写成多条查询后,各自的结果需要合并,于是用到 RRF。RRF 会把多条查询都碰巧命中的宽泛内容排得很高,又需要 Reranker 判断哪些候选真的能回答问题。

后来我还发现,Reranker 并不是万能补丁。真正的证据如果没有进入候选池,再聪明的重排序也救不回来,所以候选池本身也要保留足够的多样性。

最后,这条链路变成了:

Question
→ Query Rewrite
→ Child Vector Retrieval
→ RRF
→ Diversified Candidate Pool
→ LLM Reranker
→ Parent Context
→ LLM Answer

这个顺序对我很重要。我不是先学完这些名词,再回来画架构图;而是每次遇到一个具体失败,才去找刚好能解决它的概念。

如果只看定义,RRF 可能只是一个公式:

score = 1 / (k + rank)

但在刚遇到“多路检索结果怎么合并”时去学它,我会同时记住它解决了什么、为什么不直接比较向量距离、它的代价是什么,以及为什么后来还要加 Reranker。知识有了来路,也就更不容易忘。

把一次学习缩成一个小闭环

我现在很少给自己定“今天学习向量数据库”这样的目标。它太宽,也没有明确的结束条件。更有效的目标通常像这样:

新上传的 PDF 要自动建立 Child Index,并且处理过程不能阻塞上传接口。

围绕这个目标,我会走一遍很小的闭环:

  1. 先说清楚这一轮要解决的问题,不顺手扩展范围。
  2. 定义能观察到的验收结果,例如 Parent、Child 的数量,接口状态码,或某条证据的检索排名。
  3. 在写代码前画清输入、输出和数据流,至少知道改动会落在哪一层。
  4. 让 AI 处理样板代码和重复劳动,但追问所有自己解释不了的部分。
  5. 用真实请求、数据库状态和评测报告验证,而不是只看代码是否“像对的”。
  6. 根据结果决定下一步。做完后,再把经历归纳成依赖注入、事务边界、任务生命周期等概念。

这其实是一个“先经历,再命名”的过程。没有遇到过 Session 被提前关闭的问题时,“请求生命周期”只是一段抽象定义;亲手排查过一次以后,这个概念就有了具体的形状。

一次只增加一层复杂度

AI 很擅长给完整方案,也很容易制造信息过载。

问它怎么做一个 RAG 项目,常常会一次得到 FastAPI、PostgreSQL、pgvector、Redis、Celery、LangChain、LangGraph、Docker、Kubernetes、BM25、Reranker、Tracing 和 Evaluation。每一项可能都没错,但对刚开始的人来说,几乎等于没有路径。

SciFlow 最初处理一个 PDF 要等 27 秒。问题出现后,我先用了 FastAPI BackgroundTasks,让接口返回,再在后台继续生成 Embedding。数据库里的状态也能看到真实变化:

14:08:08  processing
14:08:10  processing
...
14:08:35  completed

做到这里,异步处理为什么存在已经很清楚了。至于 Celery 和 Redis,我没有因为它们“更像生产架构”就马上加进去。等真的遇到服务重启后任务丢失、多 Worker 协调、失败重试或任务持久化,再引入任务队列也不迟。

AI 给出的大而全方案可以当地图,但不应该直接当施工单。每多引入一个组件,就多一套配置、一组故障模式和一笔理解成本。当前问题用最小方案能解决,就先让系统继续往前走。

多问“为什么现在需要它”

我以前遇到陌生技术,习惯问“SQLAlchemy 是什么”。现在我更常问:“直接写 SQL 已经能工作,为什么这个项目现在需要 SQLAlchemy?”

同样,比起“Reranker 是什么”,我更关心“RRF 已经合并了多路结果,为什么还要 Reranker”;比起背 Parent–Child Retrieval 的定义,我会先问“为什么适合检索的小 Chunk,不适合直接作为最终 Context”。

这种问法会自然带出一条完整的因果链:

原来的方案
→ 它在什么情况下失败
→ 新方案解决了哪一部分
→ 新增了什么成本
→ 什么时候不值得用

技术选型真正有价值的地方,通常不在“用过”,而在“为什么在这里用,以及什么时候不用”。AI 可以很快解释名词,但我们仍然要把名词放回问题里。

用熟悉的技术当坐标系

从 React、TypeScript 转到 Python 后端,并不意味着过去的经验要清零。HTTP、数据流、模块边界、类型系统、异步和调试经验都还在,只是表达方式变了。

比如:

from app.routers.rag import router as rag_router

可以先类比成:

import { router as ragRouter } from "./routers/rag"

Pydantic Model 也可以暂时借助 TypeScript 类型来理解:

class AskRequest(BaseModel):
    question: str
interface AskRequest {
  question: string
}

它们当然不等价。Pydantic 还会在运行时做解析和校验,而 TypeScript 类型在编译后通常不存在。但一个不完全准确的类比,往往比完全陌生的定义更适合作为起点。先站稳,再把差异补上。

AI 在这里很像一个随时可用的翻译器。我经常直接要求它“用 React / TypeScript 工程师能理解的方式解释 SQLAlchemy Session”,然后继续追问类比在哪些地方会失效。这样既利用了旧经验,也不至于被类比带偏。

测试不是学习结束后的检查

SciFlow 最早的向量检索能返回结果,RAG 也能生成很流畅的答案。如果只是录一个 Demo,到这里完全可以停。

但固定评测集跑起来后,我才发现系统真正包含答案的证据可能连 Top-30 都进不去。后来也是靠同一组问题反复回归,才逐渐区分出不同层的问题:

证据没被召回
→ 检查解析、切片、Embedding 和 Query Rewrite
 
证据被召回但排名靠后
→ 检查 RRF、候选池和 Reranker
 
证据正确但答案错误
→ 检查 Context 和生成 Prompt
 
答案正确但评测失败
→ 检查 Evaluator 本身

最后一种情况我真的遇到过:模型答对了,只是自动评测规则没有覆盖同义表达,结果被判成失败。那次之后我开始接受一个有点绕、但很重要的事实:评测系统本身也需要被评测。

LLM 太擅长生成“看起来合理”的结果,所以“跑起来了吗”越来越不是一个好问题。更有用的问题是:我有什么证据证明它在目标场景下工作?

让 AI 实现,但把判断留给自己

我现在大量使用 AI 写代码,也不觉得从头手敲一遍才算学习。样板代码、SQL、Pydantic Model、Router,甚至一部分测试,都可以交给 AI 加速。

但有几件事不该一起外包:

  • 为什么现在要做这个改动;
  • 它是不是当前最重要的问题;
  • 有没有更简单的方案;
  • 输入、输出和失败边界是什么;
  • 用什么证据判断改动有效;
  • 它解决问题的同时又带来了什么代价。

以前“会写代码”更偏向把想法翻译成实现。现在它还包括另一层能力:把模糊问题逐步收紧成一个可以验证的软件系统,并持续判断 AI 给出的实现是否合理。

这也是为什么我不再追求“把 Python 学完”或“把 RAG 学完”。这种完成状态本来就不存在。当前知识足够解决下一个真实问题,就先往下走;遇到索引性能,再学执行计划,遇到并发,再补事务隔离。知识看起来不是按教材章节排列的,却会以问题为索引,慢慢长成一张网。

写在最后

回头看,SciFlow 从一个简单的 FastAPI CRUD,逐渐长出了 PDF Parsing、Chunking、Embedding、pgvector、Parent–Child Retrieval、Query Rewrite、RRF、Reranker、Evaluation 和后台任务。

这些名词不是学习成果的清单。真正有用的是它们背后的循环:

遇到问题
→ 描述问题
→ 建立假设
→ 补一个刚好需要的概念
→ 借助 AI 实现
→ 用结果验证
→ 修正理解
→ 进入下一个问题

AI 没有让学习变得不重要,只是把学习的重心往上推了一层。记语法、查 API、写样板代码的成本正在下降,问题定义、系统设计、Debug、验证和技术取舍反而更重要。

所以现在再接触一门新技术,我不会先问“需要多久才能学完”。我会先找一个想做成的东西,然后问:

我现在卡在哪里?为什么会卡?下一步最小的改动是什么?怎么证明它真的有效?

只要这个循环还能继续,就已经在学习了。

延伸阅读