从零搭建 RAG 知识库问答系统:我的 LangChain + Agent 学习笔记
💡 这是一篇学习经验分享,记录我在 smart-office-assistant 项目中学习 LangChain、RAG、Agent 的过程和收获。
前言
最近花了两周时间,系统学习了 LangChain 生态,从 Prompt 模板到 RAG 检索,再到 Agent 工具调用。通过 smart-office-assistant 这个项目,把所有知识点串联了起来。
这篇文章不讲"怎么实现",而是分享"我学到了什么"——那些踩过的坑、理清的概念、形成的知识体系。
一、LangChain 核心概念梳理
刚开始学 LangChain 时,概念太多容易晕。我梳理了一个清晰的认知框架:
1.1 LangChain 的三大支柱
┌─────────────────────────────────────────────────────────┐
│ LangChain 生态 │
├─────────────────┬─────────────────┬─────────────────────┤
│ LangChain │ LangGraph │ LangServe │
│ (基础组件) │ (Agent编排) │ (部署服务) │
├─────────────────┼─────────────────┼─────────────────────┤
│ - Prompt │ - StateGraph │ - REST API │
│ - LLM │ - Nodes/Edges │ - Web UI │
│ - Chains │ - Conditional │ - Streaming │
│ - Tools │ - Memory │ │
│ - Retrievers │ │ │
└─────────────────┴─────────────────┴─────────────────────┘
我的理解:
- LangChain 是积木,提供各种基础组件
- LangGraph 是图纸,告诉你怎么把积木搭起来
- LangServe 是包装,让搭好的东西能对外服务
1.2 关键概念关系图
Prompt Template (提示模板)
↓
LLM (大语言模型)
↓
Output Parser (输出解析)
↓
Chain (处理链)
↓
Agent (智能代理) ← 工具调用
↓
Memory (记忆系统)
学习心得:
| 概念 | 一句话理解 | 类比 |
|---|---|---|
| Prompt Template | 给 AI 的填空题模板 | 考试题模板 |
| LLM | 会答题的大脑 | 考生 |
| Chain | 固定流程的流水线 | 流水线工人 |
| Agent | 能自己决定下一步的决策者 | 项目经理 |
| Tools | AI 可以调用的外部能力 | 员工 |
| Memory | 让 AI 记住上下文 | 笔记本 |
二、RAG:让 AI 基于你的文档回答
2.1 RAG 解决了什么问题?
纯 LLM 的痛点:
- 知识有截止日期,无法获取最新信息
- 无法访问私有数据(公司内部文档)
- 可能产生"幻觉",编造不存在的信息
RAG 的思路:
用户提问 → 先检索相关文档 → 把文档喂给 LLM → LLM 基于文档生成答案
2.2 RAG 的核心流程
我总结了一个 RAG 五步法:
┌─────────────────────────────────────────────────────────┐
│ RAG 五步法 │
├─────────────────────────────────────────────────────────┤
│ 1. 文档加载 → 把各种格式的文档读进来 │
│ 2. 文档切分 → 把长文档切成小块(chunks) │
│ 3. 向量化 → 用 Embedding 模型把文本变成向量 │
│ 4. 存储检索 → 存入向量数据库,支持相似度搜索 │
│ 5. 生成答案 → 检索相关文档,交给 LLM 生成回答 │
└─────────────────────────────────────────────────────────┘
2.3 踩过的坑和经验
坑1:文档切分太粗或太细
问题:chunk_size 设太大,检索不精确;设太小,丢失上下文。
我的经验:
# 推荐配置(中文文档)
splitter = RecursiveCharacterTextSplitter(
chunk_size=300, # 中文300字左右
chunk_overlap=50, # 重叠50字保持连贯
separators=["\n\n", "\n", "。", ",", " "], # 优先按段落切
)
关键点:
- 中文文档 chunk_size 建议 200-500
- chunk_overlap 保持 10%-20%
- separators 按优先级排列,先尝试大粒度切分
坑2:Embedding 模型选错
问题:用英文 Embedding 模型处理中文,效果很差。
我的选择:
# 中文推荐:bge-small-zh-v1.5
# 体积小(~100MB),中文效果好
embedding = HuggingFaceEmbeddings(
model_name="AI-ModelScope/bge-small-zh-v1.5",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True},
)
模型对比:
| 模型 | 语言 | 体积 | 效果 |
|---|---|---|---|
| text-embedding-ada-002 | 英文为主 | API调用 | 英文好 |
| bge-small-zh-v1.5 | 中文 | ~100MB | 中文好 |
| bge-large-zh-v1.5 | 中文 | ~1.3GB | 中文最好 |
坑3:每次启动都重新构建向量库
问题:文档没变,但每次启动都重新构建,浪费时间。
解决方案:用哈希值检测文档变化
def get_docs_hash(docs_dir):
"""计算文档目录的哈希值"""
hash_obj = hashlib.md5()
for root, dirs, files in os.walk(docs_dir):
for filename in sorted(files):
filepath = os.path.join(root, filename)
stat = os.stat(filepath)
# 文件名 + 修改时间 + 大小 = 唯一标识
hash_obj.update(f"{filename}{stat.st_mtime}{stat.st_size}".encode())
return hash_obj.hexdigest()
经验:哈希值只在文档变化时才改变,实现了智能缓存。
2.4 RAG 优化方向
| 优化点 | 方法 | 效果 |
|---|---|---|
| 检索优化 | 使用 MMR(最大边际相关性) | 避免返回重复内容 |
| 重排序 | 对检索结果二次排序 | 提升相关性 |
| 混合检索 | 向量检索 + 关键词检索 | 兼顾语义和精确匹配 |
| 多路召回 | 多种方式检索,合并结果 | 提升召回率 |
三、Agent:让 AI 自己决定用什么工具
3.1 Agent 是什么?
我的理解:Agent = LLM + Tools + 决策能力
- Chain:固定流程,1→2→3→4
- Agent:动态决策,LLM 根据问题决定调用哪个工具
Chain 的工作方式:
用户 → Step1 → Step2 → Step3 → 回答(固定流程)
Agent 的工作方式:
用户 → LLM 判断 → 调用工具A → LLM 判断 → 调用工具B → 回答(动态决策)
3.2 工具调用的核心机制
通过 @tool 装饰器定义工具:
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""计算数学表达式。这是工具的描述,AI会根据这个判断什么时候用。
Args:
expression: 数学表达式,例如 "2 + 3 * 4"
"""
import numexpr
result = numexpr.evaluate(expression)
return f"{expression} = {result.item()}"
关键点:
- 函数名就是工具名
- docstring 是工具描述,AI 根据它判断什么时候用
- 类型注解定义参数类型
- Args 文档帮助 AI 理解参数含义
3.3 ReAct 模式
Agent 的核心是 ReAct(Reasoning + Acting)模式:
┌─────────────────────────────────────────────────────────┐
│ ReAct 循环 │
├─────────────────────────────────────────────────────────┤
│ │
│ Thought (思考) → Action (行动) → Observation (观察) │
│ ↑ │ │
│ └────────────────────────────────────┘ │
│ │
│ 直到得出最终答案 │
└─────────────────────────────────────────────────────────┘
实际例子:
问题:现在几点了?另外帮我算一下 (123 + 456) * 789
Thought: 用户问了两个问题,需要调用两个工具
Action: get_current_time()
Observation: 2024年01月15日 14:30:00
Thought: 还需要计算数学表达式
Action: calculator("(123 + 456) * 789")
Observation: (123 + 456) * 789 = 456591
Thought: 已经拿到所有信息,可以回答了
Answer: 现在是2024年1月15日14:30,(123 + 456) * 789 = 456591
3.4 create_react_agent 的魔力
LangGraph 提供了开箱即用的 Agent:
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(
model=llm,
tools=[calculator, get_current_time, ...],
)
# 直接用,Agent 自己搞定一切
result = agent.invoke({"messages": [("human", "现在几点了?")]})
一行代码创建 Agent,它会自动:
- 分析用户问题
- 决定是否需要工具
- 调用合适的工具
- 整合结果生成答案
3.5 工具设计原则
| 原则 | 说明 | 反例 |
|---|---|---|
| 单一职责 | 一个工具只做一件事 | 把计算和查询混在一起 |
| 描述清晰 | docstring 要让 AI 理解 | 写得太简略 |
| 参数明确 | 类型注解要准确 | 用 Any 类型 |
| 错误处理 | 返回有意义的错误信息 | 直接抛异常 |
四、知识体系总结
通过这个项目,我形成了一个清晰的知识体系:
┌─────────────────────────────────────────────────────────┐
│ LangChain + RAG + Agent │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 应用层 │ │
│ │ 智能助手 / 知识问答 / 自动化工具 │ │
│ └─────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 编排层 (LangGraph) │ │
│ │ Agent / 工具调用 / 条件路由 / 循环重试 │ │
│ └─────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 能力层 (LangChain) │ │
│ │ Prompt / LLM / Chain / Retriever / Tools │ │
│ └─────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 基础层 │ │
│ │ 向量数据库 / Embedding / 文档加载 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
五、学习建议
- 先跑通再理解:先把代码跑起来,看到效果,再深入理解原理
- 从简单到复杂:先搞懂 Prompt → Chain → Agent 的演进
- 动手改参数:修改 chunk_size、temperature 等参数,观察效果变化
- 读源码:LangChain 源码不难读,关键类就那几个