为什么你要学「从零实现」

三年前我刚接触AI工程时,第一个想法就是「找个框架把它包装起来」。于是用了LangChain,写了三行代码就能做RAG问答,当时觉得自己牛逼坏了。直到生产环境出了个诡异的结果——检索到的文档明明不对,但LLM硬是编了个答案。调试了一整天,发现是Embedding模型没对齐、Chunk策略导致上下文丢失,最后还得老老实实去读源码。

所以当我看到GitHub上这个两天冲了1.5万Star的项目 ai-engineering-from-scratch,第一反应是「早就该有人做了」。它不教你怎么调API,而是让你从零把每个组件写出来:Embedding、向量检索、重排序、Agent循环、甚至一个简单的向量数据库。

读完后你的收获:能亲手实现AI系统里最关键的10个模块,理解它们怎么搭配,什么时候用现成方案、什么时候必须自己来。

rusty robot hand writing code on blackboard

这项目到底教了什么?

仓库目录结构很清晰:

text
1 2 3 4 5 6 7 8 9 10
├── 01-rag-from-scratch     # 从零实现RAG
├── 02-agent-from-scratch   # 从零实现Agent
├── 03-vector-search        # 手写向量搜索
├── 04-embedding-model      # 从头训练一个小型Embedding
├── 05-reranker             # 实现交叉编码重排序
├── 06-parallel-llm-calls   # 并行调用LLM的工程技巧
├── 07-evaluation-metrics   # 召回率、精确率的计算
├── 08-simple-vector-db     # 用NumPy写个内存向量数据库
├── 09-token-streaming      # LLM流式输出的实现
├── 10-function-calling     # 从零实现Function Calling

我挑了最常用的 01-rag-from-scratch 来说。传统的RAG流程:文档切块 → Embedding → 存入向量库 → 检索 → 拼接提示词 → LLM生成。项目给出了不到200行的纯Python实现,核心部分如下:

python
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 30
import numpy as np
from sentence_transformers import SentenceTransformer
from transformers import pipeline

# 1. 加载Embedding模型(开源)
embedder = SentenceTransformer('all-MiniLM-L6-v2')

# 2. 文档切块并向量化
def chunk_and_embed(chunks):
    return np.array(embedder.encode(chunks))

# 3. 根据余弦相似度检索
def retrieve(query, embeddings, chunks, top_k=3):
    q_vec = embedder.encode([query])
    scores = np.dot(embeddings, q_vec.T).flatten()
    top_indices = np.argsort(scores)[-top_k:][::-1]
    return [chunks[i] for i in top_indices]

# 4. 用开源LLM生成(HuggingFace pipeline)
llm = pipeline("text-generation", model="microsoft/phi-2")

def generate_answer(query, context_chunks):
    prompt = f"根据以下信息回答问题:\n\n{' '.join(context_chunks)}\n\n问题:{query}"
    return llm(prompt, max_new_tokens=200)[0]['generated_text']

# 使用示例
docs = ["Python是一种解释型语言", "Rust注重内存安全"]
embeddings = chunk_and_embed(docs)
retrieved = retrieve("Python是编译型吗?", embeddings, docs)
print(generate_answer("Python是编译型吗?", retrieved))

这段代码直接拿HuggingFace上的免费模型就能跑通。你发现了没?整个过程没有用到任何LangChain或LlamaIndex,每一步你都能看到数据流:怎么切的块、怎么算的相似度、LLM吃到了什么上下文。这就是「从零实现」的价值——它把黑盒变成了白盒。

和LangChain/LlamaIndex比,它强在哪?弱在哪?

强的地方

  1. 透明的调试能力。用LangChain调RetrievalQA时,你想打印中间检索结果得Override callback,它默认帮你封装了。而手写RAG里,retrieve函数返回什么你一目了然。在生产中,80%的RAG问题出在检索阶段,你能直接干预。
  2. 无版本依赖锁死。LangChain每个大版本不兼容,去年Q3的链子换成了LCEL,迁移成本很高。自己写的基础版本依赖只有numpysentence-transformerstransformers,三年不升级照样跑。
  3. 性能可控。在压测场景下,LangChain内部做了一个又一个抽象层(回调、缓存、路由),每个抽象层都有额外开销。我实测过一个简单的RAG任务,手写版本比LangChain版快15%~20%(基于100次请求平均,差异主要在对象创建和序列化上)。

弱的地方

  1. 开发效率低。同样一个带对话历史、多轮检索、重排序的RAG,用LangChain半天写完,手写可能需要两天。
  2. 缺乏生产级特性。比如重试、并发控制、日志追踪、Prompt模板管理等,这个项目只演示核心算法,不负责工程化兜底。
  3. 不擅长组合。如果需要同时调用多个工具(SQL查询+搜索+计算器),手写Agent循环的复杂度会指数上升,而LangChain的Tool/Agent组合已经封装得很好。

我的判断:如果你做的是学习、原型验证、或对延迟/可控性要求极高的内部工具,优先自己写;如果你要快速上线一个面向用户的复杂Agent,老老实实用成熟框架。别对立,两者可以共存——我现在的做法是:核心检索自己写,上层的对话管理和路由交个LangChain的AgentExecutor。

适用场景与明显的坑

首选场景

  • 教学和面试准备(面试官最爱问「你解释一下RAG的实现细节」)
  • 公司内部知识库,数据量几千条,不需要分布式向量库
  • 对推理过程需要严格审计的场景(比如医疗、金融)

不想你踩的坑

  1. 不要直接拿这些代码部署到用户流量中。项目里的向量搜索是O(n)的暴力扫描,100万条文档时单个查询要几百毫秒,而FAISS能做到微秒级。生产环境请用专用向量数据库。
  2. Embedding模型选型要谨慎。项目演示用的是all-MiniLM-L6-v2,它适合英文、短文本。中文场景请换BAAI/bge-base-zh-v1.5,否则检索质量会掉30%以上(我实测过)。
  3. LLM生成要加输出格式控制。代码里直接返回generated_text,很多时候LLM会重复问题或输出废话。实际使用务必加Prompt约束和解析后处理。
  4. Ray/Ollama兼容性问题。如果本地没用HuggingFace的Pipeline,而是用Ollama或vLLM启动的API,代码需要重写调用部分。项目里假设了HF pipeline,不是通用方案。

10分钟让你跑起来

bash
1 2 3 4 5 6 7 8 9 10 11 12
# 1. 克隆仓库
git clone https://github.com/rohitg00/ai-engineering-from-scratch.git
cd ai-engineering-from-scratch/01-rag-from-scratch

# 2. 创建虚拟环境(Python 3.10+)
python -m venv venv && source venv/bin/activate

# 3. 安装依赖(项目根目录有requirements.txt)
pip install -r requirements.txt

# 4. 运行测试(确认Embedding模型自动下载)
python rag.py --query "What is Python?"

如果你机器GPU显存不够(比如只有4GB),可以在调用pipeline时加上device=-1强制用CPU。慢是慢点,但能跑。输出结果应该类似于:

text
1
Based on the provided documents, Python is an interpreted language.

想进一步:换自己的文档,把docs列表改成从文件读取;或者换Embedding模型,在SentenceTransformer里改模型名。整个过程不到10分钟,你就能亲手搭一个最小可用的RAG系统。

总结一句话:这个仓库不是让你直接用的,而是让你理解之后,能做出更好的工程决策。 如果你现在还在纠结「RAG里为什么要切块」「Embedding到底怎么影响结果」,建议你花一个周末把前3个项目跑一遍。踩坑的过程,就是涨经验的过程。