之前我们实战介绍了openai规范的接口调用,说明了三种提示词消息(其实还有role为tool的第四种消息,本篇会涉及到,需要大家自己挖掘)的作用,跑通了一个简单智能体,可以回答基础问题。但是大模型可以回答的问题都是它训练时候的数据,实时性几乎是没有的,而且对于不知道的问题,还会产生很多“幻觉”,这个时候我们就需要使用RAG。

        RAG 的全称是 检索增强生成(Retrieval-Augmented Generation),目前是解决大模型(LLM)“幻觉”和“知识陈旧”问题最主流、最有效的技术方案。简单来说,RAG 就是给大模型配了一个随时可查询的“外挂硬盘”,让它从“死记硬背”变成“活学活用”。

        目的就是为了用我们的数据来帮助大模型更好的回答问题,而且很多数据都是我们企业内部才有的,需要我们传递大模型才能获取之后分析结合大模型推理能力,给出我们更准确的答案。

        而为什么我们要使用向量呢?向量工作原理核心就三步:把文字翻译成坐标 → 意思越近坐标越近 → 检索就是算坐标距离,简单给大家举几个例子:

关键词检索(傻搜字) 向量检索(靠意思捞) 真实对话对比
用户问“怎么退钱”,文档写“申请退款流程”,两个字不一样 → 搜不到 ❌ 把“退钱”和“退款”映射成坐标,发现它们在数学空间里挨着 → 直接命中 ✅ 用户:怎么退钱? 文档标题:《退款操作指南》 → 关键词死在这,向量一眼看穿是同一件事。
用户手滑打“兽后福务电话多少”,关键词找不到“售后”、“服务” → 0条结果 ❌ 提取整句话的语义方向,“兽后福务”和“售后服务”坐标方向一致 → 精准命中 ✅ 用户:兽后福务怎么联系? 文档:《售后服务联系方式》 → 向量能容忍错别字,关键词直接懵。
用户问“那个APP里充的钱没用完怎么搞”,文档标题是“虚拟账户余额退还操作指引”,没一个词对得上 → 搜了个寂寞 ❌ 算出来“充钱、没用完、怎么搞”的坐标,和“余额、退还、操作”的坐标离得近 → 命中 ✅ 用户说人话:钱没用完怎么搞? 文档写官话:《余额退还指引》 → 关键词觉得这是两个世界,向量觉得就是同一句话。
用户搜“笔记本电池不耐用”,文档是英文“Laptop battery life optimization” → 中文匹配英文,一个词都撞不上 → 完全无效 ❌ 向量模型把中英文映射到同一个空间,“电池不耐用”和“battery life”坐标重叠 → 把英文文档拽出来 ✅ 用户:电池不耐用怎么办? 文档:《Laptop battery life tips》 → 向量跨语言通杀
公司有10万份文档,关键词检索要逐份读标题,响应慢得像老太太过马路 ❌ 向量数据库提前把相似的文档分到同一个“圈子”里,检索时只搜附近的圈子 → 0.1秒出结果 ✅ 同样10万份文档,关键词要挨个翻门牌号,向量直接喊一嗓子“邻居们出来一下”,附近的全响应了。

        那么下面我们开始本篇的v1基础版最后的实战吧

        1.在.env文件中添加下面 ollama 的调用信息(调用之前我们安装的向量模型)

OLLAMA_BASE_URL=http://你的ollama部署的ip:11434
EMBEDDING_MODEL=qwen3-embedding:4b

        ollama如果不在开发的机器上无法localhost访问的话,windows可以设置:

        在“系统变量”区域,点击 “新建”,变量名填写:OLLAMA_HOST,变量值填写:0.0.0.0:11434

可以通过 http://ip:11434/api/tags  测试访问。

        2.config.py文件中添加(之前的代码要保留)(192.168.0.2是我本地ollama服务器ip,后面内容中如出现,大家知道是ollama就好,代码以你们自己的ip为准)

    # Ollama 远程配置
    OLLAMA_BASE_URL: str = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
    EMBEDDING_MODEL: str = os.getenv("EMBEDDING_MODEL", "qwen3-embedding")

        3.在终端中输入下列指令添加word和excel以及ollama依赖:

