信息炼金术——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 变体:

  1. Corrective RAG(纠正型 RAG):检索 → 评估相关性 → 过滤不相关结果 → 如果剩余结果不够 → 改写查询 → 重新检索
  2. 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"。这就是子图封装的价值。


本章小结

  1. Agentic RAG ≠ 普通 RAG——多了"评估→决定是否重查→改写查询"的自适应循环,而非一次搜索就完事。
  2. Corrective RAG 的核心是评估节点——让 LLM 结构化地判断搜索结果是否充分、相关、多样。
  3. 搜索循环必须有上限——和 ReAct 一样,设置最大轮次(3 轮是经验值)。
  4. 子图封装让升级透明——Researcher 内部从模拟升级到 Agentic RAG,父图 Supervisor 和兄弟 Agent 完全无需改动。
  5. 中文社交媒体内容搜索有局限——通用搜索 API 对公众号、小红书等平台覆盖有限,长期需要专用工具。
  6. 结构化评估输出(JSON)驱动路由——这是 Agentic RAG 和条件边的标准组合方式。

关键术语

术语 释义
Agentic RAG 带"智力"的 RAG——Agent 评估检索质量,自主决定是否重查、如何重查
Corrective RAG 一种 Agentic RAG 模式:评估→过滤不相关结果→改写查询→重新检索
Self-RAG 一种 Agentic RAG 模式:生成→自检"是否有充分支持"→不够就补搜
检索质量评估 让 LLM 以 JSON 格式输出对搜索结果的判断(充分/不足 + 改进建议)
查询改写 当搜索结果不理想时,LLM 自动生成更精准的新查询词,而非人工重试

posted @ 2026-06-16 11:40  Yobeeo  阅读(0)  评论(0)    收藏  举报