从十年 Java 到智能体开发:我的全套 Python 智能体实战课程(第二帖)6-rag与向量-v1基础版本完结
之前我们实战介绍了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_URLhttp://你的ollama部署的ip:11434 EMBEDDING_MODELqwen3-embedding:4bollama如果不在开发的机器上无法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-ollama4.添加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_onlyTrue) # 遍历工作簿中的每个工作表 for sheet in wb.worksheets: # 按行迭代只获取单元格的值不包含格式 for row in sheet.iter_rows(values_onlyTrue): # 将当前行所有非空单元格的值转为字符串并用空格连接成一行文本 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 texts5.添加embeddings.pyOllama 嵌入 中文分块器# 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( modelConfig.EMBEDDING_MODEL, base_urlConfig.OLLAMA_BASE_URL, ) def get_text_splitter(): 创建并返回一个配置好的递归字符文本分割器 该分割器用于将长文档切分为适合向量化或输入语言模型的短文本块 # 返回 RecursiveCharacterTextSplitter 实例 # chunk_size: 每个文本块的最大字符数500 字符 # chunk_overlap: 相邻块之间的重叠字符数50 字符用于避免语义断裂 # separators: 优先级从高到低的分隔符列表分割器会按顺序尝试切分 # - \n\n: 段落分隔最高优先级 # - \n: 换行 # - 。: 中文句号 # - : 感叹号 # - : 问号 # - : 分号 # - : 逗号 # - : 空格 # - : 最后按字符切分兜底 return RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , , , ] )6.添加vector_store.pyChromaDB向量数据库持久化 去重索引持久化的数据会存在位于项目根目录下的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_namedocmind_kb): 获取向量库实例单例模式 如果尚未创建则初始化一个 Chroma 实例并持久化到本地目录 # 声明要修改全局变量 _vector_store global _vector_store # 如果还未创建则创建 if _vector_store is None: # 实例化 Chroma 向量数据库 _vector_store Chroma( collection_namecollection_name, # 集合名称用于区分不同知识库 embedding_functionget_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(idsids) 返回一个字典包含 ids 键值为存在的 ID 列表 existing store._collection.get(idsids)[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(textsnew_texts, idsnew_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, kk) 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(timeout60.0) # 实例化 ChatOpenAI配置为连接 DeepSeek API兼容 OpenAI 接口 llm ChatOpenAI( modelConfig.LLM_MODEL, # 模型名称从 Config 读取如 deepseek-chat api_keyConfig.DEEPSEEK_API_KEY, # API 密钥从 Config 读取 base_urlConfig.DEEPSEEK_BASE_URL, # API 基础 URL从 Config 读取 temperature0.7, # 温度参数控制生成文本的随机性0~1 http_clienthttp_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_schemaSearchInput 的意思是 # “这个工具接受什么样的参数请看 SearchInput 这个类定义。” # 这样 LangChain 会自动把 SearchInput 的字段描述description # 转换成 OpenAI兼容 DeepSeek的 function calling JSON Schema。 tool(args_schemaSearchInput) def query_knowledge_base(query: str) - str: 从企业文档知识库中检索相关信息。 当用户询问文档内容、报告数据、财报细节或任何内部资料时必须优先调用此工具。 # 调用 vector_store 模块的 search_knowledge_base 函数检索最相似的 3 个文档片段 docs search_knowledge_base(query, k3) # 如果未检索到任何文档返回提示信息 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_okTrue) # 打印提示信息引导用户放入文档 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_size500, chunk_overlap50 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( modelllm, # 使用上面初始化的语言模型 tools[query_knowledge_base], # 挂载检索工具列表形式 system_prompt( # 系统提示定义智能体的角色和行为规则 你是企业文档智能助手 DocMind。\n 1. 回答问题时优先使用 query_knowledge_base 工具检索内部知识库。\n 2. 如果工具返回未找到请如实告知用户不要编造信息。\n 3. 回答要简洁、专业基于检索到的内容进行总结。 ), checkpointermemory # 传入检查点存储器启用对话记忆 ) # ---------- 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-2022x64 版本。https://aka.ms/vs/17/release/vc_redist.x64.exe10.我们可以按照以下顺序提出问题第一问事实检索这份白皮书里提到了哪些核心技术第二问精确数字PDF 的解析准确率是多少V2.0 要加什么功能第三问多轮记忆刚才问的那些数据对应的是哪个版本的规划第四问防幻觉测试你觉得今天北京天气怎么样大家去检验一下效果吧本篇内容较多代码也比较多希望大家认真吸收每行代码都加了详细注释并且可以通过输出的日志看一下请求的报文结构结合之前的内容大家自己动动手动动脑不解之处欢迎留言我看到后会解答希望可以带给大家收获。例如看出完整 HTTP 调用链以提问“这份白皮书的核心技术”为例步骤调用方代码位置目标服务IP/域名作用是否必须1agent.invoke()内部LangGraphDeepSeek API第 1 次将用户问题 工具描述发给 DeepSeek让它决策是否调用工具✅ 必须2search_knowledge_base()内部你的代码Ollama 服务器192.168.0.2将用户问题向量化Qwen3-embedding用于查询 ChromaDB✅ 必须3agent.invoke()内部LangGraphDeepSeek API第 2 次将检索到的文档片段 用户问题发给 DeepSeek生成最终回答✅ 必须至此我们v1.0基础版本就已经开发完成了V1.0 基础版交付清单模块文件状态配置管理config.py.env✅ 完成os.getenvload_dotenvHTTP 调试日志http_utils.py✅ 完成自定义LoggingHttpClient文档解析document_loader.py✅ 完成PDF / Word / Excel嵌入 分块embeddings.py✅ 完成Ollama 向量模型向量库管理vector_store.py✅ 完成ChromaDB 持久化 MD5 去重数据模型内嵌在main_agent_rag.py✅ 完成PydanticSearchInputFieldAgent RAG 工具main_agent_rag.py✅ 完成create_agentMemorySavertool(args_schema...)测试文档./docs/xxx.docx或.pdf✅ 已提供内容复制粘贴即可依赖管理pyproject.tomluv✅ 完成含langchain-ollama、pydantic等调用时序图模块依赖关系如下图不过这个版本的RAG并不能完美支持企业级项目的检索要求我会在V2版本开始之前给大家补充一个强化版本的项目并说明一下这种传统RAG的弊端以及解决方法感谢大家这几天的支持谢谢