uv add python-docx openpyxl langchain-ollama

        4.添加 document_loader.py(统一文档解析器)

# @File  :  document_loader.py
# 导入操作系统接口模块,用于处理文件路径和扩展名
import os
# 从 typing 模块导入 List 类型,用于类型提示,指明函数返回字符串列表
from typing import List

# 从 pypdf 库导入 PdfReader,用于读取 PDF 文件
from pypdf import PdfReader
# 从 python-docx 库导入 Document 类(重命名为 DocxDocument),用于解析 Word 文档
from docx import Document as DocxDocument
# 从 openpyxl 库导入 load_workbook 函数,用于读取 Excel 文件
from openpyxl import load_workbook


def load_documents(file_path: str) -> List[str]:
    """
    根据文件后缀名自动选择解析器,返回文本片段列表
    """
    # 使用 os.path.splitext 分离文件名和扩展名,取扩展名并转为小写
    ext = os.path.splitext(file_path)[1].lower()
    # 初始化一个空列表,用于存储提取出的所有文本片段
    texts = []

    # 如果扩展名是 .pdf,使用 pypdf 解析
    if ext == ".pdf":
        # 创建 PdfReader 对象,读取 PDF 文件
        reader = PdfReader(file_path)
        # 遍历 PDF 的每一页
        for page in reader.pages:
            # 提取当前页的文本
            text = page.extract_text()
            # 如果提取的文本非空且去除首尾空白后仍有内容
            if text and text.strip():
                # 将去除首尾空白后的文本添加到列表中
                texts.append(text.strip())

    # 如果扩展名是 .docx,使用 python-docx 解析
    elif ext == ".docx":
        # 创建 Document 对象,加载 Word 文档
        doc = DocxDocument(file_path)
        # 遍历文档中的每一个段落
        for para in doc.paragraphs:
            # 如果段落文本非空且去除首尾空白后有内容
            if para.text and para.text.strip():
                # 将去除首尾空白后的段落文本添加到列表
                texts.append(para.text.strip())

    # 如果扩展名是 .xlsx,使用 openpyxl 解析
    elif ext == ".xlsx":
        # 以只读模式加载 Excel 工作簿,提高大文件读取性能
        wb = load_workbook(file_path, read_only=True)
        # 遍历工作簿中的每个工作表
        for sheet in wb.worksheets:
            # 按行迭代,只获取单元格的值(不包含格式)
            for row in sheet.iter_rows(values_only=True):
                # 将当前行所有非空单元格的值转为字符串,并用空格连接成一行文本
                row_text = " ".join([str(cell) for cell in row if cell is not None])
                # 如果拼接后的行文本去除首尾空白后有内容
                if row_text.strip():
                    # 添加到列表
                    texts.append(row_text.strip())
    # 如果扩展名不是上述任何一种,抛出异常提示不支持
    else:
        raise ValueError(f"不支持的文件格式: {ext}")

    # 如果最终解析出的文本列表为空,输出警告信息
    if not texts:
        print(f"⚠️ 警告: {file_path} 解析后为空,请检查文件内容")
    # 返回所有文本片段组成的列表
    return texts

        5.添加 embeddings.py (Ollama 嵌入 + 中文分块器)

# @File  :  embeddings.py
# 从 langchain_community.embeddings 导入 Ollama 嵌入模型类
# 该类用于通过 Ollama 服务将文本转换为向量(嵌入)
from langchain_ollama import OllamaEmbeddings

# 从 langchain_text_splitters 导入递归字符文本分割器
# 该分割器用于将长文本按指定分隔符递归切分成多个小块
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 从 config 模块导入 Config 类(通常包含项目配置,如 URL、模型名等)
from config import Config


def get_ollama_embeddings():
    """
    创建并返回一个配置好的 Ollama 嵌入模型实例
    该实例用于将文本转换为向量,供后续检索或相似度计算使用
    """
    # 打印提示信息,显示当前连接的 Ollama 服务地址(从 Config 中读取)
    print(f"🔗 连接远程 Ollama: {Config.OLLAMA_BASE_URL}")

    # 返回 OllamaEmbeddings 实例
    # model: 指定使用的嵌入模型名称(如 "nomic-embed-text")
    # base_url: 指定 Ollama 服务的 API 地址
    return OllamaEmbeddings(
        model=Config.EMBEDDING_MODEL,
        base_url=Config.OLLAMA_BASE_URL,
    )


