AI · #llm#rag#learning-path#transformer

我的大语言模型学习路径:从调用 API 到理解内部

2024.03.05 4 min 1.8k
// 目录 · contents

2023 年 GPT-3.5 出来后,我花了大概三四个月认真补了一遍 LLM 相关的知识。这篇文章是学习路径的复盘,不是系统性的教程,更像是”如果重来一次我会怎么安排顺序”的回答。

我的背景是 Java 后端,没有 ML 基础,不做模型训练,主要关注工程应用。所以这条路线是偏应用层的,不涉及 CUDA 编程和模型训练。

第一阶段:先会用,再理解

最开始我犯了一个错误:一上来就想把 Transformer 的论文啃完,结果看了一周,满脑子是 self-attention 公式,但不知道这东西能拿来做什么。

后来调整策略:先通过实际使用建立直觉,再回头理解原理

第一步是学会调用 OpenAI API,把几个实际的东西跑起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from openai import OpenAI

client = OpenAI()

# 最基础的对话
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "你是一个代码审查助手"},
{"role": "user", "content": "这段代码有什么问题?\n```java\n...\n```"}
],
temperature=0.3,
max_tokens=1000,
)
print(response.choices[0].message.content)

在这个过程中,temperaturemax_tokenstop_p 这些参数通过实验很快有了直觉——temperature=0 时输出很确定,temperature=1 时很发散。这种直觉比先读论文再调参要快得多。

第二阶段:理解 Token 和上下文窗口

这是很多工程师容易跳过但其实很重要的基础概念。

Token 不等于字符,也不等于词。OpenAI 用的是 Byte Pair Encoding(BPE),大概 1 个 token 对应英文 4 个字符,中文则大概 1-2 个字符对应 1 个 token(因为中文字符本身就比较多)。

1
2
3
4
5
6
7
8
9
import tiktoken

enc = tiktoken.encoding_for_model("gpt-3.5-turbo")
text = "用 Java 实现一个线程安全的单例模式"
tokens = enc.encode(text)
print(f"文本:{text}")
print(f"Token 数量:{len(tokens)}")
print(f"Token IDs:{tokens}")
# Token 数量:17(中文每字约 1.5 token)

理解 token 计费和上下文窗口(context window)对工程实现很重要。GPT-3.5 是 4k 或 16k token 的上下文,GPT-4 有 128k,但不是越大越好——处理超长上下文时,模型对中间内容的”注意力”会下降(所谓”lost in the middle”问题),提示词放在最前面或最后面效果通常比放在中间好。

第三阶段:RAG 系统实现

RAG(Retrieval-Augmented Generation)是 LLM 工程应用里最实用的模式之一,解决模型知识截止日期和幻觉两个主要问题。

原理很简单:用户提问 → 从文档库里检索相关片段 → 把片段塞进 prompt → 让模型基于这些片段回答。

我用 LangChain 搭了一个内部文档问答系统:

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
31
32
33
34
35
36
37
38
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import DirectoryLoader

# 1. 加载文档
loader = DirectoryLoader("./docs", glob="**/*.md")
documents = loader.load()

# 2. 分块(chunk)
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块 500 字符
chunk_overlap=50, # 相邻块重叠 50 字符,防止截断关键信息
)
chunks = splitter.split_documents(documents)

# 3. 向量化并存储
embeddings = OpenAIEmbeddings()
vectordb = Chroma.from_documents(
chunks,
embeddings,
persist_directory="./chroma_db"
)

# 4. 构建问答链
qa_chain = RetrievalQA.from_chain_type(
llm=ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0),
chain_type="stuff", # 把所有检索结果一次性塞入 prompt
retriever=vectordb.as_retriever(search_kwargs={"k": 3}),
return_source_documents=True,
)

# 5. 查询
result = qa_chain("我们的 API 如何做认证?")
print(result["result"])
print("参考文档:", [doc.metadata["source"] for doc in result["source_documents"]])

这个系统做出来之后,真正头疼的不是代码,而是召回效果。检索到的文档片段和问题不相关,模型就会胡说。影响召回质量的因素有:分块策略、embedding 模型选择、检索算法(纯向量 vs 混合检索)。这个我在另一篇文章(llm-rag-architecture)里详细写了。

第四阶段:回头理解 Transformer

有了前面的实际感受,再读 Transformer 的论文《Attention Is All You Need》和相关资料,理解就深了很多。

Karpathy 的 Let’s build GPT 视频是我见过最好的入门材料。他从零开始用 200 行 PyTorch 实现了一个简化版的 GPT,过程中解释了每个组件的作用。跟着敲一遍,self-attention 的直觉一下子就有了。

核心理解:self-attention 让序列中的每个 token 都能”看到”其他所有 token,并根据相关性加权汇总信息。这解决了 RNN 难以处理长距离依赖的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# self-attention 的核心计算(简化版)
import torch
import torch.nn.functional as F

def self_attention(Q, K, V):
# Q, K, V: (batch, seq_len, d_k)
d_k = Q.size(-1)

# 相似度分数
scores = torch.matmul(Q, K.transpose(-2, -1)) / (d_k ** 0.5)

# softmax 得到注意力权重(每个 token 对其他 token 的"关注度")
weights = F.softmax(scores, dim=-1)

# 加权汇总 Value
return torch.matmul(weights, V)

踩坑记录

踩坑 1:LangChain 版本更新太快,代码一个月就失效

LangChain 在 2023 年迭代非常激进,API 频繁改动。我写的一个 demo,两个月后跑不起来了,主要是 langchain.chat_models 被拆分到 langchain_openai 包里。

现在的习惯是把 LangChain 的版本钉死(langchain==0.0.xxx),不主动升级,升级之前先读 changelog。

踩坑 2:向量数据库选型想太多

一开始在 Pinecone、Weaviate、Qdrant、Chroma 里纠结了很久。实际上内部系统用 Chroma 已经够了(本地文件存储,不需要部署额外服务),生产用 Elasticsearch 的向量检索(公司已有 ES,不引入新依赖)。

踩坑 3:直接用 GPT-4 做原型,成本比预想高很多

测试阶段没注意 token 消耗,跑了几个批处理任务,一个月账单超出预期很多。

现在的习惯:开发和测试用 GPT-3.5,只在效果确实有差距时才切 GPT-4。或者用本地 Ollama 做开发,上线再切云端模型。

踩坑 4:embedding 维度不同不能混用

换了一个 embedding 模型(从 text-embedding-ada-002 换到 text-embedding-3-small),忘记清空旧的向量库,导致检索时维度不匹配报错。

不同 embedding 模型的向量空间不一样,换模型必须重新向量化所有文档。

参考资料

这些是我实际读过、觉得有价值的:

学 LLM 的体会:工程应用这块变化太快,与其追最新论文,不如先把 embedding、向量检索、提示工程这几个核心概念搞透,然后结合实际项目做,在实践中发现哪里不够用了再去补。

作者 · authorzt
发布 · date2024-03-05
篇幅 · length1.8k 字 · 4 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论