从最小 Agent 到向量化 RAG

从最小 Agent 到向量化 RAG

前言

上一篇文章《从零理解 LangChain Agent:一篇写给初学者的入门指南》主要解决的是:

  • LangChain 是什么
  • Agent 是什么
  • Tool 是怎么接进去的
  • 一个最小智能体应该怎么搭

当你把最小 Agent 跑通以后,下一个非常自然的问题就是:

如果模型不知道我的本地资料、团队文档和私有知识,该怎么办?

这就是 RAG 出场的地方。

本文配套项目地址:

如果你是下面这几类读者,这篇文章会比较适合你:

  • 已经跑通过最小 LangChain Agent,准备进入 RAG
  • 知道“向量检索”这个词,但还不清楚完整调用链路
  • 想做一个本地知识库问答 demo,但不想一上来堆太多组件
  • 想知道 demo 代码和生产方案之间差了哪些关键环节

这篇文章会继续沿着当前这个项目往下讲,重点回答这些问题:

  • RAG 的原理到底是什么
  • 一个最小可运行的 RAG 流程长什么样
  • 我们这个 demo 里到底用了哪些三方框架和库
  • 从 demo 走向生产,还可能接哪些更正式的组件
  • “命中向量后,内容是怎么传回 Agent 的”

如果说 Agent 解决的是“模型如何调用外部能力”,那么 RAG 解决的就是:

模型如何基于你自己的知识来回答问题。

你可以把这篇文章的目标理解成一句话:

把“文档是怎么变成模型回答依据的”这条链路讲清楚。


导读

如果你想先抓住重点,再慢慢往下看,可以先记住这 4 句话:

  1. RAG 不是训练模型,而是先找资料再回答
  2. 向量检索命中的不是“答案”,而是“证据片段”
  3. 真正回流给模型的不是向量,而是整理后的 ToolMessage
  4. 一个最小 RAG 系统,本质上就是:文档 -> 切分 -> embedding -> 向量库 -> 检索 -> 回答

在这里插入图片描述

一、什么是 RAG

RAG,全称是 Retrieval-Augmented Generation,中文通常翻译为“检索增强生成”。

它解决的问题很直接:

  • 模型有通用知识
  • 但模型默认不知道你的本地文档
  • 也不知道你的业务资料、团队知识、项目笔记

所以在很多实际场景里,单纯把问题发给模型是不够的。

这时候就需要先做一层“检索”:

  1. 先从知识库中找出和问题最相关的内容
  2. 再把这些内容交给模型组织答案

所以 RAG 的本质不是“让模型变聪明”,而是:

在回答前,先把合适的资料拿给模型看。


二、RAG 和微调有什么区别

这是很多初学者非常容易混淆的点。

1. RAG

RAG 的思路是:

  • 不改模型参数
  • 不重新训练模型
  • 只是先检索,再把资料喂给模型

适合:

  • 企业知识库问答
  • 项目文档助手
  • 本地笔记搜索
  • 法务、客服、运维文档问答

2. 微调

微调的思路是:

  • 在模型参数层面做训练
  • 改变模型的行为模式或风格

更适合:

  • 固定输出格式
  • 特定语气风格
  • 特定任务偏好
  • 领域行为适配

3. 一句话区分

你可以这样记:

  • RAG:给模型补资料
  • 微调:给模型改习惯

大部分“知识问答类”场景,第一反应通常都应该先考虑 RAG,而不是一上来做微调。


三、RAG 的核心原理

从原理上说,一个 RAG 系统通常包含两段:

1. 索引阶段

把原始资料变成“可检索”的形式。

常见步骤包括:

  • 读取文档
  • 文本切分
  • 生成 embedding 向量
  • 存入向量库

2. 检索与生成阶段

当用户提问时:

  • 把用户问题也转成 embedding
  • 在向量库里找最相近的文本片段
  • 把这些片段交给模型
  • 模型基于这些片段生成最终回答

所以 RAG 的技术关键在于:

  • 文档怎么切
  • 向量怎么生成
  • 相似度怎么搜
  • 检索结果怎么组织给模型

四、当前 Demo 的完整调用链路

我们这次在项目里实现的是一个 最小向量化本地 RAG

它的调用链路可以概括为:

用户问题
-> LangChain create_agent
-> search_local_knowledge 工具
-> app.rag.search_knowledge
-> OpenAIEmbeddings(实际走智谱兼容接口)
-> InMemoryVectorStore.similarity_search
-> 命中若干 Document 分片
-> format_search_results
-> ToolMessage
-> 最终 AIMessage