def get_text_splitter():
    """
    创建并返回一个配置好的递归字符文本分割器
    该分割器用于将长文档切分为适合向量化或输入语言模型的短文本块
    """
    # 返回 RecursiveCharacterTextSplitter 实例
    # chunk_size: 每个文本块的最大字符数(500 字符)
    # chunk_overlap: 相邻块之间的重叠字符数(50 字符),用于避免语义断裂
    # separators: 优先级从高到低的分隔符列表,分割器会按顺序尝试切分
    #   - "\n\n": 段落分隔(最高优先级)
    #   - "\n": 换行
    #   - "。": 中文句号
    #   - "!": 感叹号
    #   - "?": 问号
    #   - ";": 分号
    #   - ",": 逗号
    #   - " ": 空格
    #   - "": 最后按字符切分(兜底)
    return RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=50,
        separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
    )

        6.添加 vector_store.py(ChromaDB向量数据库持久化 + 去重索引),持久化的数据会存在位于项目根目录下的 chroma_db 文件夹中

# @File  :  vector_store.py
# 从 langchain_chroma 导入 Chroma 向量数据库类,用于存储和检索向量
from langchain_chroma import Chroma

# 从当前目录的 embeddings 模块导入获取 Ollama 嵌入模型的函数
from embeddings import get_ollama_embeddings

# 导入 hashlib 库,用于生成文本的 MD5 哈希值作为唯一 ID
import hashlib

# 声明一个全局变量,用于缓存向量库实例(单例模式)
# 初始值为 None,表示尚未初始化
_vector_store = None


def get_vector_store(collection_name="docmind_kb"):
    """
    获取向量库实例(单例模式)
    如果尚未创建,则初始化一个 Chroma 实例并持久化到本地目录
    """
    # 声明要修改全局变量 _vector_store
    global _vector_store
    # 如果还未创建,则创建
    if _vector_store is None:
        # 实例化 Chroma 向量数据库
        _vector_store = Chroma(
            collection_name=collection_name,          # 集合名称,用于区分不同知识库
            embedding_function=get_ollama_embeddings(),  # 嵌入函数,将文本转为向量
            persist_directory="./chroma_db"           # 本地持久化存储目录
        )
    # 返回已存在的或新创建的向量库实例
    return _vector_store


def index_documents(texts: list[str]):
    """
    将文本列表存入向量库(自动基于 MD5 去重)
    参数 texts: 字符串列表,每个元素是一个文本块
    返回: 向量库实例或 None(若无文本)
    """
    # 如果传入的文本列表为空,打印警告并返回 None
    if not texts:
        print("⚠️ 没有文本可索引")
        return None

    # 获取向量库单例实例
    store = get_vector_store()
    # 为每个文本块生成 MD5 哈希值作为唯一 ID(基于文本内容)
    ids = [hashlib.md5(t.encode("utf-8")).hexdigest() for t in texts]

    # 查重:尝试从集合中获取已存在的这些 ID
    try:
        # _collection.get(ids=ids) 返回一个字典,包含 'ids' 键,值为存在的 ID 列表
        existing = store._collection.get(ids=ids)['ids']
    except:
        # 如果出错(例如集合不存在或接口异常),假定没有已存在的 ID
        existing = []

    # 筛选出不在已有列表中的新 ID
    new_ids = [id for id in ids if id not in existing]
    # 如果没有新 ID,说明全部已存在,跳过索引
    if not new_ids:
        print("⏭️ 所有文本块已存在于向量库,跳过索引")
        return store

    # 根据新 ID 对应的索引位置,提取对应的新文本(保持顺序一致)
    # 注意:ids.index(id) 是 O(n) 操作,但数据量不大时可接受
    new_texts = [texts[ids.index(id)] for id in new_ids]
    # 将新文本和对应的 ID 插入向量库
    store.add_texts(texts=new_texts, ids=new_ids)
    # 打印成功信息
    print(f"✅ 成功索引 {len(new_texts)} 个新文本块")
    # 返回更新后的向量库实例
    return store


