摘要:本文记录 Agent 学习路线 Phase 2 的完整实战过程——如何用 Python 搭建一套 RAG(检索增强生成)文档问答系统,让大模型基于私有文档回答问题,而不是「凭空编造」。技术栈:LangGraph、通义千问 qwen-plus、DashScope text-embedding-v3、Chroma 向量库。

关键词:RAG、Agent、LangGraph、通义千问、Chroma、向量检索、Embedding、大模型应用


一、前言:为什么要学 RAG?

在 Phase 1 中,我学会了工具 Agent——让 LLM 调用天气 API、计算器等外部工具「动手做事」。但有一类问题工具解决不了:

「我们公司的年假有多少天?」「报销最晚什么时候提交?」

这些信息在内部 PDF、Markdown 文档里,不在 API 里,也不在 LLM 的训练数据中(或者 LLM 会瞎编)。

RAG(Retrieval-Augmented Generation,检索增强生成) 就是解决这个问题的主流方案:

  1. 先把文档切小块、转成向量,存进向量数据库(离线建库
  2. 用户提问时,先检索最相关的文档片段(在线检索
  3. 把检索结果作为「参考资料」交给 LLM 生成回答(增强生成

一句话:给 Agent 装一个「图书馆」,让它查资料再回答。


二、RAG 是什么?和 Phase 1 有什么区别?

2.1 RAG 核心流程

文档 → 加载 → 切分 → 向量化 → 存入向量库
                                    ↓
用户提问 → 问题向量化 → 相似度检索 Top-K → 拼上下文 → LLM 生成回答

2.2 Phase 1 vs Phase 2 对比

维度 Phase 1 工具 Agent Phase 2 RAG
解决什么问题 执行实时动作(查天气、算数) 基于私有文档问答
数据来源 外部 API 本地向量库
核心能力 Function Calling 向量相似度检索
工作流 agent ↔ tools 循环 retrieve → generate
典型提问 「北京天气怎么样?」 「入职满 1 年年假几天?」

2.3 架构图

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   ingest.py │ ──→ │  chroma_db/  │ ──→ │  agent.py   │
│  离线建库    │     │   向量知识库   │     │  在线问答    │
└─────────────┘     └──────────────┘     └─────────────┘
      ↑                                        ↑
   data/ 文档                              用户提问

三、项目结构

agent-rag/
├── ingest.py          # 文档入库:加载 → 切分 → 向量化 → 存 Chroma
├── retriever.py       # 检索模块:加载向量库 + similarity_search
├── agent.py           # RAG 问答:retrieve → generate 工作流
├── config.py          # 配置(API Key、Chunk 参数、Top-K 等)
├── trace.py           # Trace 日志
├── data/              # 放文档(样例:company_handbook.md)
├── chroma_db/         # 向量库(运行 ingest 后自动生成)
├── requirements.txt
├── .env               # API Key(不要提交 Git)
└── run.ps1            # 一键启动脚本

四、环境准备

4.1 安装依赖

cd agent-rag
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt

主要依赖:

  • langgraph / langchain-core / langchain-openai —— 编排与 LLM 调用
  • langchain-community —— 文档加载器、DashScope Embedding、Chroma
  • chromadb —— 向量数据库
  • dashscope —— 阿里云 DashScope SDK
  • pypdf —— PDF 解析

4.2 配置 API Key

copy .env.example .env
# 编辑 .env,填入 DASHSCOPE_API_KEY

.env 示例:

DASHSCOPE_API_KEY=sk-xxxxxxxx
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL_NAME=qwen-plus
EMBEDDING_MODEL=text-embedding-v3

如果 Phase 1 已配置过,直接复制 FirstAgent/.env 即可。

4.3 两个大模型,各司其职

模型 作用 调用时机
text-embedding-v3 文本 → 向量 ingest 入库 + 每次检索提问
qwen-plus 理解 + 生成回答 generate 节点

同一个 API Key,两次不同能力的调用。


五、ingest.py 详解:离线建库

5.1 整体流程

documents = load_documents(DATA_DIR)   # 1. 加载
chunks = split_documents(documents)    # 2. 切分
build_vectorstore(chunks)              # 3. 向量化 + 存入 Chroma

运行命令:

python ingest.py --rebuild

5.2 第一步:加载文档

支持三种格式:

格式 加载器 元数据
.md / .txt TextLoader source = 文件名
.pdf PyPDFLoader source + page(页码)

元数据很重要——后面回答要引用「来源:xxx 第 N 页」。

5.3 第二步:文档切分(Chunking)

splitter = RecursiveCharacterTextSplitter(
    chunk_size=600,       # 每块最大 600 字符
    chunk_overlap=80,     # 相邻块重叠 80 字符
    separators=["\n## ", "\n### ", "\n", "。", " ", ""],
)

为什么要切分?

  • LLM 上下文窗口有限,塞不进整本书
  • 向量检索需要「小块」才能精准命中

为什么要 overlap(重叠)?

  • 避免关键信息被切在两块边界上,导致检索时只命中一半

参数调优经验:

  • chunk_size 太大 → 检索粒度粗,噪音多
  • chunk_size 太小 → 上下文不完整
  • 本项目 600/80 是入门友好的默认值

5.4 第三步:向量化 + 入库

embeddings = DashScopeEmbeddings(
    model="text-embedding-v3",
    dashscope_api_key=DASHSCOPE_API_KEY,
)
store = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    collection_name="knowledge_base",
    persist_directory="./chroma_db",
)

这里发生了两件事:

  1. 每个 Chunk 调用 text-embedding-v3 转成高维向量
  2. 向量 + 原文 + 元数据一起写入 chroma_db/

--rebuild 参数:先删除旧向量库再重建,适合文档更新后使用。


六、retriever.py 详解:在线检索

6.1 检索流程

def retrieve(question: str, top_k: int = 4):
    store = get_vectorstore()                          # 加载 Chroma
    docs = store.similarity_search(question, k=top_k)  # 相似度检索
    context, sources = format_context(docs)             # 格式化 + 提取来源
    return context, sources

6.2 similarity_search 背后发生了什么?

用户问题「入职满1年年假几天?」
  → text-embedding-v3 把问题转成 query 向量
  → 与 chroma_db 中所有 chunk 向量计算相似度
  → 返回 Top-4 最相似的文档片段

注意:检索阶段也会调用 embedding 模型,不只是 ingest 时才用。

6.3 格式化上下文

检索到的片段会被包装成 LLM 可读的格式:

【片段1 | 来源: company_handbook.md】
入职满 1 年的员工享有 15 天带薪年假...

【片段2 | 来源: company_handbook.md】
入职满 3 年的员工享有 18 天带薪年假...

6.4 向量库不存在时的处理

if not CHROMA_DIR.exists():
    raise FileNotFoundError("向量库不存在,请先运行: python ingest.py")

七、agent.py 详解:RAG 问答工作流

7.1 LangGraph 两节点工作流

workflow.add_node("retrieve", retrieve_node)   # 检索
workflow.add_node("generate", generate_node)   # 生成

workflow.add_edge(START, "retrieve")
workflow.add_edge("retrieve", "generate")
workflow.add_edge("generate", END)

与 Phase 1 的 agent ↔ tools 循环不同,RAG 是单向流水线

用户提问 → retrieve(检索资料)→ generate(LLM 生成回答)→ 输出

7.2 retrieve 节点

@trace_node("retrieve")
def retrieve_node(state):
    question = get_latest_question(state["messages"])  # 取最新用户问题
    context, sources = retrieve(question)               # 调用检索
    return {"context": context, "sources": sources}

7.3 generate 节点

@trace_node("generate")
def generate_node(state):
    context = state.get("context", "")
    if not context.strip():
        context = "(未检索到任何相关资料)"

    system = RAG_PROMPT.format(context=context)
    messages = [SystemMessage(content=system)] + state["messages"]
    response = llm.invoke(messages)
    return {"messages": [response]}

核心设计:把检索结果塞进 System Prompt 的 {context} 占位符,LLM 只能「开卷考试」。

7.4 RAG Prompt 设计

你是企业知识库问答助手,只能根据「参考资料」回答问题。

规则:
1. 只使用参考资料中的信息,不要编造。
2. 如果参考资料不足以回答,明确说「知识库中没有找到相关信息」。
3. 回答末尾用「引用来源:」列出你用到的来源。
4. 用简洁的中文回答。

参考资料:
{context}

Prompt 设计是 RAG 质量的关键之一——约束 LLM 不要脱离资料胡编。

7.5 会话记忆

memory = MemorySaver()
config = {"configurable": {"thread_id": "1"}}
  • 多轮对话保留历史(如「那病假呢?」能接上文)
  • 输入 clear 切换 thread_id,清空记忆

八、实战测试

8.1 入库

python ingest.py --rebuild

预期输出:

已加载: company_handbook.md
切分完成: N 个文档块
向量库已保存: chroma_db
入库完成!

8.2 问答

python agent.py

8.3 测试用例

提问 预期回答 考察点
入职满 1 年的员工有多少天年假? 15 天 基本检索
报销最晚多少天内提交? 30 日 跨章节检索
公司上市代码是多少? 知识库中没有找到 防幻觉

8.4 Trace 日志解读

[输入] 用户: 入职满1年的员工有多少天年假?
>> 进入节点: retrieve
[检索] 命中 2 个片段,来源: company_handbook.md
[检索] 片段1: 入职满 1 年的员工享有 15 天带薪年假...
<< 完成节点: retrieve (320 ms)
>> 进入节点: generate
<< 完成节点: generate (850 ms)

🤖 Agent:入职满 1 年的员工享有 15 天带薪年假。
引用来源:company_handbook.md

日志对应关系:

日志 含义
>> 进入节点: retrieve 开始检索
[检索] 命中 N 个片段 向量搜索成功
>> 进入节点: generate 开始 LLM 生成
引用来源 Prompt 约束生效

九、核心问题深入理解

9.1 为什么检索不到时要说「不知道」?

这是 RAG 和纯聊天的本质区别

纯 LLM 聊天 RAG
知识来源 模型训练数据(可能过时/错误) 仅检索到的文档片段
不知道时 可能编造一个看起来合理的答案 必须说「知识库中没有」

本项目有三层保障:

  1. 检索层:没命中 → context 为空 → 填入「未检索到任何相关资料」
  2. Prompt 层:规则 1「只使用参考资料」+ 规则 2「不足时说不知道」
  3. temperature=0:降低 LLM 随机性,减少编造

即使 LLM「知道」答案,只要资料里没有,也应该拒绝回答——这是 RAG 的核心原则。

9.2 RAG 完整数据流(一图总结)

┌─────────────────────────────────────────────────────────┐
│                    离线阶段(ingest.py)                  │
│                                                         │
│  data/文档 → 加载 → 切分(600/80) → embedding-v3 向量化  │
│                                         ↓               │
│                                   chroma_db/ 持久化      │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                    在线阶段(agent.py)                   │
│                                                         │
│  用户提问 → HumanMessage                                 │
│       ↓                                                 │
│  retrieve: 问题 → embedding-v3 → similarity_search(Top4) │
│       ↓                                                 │
│  context + sources                                      │
│       ↓                                                 │
│  generate: RAG_PROMPT + qwen-plus → 带引用的回答         │
└─────────────────────────────────────────────────────────┘

十、踩坑记录

10.1 模块找不到

ModuleNotFoundError: No module named 'langchain_openai'

原因:没激活虚拟环境,用了系统 Python。

解决.venv\Scripts\activate 后再运行,或用 .\run.ps1

10.2 API Key 未配置

ValueError: 未配置有效的 DASHSCOPE_API_KEY

解决:复制 .env.example.env,填入真实 Key。

10.3 向量库不存在

FileNotFoundError: 向量库不存在,请先运行: python ingest.py

解决:先 python ingest.py --rebuild 建库,再 python agent.py

10.4 检索不准

  • 调小 CHUNK_SIZE 或增大 TOP_K
  • 检查文档编码(中文用 UTF-8)
  • 换更好的 Embedding 模型

10.5 回答幻觉(编造)

  • 加强 Prompt 约束
  • 设置 temperature=0
  • 检查检索结果是否真的包含答案

十一、学习总结

11.1 Phase 2 通关清单

  • 理解 RAG 四步:加载 → 切分 → 向量化 → 检索生成
  • 跑通 ingest.pyagent.py
  • 看懂 [检索] Trace 日志
  • data/ 添加自己的文档并重新入库
  • 解释「检索不到时为什么要说不知道」
  • 区分 qwen-plus(问答)和 text-embedding-v3(向量化)的分工

11.2 关键收获

  1. RAG = 检索 + 生成,不是纯 LLM 聊天
  2. 离线建库 + 在线检索 是工业 RAG 的标准分工
  3. Prompt 约束检索质量 同样重要
  4. 两个模型 各干各的:Embedding 负责「找」,LLM 负责「说」
  5. Trace 日志 是理解 RAG 工作流的最佳工具

11.3 与 Phase 1 的统一视角

Phase 1:给 Agent 装「手」(工具调用)
Phase 2:给 Agent 装「图书馆」(RAG 检索)
Phase 2.5(下一步):把 RAG 封装成工具,与 Phase 1 合体

合体后的 Agent 可以同时:

  • 问天气 → get_weather
  • 算税费 → calculator
  • 查制度 → search_knowledge_base(RAG)

这才是工业级 Agent 的常见形态。


十二、参考资料


系列文章导航


如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎在评论区交流。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