为什么你要学「从零实现」
三年前我刚接触AI工程时,第一个想法就是「找个框架把它包装起来」。于是用了LangChain,写了三行代码就能做RAG问答,当时觉得自己牛逼坏了。直到生产环境出了个诡异的结果——检索到的文档明明不对,但LLM硬是编了个答案。调试了一整天,发现是Embedding模型没对齐、Chunk策略导致上下文丢失,最后还得老老实实去读源码。
所以当我看到GitHub上这个两天冲了1.5万Star的项目 ai-engineering-from-scratch,第一反应是「早就该有人做了」。它不教你怎么调API,而是让你从零把每个组件写出来:Embedding、向量检索、重排序、Agent循环、甚至一个简单的向量数据库。
读完后你的收获:能亲手实现AI系统里最关键的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实现,核心部分如下:
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比,它强在哪?弱在哪?
强的地方:
- 透明的调试能力。用LangChain调RetrievalQA时,你想打印中间检索结果得Override callback,它默认帮你封装了。而手写RAG里,
retrieve函数返回什么你一目了然。在生产中,80%的RAG问题出在检索阶段,你能直接干预。 - 无版本依赖锁死。LangChain每个大版本不兼容,去年Q3的链子换成了LCEL,迁移成本很高。自己写的基础版本依赖只有
numpy、sentence-transformers、transformers,三年不升级照样跑。 - 性能可控。在压测场景下,LangChain内部做了一个又一个抽象层(回调、缓存、路由),每个抽象层都有额外开销。我实测过一个简单的RAG任务,手写版本比LangChain版快15%~20%(基于100次请求平均,差异主要在对象创建和序列化上)。
弱的地方:
- 开发效率低。同样一个带对话历史、多轮检索、重排序的RAG,用LangChain半天写完,手写可能需要两天。
- 缺乏生产级特性。比如重试、并发控制、日志追踪、Prompt模板管理等,这个项目只演示核心算法,不负责工程化兜底。
- 不擅长组合。如果需要同时调用多个工具(SQL查询+搜索+计算器),手写Agent循环的复杂度会指数上升,而LangChain的Tool/Agent组合已经封装得很好。
我的判断:如果你做的是学习、原型验证、或对延迟/可控性要求极高的内部工具,优先自己写;如果你要快速上线一个面向用户的复杂Agent,老老实实用成熟框架。别对立,两者可以共存——我现在的做法是:核心检索自己写,上层的对话管理和路由交个LangChain的AgentExecutor。
适用场景与明显的坑
首选场景:
- 教学和面试准备(面试官最爱问「你解释一下RAG的实现细节」)
- 公司内部知识库,数据量几千条,不需要分布式向量库
- 对推理过程需要严格审计的场景(比如医疗、金融)
不想你踩的坑:
- 不要直接拿这些代码部署到用户流量中。项目里的向量搜索是
O(n)的暴力扫描,100万条文档时单个查询要几百毫秒,而FAISS能做到微秒级。生产环境请用专用向量数据库。 - Embedding模型选型要谨慎。项目演示用的是
all-MiniLM-L6-v2,它适合英文、短文本。中文场景请换BAAI/bge-base-zh-v1.5,否则检索质量会掉30%以上(我实测过)。 - LLM生成要加输出格式控制。代码里直接返回
generated_text,很多时候LLM会重复问题或输出废话。实际使用务必加Prompt约束和解析后处理。 - Ray/Ollama兼容性问题。如果本地没用HuggingFace的Pipeline,而是用Ollama或vLLM启动的API,代码需要重写调用部分。项目里假设了HF pipeline,不是通用方案。
10分钟让你跑起来
# 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。慢是慢点,但能跑。输出结果应该类似于:
Based on the provided documents, Python is an interpreted language.
想进一步:换自己的文档,把docs列表改成从文件读取;或者换Embedding模型,在SentenceTransformer里改模型名。整个过程不到10分钟,你就能亲手搭一个最小可用的RAG系统。
总结一句话:这个仓库不是让你直接用的,而是让你理解之后,能做出更好的工程决策。 如果你现在还在纠结「RAG里为什么要切块」「Embedding到底怎么影响结果」,建议你花一个周末把前3个项目跑一遍。踩坑的过程,就是涨经验的过程。