def search_knowledge_base(query: str, k: int = 3):
    """
    检索接口(供 Agent 工具调用)
    根据查询文本在向量库中检索最相似的 k 个文本块
    参数 query: 查询字符串
    参数 k: 返回结果数量,默认为 3
    返回: 文档列表(LangChain Document 对象),若异常返回空列表
    """
    # 获取向量库实例
    store = get_vector_store()
    # 尝试执行相似度检索
    try:
        # 调用 similarity_search 进行检索,返回文档列表
        return store.similarity_search(query, k=k)
    except Exception as e:
        # 捕获任何异常并打印警告
        print(f"⚠️ 检索异常: {e}")
        # 返回空列表,避免调用方出错
        return []

        7.添加 main_agent_rag.py 用来执行程序(不和前两个main重复)

# @File  :  main_agent_rag.py
# 导入操作系统接口模块,用于文件和目录操作
import os

# 从 langchain_core.messages 中导入 AIMessage 类型,用于判断消息类型
from langchain_core.messages import AIMessage

# 从 langchain_openai 导入 ChatOpenAI 类,用于创建兼容 OpenAI API 的聊天模型客户端(此处连接 DeepSeek)
from langchain_openai import ChatOpenAI

# 从 langchain.agents 导入 create_agent 函数,用于创建能调用工具的智能体
from langchain.agents import create_agent

# 从 langchain.tools 导入 tool 装饰器,用于将普通函数声明为智能体可调用的工具
from langchain.tools import tool

# 从 langgraph.checkpoint.memory 导入 MemorySaver,用于在内存中保存对话状态(记忆)
from langgraph.checkpoint.memory import MemorySaver

# 从当前目录的 config 模块导入 Config 类,包含环境变量等配置
from config import Config

# 从 http_utils 模块导入 LoggingHttpClient,用于带日志记录的 HTTP 请求客户端
from http_utils import LoggingHttpClient

# 从 document_loader 模块导入 load_documents 函数,用于解析 PDF/Word/Excel 文件为文本列表
from document_loader import load_documents

# 从 embeddings 模块导入 get_text_splitter 函数,用于获取文本分割器实例
from embeddings import get_text_splitter

# 从 vector_store 模块导入 index_documents(索引文档)和 search_knowledge_base(检索知识库)函数
from vector_store import index_documents, search_knowledge_base

# 导入 Pydantic 的基类和 Field 工具
# 从 pydantic 库中导入 BaseModel 和 Field ,类似于 Java 中继承一个数据基类(比如 Record 或 Lombok 的 @Data),然后使用注解来定义字段的属性
from pydantic import BaseModel, Field

# ---------- 1. 配置与 LLM 初始化 ----------
# 调用 Config 类的 validate 方法,检查必要的环境变量是否设置完整
Config.validate()

# 创建 LoggingHttpClient 实例,设置超时时间 60 秒,用于发送 HTTP 请求并记录日志
http_client = LoggingHttpClient(timeout=60.0)

# 实例化 ChatOpenAI,配置为连接 DeepSeek API(兼容 OpenAI 接口)
llm = ChatOpenAI(
    model=Config.LLM_MODEL,  # 模型名称,从 Config 读取(如 "deepseek-chat")
    api_key=Config.DEEPSEEK_API_KEY,  # API 密钥,从 Config 读取
    base_url=Config.DEEPSEEK_BASE_URL,  # API 基础 URL,从 Config 读取
    temperature=0.7,  # 温度参数,控制生成文本的随机性(0~1)
    http_client=http_client  # 传入自定义 HTTP 客户端
)


# ---------- 2. 定义检索工具(必须用 @tool) ----------

