LangGraph + Qdrant 搭本地 RAG 全流程:从零到可演示 Agent 的 6 个小时

LangGraph 与 Qdrant 都不是新东西了,但「本地一键跑通 RAG」依然是一篇值得写的文章——你写 LangChain 时多半会跳过 `create_agent` 的黑盒,自己用 `StateGraph` 拼工作流;向量库选 Qdrant 而不是 pgvector 的理由,过去一年里变化不大,但 1.18 版本之后引入的「量化向量 + GPU 索引」已经悄悄改变了 1M 以下规模的检索体验。本文按照「从环境到 demo」的可复现顺序写,所有版本号、端口、代码片段都来自 2026-06 的实测。

一、为什么是 LangGraph + Qdrant,而不是 LangChain + pgvector

LangGraph 是 LangChain 团队推出的「低层编排框架」——langgraph==1.2.6 在 2026-06-18 发布(GitHub Releases 数据),README 写得很直白:「Build resilient agents」「Trusted by companies shaping the future of agents – including Klarna, Replit, Elastic」(langgraph github)。它和 LangChain 的关系不是替代,而是分层:LangChain 提供模型/工具/检索器的高层抽象,LangGraph 负责把这些节点串成可暂停、可恢复、可分支的有向图。HN 上 83 分的「We chose LangGraph to build our coding agent」(qodo.ai 博客,HN 83 pts)就是这种分层思路的现实样本。

Qdrant 选型的理由在 2026 年依然站得住脚:Apache 2.0、单二进制 Rust 写就、1.18.2(2026-06-04 发布,qdrant github)、单机能撑住亿级向量且自带 REST/gRPC 双协议。最关键的是 Qdrant 1.7+ 引入了「built-in hybrid search」(HN 28 pts,qdrant.tech 文章),稠密向量 + BM25 sparse 在同一 collection 内并行打分,省掉了自己拼 ElasticSearch 的麻烦。


[User Q] --> [Retrieve node]
[Retrieve node] --> [Qdrant: dense + sparse]
[Qdrant: dense + sparse] --> [Top-K docs]
[Top-K docs] --> [LLM synth]
[LLM synth] --> {Citation check?}
{Citation check?} -- yes --> [Final answer]
{Citation check?} -- no  --> [Refiner: rewrite/refine]
[Refiner: rewrite/refine] --> [Retrieve node]

二、30 分钟把环境搭起来


User       LangGraph  Qdrant   fastembed  LLM
 |             |          |         |        |
 |--question-->|          |         |        |
 |             |--embed----------------------------->|
 |             |          |         |--vec-->|
 |             |          |         |--vec---|
 |             |--search->|         |        |
 |             |          |--hits-->|        |
 |             |--prompt+ctx---------------->|--LLM-->|
 |             |          |         |        |--draft->|
 |             |--citation-check------>|        |
 |             |          |         |        |
 |  if cited: --final-->|          |        |
 |  else:                |          |        |
 |             |--refine->|--hits2->|--LLM-->|--final-->|

2.1 Qdrant(Docker 一行)


docker run -d --name qdrant \
  -p 6333:6333 -p 6334:6334 \
  -v $(pwd)/qdrant_storage:/qdrant/storage \
  qdrant/qdrant:v1.18.2
  • 6333 REST,6334 gRPC;本地调 REST 即可
  • 挂卷后数据可持久化,容器重启不丢

2.2 Python 端


python -m venv .venv && source .venv/bin/activate
pip install -U langgraph==1.2.6 langchain-qdrant qdrant-client fastembed
  • `fastembed` 是 Qdrant 官方维护的嵌入库,自带 BGE-M3、BCE、ONNX 推理,不依赖 PyTorch(家用 Mac mini M4 Pro 也能跑)
  • 不要装 `torch`——会让 wheel 体积翻 5 倍,本地 RAG 用不上反向传播

三、把文档变成向量:3 步入库


from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from fastembed import TextEmbedding

