从零搭建RAG知识库问答系统(零基础自学agent日记)

10 阅读6分钟

从零搭建 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能自己决定下一步的决策者项目经理
ToolsAI 可以调用的外部能力员工
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: 2024011514:30:00

Thought: 还需要计算数学表达式
Action: calculator("(123 + 456) * 789")
Observation: (123 + 456) * 789 = 456591

Thought: 已经拿到所有信息,可以回答了
Answer: 现在是202411514: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 / 文档加载             │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
└─────────────────────────────────────────────────────────┘

五、学习建议

  1. 先跑通再理解:先把代码跑起来,看到效果,再深入理解原理
  2. 从简单到复杂:先搞懂 Prompt → Chain → Agent 的演进
  3. 动手改参数:修改 chunk_size、temperature 等参数,观察效果变化
  4. 读源码:LangChain 源码不难读,关键类就那几个

六、项目地址


下一篇:用 LangGraph 构建智能客服系统(零基础自学agent日记)