# 定义一个“参数校验模型”类,继承自 BaseModel
# 这里的类名 SearchInput 相当于 Java 中定义一个 POJO(普通 Java 对象),
# 专门用来接收 Agent 调用工具时传入的参数。
class SearchInput(BaseModel):

    # 定义具体的字段 query,类型为字符串 str
    # : str 是类型注解(类似 Java 的泛型或类型声明,但 Python 是运行时提示)
    # 注意这里的写法:query: str = Field(...)
    # 这等价于 Java 中定义一个 private String query; 字段,
    # 然后用 @NotNull 或 @Size 等注解去描述它的约束或说明。
    query: str = Field(
        # description: 这是最核心的部分!
        # 这个描述字段不会用来校验“用户输入的字符串”是否合法,
        # 而是用来“告诉大模型(DeepSeek)这个参数是干嘛用的”。
        # 当 Agent 决定调用这个工具时,LLM 会读取这个 description 里的内容,
        # 从而知道它应该从用户的问题里提取什么信息来填入这个 query 字段。
        description="用户问题中的核心检索关键词,提取最相关的名词或短语"
    )

# 使用 @tool 装饰器,并指定参数校验模型
# @tool 是 LangChain 提供的“魔法开关”,它把下面的普通 Python 函数变成 LLM 能够理解并调用的“工具(Tool)”。
# args_schema=SearchInput 的意思是:
# “这个工具接受什么样的参数?请看 SearchInput 这个类定义。”
# 这样 LangChain 会自动把 SearchInput 的字段描述(description)
# 转换成 OpenAI(兼容 DeepSeek)的 function calling JSON Schema。
@tool(args_schema=SearchInput)
def query_knowledge_base(query: str) -> str:
    """
    从企业文档知识库中检索相关信息。
    当用户询问文档内容、报告数据、财报细节或任何内部资料时,必须优先调用此工具。
    """
    # 调用 vector_store 模块的 search_knowledge_base 函数,检索最相似的 3 个文档片段
    docs = search_knowledge_base(query, k=3)
    # 如果未检索到任何文档,返回提示信息
    if not docs:
        return "知识库中未找到相关信息,请先上传文档。"

    # 将所有检索到的文档内容用分隔符连接成一个上下文字符串
    context = "\n\n---\n\n".join([doc.page_content for doc in docs])
    # 返回包含上下文的字符串,供智能体参考
    return f"相关文档片段如下:\n{context}"


# ---------- 3. 启动时自动索引 ./docs 文件夹 ----------

def ingest_documents():
    """读取 ./docs 目录下的文档,解析、分块并索引到向量库"""
    # 定义文档目录为当前目录下的 docs 文件夹
    docs_dir = "./docs"
    # 如果该目录不存在,则创建它
    if not os.path.exists(docs_dir):
        os.makedirs(docs_dir, exist_ok=True)
        # 打印提示信息,引导用户放入文档
        print("📁 已创建 docs 文件夹,请放入 PDF/Word/Excel 文件后重启程序以自动索引。")
        return

    # 列出 docs 目录下所有以 .pdf、.docx、.xlsx 结尾的文件
    files = [f for f in os.listdir(docs_dir) if f.endswith((".pdf", ".docx", ".xlsx"))]
    # 如果没有符合条件的文件,提示并返回
    if not files:
        print("📂 docs 文件夹为空,请放入文档。")
        return

    # 初始化空列表,用于存放所有文档解析出的文本片段
    all_texts = []
    # 遍历每个文件
    for filename in files:
        # 拼接完整文件路径
        file_path = os.path.join(docs_dir, filename)
        # 打印正在解析的提示
        print(f"📄 正在解析: {filename}")
        # 调用 load_documents 函数解析文件,返回文本列表
        texts = load_documents(file_path)
        # 如果解析结果非空,将其扩展到 all_texts 列表
        if texts:
            all_texts.extend(texts)

    # 如果所有文件解析后都没有有效文本,提示并返回
    if not all_texts:
        print("⚠️ 未解析到有效文本内容")
        return

    # 获取文本分割器实例(配置 chunk_size=500, chunk_overlap=50)
    splitter = get_text_splitter()
    # 将 all_texts 中的所有段落用换行符连接成一个长字符串,然后进行分块
    chunks = splitter.split_text("\n".join(all_texts))
    # 打印切分后的文本块数量
    print(f"✂️ 共切分为 {len(chunks)} 个文本块")

    # 调用 index_documents 函数将分块后的文本索引到向量库(内部自动基于 MD5 去重)
    index_documents(chunks)