也就是说:

  • 向量命中后,并不是把“向量本身”传回模型
  • 而是把命中的 Document 分片整理成文本
  • 再作为工具返回值交给 LangChain
  • LangChain 再把它包装成 ToolMessage
  • 最后模型基于 ToolMessage 生成面向用户的回答

这是理解 RAG 和 Agent 如何协作的关键。

如果再压缩成更好记的版本,就是这三句话:

  • Agent 负责决定“要不要查知识库”
  • RAG 负责把“相关资料片段”找出来
  • 模型负责把“资料片段”组织成最终答案

五、当前 Demo 的流程图

下面这张图基本就是当前项目里向量化 RAG 的真实执行过程:

用户提问

LangChain Agent

是否需要知识库检索?

模型直接回答

调用 search_local_knowledge 工具

读取 data 目录文档

文本切分为多个 chunks

OpenAIEmbeddings 生成向量

InMemoryVectorStore 建立/复用索引

similarity_search 召回相关 chunks

format_search_results 整理为证据文本

LangChain 包装成 ToolMessage

模型读取 ToolMessage

生成最终 AIMessage

如果你把这张图理解透了,RAG 的主流程就已经抓住了。


六、当前 Demo 里到底用了哪些三方框架和库

如果只看这次 RAG 主链路,核心相关的库其实不多,主要就是下面 4 类。

1. LangChain

LangChain 在这里负责“把 RAG 工具接进 Agent”。

主要承担的角色包括:

  • create_agent
  • Tool 调用
  • 消息链管理
  • ToolMessage 回流

你可以把它理解成:

负责把“模型、工具、消息”组织起来的应用层框架。

2. langchain_openai

这里主要用到了:

  • ChatOpenAI
  • OpenAIEmbeddings

它们分别负责:

  • 聊天模型调用
  • 文本向量化

虽然封装名里带 OpenAI,但当前项目实际接的是智谱兼容接口:

  • GLM-5.1
  • embedding-3

3. langchain_core

RAG 这一层最关键的两个对象来自这里:

  • Document
  • InMemoryVectorStore

其中:

  • Document 负责承载文本内容和来源信息
  • InMemoryVectorStore 负责保存向量并执行相似度检索

这两个对象基本就是当前 demo 的“最小 RAG 数据骨架”。

4. numpy

numpy 是向量检索里一个非常底层但必要的依赖。

在当前项目中,它主要用于:

  • 支撑 InMemoryVectorStore 的向量相似度计算

也就是说:

  • embedding 模型负责“生成向量”
  • numpy 负责“比较向量”

七、当前 Demo 的实现结构怎么理解

这次项目里的向量化 RAG 主要分布在三个地方。

1. app/rag.py

这是 RAG 的核心实现层。

它主要负责:

  • 列出知识库文件
  • 加载文档
  • 文本切分
  • 构建 embedding 模型
  • 构建内存向量库
  • 做相似度搜索
  • 格式化检索结果

你可以把它理解成:

“RAG 引擎层”

2. app/tools.py

这里把 RAG 包装成了 Agent 可调用工具:

  • list_knowledge_base_files()
  • search_local_knowledge()

也就是说,RAG 在 Agent 眼里并不是一个抽象系统,而是:

一个可以调用的 Tool。

3. app/main.py

这里负责:

  • 创建 Agent
  • 注册工具
  • 维护多轮对话消息
  • 打印 ToolMessage
  • 打印最终回答

所以主程序真正做的是:

把模型、RAG 工具和消息链路串起来。


补充:关键代码骨架长什么样

如果你想把“概念”快速落到“代码理解”上,最值得先看的其实是下面这三段。

1. 文档切分

这一步的目的,是把原始知识文件拆成更适合检索的小块。

def split_text_into_chunks(
    text: str,
    chunk_size: int = 400,
    chunk_overlap: int = 80,
) -> list[str]:
    chunks: list[str] = []
    start = 0

    while start < len(text):
        end = min(start + chunk_size, len(text))
        chunk = text[start:end].strip()
        if chunk:
            chunks.append(chunk)

        if end == len(text):
            break

        start = end - chunk_overlap

    return chunks

你不用一开始就纠结最优切分算法。
先理解:

  • 为什么要切分
  • 为什么要 overlap
  • chunk 大小会怎样影响召回

这三个问题更重要。

2. 构建 embedding 和向量库

