文档加载器 + 文本分割器 + 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,导致输出格式不对。