# ---------- 4. 创建 Agent(带记忆 + 检索工具) ----------

# 实例化 MemorySaver,用于在内存中保存对话检查点(实现多轮记忆)
memory = MemorySaver()

# 使用 create_agent 创建智能体
agent = create_agent(
    model=llm,  # 使用上面初始化的语言模型
    tools=[query_knowledge_base],  # 挂载检索工具(列表形式)
    system_prompt=(  # 系统提示,定义智能体的角色和行为规则
        "你是企业文档智能助手 DocMind。\n"
        "1. 回答问题时,优先使用 query_knowledge_base 工具检索内部知识库。\n"
        "2. 如果工具返回'未找到',请如实告知用户,不要编造信息。\n"
        "3. 回答要简洁、专业,基于检索到的内容进行总结。"
    ),
    checkpointer=memory  # 传入检查点存储器,启用对话记忆
)

# ---------- 5. 启动索引 + 对话循环 ----------

# 判断是否作为主程序运行(而非被导入)
if __name__ == "__main__":
    # 先执行索引函数,解析并索引 docs 文件夹中的文档(后续重启会基于 MD5 跳过已索引内容)
    ingest_documents()

    # 打印启动界面分隔线
    print("=" * 50)
    # 打印启动提示信息
    print("DocMind V1.0 已启动 (Agent + RAG + 多轮记忆)")
    # 打印退出指令
    print("输入 'exit' 退出")
    # 打印分隔线
    print("=" * 50)

    # 固定线程 ID,用于标识当前用户的对话会话
    thread_id = "user_001"

    # 进入无限循环,持续与用户交互
    while True:
        # 获取用户输入,并显示提示符 "你: "
        user_input = input("\n你: ")
        # 如果用户输入 exit(不区分大小写),跳出循环结束程序
        if user_input.lower() == "exit":
            break
        # 如果用户输入为空或仅空白字符,跳过本次循环,继续等待输入
        if not user_input.strip():
            continue

        # 调用智能体的 invoke 方法,传入用户消息和配置(包含 thread_id 以维持记忆)
        result = agent.invoke(
            {"messages": [{"role": "user", "content": user_input}]},
            config={"configurable": {"thread_id": thread_id}}
        )

        # 初始化变量用于存储最后一条 AI 消息
        last_ai_msg = None
        # 逆序遍历 result['messages'](最新消息在前)
        for msg in reversed(result['messages']):
            # 如果消息是 AIMessage 实例且内容非空,则记录下来并终止循环
            if isinstance(msg, AIMessage) and msg.content:
                last_ai_msg = msg
                break

        # 打印分隔线
        print("-" * 50)
        # 如果找到了有效的 AI 消息,打印其内容
        if last_ai_msg:
            print(f"🤖 DocMind: {last_ai_msg.content}")
        else:
            # 否则提示未获取到有效回复
            print("🤖 DocMind: (未获取到有效回复)")
        # 打印分隔线
        print("-" * 50)

        大家可以看到,我们在代码中有一个@tool并配合了pydantic,由于需要配合create_agent使用,我就把本来应该V2版本的内容(functioncall)加上了,作为rag的数据来源方法。

        8.在根目录下创建一个docs文件夹,并放入一个docmind.docx文件,内容如下:

DocMind 企业级智能文档助手 - 技术架构白皮书 (V1.0)

一、项目背景

DocMind 是一款面向企业的智能文档分析助手,旨在帮助业务人员快速从海量 PDF、Word 和 Excel 文件中提取关键信息。项目基于 Python 3.14 开发,采用最新的 LangChain 1.0 框架构建。

二、核心技术栈

1. 大语言模型(推理):使用 DeepSeek-v4-flash 作为核心推理引擎,通过兼容 OpenAI 的 API 接口进行调用。

2. 向量嵌入模型(检索):部署 Qwen3-embedding 模型,运行在局域网服务器(IP: 192.168.0.2)的 Ollama 服务上,用于将文档切片生成 4096 维的语义向量。