这一步的目标,是把文本 chunk 变成可做相似度搜索的向量索引。

def build_embeddings_model() -> OpenAIEmbeddings:
    return OpenAIEmbeddings(
        model=os.getenv("EMBEDDING_MODEL", "embedding-3"),
        api_key=os.getenv("ZAI_API_KEY"),
        base_url=os.getenv("ZAI_BASE_URL"),
        dimensions=int(os.getenv("EMBEDDING_DIMENSIONS", "1024")),
    )


def build_vector_store() -> InMemoryVectorStore:
    documents = load_knowledge_documents()
    chunked_documents = split_documents_into_chunks(documents)
    embeddings = build_embeddings_model()

    vector_store = InMemoryVectorStore(embedding=embeddings)
    vector_store.add_documents(chunked_documents)
    return vector_store

这里要抓住的重点不是语法,而是职责划分:

  • Embeddings 负责把文本变成向量
  • VectorStore 负责保存向量并支持检索

3. 检索结果是如何回流给 Agent 的

这一步是很多人第一次学 RAG 时最容易模糊的地方。

def search_local_knowledge(query: str, max_results: int = 3) -> str:
    results = search_knowledge(query=query, max_results=max_results)
    return format_search_results(results)

这里看起来很短,但背后真正发生的是:

  • search_knowledge() 返回命中的 Document
  • format_search_results() 把它们整理成普通文本
  • 这段文本作为工具返回值被 LangChain 包装成 ToolMessage
  • 模型再基于这条 ToolMessage 继续回答

所以你可以把这段代码理解成:

把“向量检索结果”翻译成“模型能继续使用的上下文消息”。


八、为什么一定要文本切分

很多人第一次学 RAG 时会问:

为什么不直接把整篇文档拿去做检索?

原因很简单:

1. 整篇文档通常太长

长文会导致:

  • 检索粒度太粗
  • 命中不精准
  • 无关上下文太多

2. 小块更适合定位知识点

一个问题通常只需要命中:

  • 某个段落
  • 某几句定义
  • 某个步骤说明

而不是整篇文章。

3. 更利于后续拼接上下文

RAG 最后送给模型的上下文预算是有限的。

如果你每次都塞整篇文档:

  • 浪费上下文
  • 降低回答聚焦度
  • 成本更高

所以文本切分几乎是所有 RAG 系统的基础动作。


九、向量检索和关键词检索有什么不同

你这次项目升级里,最大的变化之一就是:

从“关键词匹配”升级成了“向量语义检索”。

1. 关键词检索

优点:

  • 容易理解
  • 容易调试
  • 不依赖 embedding 模型

缺点:

  • 更依赖字面重合
  • 对同义表达不够敏感
  • 中文场景下更容易受分词影响

2. 向量检索

优点:

  • 更能处理语义相近表达
  • 不要求用户问题和原文措辞一模一样
  • 更接近真实生产效果

缺点:

  • 需要 embedding 模型
  • 需要向量库
  • 调试成本更高

对学习顺序来说,最合理的路径通常是:

  1. 先做最小关键词检索,理解主流程
  2. 再升级成 embedding + 向量库

你当前这个项目,正好就走完了这条路线。


十、向量命中之后,是怎么传给 Agent 的

这是这次实现里最值得你真正吃透的一段。

很多人会误以为:

  • 模型直接看到向量
  • 或者模型直接访问向量库

其实都不是。

真实链路是:

第一步

similarity_search(...) 命中若干 Document

这些 Document 里包含:

  • 文本内容
  • 来源文件名
  • chunk 编号

第二步

把这些 Document 整理成一段普通文本

也就是类似这种结构:

