信息炼金术——Agentic RAG 与内容分析 — LangGraph 实战——构建跨平台爆款图文 Agent 第7篇
第7章:信息炼金术——Agentic RAG 与内容分析
本章目标
读完本章你会:
- 能用真实的 Web 搜索 API 替换模拟数据,让 Agent 真正"上网冲浪"
- 能实现 Agentic RAG——不只是搜了扔给 LLM,而是检索→评估→重查→提炼的自适应循环
- 能设计内容质量评估节点,过滤掉不相关或低质量的结果
- 能实现 Corrective RAG:结果不够好时自动修正查询词重新搜索
知识讲解
从一个生活例子开始
你在大学图书馆做研究,课题是"社交媒体爆款内容的共性规律"。
普通 RAG(检索增强生成):
你告诉图书管理员:"帮我找社交媒体爆款的书"
管理员抱来 5 本书 → 你全读了 → 写报告
问题是:5 本书里可能有 3 本不相关(管理员理解错了你的需求),但你照单全收——把不相关的内容也写进了报告。
Agentic RAG(智能检索增强生成):
你告诉图书管理员:"帮我找社交媒体爆款的书"
管理员抱来 5 本书 → 你翻了翻目录 →
"第 3 本和第 5 本不相关,退回去"
"另外,关于'标题设计'的书没有——帮我再找一下"
管理员又抱来 3 本书 → 你翻了翻 →
"这些可以了,现在提炼核心规律"
→ 写报告
Agentic RAG 的核心差异:检索不是一次性的,Agent 会评估结果质量,决定是否需要换个角度重新搜。
在 LangGraph 里,这就是一个有评估和反馈的图结构:
search → [评估质量] → 不够好?→ refine_query → search(循环)
→ 够了 → analyze → generate
工作原理
传统 RAG vs Agentic RAG
| 传统 RAG | Agentic RAG | |
|---|---|---|
| 检索次数 | 1 次 | 自适应,不够就重查 |
| 结果评估 | 无(或简单的相似度排序) | LLM 评估相关性 + 信息完整度 |
| 查询优化 | 无 | 自动改写查询词 |
| 适用场景 | 简单问答 | 需要多角度搜索的复杂分析 |
本章聚焦两种 Agentic RAG 变体:
- Corrective RAG(纠正型 RAG):检索 → 评估相关性 → 过滤不相关结果 → 如果剩余结果不够 → 改写查询 → 重新检索
- Self-RAG(自反思 RAG):检索 → 生成带引用的回答 → 自检"我的回答有没有充分支持"→ 不够就补搜
对于 LocalTrend 的场景(分析爆款规律),Corrective RAG 更合适——我们需要确保收集到的数据覆盖多个维度(标题模式、内容结构、互动设计),而非仅依赖一次搜索。
图结构:Corrective RAG 的 LangGraph 实现
START
↓
search_web(调用搜索 API)
↓
evaluate_results(LLM 评估:相关?充分?)
↓
条件路由:
├─ 充分 → extract_insights(提炼规律)
│ ↓
│ END
└─ 不足 → refine_query(改写查询词)
↓
search_web(循环)
关键设计:
- 循环次数有上限(和 ReAct 一样需要防护)
evaluate_results返回结构化的评估("充分"或"不足+改进方向")- 每次重查的查询词都比上次更精准
思考一下: Corrective RAG 的循环和第 2 章的 ReAct 循环有什么本质区别?ReAct 是"思考→行动→观察"的通用循环,Corrective RAG 是专门为检索质量设计的专用循环——它的行动只限于"搜索"和"改查询",评估标准是"信息充分度"而非"任务完成度"。
代码实战
环境准备
本章需要 Web 搜索能力。推荐 Tavily(专为 AI Agent 设计的搜索 API,免费额度每月 1000 次):
pip install -U tavily-python
注册免费 API key:https://tavily.com (注册即送额度,无需绑卡)
备选方案:如果没有 Tavily key,使用
duckduckgo-search(完全免费,无需 key):pip install -U duckduckgo-search代码中会标注两种实现。
基础版:Corrective RAG 的最简循环
从最简的 Corrective RAG 图开始——搜索 → 评估 → 重查/继续。新建文件 chapter07_corrective_rag.py:
"""
第 7 章 基础演示:Corrective RAG
搜索 → 评估质量 → 不够就改写查询重搜
"""
import os
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI
# ============================================================
# 配置
# ============================================================
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY", "your-api-key-here"),
base_url="https://api.deepseek.com",
temperature=0.3, # 低温度——评估任务需要一致性
)
# ============================================================
# 搜索工具(两套实现,按你的 API key 情况选择)
# ============================================================
# --- 方案 A:Tavily(推荐)---
def search_web_tavily(query: str, max_results: int = 5) -> list[dict]:
"""使用 Tavily API 搜索网页"""
try:
from tavily import TavilyClient
client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY", ""))
response = client.search(query, max_results=max_results)
return [
{"title": r.get("title", ""), "content": r.get("content", ""), "url": r.get("url", "")}
for r in response.get("results", [])
]
except Exception as e:
print(f" ⚠️ Tavily 搜索失败: {e}")
return []
# --- 方案 B:DuckDuckGo(免费备选)---
def search_web_duckduckgo(query: str, max_results: int = 5) -> list[dict]:
"""使用 DuckDuckGo 搜索网页(免费,无需 API key)"""
try:
from duckduckgo_search import DDGS
results = []
with DDGS() as ddgs:
for r in ddgs.text(query, max_results=max_results):
results.append({
"title": r.get("title", ""),
"content": r.get("body", ""),
"url": r.get("href", ""),
})
return results
except Exception as e:
print(f" ⚠️ DuckDuckGo 搜索失败: {e}")
return []
# 选择搜索实现——根据你的环境切换
def search_web(query: str, max_results: int = 5) -> list[dict]:
"""统一搜索接口。优先用 Tavily,fallback 到 DuckDuckGo。"""
tavily_key = os.getenv("TAVILY_API_KEY", "")
if tavily_key:
return search_web_tavily(query, max_results)
print(" 未检测到 TAVILY_API_KEY,使用免费的 DuckDuckGo 搜索")
return search_web_duckduckgo(query, max_results)
# ============================================================
# State
# ============================================================
class RAGState(TypedDict):
# 用户原始问题
query: str
# 当前搜索词(可能被 refine 修改)
search_query: str
# 所有搜索结果(多轮累积)
all_results: Annotated[list[dict], lambda x, y: (x or []) + (y or [])]
# 本轮搜索结果
current_results: list[dict]
# 当前迭代轮次
iteration: int
# 评估结果
is_sufficient: bool
evaluation_feedback: str
# 最终分析
final_insights: str
# ============================================================
# 节点
# ============================================================
def search_node(state: RAGState) -> dict:
""" 执行 Web 搜索"""
query = state.get("search_query", state["query"])
iteration = state.get("iteration", 0) + 1
print(f"\n [搜索] 第 {iteration} 轮: '{query}'")
results = search_web(query, max_results=5)
if not results:
print(" ⚠️ 未获取到搜索结果(网络问题或 API 限制)")
return {"current_results": [], "iteration": iteration}
for i, r in enumerate(results):
print(f" [{i+1}] {r['title'][:60]}")
return {
"current_results": results,
"all_results": results, # reducer 自动累积
"iteration": iteration,
}
def evaluate_node(state: RAGState) -> dict:
""" 评估搜索结果质量——Agentic RAG 的核心"""
results = state["current_results"]
query = state.get("search_query", state["query"])
all_count = len(state.get("all_results", []))
iteration = state.get("iteration", 0)
print(f"\n [评估] 第 {iteration} 轮结果({len(results)} 条,累计 {all_count} 条)")
if not results:
# 没有结果时强制重试一次(换个查询词)
if iteration < 3:
return {
"is_sufficient": False,
"evaluation_feedback": "没有搜索结果,请用更通用的关键词重新搜索"
}
return {"is_sufficient": True, "evaluation_feedback": "已达最大重试次数"}
# 让 LLM 评估结果质量
results_text = "\n\n".join([
f"[{i+1}] 标题: {r['title']}\n内容: {r['content'][:200]}"
for i, r in enumerate(results[:5])
])
response = llm.invoke([
SystemMessage(content=(
"你是一个搜索结果评估专家。评估搜索结果是否足够回答查询。\n"
"评估标准:\n"
"1. 相关性——结果和查询主题相关吗?\n"
"2. 多样性——覆盖了不同角度吗?\n"
"3. 充分性——信息量足够支撑后续分析吗?\n\n"
"回复一个 JSON:\n"
'{"sufficient": true/false, "feedback": "评估意见", "suggestion": "如果不足,建议的新查询词"}'
)),
HumanMessage(content=(
f"原始查询: {query}\n\n搜索结果({len(results)}条):\n{results_text}\n\n"
f"累计已获取 {all_count} 条结果,当前第 {iteration} 轮搜索。\n"
f"如果已累计足够信息或已达 3 轮——sufficient 应为 true。"
))
])
# 解析 LLM 的 JSON 响应
import json
try:
content = response.content.strip()
# 提取 JSON(LLM 有时会在 JSON 前后加说明文字)
if "{" in content and "}" in content:
start = content.index("{")
end = content.rindex("}") + 1
content = content[start:end]
evaluation = json.loads(content)
except json.JSONDecodeError:
print(" ⚠️ LLM 评估结果解析失败,默认继续")
evaluation = {"sufficient": True, "feedback": "解析失败,默认通过"}
print(f" 充分: {evaluation['sufficient']}, 意见: {evaluation.get('feedback', '')[:80]}")
return {
"is_sufficient": evaluation["sufficient"],
"evaluation_feedback": evaluation.get("feedback", ""),
"search_query": evaluation.get("suggestion", query) if not evaluation["sufficient"] else query,
}
def refine_query_node(state: RAGState) -> dict:
""" 查询改写——当结果不充分时,换个角度搜"""
query = state.get("search_query", state["query"])
feedback = state.get("evaluation_feedback", "")
print(f"\n [改写查询] 原查询: '{query}'")
print(f" 评估意见: {feedback}")
response = llm.invoke([
HumanMessage(content=(
f"之前的搜索查询是: '{query}'\n"
f"评估意见: {feedback}\n\n"
f"请生成一个新的、更精准的搜索查询词(1-2 句话即可,直接返回新查询词)。"
))
])
new_query = response.content.strip()
print(f" 新查询: '{new_query}'")
return {"search_query": new_query}
def extract_insights(state: RAGState) -> dict:
"""基于所有累积搜索结果,提炼核心洞察"""
all_results = state.get("all_results", [])
query = state.get("query", "")
print(f"\n [提炼] 从 {len(all_results)} 条结果中提取洞察...")
combined = "\n\n".join([
f"来源 {i+1}: {r['title']}\n{r['content'][:300]}"
for i, r in enumerate(all_results[:10]) # 取前 10 条以免超出 context
])
response = llm.invoke([
HumanMessage(content=(
f"研究课题: {query}\n\n"
f"以下是收集到的网页搜索结果:\n{combined}\n\n"
f"请提炼 3-5 条关键洞察,每条注明信息来源(标题)。"
))
])
print(f" ✅ 提炼完成({len(response.content)} 字符)")
return {"final_insights": response.content}
# ============================================================
# 路由
# ============================================================
def quality_router(state: RAGState) -> Literal["refine", "extract", "__end__"]:
"""评估后路由:重搜 or 提炼 or 结束"""
if state.get("is_sufficient", False):
# 有足够信息 → 提炼
if state.get("all_results"):
return "extract"
return END # 完全没结果,结束
# 信息不足 → 改写查询重搜
if state.get("iteration", 0) >= 3:
print(" ⚠️ 已达最大搜索轮次,强制进入提炼")
return "extract" if state.get("all_results") else END
return "refine"
# ============================================================
# 构建图
# ============================================================
def build_corrective_rag():
builder = StateGraph(RAGState)
builder.add_node("search", search_node)
builder.add_node("evaluate", evaluate_node)
builder.add_node("refine_query", refine_query_node)
builder.add_node("extract_insights", extract_insights)
builder.add_edge(START, "search")
builder.add_edge("search", "evaluate")
builder.add_conditional_edges(
"evaluate", quality_router,
{"refine": "refine_query", "extract": "extract_insights", END: END}
)
builder.add_edge("refine_query", "search") # 循环
builder.add_edge("extract_insights", END)
return builder.compile()
# ============================================================
# 运行
# ============================================================
if __name__ == "__main__":
graph = build_corrective_rag()
query = "2026年社交媒体爆款内容的写作规律和标题设计技巧"
print("=" * 60)
print(f" 研究课题: {query}")
print("=" * 60)
result = graph.invoke({
"query": query,
"search_query": query,
"all_results": [],
"current_results": [],
"iteration": 0,
"is_sufficient": False,
"evaluation_feedback": "",
"final_insights": "",
})
print(f"\n{'='*60}")
print(f" 最终洞察(共搜索 {result['iteration']} 轮,累积 {len(result['all_results'])} 条结果)")
print(f"{'='*60}")
print(result["final_insights"])
逐行解析
all_results: Annotated[list[dict], lambda x, y: (x or []) + (y or [])]
这是一个自定义 reducer。和 operator.add 不同,它需要处理初始值可能为 None 的情况。lambda x, y: (x or []) + (y or []) 确保即便 x 或 y 是 None,也能正确追加。这种自定义 lambda reducer 用于和 operator.add 语义相同但需要额外空值处理的情况。
evaluate_node 中的结构化评估
这是 Agentic RAG 区别于传统 RAG 的关键。传统 RAG 不做评估——搜完就用。evaluate_node 让 LLM 以 JSON 格式输出结构化的评估结果:是否充分、为什么、建议的新查询词。这个 JSON 被解析后驱动图的下一步路由。
quality_router 中的三重保护
充分 + 有结果 → extract(提炼)
不足 + 未达上限 → refine(重搜)
不足 + 已达上限(3轮)→ extract(强制提炼)
完全没结果 → END(优雅退出)
这和第 2 章的循环防护一脉相承——永远不会出现无限循环。
扩展版:LocalTrend Researcher 子图升级
把 Corrective RAG 作为第 6 章 create_researcher() 的内部实现:
"""
第 7 章 扩展演示:LocalTrend Researcher 子图升级为 Agentic RAG
替换第 6 章的模拟搜索,接入真实 Web 搜索 + 质量评估
"""
import os
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI
# 复用基础版的 search_web 函数
from chapter07_corrective_rag import search_web
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY", "your-api-key-here"),
base_url="https://api.deepseek.com",
temperature=0.3,
)
# ============================================================
# 子图内部 State(与第 6 章 TeamState 兼容,但多了 RAG 专用字段)
# ============================================================
class ResearcherState(TypedDict):
# 来自父图
messages: Annotated[list, add_messages]
research_results: str
# RAG 专用
search_query: str
current_results: list[dict]
all_findings: Annotated[list[dict], lambda x, y: (x or []) + (y or [])]
rag_iteration: int
is_sufficient: bool
# ============================================================
# 升级版 Researcher——内部使用 Corrective RAG
# ============================================================
def create_rag_researcher():
"""Researcher 子图——内部是多轮搜索+评估的 Corrective RAG"""
def init_search(state: ResearcherState) -> dict:
"""从消息历史中提取搜索意图"""
user_msgs = [m for m in state["messages"] if isinstance(m, HumanMessage)]
topic = user_msgs[-1].content if user_msgs else "社交媒体爆款趋势"
query = f"{topic} 写作技巧 标题设计 2026"
print(f" [RAG Researcher] 初始查询: '{query}'")
return {"search_query": query}
def search_round(state: ResearcherState) -> dict:
"""执行一轮搜索"""
query = state.get("search_query", "")
iteration = state.get("rag_iteration", 0) + 1
print(f" [搜索 第{iteration}轮] {query}")
results = search_web(query, max_results=4)
for i, r in enumerate(results):
print(f" [{i+1}] {r['title'][:50]}")
return {
"current_results": results,
"all_findings": results,
"rag_iteration": iteration,
}
def evaluate_quality(state: ResearcherState) -> dict:
"""评估本轮结果的质量"""
results = state.get("current_results", [])
iteration = state.get("rag_iteration", 0)
if not results or iteration >= 3:
return {"is_sufficient": True}
# 快速评估:结果是否包含多个不同角度的信息
combined = " ".join([r.get("content", "")[:200] for r in results])
response = llm.invoke([
SystemMessage(content="评估搜索结果,回复 JSON: {\"sufficient\": true/false, \"angle\": \"建议的新角度\"}"),
HumanMessage(content=combined),
])
import json
try:
content = response.content
if "{" in content:
content = content[content.index("{"):content.rindex("}")+1]
eval_result = json.loads(content)
except json.JSONDecodeError:
eval_result = {"sufficient": True, "angle": ""}
if not eval_result["sufficient"]:
new_query = f"{state['search_query']} {eval_result.get('angle', '')}"
print(f" 信息不足,换个角度: {eval_result.get('angle', '')}")
return {"is_sufficient": False, "search_query": new_query}
return {"is_sufficient": True}
def synthesize(state: ResearcherState) -> dict:
"""汇总所有搜索结果为 research_results(对接父图)"""
findings = state.get("all_findings", [])
print(f" [汇总] {len(findings)} 条发现")
combined = "\n".join([
f"- [{f.get('title', '')}] {f.get('content', '')[:200]}"
for f in findings[:10]
])
response = llm.invoke([
HumanMessage(content=(
f"基于以下搜索结果,总结 3-5 条爆款内容的写作规律(含标题、结构、互动设计):\n{combined}"
))
])
summary = response.content
print(f" ✅ [RAG Researcher] 完成({len(summary)} 字符)")
return {
"research_results": summary,
"messages": [AIMessage(content=f" 搜索分析结果({len(findings)} 条数据源):\n{summary}")],
}
# 路由
def rag_router(state: ResearcherState) -> Literal["search", "synthesize"]:
if state.get("is_sufficient", False) or state.get("rag_iteration", 0) >= 3:
return "synthesize"
return "search"
# 构建子图
builder = StateGraph(ResearcherState)
builder.add_node("init_search", init_search)
builder.add_node("search_round", search_round)
builder.add_node("evaluate_quality", evaluate_quality)
builder.add_node("synthesize", synthesize)
builder.add_edge(START, "init_search")
builder.add_edge("init_search", "search_round")
builder.add_edge("search_round", "evaluate_quality")
builder.add_conditional_edges("evaluate_quality", rag_router, {
"search": "search_round",
"synthesize": "synthesize",
})
builder.add_edge("synthesize", END)
return builder.compile()
# ============================================================
# 测试:独立运行 Researcher
# ============================================================
if __name__ == "__main__":
researcher = create_rag_researcher()
print("=" * 60)
print(" RAG Researcher 独立测试")
print("=" * 60)
result = researcher.invoke({
"messages": [HumanMessage(content="AI 爆款内容写作规律")],
"research_results": "",
"search_query": "",
"current_results": [],
"all_findings": [],
"rag_iteration": 0,
"is_sufficient": False,
})
print(f"\n{'='*60}")
print(" 最终研究结果:")
print("=" * 60)
print(result["research_results"])
替换到第 6 章的 LocalTrend 团队
只需要一行改动——在 build_localtrend_team() 中:
# 第 6 章(旧):
builder.add_node("researcher", create_researcher())
# 第 7 章(新)——直接用 RAG 版替换:
from chapter07_rag_researcher import create_rag_researcher
builder.add_node("researcher", create_rag_researcher())
子图对外接口完全兼容——父 Supervisor 只看到 research_results 字段被填写了,完全不知道内部从"1 次模拟搜索"变成了"多轮 Corrective RAG"。这就是子图封装的价值。
本章小结
- Agentic RAG ≠ 普通 RAG——多了"评估→决定是否重查→改写查询"的自适应循环,而非一次搜索就完事。
- Corrective RAG 的核心是评估节点——让 LLM 结构化地判断搜索结果是否充分、相关、多样。
- 搜索循环必须有上限——和 ReAct 一样,设置最大轮次(3 轮是经验值)。
- 子图封装让升级透明——Researcher 内部从模拟升级到 Agentic RAG,父图 Supervisor 和兄弟 Agent 完全无需改动。
- 中文社交媒体内容搜索有局限——通用搜索 API 对公众号、小红书等平台覆盖有限,长期需要专用工具。
- 结构化评估输出(JSON)驱动路由——这是 Agentic RAG 和条件边的标准组合方式。
关键术语
| 术语 | 释义 |
|---|---|
| Agentic RAG | 带"智力"的 RAG——Agent 评估检索质量,自主决定是否重查、如何重查 |
| Corrective RAG | 一种 Agentic RAG 模式:评估→过滤不相关结果→改写查询→重新检索 |
| Self-RAG | 一种 Agentic RAG 模式:生成→自检"是否有充分支持"→不够就补搜 |
| 检索质量评估 | 让 LLM 以 JSON 格式输出对搜索结果的判断(充分/不足 + 改进建议) |
| 查询改写 | 当搜索结果不理想时,LLM 自动生成更精准的新查询词,而非人工重试 |

浙公网安备 33010602011771号