3. 向量数据库:采用 ChromaDB 进行本地持久化存储,所有向量数据保存在项目根目录的 ./chroma_db 文件夹下。

4. 智能体框架:基于 LangGraph 的 create_agent 标准接口构建,具备多轮对话记忆(MemorySaver)和自主工具调用能力。

三、核心性能指标

- 文档解析准确率:PDF 解析准确率 99.2%,Word 解析准确率 99.8%,Excel 解析准确率 97.5%。

- 检索平均延迟:在 500 个文本块(每块 500 字符)中检索 Top-3,平均耗时 280 毫秒(纯 CPU 计算)。

- 幻觉控制率:基于 RAG 的上下文生成,事实性幻觉率从纯 LLM 生成的 45% 降低至 3.7%。

四、后续版本规划

V2.0 将引入 Text-to-SQL 功能,允许用户通过自然语言查询企业 PostgreSQL 数据库;V3.0 将引入 HyDE 和 Rerank 高级检索策略;V4.0 将接入 LLM as Judge 生产级自愈机制。

 此时我们的项目结构如下(test.py是我自己创建的,与本项目无关):

        9.执行 main_agent_rag.py

        如果报错,可能是环境问题,需要安装Visual C++ Redistributable for Visual Studio 2015-2022(x64 版本)。

https://aka.ms/vs/17/release/vc_redist.x64.exe

        10.我们可以按照以下顺序提出问题:

        第一问(事实检索)
        这份白皮书里提到了哪些核心技术?

        第二问(精确数字)
        PDF 的解析准确率是多少?V2.0 要加什么功能?

        第三问(多轮记忆)
        刚才问的那些数据,对应的是哪个版本的规划?

        第四问(防幻觉测试)
        你觉得今天北京天气怎么样?

        大家去检验一下效果吧,本篇内容较多,代码也比较多,希望大家认真吸收,每行代码都加了详细注释,并且可以通过输出的日志,看一下请求的报文结构,结合之前的内容,大家自己动动手动动脑,不解之处欢迎留言,我看到后会解答,希望可以带给大家收获。

        例如看出:

完整 HTTP 调用链(以提问“这份白皮书的核心技术?”为例)

步骤 调用方(代码位置) 目标服务(IP/域名) 作用 是否必须
1 agent.invoke() 内部(LangGraph) DeepSeek API(第 1 次) 将用户问题 + 工具描述发给 DeepSeek,让它决策是否调用工具 ✅ 必须
2 search_knowledge_base() 内部(你的代码) Ollama 服务器(192.168.0.2) 将用户问题向量化(Qwen3-embedding),用于查询 ChromaDB ✅ 必须
3 agent.invoke() 内部(LangGraph) DeepSeek API(第 2 次) 将检索到的文档片段 + 用户问题,发给 DeepSeek 生成最终回答 ✅ 必须

       

        至此我们v1.0基础版本就已经开发完成了:

V1.0 基础版交付清单

模块 文件 状态
配置管理 config.py + .env ✅ 完成(os.getenv + load_dotenv
HTTP 调试日志 http_utils.py ✅ 完成(自定义 LoggingHttpClient
文档解析 document_loader.py ✅ 完成(PDF / Word / Excel)
嵌入 + 分块 embeddings.py ✅ 完成(Ollama + 向量模型)
向量库管理 vector_store.py ✅ 完成(ChromaDB 持久化 + MD5 去重)
数据模型 内嵌在main_agent_rag.py ✅ 完成(Pydantic SearchInput + Field
Agent + RAG 工具 main_agent_rag.py ✅ 完成(create_agent + MemorySaver + @tool(args_schema=...)
测试文档 ./docs/xxx.docx 或 .pdf ✅ 已提供内容(复制粘贴即可)
依赖管理 pyproject.tomluv ✅ 完成(含 langchain-ollamapydantic 等)

调用时序图:

模块依赖关系如下图:

        不过这个版本的RAG并不能完美支持企业级项目的检索要求,我会在V2版本开始之前,给大家补充一个强化版本的项目,并说明一下这种传统RAG的弊端以及解决方法,感谢大家这几天的支持,谢谢!!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