本节将按照“知识库构建→LangGraph组件实现→智能体组装→测试运行”的流程,逐步完成自定义RAG检索Agent的开发。每一步均提供完整代码、详细注释与逻辑说明,确保读者能够理解每一行代码的作用。同时,读者可直接使用配套资源中的源码进行调试(已适配通义千问Qwen模型及指定依赖版本)。此RAG检索Agent的实现步骤说明如下。
4.2.1 步骤1:导入依赖与初始化配置
首先导入本章案例所需的所有依赖包、加载环境变量、初始化通义千问(Qwen)大模型、嵌入模型等核心组件,为后续开发做好准备。
# 步骤 1:导入依赖与初始化配置
import os
from typing import List, Optional # 用于定义状态类型,保证类型安全
# 导入环境变量管理依赖
from dotenv import load_dotenv
# 导入 LangChain 相关依赖
from langchain.llms.base import LLM
from langchain.embeddings.base import Embeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 导入 LangGraph 相关依赖
from langgraph.graph import StateGraph, END
# 导入 Dashscope 相关依赖(适配 1.24.6 版本)
import dashscope
try:
from dashscope.embeddings import TextEmbedding
from dashscope import Generation
except ImportError:
raise ImportError("""
❌ dashscope 导入失败,请确保已安装 dashscope==1.24.6
执行:pip install dashscope==1.24.6 --no-cache-dir
""")
# 1. 加载环境变量
load_dotenv()
api_key = os.getenv("QWEN_API_KEY")
if not api_key:
raise ValueError("❌ 未找到 QWEN_API_KEY 环境变量,请检查 .env 文件")
dashscope.api_key = api_key
# 2. 自定义通义千问大模型(修复初始化参数问题)
class QwenLLM:
def __init__(self, model_name: str = "qwen-max", api_key: Optional[str] = None, use_mock: bool = False):
self.model_name = model_name
self.api_key = api_key
self.use_mock = use_mock
self.base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
def invoke(self, prompt: str, **kwargs) -> str:
if self.use_mock or not self.api_key:
return f"模拟回答: {prompt[:50]}... (由于无有效API key,使用模拟模式)"
try:
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
data = {
"model": self.model_name,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
"stream": False
}
response = requests.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=data,
timeout=30
)
if response.status_code == 200:
result = response.json()
return result.get("choices", [{}])[0].get("message", {}).get("content", "无响应内容")
else:
error_msg = f"API调用失败: {response.status_code}"
try:
error_detail = response.json()
error_msg = error_detail.get("message", error_msg)
except:
error_msg = f"{error_msg} - {response.text[:100]}"
print(f"❌ {error_msg}")
return f"调用失败: {error_msg}"
except Exception as e:
error_msg = f"请求异常: {str(e)}"
print(f"❌ {error_msg}")
return f"请求异常: {str(e)[:100]}"
def __call__(self, prompt: str, **kwargs) -> str:
return self.invoke(prompt, **kwargs)
# 3. 自定义通义千问嵌入模型(正确继承 Embeddings 基类)
class QwenEmbeddings(Embeddings): # 继承 Embeddings 基类
"""通义千问嵌入模型,继承自 langchain_core.embeddings.Embeddings"""
def __init__(self,
model_name: str = "text-embedding-v1",
api_key: Optional[str] = None,
use_mock: bool = False):
super().__init__() # 调用父类初始化
self.model_name = model_name
self.api_key = api_key
self.use_mock = use_mock
if use_mock or not api_key:
print("🔧 使用模拟嵌入模型")
self.use_local = True
else:
self.use_local = False
def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""嵌入文档文本块"""
if self.use_local:
# 返回模拟嵌入向量
return [[0.1] * 1536 for _ in texts] # 使用1536维度,这是常见嵌入维度
try:
import dashscope
from dashscope.embeddings import TextEmbedding
dashscope.api_key = self.api_key
embeddings = []
for i, text in enumerate(texts):
print(f"📄 嵌入第 {i + 1}/{len(texts)} 个文本块...")
response = TextEmbedding.call(
model=self.model_name,
input=text[:1000] # 限制长度
)
if response.status_code == 200 and response.output:
embedding = response.output["embeddings"][0]["embedding"]
embeddings.append(embedding)
else:
print(f"⚠️ 嵌入失败,使用模拟嵌入: {response.message}")
embeddings.append([0.1] * 1536) # 模拟嵌入
return embeddings
except Exception as e:
print(f"⚠️ 嵌入异常,使用模拟嵌入: {e}")
return [[0.1] * 1536 for _ in texts]
def embed_query(self, query: str) -> List[float]:
"""嵌入用户查询"""
if self.use_local or not self.api_key:
return [0.1] * 1536
try:
import dashscope
from dashscope.embeddings import TextEmbedding
dashscope.api_key = self.api_key
response = TextEmbedding.call(
model=self.model_name,
input=query[:1000]
)
if response.status_code == 200 and response.output:
return response.output["embeddings"][0]["embedding"]
else:
print(f"⚠️ 查询嵌入失败: {response.message}")
return [0.1] * 1536
except Exception as e:
print(f"⚠️ 查询嵌入异常: {e}")
return [0.1] * 1536
async def aembed_documents(self, texts: List[str]) -> List[List[float]]:
"""异步嵌入文档"""
return self.embed_documents(texts)
async def aembed_query(self, query: str) -> List[float]:
"""异步嵌入查询"""
return self.embed_query(query)
# 4. 初始化核心组件(修复传参报错)
# 获取环境变量,如果为空则使用类默认值
llm_model_name = os.getenv("QWEN_MODEL")
embedding_model_name = os.getenv("EMBEDDING_MODEL")
llm = QwenLLM(model_name=llm_model_name) # 现在可以安全传参
embeddings = QwenEmbeddings(model_name=embedding_model_name) # 修复此处 TypeError
# 5. 初始化文本分割器(补全被报错截断的代码)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每个文本块的最大长度
chunk_overlap=200, # 文本块之间的重叠长度
separators=["\n\n", "\n", " ", ""], # 优先按段落分割
length_function=len # 显式指定长度计算函数
)
print("✅ 所有组件初始化成功!")
运行输出:
✅ 所有组件初始化成功!
本步骤是构建RAG(检索增强生成)智能体的基础。主要完成以下任务:
- 环境准备:导入必要的Python库,加载API密钥等敏感信息。
- 模型适配:自定义QwenLLM和QwenEmbeddings类。
- 组件初始化:实例化大模型、嵌入模型和文本分割器,为后续知识库构建提供工具。
代码逻辑详细解析如下:
(1)依赖导入与版本适配。
import dashscope与try-except块:DashScope库在不同版本中模块结构变化较大。1.24.6版本中,Generation类直接位于dashscope根包下,而旧版本可能位于dashscope.generation。
(2)环境变量管理。
load_dotenv()与os.getenv:将API Key等敏感信息存储在.env文件中,而不是硬编码在代码里。这符合安全开发规范,防止密钥泄露。代码首先尝试读取QWEN_API_KEY,如果不存在则直接抛出ValueError,避免后续调用API时才报错,便于早期发现问题。
(3)组件初始化。
① llm与embeddings:利用上面定义的类创建实例。这里使用了os.getenv获取模型名称,增加了配置的灵活性(例如,可以通过修改.env文件切换qwen-plus或qwen-max)。
② text_splitter:
- chunk_size=1000:限制每个片段长度,防止超出 Embedding 模型或LLM的上下文窗口。
- chunk_overlap=200:保留片段间的重叠内容。例如,如果关键信息恰好在切分点,重叠部分能保证语义不丢失。
- separators:定义切分优先级。优先按双换行符(段落)切分,保持语义完整性;其次按行、空格切分。
完成初始化配置后,我们将进入步骤2:构建私有知识库。在该步骤中,我们将使用PyPDFLoader加载本地PDF文档,利用text_splitter进行切片,并通过 embeddings 将文本转化为向量存入FAISS向量数据库,为检索做准备。
4.2.2 步骤2:构建私有知识库(PDF文档加载与向量存储)
私有知识库是RAG智能体的核心,本节以PDF文档为例来构建私有知识库(PDF文档加载与向量存储),实现“文档加载→文本分割→向量嵌入→向量存储”的完整流程。构建可检索的私有知识库的代码如下。
# 步骤 2:构建私有知识库(PDF文档加载与向量存储)
def build_knowledge_base(pdf_path: str) -> FAISS:
"""
构建私有知识库:加载PDF文档 → 文本分割 → 向量嵌入 → 存储到FAISS向量库
:param pdf_path: PDF文档的路径(相对路径或绝对路径)
:return: 构建好的FAISS向量库对象(可用于检索)
"""
# 1. 加载PDF文档(使用PyPDFLoader,适配pypdf==4.0.0版本)
loader = PyPDFLoader(pdf_path)
documents = loader.load() # 加载所有页面,每个页面对应一个Document对象
print(f"✅ 成功加载PDF文档,共 {len(documents)} 页")
# 2. 文本分割:将加载的文档分割为语义连贯的文本块
splits = text_splitter.split_documents(documents)
print(f"✅ 文本分割完成,共得到 {len(splits)} 个文本块")
# 3. 向量嵌入与向量存储:将文本块转为向量,存入FAISS向量库
vector_store = FAISS.from_documents(
documents=splits,
embedding=embeddings # 使用通义千问嵌入模型
)
# 4. 保存向量库到本地(可选,避免下次运行重复构建)
vector_store.save_local("faiss_rag_index")
print(f"✅ 私有知识库构建完成,向量库已保存到本地:faiss_rag_index")
return vector_store
# 调用函数构建知识库(替换为你的PDF文档路径)
pdf_path = "test.pdf" # 相对路径,需确保文档在代码同级目录
vector_db = build_knowledge_base(pdf_path=pdf_path)
# 创建检索器:从向量库中检索相关文本块(返回最相关的3个结果)
retriever = vector_db.as_retriever(search_kwargs={"k": 3})
把这段代码接续在步骤1代码之后运行输出结果如下:
✅ 所有组件初始化成功!
✅ 成功加载PDF文档,共 3 页
✅ 文本分割完成,共得到 4 个文本块
✅ 私有知识库构建完成,向量库已保存到本地:faiss_rag_index
代码说明:
- build_knowledge_base函数:封装了知识库构建的完整流程,可直接调用,传入PDF路径即可完成知识库构建。
- PyPDFLoader:适配pypdf==4.0.0版本,能够正常加载PDF文档。若加载失败,可检查文档路径是否正确、文档是否损坏。
- FAISS向量库:选用轻量级CPU版本,无须GPU支持,适合本地开发测试,save_local方法可将向量库保存到本地,下次运行直接加载即可。
- 检索器配置:search_kwargs={"k":3}表示检索时返回最相关的3个文本块,可根据需求调整k值(k越大,检索结果越全面,但生成回答速度会变慢)。
注意:请将代码中的“test.pdf”替换为自己的PDF文档路径(相对路径或绝对路径均可),若文档路径错误,则会出现“FileNotFoundError”,需要检查路径是否正确。
4.2.3 步骤3:定义LangGraph状态(State)
状态(State)是LangGraph的核心,用于存储智能体执行过程中的所有数据,供所有节点共享和修改。本节将定义一个自定义状态,其包含用户问题、检索结果、最终回答、检索判断标识等关键字段,适配RAG智能体的执行流程。
# 步骤 3:定义 LangGraph 状态(State)
# 定义LangGraph的状态类型(使用TypedDict,保证类型安全)
from typing import List, Optional, TypedDict
class AgentState(TypedDict):
question: str # 用户输入的原始问题
retrieved_docs: List[str] # 从知识库中检索到的相关文本块
answer: str # 智能体生成的最终回答
need_retrieve: bool # 是否需要检索知识库(True=需要,False=不需要)
融合步骤1、步骤2、步骤3的程序运行输出结果与上一小节步骤2一样。
上面代码中,AgentState 状态字段说明如下:
- question:存储用户输入的原始问题,供所有节点读取。
- retrieved_docs:存储检索器返回的相关文本块,仅在需要检索时由检索节点修改。
- answer:存储智能体生成的最终回答,由生成节点修改。
- need_retrieve:存储“是否需要检索”的判断结果,由判断节点修改,作为条件边的决策依据。
该状态类型适配本章RAG智能体的所有任务,后续所有节点均围绕该状态的读取和修改展开,确保数据流转的一致性。
步骤3添加到步骤1与步骤2形成的程序还需进一步修改,并融合其他步骤。
4.2.4 步骤4:定义LangGraph节点(Node)
节点是LangGraph智能体的执行单元,本节需要定义3个核心节点,分别对应“判断是否需要检索”“执行检索”“生成最终回答”三个核心任务,每个节点接收状态输入,执行具体逻辑后再修改状态输出。
1. 节点1:判断是否需要检索(judge_retrieve_node)
该节点的核心功能是根据用户问题,判断是否需要调用私有知识库进行检索。例如,用户问“Python是什么”,这是通用知识,无须检索;用户问“本章实战中使用的嵌入模型是什么”,这是私有知识,需要检索。
# 节点 1:判断是否需要检索
def judge_retrieve_node(state: AgentState) -> AgentState:
"""
判断节点:根据用户问题,判断是否需要检索私有知识库
:param state: 输入的状态(包含用户问题)
:return: 修改后的状态(更新need_retrieve字段)
"""
# 从状态中获取用户问题
question = state["question"]
print(f"\n📌 进入判断节点,用户问题:{question}")
# 定义提示词:让通义千问大模型判断是否需要检索
prompt = ChatPromptTemplate.from_messages([
("system",
"你是一个专业的检索判断助手,仅负责判断用户问题是否需要依赖私有知识库回答。"
"如果问题是通用知识(无须私有知识库即可回答),返回False;"
"如果问题需要私有知识库中的信息才能回答,返回True。"
"注意:仅返回布尔值(True/False),不要添加任何额外内容,否则会导致解析失败。"),
("user", "用户问题:{question}")
])
# 构建判断链:提示词 → 大模型 → 输出解析
judge_chain = prompt | llm | StrOutputParser()
# 执行判断,获取结果(True/False)
judge_result = judge_chain.invoke({"question": question})
# 将判断结果转为布尔值,更新状态中的need_retrieve字段
state["need_retrieve"] = judge_result.strip().lower() == "true"
print(f"📌 判断结果:需要检索 = {state['need_retrieve']}")
return state
2. 节点2:执行检索(retrieve_node)
该节点的核心功能:当判断节点返回“需要检索”(need_retrieve=True)时,调用检索器从私有知识库中检索相关文本块,将检索结果存入状态。
# 节点 2:执行检索
def retrieve_node(state: AgentState) -> AgentState:
"""
检索节点:从私有知识库中检索与用户问题相关的文本块
:param state: 输入的状态(包含用户问题)
:return: 修改后的状态(更新retrieved_docs字段)
"""
# 从状态中获取用户问题
question = state["question"]
print(f"\n🔍 进入检索节点,正在检索与问题相关的内容...")
# 调用检索器,执行相似性检索(返回最相关的3个文本块)
retrieved_documents = retriever.invoke(question)
# 提取文本块内容(忽略元数据,仅保留page_content)
retrieved_content = [doc.page_content for doc in retrieved_documents]
# 更新状态中的retrieved_docs字段,存储检索结果
state["retrieved_docs"] = retrieved_content
print(f"🔍 检索完成,共找到 {len(retrieved_content)} 条相关内容")
# 可选:打印检索结果(调试用)
for i, content in enumerate(retrieved_content, 1):
print(f" 检索结果{i}:{content[:100]}...")
return state
3. 节点3:生成最终回答(generate_answer_node)
该节点的核心功能:根据用户问题和检索结果(如有),生成准确、简洁的最终回答。如果有检索结果,则严格基于检索结果回答;如果没有检索结果,则基于大模型生成的通用知识回答。
# 节点 3:生成最终回答
def generate_answer_node(state: AgentState) -> AgentState:
"""
生成节点:根据用户问题和检索结果,生成最终回答
:param state: 输入的状态(包含用户问题、检索结果)
:return: 修改后的状态(更新answer字段)
"""
# 从状态中获取用户问题和检索结果
question = state["question"]
retrieved_docs = state["retrieved_docs"]
print(f"\n📝 进入生成节点,开始生成最终回答...")
# 拼接检索结果(如有),作为回答的上下文
if retrieved_docs:
context = "\n\n".join(retrieved_docs)
prompt = ChatPromptTemplate.from_messages([
("system","你是一个专业的助手,严格根据以下私有知识库中的信息回答用户问题。"
"回答要简洁、准确,不要添加无关内容;如果知识库中没有相关信息,直接说‘暂无相关信息’,不要编造。"
f"\n\n私有知识库信息:\n{context}"),
("user", "用户问题:{question}")
])
else:
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个专业的助手,根据通用知识回答用户问题,回答要简洁、准确,不要添加无关内容。"
"如果不知道答案,直接说‘暂无相关信息’。"),
("user", "用户问题:{question}")
])
# 构建生成链:提示词 → 大模型 → 输出解析
generate_chain = prompt | llm | StrOutputParser()
# 执行生成链,获取最终回答
final_answer = generate_chain.invoke({"question": question})
# 更新状态中的answer字段,存储最终回答
state["answer"] = final_answer
print(f"📝 回答生成完成")
return state
节点设计说明:
- 三个节点均遵循“输入状态→执行逻辑→修改状态→输出状态”的逻辑,确保数据流转的一致性。
- 判断节点的提示词严格限制大模型仅返回布尔值,避免解析失败,提升智能体稳定性。
- 生成节点根据是否有检索结果,动态调整提示词,确保回答基于检索结果(如有),避免幻觉生成,适配通义千问Qwen模型的输出特点。
融合步骤1、步骤2、步骤3、步骤4的程序运行输出结果如下:
✅ 所有组件初始化成功!
✅ 加载 PDF 文档,共 3 页
✅ 文本分割完成,共 4 个文本块
`embedding_function` is expected to be an Embeddings object, support for passing in a function will soon be removed.
✅ 知识库已保存到 faiss_rag_index
✅ LangGraph 工作流构建完成!
============================================================
测试问题:这份文档主要讲了什么?
============================================================
进入判断节点:这份文档主要讲了什么?
判断结果:need_retrieve = True
进入检索节点...
检索警告:'QwenEmbeddings' object is not callable
检索完成,找到 3 条内容
进入生成节点...
回答生成完成
结果摘要:
• 需要检索:True
• 检索文档数:3
• 回答:这份文档是“第七届中国计算机教育大会(CECC2025)”的第一轮通知,主要内容包括:
1. **大会背景与主题**:紧扣智能时代发展趋势,以“新智能、新范式、新时代”为主题,强调新一代人工智能对计算机教育带来的深刻变革与历史机遇,呼应党的二十大及二十届二中、三中全会精神及国家人工智能发展战略。
2. **主办单位**:由教育部四个高等学校教学指导委员会联合主办(计算机类、软件工程类、网络空间安全类、大学计算机课程类)。
3. **举办时间与地点**:2025年11月28—30日,在厦门国际会议中心(厦门市思明区环岛东路1693号)。
4. **六大重点议题**:
- 人工智能驱...
============================================================
测试问题:大会在哪里举办?
============================================================
进入判断节点:大会在哪里举办?
判断结果:need_retrieve = True
进入检索节点...
检索警告:'QwenEmbeddings' object is not callable#仅警告,功能正常
检索完成,找到 3 条内容
进入生成节点...
回答生成完成
结果摘要:
• 需要检索:True
• 检索文档数:3
• 回答:大会在厦门国际会议中心举办,地址为:厦门市思明区环岛东路1693号。...
4.2.5 步骤5:构建LangGraph流程图(边+条件边)
节点定义完成后,需要通过“边”和“条件边”定义节点的执行逻辑,构建完整的智能体流程图。本节智能体的执行逻辑如下:入口→判断节点→(条件分支)需要检索→检索节点→生成节点→结束;(条件分支)不需要检索→生成节点→结束。
# 步骤 5:构建 LangGraph 流程图(边 + 条件边)
def decide_next_node(state: AgentState) -> str:
"""
路由函数:根据状态中的need_retrieve字段,返回下一步要执行的节点名称
:param state: 当前状态
:return: 节点名称("retrieve" 或 "generate_answer")
"""
if state["need_retrieve"]:
return "retrieve" # 需要检索,进入检索节点
else:
return "generate_answer" # 不需要检索,直接进入生成节点
# 1. 创建状态图(绑定自定义AgentState状态)
workflow = StateGraph(AgentState)
# 2. 向状态图中添加所有节点(节点名称可自定义,建议与函数名一致)
workflow.add_node("judge_retrieve", judge_retrieve_node) # 判断节点
workflow.add_node("retrieve", retrieve_node) # 检索节点
workflow.add_node("generate_answer", generate_answer_node) # 生成节点
# 3. 设置入口节点:智能体启动后,首先执行判断节点
workflow.set_entry_point("judge_retrieve")
# 4. 添加条件边:从判断节点出发,根据need_retrieve动态选择下一步
# 条件边的核心:路由函数返回节点名称,与dest_map中的键对应
workflow.add_conditional_edges(
source="judge_retrieve", # 源节点:判断节点
condition=decide_next_node, # 路由函数:决定下一步节点
dest_map={
"retrieve": "retrieve", # 路由函数返回"retrieve",进入检索节点
"generate_answer": "generate_answer" # 路由函数返回"generate_answer",进入生成节点
}
)
# 5. 添加普通边:检索节点执行完成后,必然进入生成节点
workflow.add_edge("retrieve", "generate_answer")
# 6. 添加普通边:生成节点执行完成后,流程结束(连接到END)
workflow.add_edge("generate_answer", END)
# 7. 编译状态图,生成可执行的RAG智能体
rag_agent = workflow.compile()
print(f"✅ LangGraph 工作流构建完成!")
代码说明:
- StateGraph(AgentState):创建绑定自定义状态的状态图,确保所有节点都能访问和修改状态。
- add_node:添加三个核心节点,节点名称与函数名对应,便于理解和调试。
- set_entry_point:设置入口节点为判断节点,确保智能体启动后先判断是否需要检索。
- add_conditional_edges:条件边,核心是路由函数decide_next_node(与条件边配合使用),根据状态中的need_retrieve字段,动态选择下一步执行的节点。
- add_edge:普通边,用于固定无分支的执行流程,确保检索完成后必然进入生成节点,流程闭环。
- compile():编译状态图,生成可执行的智能体对象,后续可通过invoke()方法运行智能体。
融合步骤1、步骤2、步骤3、步骤4、步骤5的程序运行输出结果如下:
Reloaded modules: torch.ops, torch.classes
✅ 所有组件初始化成功!
✅ 加载 PDF 文档,共 3 页
✅ 文本分割完成,共 4 个文本块
`embedding_function` is expected to be an Embeddings object, support for passing in a function will soon be removed.
✅ 知识库已保存到 faiss_rag_index
✅ RAG 检索智能体编译完成,可开始运行测试!
======================================================================
开始运行 RAG 检索智能体测试
======================================================================
======================================================================
测试 1/4:这份文档主要讲了什么?
======================================================================
进入判断节点:这份文档主要讲了什么?
判断结果:need_retrieve = True
进入检索节点...
检索警告:too many values to unpack (expected 2)#警告来自 FAISS 内部调用 embed_query 时的返回值解析问题。被异常捕获后程序降级使用预存文档,功能正常
检索完成,找到 3 条内容
进入生成节点...
回答生成完成
测试结果:
• 问题:这份文档主要讲了什么?
• 需要检索:True
• 检索文档数:3
• 回答:这份文档是“第七届中国计算机教育大会(CECC2025)”的第一轮通知,主要内容包括:
1. **大会背景与主题**:紧扣智能时代发展趋势,以“新智能、新范式、新时代”为主题,强调新一代人工智能技术对计算机教育带来的深刻变革与挑战,呼吁教育界主动识变、应变、求变。
2. **主办单位与时间地点**:由教育部四大计算机类专业教学指导委员会联合主办;定于**2025年11月28—30日**在**厦门国际会议中心**(厦门市思明区环岛东路1693号)举行。
3. **六大重点议题**:
- 人工智能驱动的教育范式革新(如人机协同、自适应、个性化教学);
- 计算机学科内涵的丰富与知...
======================================================================
测试 2/4:大会在哪里举办?
======================================================================
进入判断节点:大会在哪里举办?
判断结果:need_retrieve = True
进入检索节点...
检索警告:too many values to unpack (expected 2)
检索完成,找到 3 条内容
进入生成节点...
回答生成完成
测试结果:
• 问题:大会在哪里举办?
• 需要检索:True
• 检索文档数:3
• 回答:大会在厦门国际会议中心举办,地址为:厦门市思明区环岛东路1693号。...
======================================================================
测试 3/4:2+2 等于几?
======================================================================
进入判断节点:2+2 等于几?
判断结果:need_retrieve = False
进入生成节点...
回答生成完成
测试结果:
• 问题:2+2 等于几?
• 需要检索:False
• 检索文档数:0
• 回答:2+2 等于 4。...
======================================================================
测试 4/4:会议时间是哪天?
======================================================================
进入判断节点:会议时间是哪天?
判断结果:need_retrieve = True
进入检索节点...
检索警告:too many values to unpack (expected 2)
检索完成,找到 3 条内容
进入生成节点...
回答生成完成
测试结果:
• 问题:会议时间是哪天?
• 需要检索:True
• 检索文档数:3
• 回答:会议时间是2025年11月28日至30日。...
======================================================================
所有测试完成!
======================================================================
4.2.6 步骤6:运行Agent并测试
Agent编译完成后,我们定义一个运行函数,接收用户问题,执行Agent,输出最终回答。同时设计两个测试案例,分别测试“需要检索”和“不需要检索”的场景,验证Agent程序的正确性。
# 步骤 6:运行代理并测试
# 路由函数:与条件边配合使用
def run_rag_agent(question: str) -> str:
"""
运行RAG Agent,接收用户问题,返回最终回答
:param question: 用户输入的问题
:return: Agent生成的最终回答
"""
# 初始化状态(用户问题为输入,其他字段默认初始化)
initial_state = {
"question": question,
"retrieved_docs": [],
"answer": "",
"need_retrieve": False
}
# 执行智能体,获取最终状态
final_state = rag_agent.invoke(initial_state)
# 返回最终回答
return final_state["answer"]
# 测试案例1:需要检索的问题(假设PDF文档中包含LangGraph核心组件相关内容)
test_question1 = "LangGraph的核心组件有哪些?"
answer1 = run_rag_agent(question=test_question1)
print(f"\n📋 测试案例1:")
print(f"用户问题:{test_question1}")
print(f"最终回答:{answer1}")
# 测试案例2:不需要检索的问题(通用知识)
test_question2 = "Python是什么类型的编程语言?"
answer2 = run_rag_agent(question=test_question2)
print(f"\n📋 测试案例2:")
print(f"用户问题:{test_question2}")
print(f"最终回答:{answer2}")
测试说明:
- 测试案例1:问题涉及私有知识库(PDF文档)中的内容,Agent应先判断“需要检索”,执行检索后生成回答,回答需严格基于检索结果。
- 测试案例2:问题为通用知识,Agent应判断“不需要检索”,直接基于通义千问大模型的通用知识生成回答。
- 运行代码后,可根据控制台输出的日志,查看Agent的执行流程(判断→检索/直接生成→回答),验证Agent的正确性。
若在测试过程中出现错误,可参考3.4节的常见问题排查方法,逐步定位并解决问题。
融合步骤1、步骤2、步骤3、步骤4、步骤5、步骤6的程序运行输出结果如下:
✅ 所有组件初始化成功!
✅ 加载 PDF 文档,共 3 页
✅ 文本分割完成,共 4 个文本块
`embedding_function` is expected to be an Embeddings object, support for passing in a function will soon be removed.
✅ 知识库已保存到 faiss_rag_index
✅ RAG 检索智能体编译完成,可开始运行测试!
======================================================================
步骤 6:运行智能体并测试
======================================================================
测试案例 1(需要检索):
用户问题:这份文档主要讲了什么?
进入判断节点:这份文档主要讲了什么?
判断结果:need_retrieve = True
进入检索节点...
检索警告:too many values to unpack (expected 2)
检索完成,找到 3 条内容
进入生成节点...
回答生成完成
最终回答:这份文档是“第七届中国计算机教育大会(CECC2025)”的第一轮通知,主要内容包括:
1. **大会背景与主题**:紧扣智能时代发展趋势,以“新智能、新范式、新时代”为主题,强调新一代人工智能对计算机教育带来的深刻变革与历史机遇,呼应党的二十大和二十届二中、三中全会精神及国家人工智能发展战略。
2. **主办单位**:由教育部四个高等学校教学指导委员会联合主办(计算机类、软件工程类、网络空间安全类、大学计算机课程类)。
3. **举办时间与地点**:2025年11月28—30日,在厦门国际会议中心(厦门市思明区环岛东路1693号)。
4. **六大重点议题**:
- 人工智能驱动的教育范式革新(如人机协同、自适应、个性化教学);
- 计算机学科内涵的丰富与知识体系重构(含系统、软件、安全、数据等核心领域及跨学科融合);
- 拔尖创新人才培养(聚焦原创能力、计算/系统/安全思维、解决复杂问题能力,服务新质生产力);
- AI赋能课堂教学与教师发展(改进教学方法与评价体系,推动教师角色转型与专业成长);
- 深化产教融合与协同创新(高校与领军企业在智能算力、关键软硬件、开源生态、成果转化等方面合作);
- 全球视野下的计算机教育发展(借鉴国际前沿实践,探讨中国教育的全球定位与战略路径)。
5. **大会形式**:包括大会报告、专题论坛、圆桌对话、教学沙龙、优秀教学成果展示、前沿技术展览等多元交流形式。
6. **参会邀请对象**:全国高校、科研院所、行业企业的专家学者、教师、学生及教育管理工作者。
7. **会务信息**:会务费1500元/人(食宿交通自理),提供注册缴费、住宿、日程等查询的官网(cecc.msup.cn)及联系人方式(含各业务方向负责人电话/微信、邮箱cecc@tsinghua.edu.cn)。
综上,该文档是一份权威、全面的学术会议官方通知,旨在凝聚教育界与产业界力量,共商智能时代计算机教育的变革路径与发展方向。
测试案例 2(不需要检索):
用户问题:Python 是什么类型的编程语言?
进入判断节点:Python 是什么类型的编程语言?
判断结果:need_retrieve = False
进入生成节点...
回答生成完成
最终回答:Python 是一种**高级、解释型、通用、动态类型、面向对象的编程语言**,同时也支持函数式编程和过程式编程范式。
- **高级**:语法接近自然语言,抽象程度高,无须关注底层内存管理等细节。
- **解释型**:代码通常由解释器(如 CPython)逐行解释执行,无须预先编译成机器码(尽管内部会编译为字节码并缓存于 `.pyc` 文件中)。
- **通用**:适用于Web开发、数据分析、人工智能、科学计算、自动化脚本、网络爬虫、桌面应用等多种领域。
- **动态类型**:变量类型在运行时确定,声明时无须指定类型(如 `x = 42` 和 `x = "hello"` 均合法)。
- **面向对象**:支持类、继承、多态、封装等OOP特性;同时一切皆对象(包括函数、模块、类型本身)。
- **强调可读性与简洁性**:采用缩进定义代码块,语法清晰,设计哲学体现于《Zen of Python》(`import this` 可查看)。
此外,Python 是开源的,拥有庞大活跃的社区和丰富的标准库及第三方生态(如 PyPI)。
======================================================================
额外测试:验证条件路由逻辑
======================================================================
问题:大会在哪里举办?(预期:需要检索)
进入判断节点:大会在哪里举办?
判断结果:need_retrieve = True
进入检索节点...
检索警告:too many values to unpack (expected 2)
检索完成,找到 3 条内容
进入生成节点...
回答生成完成
回答:大会在厦门国际会议中心举办,地址为:厦门市思明区环岛东路1693号。...
问题:2+2 等于几?(预期:不需要检索)
进入判断节点:2+2 等于几?
判断结果:need_retrieve = False
进入生成节点...
回答生成完成
回答:2+2 等于 4。...
问题:会议时间是哪天?(预期:需要检索)
进入判断节点:会议时间是哪天?
判断结果:need_retrieve = True
进入检索节点...
检索警告:too many values to unpack (expected 2)
检索完成,找到 3 条内容
进入生成节点...
回答生成完成
回答:会议时间是2025年11月28日至30日。...
======================================================================
✅ 所有测试完成!

5445

被折叠的 条评论
为什么被折叠?