[来源] langchain_rag.md (chunk #1)
[内容]
RAG 是 Retrieval-Augmented Generation...

第三步

工具函数把这段文本作为返回值交给 LangChain

第四步

LangChain 自动把它包装成 ToolMessage

第五步

模型继续读取这条 ToolMessage,生成最终回答

所以你一定要记住:

传回模型的不是“向量”,而是“向量检索命中的证据文本”。

这也是为什么我们后来又加了 ToolMessage 打印功能。

因为只有看见这条消息,你才会真正理解:

  • RAG 返回了什么
  • 模型是基于什么作答的

十一、生产环境里常见会用到哪些三方库或组件

当前项目是一个非常适合学习的最小实现,但如果走向生产,通常会逐步换成更正式的组件。

下面这些都是常见选择。

1. 向量数据库

当前 demo 用的是:

  • InMemoryVectorStore

生产里更常见的是:

  • Qdrant
  • Milvus
  • Weaviate
  • Pinecone
  • pgvector
  • Elasticsearch / OpenSearch
  • Redis Vector Search

它们通常提供:

  • 持久化存储
  • 更大规模数据支持
  • 过滤条件
  • 更好的查询能力
  • 分布式能力

2. 文档解析库

当前 demo 只读 .txt.md,但生产里常常要处理:

  • PDF
  • Word
  • HTML
  • 网页
  • 表格

常见会搭配的库包括:

  • pypdf
  • unstructured
  • beautifulsoup4
  • python-docx
  • pandas

3. 更正式的切分器

当前 demo 用的是手写固定窗口切分。

生产里更常见的是:

  • RecursiveCharacterTextSplitter
  • markdown-aware splitter
  • token-aware splitter

这些切分器通常更适合:

  • 保持段落结构
  • 控制 token 预算
  • 减少语义断裂

4. 重排与混合检索

很多生产系统不会只做单次向量检索。

常见增强包括:

  • 关键词检索 + 向量检索混合
  • reranker 重排
  • 多路召回

常见会接入:

  • bge-reranker
  • Cohere rerank
  • Elasticsearch BM25

5. 观测与调试

生产里你通常还会需要:

  • 调用链路追踪
  • 检索结果评估
  • 提示词调试
  • 成本与延迟观察

常见会考虑:

  • LangSmith
  • 自己的日志与埋点系统
  • APM / tracing 平台

十二、补充:如果你想快速自己做一个最小 RAG Demo

可以按下面这个顺序来,尽量不要一口气引入太多组件:

  1. 先只支持 .txt.md
  2. 先做固定窗口切分
  3. 先用一个 embedding 模型 + 一个最小向量库
  4. 先做 Top-K 相似度检索
  5. 最后再把结果包装成 Tool 返回给 Agent

换句话说,第一版不要着急上:

  • PDF 解析
  • 多向量库切换
  • 混合检索
  • rerank
  • LangGraph 多步骤流程

先把最短链路跑通,比一开始把系统做复杂更重要。


十三、当前 Demo 的局限在哪里

这一步也很重要,因为它能帮你区分:

哪些是学习阶段的“刻意简化”,哪些是以后要升级的地方。

1. 向量库存放在内存里

优点:

  • 简单
  • 易懂

缺点:

  • 进程结束就丢
  • 不适合大规模

2. 文档格式支持少

当前只支持:

  • .txt
  • .md

生产里通常远远不够。

3. 没有持久化索引

现在每次真正需要时会自动构建或重建索引。

更正式的方案通常会做:

  • 向量持久化
  • 增量更新
  • 重建策略

4. 没有 rerank

现在是“直接相似度搜索 -> 返回结果”。

生产里为了提高精度,经常会:

  • 先粗召回
  • 再精排

十四、如果你继续往下学,下一步应该是什么

如果你已经把这一版向量化 RAG 跑通,下一阶段我建议按这个顺序继续:

1. 先学持久化向量库

比如:

  • Qdrant
  • pgvector
  • Milvus

目标是理解:

  • 为什么 demo 版内存向量库不够
  • 生产里为什么需要持久化

2. 再学更正式的文档解析

把知识库来源从:

  • txt / md

扩展到:

  • PDF
  • Word
  • HTML

3. 再学混合检索和重排

目标是理解:

  • 召回不等于最终排序
  • RAG 精度往往靠检索策略细化出来

4. 最后再进入 LangGraph

当你开始想做这些时,再上 LangGraph:

  • 多步骤 RAG 流程
  • 人工审核
  • 多 Agent 协作
  • 可恢复状态流

结语

从学习路径上看,RAG 是 Agent 入门之后最值得尽快掌握的一步。

因为它会让你第一次真正接触到:

  • 模型之外的私有知识
  • 文档到检索的完整链路
  • 向量化和相似度搜索
  • ToolMessage 如何成为模型回答的依据

如果说最小 Agent 帮你理解的是:

模型如何调用外部能力

那么向量化 RAG 帮你理解的就是:

模型如何基于你的知识来回答问题

当你把这条链路真正跑通之后,你对大模型应用开发的理解会明显进入下一层。

所以这一步最重要的不是“先记住多少名词”,而是:

把文档加载、文本切分、向量检索、ToolMessage 回流、最终回答这整条链路看清楚。

这一步看清楚了,后面无论你做知识库助手、企业问答还是代码问答,都会顺很多。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