欢迎光临
我们一直在努力

【Agent 实战】Phase 2:从零搭建 RAG 文档问答系统(LangGraph + 通义千问 + Chroma)

摘要:本文记录 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,检索增强生成) 就是解决这个问题的主流方案:

  • 先把文档切小块、转成向量,存进向量数据库(离线建库)
  • 用户提问时,先检索最相关的文档片段(在线检索)
  • 把检索结果作为「参考资料」交给 LLM 生成回答(增强生成)
  • 一句话:给 Agent 装一个「图书馆」,让它查资料再回答。


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

    2.1 RAG 核心流程

    文档 → 加载 → 切分 → 向量化 → 存入向量库

    用户提问 → 问题向量化 → 相似度检索 Top-K → 拼上下文 → LLM 生成回答

    2.2 Phase 1 vs Phase 2 对比

    维度Phase 1 工具 AgentPhase 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",
    )

    这里发生了两件事:

  • 每个 Chunk 调用 text-embedding-v3 转成高维向量
  • 向量 + 原文 + 元数据一起写入 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
    知识来源 模型训练数据(可能过时/错误) 仅检索到的文档片段
    不知道时 可能编造一个看起来合理的答案 必须说「知识库中没有」

    本项目有三层保障:

  • 检索层:没命中 → context 为空 → 填入「未检索到任何相关资料」
  • Prompt 层:规则 1「只使用参考资料」+ 规则 2「不足时说不知道」
  • 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.py 和 agent.py
    • 看懂 [检索] Trace 日志
    • 往 data/ 添加自己的文档并重新入库
    • 解释「检索不到时为什么要说不知道」
    • 区分 qwen-plus(问答)和 text-embedding-v3(向量化)的分工

    11.2 关键收获

  • RAG = 检索 + 生成,不是纯 LLM 聊天
  • 离线建库 + 在线检索 是工业 RAG 的标准分工
  • Prompt 约束 和 检索质量 同样重要
  • 两个模型 各干各的:Embedding 负责「找」,LLM 负责「说」
  • 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 的常见形态。


    十二、参考资料

    • LangGraph 官方文档
    • 阿里云 DashScope 文档
    • Chroma 向量数据库
    • 本项目源码:agent-rag/ 目录

    系列文章导航

    • Phase 1:工具 Agent(LangGraph + Function Calling)
    • Phase 2:RAG 文档问答(本文)
    • Phase 2.5:RAG + 工具 Agent 合体 —— 待发布
    • Phase 3+:复杂工作流、多 Agent 协作、生产化部署 —— 规划中

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

    赞(0)
    未经允许不得转载:171主机测评 » 【Agent 实战】Phase 2:从零搭建 RAG 文档问答系统(LangGraph + 通义千问 + Chroma)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址