LangChain实现简易版-----PDF 文档问答机器人
文档加载器 + 文本分割器 + PromptTemplate + LLM
原理(极简版,不学向量也能懂)
- 加载 PDF 全部文本
- 分割成多个语义完整文本块
- 用户提问 → 简单匹配最相关的文本块
- 把「相关文档片段 + 用户问题」塞进提示词
- 强制 LLM只能看给的文档片段回答,不准瞎编
第一步 安装依赖
pip install -U langchain langchain-openai langchain-community pypdf python-dotenv第二步 完整可运行代码(纯基础知识点,无 RAG 无向量库)
# 文档问答机器人 import os from dotenv import load_dotenv from langchain_core import documents from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter # 加载环境变量 load_dotenv() # ====================== 1. 配置 ================================= PDF_PATH = "Dubbo面试.pdf" CHUNK_SIZE = 1200 # 单个块最大的字符数(推荐1000 - 2000) CHUNK_OVERLAP = 200 # 相邻块的重叠字符数(推荐200 - 400) 保留上下文 # 初始化大模型 LLM llm = ChatOpenAI( api_key=os.getenv("QWEN_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", model='qwen3-max', # 必须用 pro模型,支持工具调用 temperature=0 ) # ======================2, 加载PDF ================================= def load_PDF(pdf_path: str): """ 加载本地 PDF 文档 :param pdf_path: PDF 文件路径 :return: Document 对象列表(每个元素是 PDF 的一页) """ print(f'📄 正在加载,PDF:{PDF_PATH}...') # 初始化PDF加载器 loader = PyPDFLoader(pdf_path) #加载PDF 每个元素 对应pdf一页 documents = loader.load() print(f'✅️PDF文档加载完成!共{len(documents)}页 \n') return documents # ======================= 3. 文本语义分割 =============================== def split_documents(documents): """ 用 RecursiveCharacterTextSplitter 进行语义完整的文本分割 :param documents: 加载后的 Document 对象列表 :return: 分割后的文本块列表 """ print("✂️ 正在进行语义完整的文本分割...") # ✅️ 核心:初始化 RecursiveCharacterTextSplitter (最推荐的分割器) text_splitter = RecursiveCharacterTextSplitter( chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP, # 分割优先级(按顺序,优先保留段落、句子) separators=["\n\n", "\n", "。", "!", "?", " ", ""], ) # 执行分割 split_chunks = text_splitter.split_documents(documents) print(f"✅ 文本分割完成!共生成 {len(split_chunks)} 个文本块\n") return split_chunks # ====================== 4. 简单的文本匹配 ========================== def get_relevant_chunk(question, chunks): """ 简易关键词匹配:找出和问题最相关的文档块 不用向量,纯字符串包含匹配 """ # 提取问题关键词 (简单按空格拆分) q_words = set(question.replace(",", " ").replace("。", " ").split()) print(f'q_words: {q_words}') best_chunk = None max_score = 0 for chunk in chunks: content = chunk.page_content # 统计命中关键词数量 hit = sum(1 for word in q_words if word in content) if hit > max_score: max_score = hit best_chunk = content # 没有任何匹配 返回空 if max_score == 0: return None return best_chunk # ============================ # 5,构建问答prompt + 普通chain ============================== # 强约束 只能用文档内容 不能编造 prompt = ChatPromptTemplate.from_template(""" 你是文档专属问答助手,**严格遵守以下规则**: 1. 只能依据【文档内容】回答用户问题 2. 文档里没有相关信息,直接回复:文档中没有相关内容 3. 绝对不能自己编造、不能使用外部知识 4. 回答简洁准确 【文档内容】 {context} 【用户问题】 {question} """) # 普通链式组装 chain = prompt | llm | StrOutputParser() # ===================== 6. 对话主循环 ========================================== def main(): print("===== 📚 简易PDF文档问答机器人(无向量库、无RAG)=====") print("正在加载并处理文档...\n") # 加载+分割 documents = load_PDF(PDF_PATH) chunks = split_documents(documents) print("🤖 文档加载完毕,可以开始提问,输入 q 退出\n") while True: question = input("你:").strip() if question.lower() == "q": print("🤖 再见!") break if not question: continue # 匹配相关文档片段 context = get_relevant_chunk(question, chunks) if not context: print("🤖:文档中没有相关内容\n") continue # 传入文档片段+问题,让LLM回答 ans = chain.invoke({ "context": context, "question": question }) print(f"🤖:{ans}\n") if __name__ == "__main__": main()第三步 .env 文件配置
DOUBAO_API_KEY=你的豆包密钥四、用到的知识点(全是你学过的)
- PyPDFLoader文档加载器
- RecursiveCharacterTextSplitter语义文本分割器
- ChatPromptTemplate提示词模板
- ChatOpenAI大模型调用
- LCEL 链式调用
| - StrOutputParser输出解析器
五、运行效果
- 问文档里有的内容 → 精准基于文档回答
- 问文档里没有的 → 自动回复:
文档中没有相关内容 - 不会瞎编、不会扯外面知识
===== 📚 简易PDF文档问答机器人(无向量库、无RAG)===== 正在加载并处理文档... 📄 正在加载,PDF:Dubbo面试.pdf... ✅️PDF文档加载完成!共15页 ✂️ 正在进行语义完整的文本分割... ✅ 文本分割完成!共生成 16 个文本块 🤖 文档加载完毕,可以开始提问,输入 q 退出 你:Dubbo 支持哪些协议,每种协议的应用场景,优缺点 q_words: {'支持哪些协议,每种协议的应用场景,优缺点', 'Dubbo'} 🤖:Dubbo 支持以下协议,每种协议的应用场景和优缺点如下: - **dubbo**:单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。传输协议 TCP,异步,Hessian 序列化。 - **rmi**:采用 JDK 标准的 rmi 协议实现,传输参数和返回参数对象需实现 Serializable 接口,使用 java 标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议 TCP。多个短连接,TCP 协议传输,同步传输,适用常规的远程服务调用和 rmi 互操作。在依赖低版本的 Common-Collections 包时,java 序列化存在安全漏洞。 - **webservice**:基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用。 - **http**:基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消费者,需要给应用程序和浏览器 JS 调用。 - **hessian**:集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为服务器时默认实现,提供与 Hessian 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件。 - **memcache**:基于 memcached 实现的 RPC 协议。 - **redis**:基于 redis 实现的 RPC 协议。 你:q 🤖 再见!🔴 先预判一下你的问题
这个报错ValueError: Invalid input type <class 'dict'>是 LangChain 里最经典的链结构错误,大概率是这 3 种情况之一:
直接把字典传给了大模型你可能写了
llm.invoke({"question": "xxx"}),但大模型只接受字符串或消息列表。链的顺序写错了你可能写了
{...} | llm,但正确的应该是prompt | llm。PromptTemplate 后面没接 llm,或者接错了你可能在链里混用了不同类型的 Runnable,导致输出格式不对。