client = QdrantClient(url="http://localhost:6333")
client.create_collection(
    collection_name="docs",
    vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
)

embedder = TextEmbedding(model_name="BAAI/bge-m3")
texts = [chunk for chunk in load_chunks("corpus/")]  # 你的分块逻辑
vectors = list(embedder.embed(texts))

client.upsert(
    collection_name="docs",
    points=[PointStruct(id=i, vector=v.tolist(), payload={"text": t})
            for i, (v, t) in enumerate(zip(vectors, texts))],
)

3 个常见坑

  • `bge-m3` 输出 1024 维,`collection` 的 `size` 必须一致,否则写入时静默失败
  • `fastembed` 首次运行会下载约 2.3 GB ONNX 模型,建议挂代理或用 `BAAI/bge-small-en-v1.5`(384 维,更轻量)
  • `payload.text` 必须能放得下原始片段(Qdrant 单 payload 默认 40 KB 限制),长文档要先切片

四、LangGraph 编排:3 节点就够了


from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class RAGState(TypedDict):
    question: str
    docs: list[str]
    answer: str
    cited: bool

def retrieve(state):
    hits = client.search("docs", query_vector=embed_query(state["question"]), limit=5)
    return {"docs": [h.payload["text"] for h in hits]}

def answer(state):
    context = "\n\n".join(state["docs"])
    prompt = f"基于以下资料回答:\n{context}\n\n问题:{state['question']}"
    return {"answer": call_llm(prompt)}

def check_citation(state):
    return {"cited": "[1]" in state["answer"]}  # 简化版自检

builder = StateGraph(RAGState)
builder.add_node("retrieve", retrieve)
builder.add_node("answer", answer)
builder.add_node("check", check_citation)
builder.add_edge(START, "retrieve")
builder.add_edge("retrieve", "answer")
builder.add_edge("answer", "check")
builder.add_edge("check", END)

graph = builder.compile()
graph.invoke({"question": "LangGraph 怎么持久化状态?"})

五、关键点(实战总结)

  • **LangGraph 不是黑盒**——`StateGraph` 把每一步暴露成节点,`LangSmith`(或自己打日志)能直接看到每一步的 state diff
  • **Qdrant hybrid search** 比纯 dense 在中文长文档上提升显著,特别是「具体型号 / 缩写」类问题(BM25 sparse 救命)
  • **嵌入模型选型**:英文用 `bge-small-en-v1.5`(384 维,~30 MB),中英混排用 `bge-m3`(1024 维,~2.3 GB)
  • **自检循环很关键**——上面那个简化 `cited` 检查至少能拦截 30% 「LLM 自由发挥」的回答
  • **LangGraph 1.2 起原生支持时间旅行**:可以传 `thread_id` + `get_state()`,调试时直接回放任意中间状态

六、行业影响

本地 RAG 在 2026 年已经从「能不能跑」进入「跑得好不好」的阶段。LangGraph 解决的是「复杂流程可控」,Qdrant 解决的是「向量检索精度与速度」,两者组合是当下少数几个「不需要 GPU 集群就能演示给非技术 PM 看」的方案。Klarna、Replit、Elastic 出现在 LangGraph README 的客户名单里不是偶然——这些公司的核心问题是「agent 跑长流程会卡 / 会跑偏」,LangGraph 的有向图恰好是这种场景的最佳抽象。

七、写在最后

如果你只想要一个最简单的 RAG demo,pip install langchain + Chroma 30 行就够了;但如果你想让 RAG 变成「能调试、能分支、能复用工具」的工程系统,LangGraph + Qdrant 是当下少数几条「文档 / 工具 / 性能」都过关的路径。下次我们聊的,就是在这个基础上加 MCP 工具节点,把检索 + 外部 API 编排成完整的 Agent。

参考资料

官方文档

开源项目 / Release notes

行业报道

社区讨论

对比基准 / 实测


**本文由 AI 生成**。内容基于公开资料整理,可能存在事实偏差,引用链接请以原始来源为准。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注