LangChain 实战:高级 RAG 与工具增强 Agent 深度构建指南

Agent 0 次阅读
LangChain 实战:高级 RAG 与工具增强 Agent 深度构建指南

从 Multi-Query RAG 到多工具协同 Agent——掌握 LangChain 生产级 AI 应用开发的核心范式,构建真正"能查、能用、能推理"的智能体

架构全景图

目录

  1. 背景与概念:为什么你的 RAG 不够"聪明"
  2. RAG 核心模式深度解析
  3. 工具增强 Agent 架构设计
  4. 实战一:构建多策略自适应 RAG 系统
  5. 实战二:工具增强 Agent 完整实现
  6. 进阶:LangGraph 多 Agent 协作模式
  7. 生产部署与性能优化
  8. 常见问题 FAQ
  9. 总结与展望

一、背景与概念

1.1 RAG 的进化之路

检索增强生成(RAG)已经成为大语言模型(LLM)落地最主流的范式之一。它的核心思想并不复杂:在 LLM 回答用户问题之前,先从外部知识库中检索相关文档片段,然后将这些片段作为上下文注入 Prompt,让模型基于可靠信源生成回答。

但如果你已经在生产环境中跑过 RAG 系统,你一定遇到过这些问题:

  • 检索不准:用户问"Python 中 asyncawait 的区别",检索系统却返回了一篇关于 JavaScript 异步编程的文章
  • 上下文缺失:返回的文档片段太短,缺少必要的背景信息,模型只能"盲人摸象"
  • 工具脱节:RAG 只能"查文档",无法执行实际操作(查数据库、调 API、发邮件)
  • 多步推理困难:复杂问题需要拆解成多个子任务,单次检索+生成的模式力不从心

概念关系图

这些痛点催生了 LangChain 在 RAG 和 Agent 领域的一系列深度能力。本篇文章将带你跳出"向量库 + LLM"的初级组合,探索以下高级范式:

范式 解决的核心问题 关键技术
多策略 RAG 检索精度不足 Multi-Query、Parent-Document、Contextual Compression
工具增强 Agent RAG 只能查不能做 Tool/Function Calling、ReAct Agent
多 Agent 协作 复杂任务拆解 LangGraph、StateGraph、条件路由
生产级优化 延迟、成本、可靠性 Streaming、Caching、Fallback

1.2 本文读者收益

读完本文,你将能够:

  1. 理解并实现 4 种高级 RAG 检索策略,知道何时用哪种
  2. 构建 一个能调用多种外部工具的 Agent(搜索引擎、数据库、API、代码执行器)
  3. 掌握 LangGraph 的基础用法,搭建多 Agent 协作流水线
  4. 部署 一个支持流式输出、缓存、容错的生产级 AI 应用

二、RAG 核心模式深度解析

2.1 Multi-Query RAG:一个问题,多种问法

问题场景:用户的原始问题措辞模糊或不完整,单次检索难以命中正确的文档。

解决思路:让 LLM 从不同角度重新表述用户问题,生成 3-5 个变体查询,然后合并所有检索结果,去重后作为上下文。

from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma

# 初始化向量库
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(
    collection_name="tech_docs",
    embedding_function=embeddings,
    persist_directory="./chroma_db"
)

# 创建基础检索器
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

# 包装为 Multi-Query 检索器
llm = ChatOpenAI(model="gpt-4o", temperature=0)
multi_query_retriever = MultiQueryRetriever.from_llm(
    retriever=base_retriever,
    llm=llm,
    include_original=True  # 保留原始查询
)

# 使用示例
question = "怎么让 AI 应用跑得更快"
docs = multi_query_retriever.invoke(question)

# LLM 会自动生成类似以下变体:
# - "如何优化 AI 应用的推理速度"
# - "AI 应用性能调优的方法有哪些"
# - "加速大模型应用响应的技术方案"
# - "减少 LLM API 调用延迟的最佳实践"
# - "AI 应用吞吐量提升策略"

适用场景

  • 用户问题简短模糊(如"怎么优化"、"出错了怎么办")
  • 知识库覆盖领域广,同一概念有多种表述方式
  • 需要确保检索召回率(宁可多召回也不遗漏)

注意事项

  • 每次检索会调用 LLM 生成变体 + N 次向量搜索,延迟和成本增加 N 倍
  • 建议对变体数量做限制(3-5 个为宜),配合结果去重减少冗余

2.2 Parent-Document RAG:小块检索,大块回答

问题场景:为了检索精度,文档通常被切成较小的 chunk(如 256-512 tokens),但这导致检索到的片段缺少上下文,模型无法理解完整语义。

解决思路:建立一个"双层索引"——检索时用小块(child),返回结果时反查其所属的大块(parent),将完整的父文档作为上下文。

from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 子文档分割器(小块,用于检索)
child_splitter = RecursiveCharacterTextSplitter(
    chunk_size=300,
    chunk_overlap=50
)

# 父文档分割器(大块,用于返回)
parent_splitter = RecursiveCharacterTextSplitter(
    chunk_size=2000,
    chunk_overlap=200
)

# 文档存储(用于子→父映射)
store = InMemoryStore()

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
    search_kwargs={"k": 3}  # 返回 3 个父文档
)

# 添加文档
retriever.add_documents(raw_documents)

# 检索时返回完整的父文档
results = retriever.invoke("Rust 的所有权系统如何工作")
# 每个 result 包含约 2000 tokens 的完整段落

效果对比

方案 检索精度 上下文完整性 存储开销
小块检索 + 小块返回 ⭐⭐⭐⭐⭐ ⭐⭐
大块检索 + 大块返回 ⭐⭐ ⭐⭐⭐⭐⭐
小块检索 + 大块返回(本文方案) ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 中(双份向量)

2.3 Contextual Compression RAG:检索后再"提纯"

问题场景:即使检索到了相关文档,其中也包含大量与当前问题无关的信息,白白占用上下文窗口。

解决思路:在检索结果返回后,再用 LLM 对每个文档片段进行"压缩"——提取与问题相关的核心句子,丢弃无关内容。

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

# 创建压缩器
compressor = LLMChainExtractor.from_llm(llm)

# 包装检索器
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=base_retriever
)

# 检索 + 自动压缩
compressed_docs = compression_retriever.invoke(
    "如何在 Kubernetes 中配置 HPA 自动扩缩容"
)
# 返回的每个文档只保留了与 HPA 配置相关的句子

更高效的替代方案:如果你觉得每篇文档都调一次 LLM 成本太高,可以使用 LLMChainFilter——它不提取内容,只是判断文档是否相关,做二分类过滤:

from langchain.retrievers.document_compressors import LLMChainFilter

filter_compressor = LLMChainFilter.from_llm(llm)
filter_retriever = ContextualCompressionRetriever(
    base_compressor=filter_compressor,
    base_retriever=base_retriever
)

2.4 Self-Query RAG:元数据感知检索

问题场景:知识库中的文档带有丰富的元数据(日期、作者、类别、版本号等),但传统 RAG 只用语义相似度检索,完全忽略了这些结构化信息。

from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo

# 定义元数据字段
metadata_field_info = [
    AttributeInfo(
        name="source",
        description="文档来源:官方文档、社区博客、学术论文",
        type="string",
    ),
    AttributeInfo(
        name="published_date",
        description="文档发布日期,格式 YYYY-MM-DD",
        type="string",
    ),
    AttributeInfo(
        name="version",
        description="框架/库的版本号",
        type="string",
    ),
]

document_content_description = "技术文档和教程"

self_query_retriever = SelfQueryRetriever.from_llm(
    llm=llm,
    vectorstore=vectorstore,
    document_contents=document_content_description,
    metadata_field_info=metadata_field_info,
    verbose=True
)

# 用户自然语言查询 → LLM 自动解析出语义+元数据过滤条件
docs = self_query_retriever.invoke(
    "找 2025 年之后发布的关于 LangChain v0.3 的官方文档"
)

# LLM 内部生成的查询结构(自动推断):
# {
#   "query": "LangChain v0.3",
#   "filter": "and(eq('source', '官方文档'), gte('published_date', '2025-01-01'), eq('version', '0.3'))"
# }

2.5 四种策略对比总结

RAG策略对比图

策略 擅长场景 额外成本 实现复杂度
Multi-Query 问题模糊、需要高召回 LLM 调用 × (N+1) ⭐⭐
Parent-Document 需要完整上下文 双份向量存储 ⭐⭐
Contextual Compression 上下文窗口紧张 LLM 调用 × 文档数
Self-Query 元数据丰富的知识库 LLM 调用 × 1 ⭐⭐⭐

2.6 组合策略:在实际项目中如何选择

在实际项目中,很少只用单一策略。更常见的做法是根据场景组合使用。以下是三种典型的组合模式:

模式一:高精度知识库(文档 + FAQ)

Self-Query(按版本/日期过滤) → Parent-Document(返回完整上下文)

适合企业知识库、API 文档等结构化内容丰富的场景。先用 Self-Query 精准定位到特定版本的文档,再用 Parent-Document 返回完整章节,确保上下文完整。

模式二:通用问答助手

Multi-Query(提高召回) → Contextual Compression(压缩提纯)

适合对话式 AI 助手。Multi-Query 确保不遗漏相关文档,Compression 确保不浪费上下文窗口。这个组合在 LangChain 社区中被广泛验证为"最佳性价比组合"。

模式三:研究分析型

Multi-Query → Parent-Document → LLM 重排序(Re-rank) → Top-K

适合需要深度分析和交叉验证的场景(如学术研究、竞品分析)。Triple-check 机制最大化信息完整性,代价是延迟较高。

2.7 RAG 评估:你怎么知道策略选对了

选择 RAG 策略不是凭感觉,需要数据说话。推荐使用 RAGAS(RAG Assessment)框架进行评估:

from ragas import evaluate
from ragas.metrics import (
    faithfulness,          # 答案是否基于提供的上下文
    answer_relevancy,      # 答案是否切题
    context_recall,        # 检索是否召回了必要信息
    context_precision,     # 检索结果中相关文档的比例
)

# 准备测试数据集
from datasets import Dataset
eval_dataset = Dataset.from_dict({
    "question": ["什么是 RAG?", "如何优化向量检索速度?", "Chroma 和 Pinecone 的区别是什么?"],
    "answer": ["RAG 是检索增强生成...", "可以通过索引优化...", "Chroma 是开源嵌入式数据库..."],
    "contexts": [
        ["RAG (Retrieval-Augmented Generation) 是一种结合检索和生成的技术..."],
        ["向量检索优化方法包括:使用 HNSW 索引、量化压缩、预过滤..."],
        ["Chroma 是开源向量数据库,Pinecone 是托管向量数据库..."],
    ],
})

# 执行评估
results = evaluate(eval_dataset, metrics=[faithfulness, answer_relevancy, context_recall, context_precision])
print(f"忠实度: {results['faithfulness']:.2f}")
print(f"切题度: {results['answer_relevancy']:.2f}")
print(f"召回率: {results['context_recall']:.2f}")
print(f"精确率: {results['context_precision']:.2f}")

评估的黄金法则:至少准备 50 个真实用户问题作为测试集,分别用不同策略跑一遍,对比各指标。不要凭直觉选策略,用数据决策才能真正优化你的 RAG 系统。


三、工具增强 Agent 架构设计

3.1 Agent 的"双手":从只读到读写

传统 RAG 像一个"只读"的图书管理员——只能查资料,不能做任何实际操作。工具增强 Agent 则赋予了 AI"双手",让它能够:

  • 🔍 搜索网页:获取最新信息,弥补 LLM 知识截止日期
  • 🗄️ 查询数据库:直接执行 SQL,获取结构化数据
  • 🌐 调用 API:操作外部服务(发送邮件、创建工单、触发流水线)
  • 💻 执行代码:运行 Python 脚本,做数据分析和可视化
  • 📊 操作文件:读写本地或云存储中的文件

3.2 ReAct Agent:推理与行动的交织

ReAct(Reasoning + Acting)是 LangChain 中最经典的 Agent 范式。它的核心循环是:

Thought → Action → Observation → Thought → Action → ... → Final Answer

Agent流程图

from langchain.agents import create_react_agent, AgentExecutor
from langchain.tools import Tool
from langchain_community.tools import DuckDuckGoSearchRun
import sqlite3

# 定义工具集
# 工具1:网页搜索
search_tool = DuckDuckGoSearchRun()

# 工具2:数据库查询
def query_database(sql: str) -> str:
    """执行 SQL 查询并返回结果。"""
    conn = sqlite3.connect("knowledge.db")
    cursor = conn.cursor()
    try:
        cursor.execute(sql)
        results = cursor.fetchall()
        return str(results)
    except Exception as e:
        return f"错误: {e}"
    finally:
        conn.close()

db_tool = Tool(
    name="database_query",
    func=query_database,
    description="执行 SQL 查询数据库。输入应为完整的 SQL 语句。"
)

# 工具3:Python 代码执行器
def execute_python(code: str) -> str:
    """执行 Python 代码并返回 stdout 输出。"""
    import subprocess
    result = subprocess.run(
        ["python3", "-c", code],
        capture_output=True, text=True, timeout=10
    )
    return result.stdout or result.stderr

python_tool = Tool(
    name="python_executor",
    func=execute_python,
    description="执行 Python 代码。用于计算、数据处理、图表生成。"
)

# 工具4:当前时间
from datetime import datetime
time_tool = Tool(
    name="current_time",
    func=lambda _: datetime.now().isoformat(),
    description="获取当前日期和时间。无输入参数。"
)

tools = [search_tool, db_tool, python_tool, time_tool]

# 创建 Agent
from langchain_openai import ChatOpenAI
from langchain import hub

llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 使用 LangChain Hub 中的 ReAct prompt 模板
prompt = hub.pull("hwchase17/react")

agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=10,
    handle_parsing_errors=True
)

# 使用 Agent
response = agent_executor.invoke({
    "input": "帮我查一下今天几号,然后搜索 LangChain 今天有没有什么新闻,最后用 Python 计算一下 2026 年还剩下多少天"
})

3.3 OpenAI Functions Agent:更稳定、更精确

ReAct Agent 依赖 Prompt 工程来解析 LLM 输出中的 ActionAction Input 等标记,偶尔会出现解析失败。OpenAI Functions Agent 利用 Function Calling 原生能力,工具调用更可靠:

from langchain.agents import create_openai_functions_agent

# 同样的工具列表,不同的 Agent 类型
functions_agent = create_openai_functions_agent(llm, tools, prompt)
functions_executor = AgentExecutor(
    agent=functions_agent,
    tools=tools,
    verbose=True,
    max_iterations=5
)

# Functions Agent 内部使用 OpenAI 的 tool_choice 机制
# LLM 返回的不是文本标记,而是结构化的 function_call JSON
response = functions_executor.invoke({
    "input": "搜索 LangGraph 最新版本号,用 Python 判断它是否大于 0.2.0"
})

两种 Agent 对比:

特性 ReAct Agent Functions Agent
模型要求 任意 LLM 支持 Function Calling 的模型
调用稳定性 偶尔解析失败 结构化输出,极其稳定
并行工具调用 不支持 支持(一次返回多个 tool_calls)
Prompt 控制力 高(可定制推理链) 低(依赖模型内部机制)
推荐场景 开源模型、需要精细控制 OpenAI/Azure/兼容接口

四、实战一:构建多策略自适应 RAG 系统

4.1 系统架构

我们将构建一个自适应 RAG 路由器:根据用户问题的特征(模糊度、复杂度、元数据需求),自动选择最优检索策略。

用户问题 → 问题分类器 → [多策略路由]
                          ├─ 简单事实查询 → 基础 RAG
                          ├─ 模糊问题 → Multi-Query RAG
                          ├─ 需要上下文 → Parent-Document RAG
                          ├─ 元数据过滤 → Self-Query RAG
                          └─ 复杂综合查询 → Multi-Query + Compress

4.2 完整实现

from typing import Literal
from pydantic import BaseModel, Field
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

# === 步骤 1:问题分类器 ===
class QueryAnalysis(BaseModel):
    """LLM 对用户问题的结构化分析"""
    complexity: Literal["simple", "fuzzy", "contextual", "metadata", "complex"] = Field(
        description="问题类型:simple=简单事实,fuzzy=表述模糊,contextual=需要上下文,metadata=元数据过滤,complex=综合复杂"
    )
    needs_compression: bool = Field(
        description="是否需要对检索结果进行压缩提纯"
    )

analysis_prompt = ChatPromptTemplate.from_messages([
    ("system", """分析用户问题的特征。
- simple: 直接的事实查询,如"X 是什么"
- fuzzy: 问题表述模糊、简短,如"怎么优化"
- contextual: 答案需要大量上下文才能理解
- metadata: 问题包含时间、版本、来源等过滤条件
- complex: 需要综合多种策略
只返回 JSON。"""),
    ("human", "{question}")
])

analyzer = analysis_prompt | llm.with_structured_output(QueryAnalysis)

# === 步骤 2:多策略检索器工厂 ===
class AdaptiveRAGRetriever:
    def __init__(self, vectorstore, llm):
        self.vectorstore = vectorstore
        self.llm = llm
        self._init_retrievers()

    def _init_retrievers(self):
        """初始化所有检索策略"""
        self.base = self.vectorstore.as_retriever(search_kwargs={"k": 5})

        self.multi_query = MultiQueryRetriever.from_llm(
            retriever=self.base, llm=self.llm
        )

        self.parent_doc = ParentDocumentRetriever(
            vectorstore=self.vectorstore,
            docstore=InMemoryStore(),
            child_splitter=RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50),
            parent_splitter=RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=200),
        )

        compressor = LLMChainExtractor.from_llm(self.llm)
        self.compressed = ContextualCompressionRetriever(
            base_compressor=compressor,
            base_retriever=self.multi_query
        )

    def retrieve(self, question: str, analysis: QueryAnalysis):
        """根据问题分析结果选择策略"""
        strategy_map = {
            "simple": self.base,
            "fuzzy": self.multi_query,
            "contextual": self.parent_doc,
            "metadata": self.base,  # Self-Query 需要额外配置,此处简化
            "complex": self.compressed,
        }
        retriever = strategy_map.get(analysis.complexity, self.base)
        print(f"📊 问题类型: {analysis.complexity}, 压缩: {analysis.needs_compression}")
        docs = retriever.invoke(question)

        # 额外压缩(如果需要)
        if analysis.needs_compression and analysis.complexity != "complex":
            docs = compressor.compress_documents(docs, question)

        return docs

# === 步骤 3:完整的 RAG 链 ===
def build_adaptive_rag_chain(vectorstore, llm):
    retriever_engine = AdaptiveRAGRetriever(vectorstore, llm)

    rag_prompt = ChatPromptTemplate.from_messages([
        ("system", """你是一个技术助手。根据提供的上下文回答问题。
如果上下文不足以回答,请明确说明。
上下文:
{context}"""),
        ("human", "{question}")
    ])

    def adaptive_retrieve(inputs):
        question = inputs["question"]
        analysis = analyzer.invoke({"question": question})
        docs = retriever_engine.retrieve(question, analysis)
        return {"context": docs, "question": question, "analysis": analysis}

    chain = (
        RunnablePassthrough.assign(context_and_question=adaptive_retrieve)
        | (lambda x: {
            "context": "\n\n---\n\n".join(
                d.page_content for d in x["context_and_question"]["context"]
            ),
            "question": x["context_and_question"]["question"]
        })
        | rag_prompt
        | llm
        | StrOutputParser()
    )

    return chain

# === 使用示例 ===
chain = build_adaptive_rag_chain(vectorstore, llm)

# 简单问题 → 基础 RAG
result = chain.invoke({"question": "什么是 VectorStore"})

# 模糊问题 → Multi-Query RAG
result = chain.invoke({"question": "怎么加速"})

# 复杂问题 → Multi-Query + Compression
result = chain.invoke({"question": "比较 Chroma、Pinecone 和 Weaviate 在性能、成本和易用性上的差异,并给出生产环境选型建议"})

五、实战二:工具增强 Agent 完整实现

5.1 场景设定

构建一个技术问答助手 Agent,它能:

  1. 从向量知识库检索技术文档(RAG 工具)
  2. 实时搜索网页获取最新信息(搜索工具)
  3. 执行 Python 代码做计算和验证(代码工具)
  4. 查询数据库获取统计数据(数据库工具)
  5. 智能选择工具组合完成任务

5.2 工具定义与注册

from langchain.tools import tool
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# === 工具 1:知识库检索(RAG 工具) ===
@tool
def search_knowledge_base(query: str) -> str:
    """从内部技术知识库中检索相关文档。适合查找概念、教程、最佳实践。"""
    docs = vectorstore.similarity_search(query, k=3)
    results = []
    for i, doc in enumerate(docs, 1):
        source = doc.metadata.get("source", "未知来源")
        results.append(f"[{i}] 来源: {source}\n{doc.page_content[:500]}")
    return "\n\n---\n\n".join(results) if results else "未找到相关文档"

# === 工具 2:网页搜索 ===
@tool
def web_search(query: str) -> str:
    """搜索互联网获取最新信息。适合查找新闻、最新版本、社区讨论。"""
    # 使用免费搜索引擎(需安装 duckduckgo-search)
    from duckduckgo_search import DDGS
    with DDGS() as ddgs:
        results = list(ddgs.text(query, max_results=3))
    return "\n\n".join(
        f"[{i+1}] {r['title']}\n{r['body'][:300]}\n链接: {r['href']}"
        for i, r in enumerate(results)
    )

# === 工具 3:Python 执行器 ===
@tool
def run_python(code: str) -> str:
    """执行 Python 代码并返回结果。用于计算、数据分析、格式转换。
    代码中可用 print() 输出结果。"""
    import io, sys, traceback
    old_stdout = sys.stdout
    sys.stdout = buffer = io.StringIO()
    try:
        # 安全限制:禁用危险操作
        safe_globals = {
            "__builtins__": {
                k: v for k, v in __builtins__.items()
                if k not in ["__import__", "eval", "exec", "open", "compile"]
            },
            "print": print, "range": range, "len": len,
            "int": int, "float": float, "str": str, "list": list,
            "dict": dict, "set": set, "tuple": tuple, "bool": bool,
            "sum": sum, "max": max, "min": min, "sorted": sorted,
            "abs": abs, "round": round, "enumerate": enumerate,
            "zip": zip, "map": map, "filter": filter,
        }
        exec(code, safe_globals, {})
        return buffer.getvalue() or "代码执行完毕(无输出)"
    except Exception as e:
        return f"执行错误: {traceback.format_exc()}"
    finally:
        sys.stdout = old_stdout

# === 工具 4:数据库查询 ===
@tool
def query_stats(sql: str) -> str:
    """查询统计数据库。表结构:
    - articles(id, title, category, views, published_date)
    - users(id, name, join_date, active)
    输入完整的 SQL SELECT 语句。"""
    import sqlite3
    conn = sqlite3.connect("stats.db")
    try:
        cur = conn.cursor()
        cur.execute(sql)
        rows = cur.fetchall()
        cols = [d[0] for d in cur.description]
        # 格式化为表格
        header = " | ".join(cols)
        separator = "-+-".join("-" * len(c) for c in cols)
        body = "\n".join(" | ".join(str(v) for v in row) for row in rows[:20])
        return f"{header}\n{separator}\n{body}\n\n共 {len(rows)} 条记录"
    except Exception as e:
        return f"SQL 错误: {e}"
    finally:
        conn.close()

# 工具注册
all_tools = [search_knowledge_base, web_search, run_python, query_stats]

5.3 Agent 创建与对话管理

from langchain.agents import create_openai_functions_agent
from langchain.agents import AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI

# 增强版 System Prompt
system_prompt = """你是一个技术问答助手,拥有以下能力:

1. **知识库检索**:查询内部技术文档和教程
2. **网页搜索**:获取互联网上的最新信息
3. **Python 执行**:运行代码进行计算和分析
4. **数据库查询**:查询统计数据

工作原则:
- 技术概念和教程优先使用知识库,实时信息使用网页搜索
- 需要计算时使用 Python,不要手动心算
- 如果知识库找不到答案,用网页搜索补充
- 每一步都向用户说明你在做什么
- 如果工具返回错误,换一种方式重试,最多 2 次"""

prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

llm = ChatOpenAI(model="gpt-4o", temperature=0)

agent = create_openai_functions_agent(llm, all_tools, prompt)

executor = AgentExecutor(
    agent=agent,
    tools=all_tools,
    verbose=True,
    max_iterations=8,
    handle_parsing_errors=True,
    return_intermediate_steps=True
)

# === 多轮对话示例 ===
conversations = [
    "LangChain 中 LCEL 是什么?和传统 Chain 有什么区别?",
    "用 Python 计算一下:如果每天写 500 行代码,一年能写多少行?",
    "搜索一下今天 AI 领域有什么大新闻",
]

for query in conversations:
    print(f"\n{'='*60}")
    print(f"👤 用户: {query}")
    result = executor.invoke({"input": query})
    print(f"🤖 助手: {result['output']}")

六、进阶:LangGraph 多 Agent 协作模式

6.1 为什么需要多 Agent

单个 Agent 面对复杂任务时,容易陷入"什么都会但什么都不精"的困境——工具太多导致选择困难,Prompt 太长降低准确率,错误容易级联放大。

多 Agent 架构的核心思想是分工协作:每个 Agent 只负责自己擅长的子任务,通过消息传递协调工作。

进阶架构图

6.2 LangGraph 基础

LangGraph 是 LangChain 生态中的状态图编排框架,它把 Agent 看作图中的节点,把消息传递看作边,可以灵活地定义 Agent 之间的控制流。

from typing import TypedDict, Annotated, Sequence
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage

# === 定义全局状态 ===
class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]
    next_agent: str
    task_result: dict

# === 定义各 Agent 节点 ===
def researcher_node(state: AgentState) -> AgentState:
    """研究员 Agent:负责信息检索"""
    researcher_prompt = ChatPromptTemplate.from_messages([
        ("system", "你是研究员。搜索并整理与任务相关的信息。"),
        MessagesPlaceholder(variable_name="messages"),
    ])
    researcher_chain = researcher_prompt | llm.bind_tools([web_search, search_knowledge_base])
    response = researcher_chain.invoke({"messages": state["messages"]})
    return {"messages": [response], "next_agent": "analyst"}

def analyst_node(state: AgentState) -> AgentState:
    """分析师 Agent:负责数据分析和计算"""
    analyst_prompt = ChatPromptTemplate.from_messages([
        ("system", "你是数据分析师。基于研究员提供的信息,进行深度分析和计算。"),
        MessagesPlaceholder(variable_name="messages"),
    ])
    analyst_chain = analyst_prompt | llm.bind_tools([run_python, query_stats])
    response = analyst_chain.invoke({"messages": state["messages"]})
    return {"messages": [response], "next_agent": "writer"}

def writer_node(state: AgentState) -> AgentState:
    """写手 Agent:负责整合输出"""
    writer_prompt = ChatPromptTemplate.from_messages([
        ("system", """你是技术写手。基于前面的研究和分析,撰写一份结构清晰的技术报告。
格式要求:标题 → 摘要 → 详细分析 → 结论。"""),
        MessagesPlaceholder(variable_name="messages"),
    ])
    response = llm.invoke(writer_prompt.format_messages(messages=state["messages"]))
    return {"messages": [response], "next_agent": END}

# === 构建状态图 ===
workflow = StateGraph(AgentState)

# 添加节点
workflow.add_node("researcher", researcher_node)
workflow.add_node("analyst", analyst_node)
workflow.add_node("writer", writer_node)

# 定义边:线性流水线
workflow.set_entry_point("researcher")
workflow.add_edge("researcher", "analyst")
workflow.add_edge("analyst", "writer")
workflow.add_edge("writer", END)

# 编译运行
app = workflow.compile()

result = app.invoke({
    "messages": [
        HumanMessage(content="分析 Rust 语言在 2025-2026 年的发展趋势,包括社区增长、企业采用率和主要用例变化")
    ],
    "next_agent": "researcher",
    "task_result": {}
})

print(result["messages"][-1].content)

6.3 条件路由:智能任务分发

线性流水线适合固定流程,但更灵活的方案是让"调度员"根据任务类型动态路由:

def supervisor_node(state: AgentState) -> AgentState:
    """调度员:分析任务并决定下一步交给谁"""
    supervisor_prompt = ChatPromptTemplate.from_messages([
        ("system", """你是任务调度员。分析当前任务,决定下一步执行者:
- 需要搜索/检索 → researcher
- 需要计算/分析 → analyst  
- 需要综合输出 → writer
- 任务完成 → FINISH

只回复角色名:researcher / analyst / writer / FINISH"""),
        MessagesPlaceholder(variable_name="messages"),
    ])
    response = llm.invoke(supervisor_prompt.format_messages(messages=state["messages"]))
    next_agent = response.content.strip()
    return {"messages": [response], "next_agent": next_agent}

# 重新构建图(条件路由版)
workflow_v2 = StateGraph(AgentState)

workflow_v2.add_node("supervisor", supervisor_node)
workflow_v2.add_node("researcher", researcher_node)
workflow_v2.add_node("analyst", analyst_node)
workflow_v2.add_node("writer", writer_node)

workflow_v2.set_entry_point("supervisor")

# 条件边:根据 supervisor 决策路由
def route_next(state: AgentState) -> str:
    return state["next_agent"] if state["next_agent"] != "FINISH" else END

workflow_v2.add_conditional_edges("supervisor", route_next, {
    "researcher": "researcher",
    "analyst": "analyst",
    "writer": "writer",
    END: END
})

# 所有 worker 完成后回到 supervisor
workflow_v2.add_edge("researcher", "supervisor")
workflow_v2.add_edge("analyst", "supervisor")
workflow_v2.add_edge("writer", "supervisor")

app_v2 = workflow_v2.compile()

6.4 人机协作(Human-in-the-Loop)

生产环境中,纯自动化的 Agent 流程有时需要人类介入——比如涉及敏感操作(发送邮件、修改数据库)时需要人类审批。LangGraph 原生支持打断点:

from langgraph.checkpoint.sqlite import SqliteSaver

# 使用 SQLite 持久化状态(支持暂停/恢复)
memory = SqliteSaver.from_conn_string("checkpoints.db")

workflow_with_human = StateGraph(AgentState)
# ... 添加节点 ...

# 在关键节点前设置打断点
workflow_with_human.add_node("draft_email", draft_email_node)
workflow_with_human.add_node("human_approval", human_approval_node)
workflow_with_human.add_node("send_email", send_email_node)

workflow_with_human.add_edge("draft_email", "human_approval")

# 在 human_approval 前打断
app_with_human = workflow_with_human.compile(
    checkpointer=memory,
    interrupt_before=["human_approval"]  # 🔴 在此节点前暂停
)

# 第一次运行:会在 human_approval 前暂停
config = {"configurable": {"thread_id": "user-123"}}
for event in app_with_human.stream(input_data, config):
    print(event)

# 获取当前状态,供人类审查
state = app_with_human.get_state(config)
print(f"待审批内容: {state.values}")

# 人类审查通过后,继续执行
app_with_human.update_state(config, {"approved": True})
for event in app_with_human.stream(None, config):  # None = 无新输入,继续
    print(event)

6.5 并行执行:多路搜索 + 结果聚合

当任务可以拆分为独立并行子任务时,LangGraph 的 Send API 可以同时派发多个节点:

from langgraph.graph import StateGraph, END
from langgraph.constants import Send
from typing import List, TypedDict
import operator

class ParallelState(TypedDict):
    queries: List[str]           # 需要搜索的子问题列表
    search_results: List[str]    # 聚合后的搜索结果
    final_answer: str

def split_to_queries(state: ParallelState):
    """将复杂问题拆分为多个子查询"""
    # 这里用 LLM 拆解,示例中直接硬编码
    return {"queries": [
        "LangChain 最新版本特性",
        "LangGraph 多 Agent 模式",
        "LangSmith 监控最佳实践"
    ]}

def search_single_query(state: dict):
    """搜索单个子查询"""
    query = state["query"]
    result = web_search(query)  # 假设 web_search 是已定义的工具
    return {"search_results": [result]}

def merge_results(state: ParallelState):
    """合并所有搜索结果并生成最终答案"""
    all_results = "\n\n".join(state["search_results"])
    final = llm.invoke(f"基于以下搜索结果回答: {all_results}")
    return {"final_answer": final.content}

# 关键:用 Send 并行派发
def continue_to_searches(state: ParallelState):
    """为每个子查询创建一个 Send"""
    return [Send("search", {"query": q}) for q in state["queries"]]

workflow_parallel = StateGraph(ParallelState)
workflow_parallel.add_node("splitter", split_to_queries)
workflow_parallel.add_node("search", search_single_query)
workflow_parallel.add_node("merger", merge_results)

workflow_parallel.set_entry_point("splitter")
workflow_parallel.add_conditional_edges("splitter", continue_to_searches)
workflow_parallel.add_edge("search", "merger")
workflow_parallel.add_edge("merger", END)

app_parallel = workflow_parallel.compile()
# 3 个子查询并行搜索,总耗时 ≈ 最慢的那个
result = app_parallel.invoke({})

这个模式的威力在于:子查询数量和复杂度可以动态变化,LangGraph 自动管理并行执行。对于"比较 A、B、C 三个框架"这类任务,并行搜索比串行快 2-3 倍。

6.6 多 Agent 架构选型指南


七、生产部署与性能优化

7.1 流式输出

生产环境必须支持流式输出,让用户看到逐字生成的响应:

# Agent 流式输出
async for chunk in executor.astream_events(
    {"input": "搜索 LangChain 最新版本"},
    version="v2"
):
    kind = chunk["event"]
    if kind == "on_chat_model_stream":
        content = chunk["data"]["chunk"].content
        if content:
            print(content, end="", flush=True)  # 实时输出

# LangGraph 流式输出
async for event in app.astream_events(
    {"messages": [HumanMessage(content="...")]},
    version="v2"
):
    # event 包含节点名称、输入输出、中间步骤
    print(f"节点: {event.get('name')}, 事件: {event['event']}")

7.2 缓存策略

from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache
import hashlib

# LLM 响应缓存
set_llm_cache(InMemoryCache())

# 自定义检索结果缓存
from functools import lru_cache

class CachedRetriever:
    def __init__(self, retriever, maxsize=128):
        self.retriever = retriever
        self._cache = {}

    def invoke(self, query: str):
        cache_key = hashlib.md5(query.encode()).hexdigest()
        if cache_key in self._cache:
            return self._cache[cache_key]
        result = self.retriever.invoke(query)
        self._cache[cache_key] = result
        return result

7.3 错误处理与降级

from tenacity import retry, stop_after_attempt, wait_exponential

class RobustAgent:
    def __init__(self, executor, fallback_chain=None):
        self.executor = executor
        self.fallback_chain = fallback_chain

    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10)
    )
    def invoke(self, input_text: str) -> str:
        try:
            result = self.executor.invoke({"input": input_text})
            return result["output"]
        except Exception as e:
            print(f"Agent 执行失败 (尝试降级): {e}")
            if self.fallback_chain:
                return self.fallback_chain.invoke(input_text)
            raise

# 降级链:不依赖工具,纯 LLM 回答
fallback_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是技术助手。在不使用外部工具的情况下尽力回答。如果不确定,请诚实说明。"),
    ("human", "{input}")
])
fallback_chain = fallback_prompt | llm | StrOutputParser()

robust_agent = RobustAgent(executor, fallback_chain)

7.4 可观测性:LangSmith 集成

追踪 Agent 的每一步行为是生产运维的基本要求。LangSmith 提供了开箱即用的解决方案:

import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "ls__your_key_here"
os.environ["LANGCHAIN_PROJECT"] = "my-rag-agent-prod"

# 此后所有 LangChain/LangGraph 调用自动上报 trace
# 在 app.langchain.com 查看:
# - 每次调用的完整链路(LLM → Retriever → Tool)
# - 每步的输入/输出/耗时/Token 消耗
# - Agent 推理轨迹(Thought → Action → Observation)
# - 错误堆栈和重试记录

# 也可以手动添加自定义 trace
from langsmith import traceable

@traceable(name="my_custom_rag_step")
def custom_rag_query(query: str) -> str:
    # 你的自定义逻辑
    return result

7.5 成本优化:选择合适的模型组合

Agent 系统中最大的成本来自 LLM 调用。一个优化策略是大小模型混合

# 复杂推理(Agent 决策)用大模型
agent_llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 简单任务(检索问题变体生成、文档过滤)用小模型
light_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 在 Multi-Query Retriever 中使用小模型
multi_query_retriever = MultiQueryRetriever.from_llm(
    retriever=base_retriever,
    llm=light_llm,  # 变体生成不需要 4o
    include_original=True
)

# 在 Contextual Compression 中也用小模型
compressor = LLMChainExtractor.from_llm(light_llm)

月成本估算(10万次查询/月)

策略 模型组合 月成本 质量
全量 gpt-4o 4o × 所有步骤 ~$2000 ⭐⭐⭐⭐⭐
混合策略 4o × 决策 + mini × 检索 ~$600 ⭐⭐⭐⭐
全量 mini mini × 所有步骤 ~$200 ⭐⭐⭐
混合 + 缓存 同上 + 热点缓存 ~$400 ⭐⭐⭐⭐

7.6 性能基准参考

指标 基础 RAG Multi-Query RAG Agent (3 tools) Agent (6+ tools)
首次响应延迟 1.2s 3.8s 2.5s 4.2s
完整响应时间 3.5s 8.2s 6.0s 15.0s
LLM 调用次数 1 5 (1 生成 + 4 检索) 2-4 4-10
Token 消耗 ~800 ~3200 ~2000 ~6000
推荐模型 gpt-4o-mini gpt-4o gpt-4o gpt-4o

八、常见问题 FAQ

Q1:Multi-Query RAG 变体数量设多少最合适?

A:根据经验,3-5 个变体是最佳平衡点。少于 3 个和直接用原问题差别不大,多于 5 个收益递减明显,但成本线性增长。如果你的领域术语变体特别多(如医学术语、法律条文),可以适当增加到 7 个。

Q2:Parent-Document RAG 的父子块大小怎么定?

A:子块建议 200-400 tokens(保证检索精度),父块建议 1500-3000 tokens(保证上下文完整性)。可以用以下经验公式:父块大小 ≈ 子块大小 × 8。如果文档本身短小(如 FAQ 条目),直接用全文作文档即可。

Q3:Functions Agent 和 ReAct Agent 怎么选?

A:如果你用 OpenAI/Azure 的模型,无脑选 Functions Agent——更稳定、更快速、支持并行工具调用。如果你用开源模型(Llama、Qwen 等),且该模型不支持原生 Function Calling,那就用 ReAct Agent,配合 few-shot examples 提高解析成功率。

Q4:Agent 老是调用错误的工具怎么办?

A:三个优化方向:(1) 工具描述写得更精确——不仅描述功能,还要说明"什么情况下用、什么情况下不用";(2) 给 Agent 添加 few-shot examples 展示正确用法;(3) 在 System Prompt 中明确工具选择规则。如果还不行,考虑拆分成多个专业 Agent,缩小每个 Agent 的工具集。

Q5:LangGraph 和 LangChain Agent 的关系是什么?

A:LangChain Agent 是"单个 Agent + 工具"的封装,适合相对简单的问答任务。LangGraph 是更底层的状态图框架,你可以用它实现任意复杂的多 Agent 协作模式(循环、条件分支、人机协作)。可以理解为:Agent 是 LangGraph 的一个特例——单节点 + 工具循环。

Q6:生产环境中怎么监控 Agent 的行为?

A:推荐 LangSmith(LangChain 官方的可观测性平台),设置 LANGCHAIN_TRACING_V2=true 即可自动记录每次调用链路的完整 trace。也可以自建监控:在每个工具调用前后打日志,记录工具名、输入参数、执行耗时、返回结果。特别关注 max_iterations 触发的截断——这说明 Agent 陷入了循环。

Q7:上下文窗口总是不够用怎么办?

A:(1) 优先使用 Contextual Compression,在检索后就压缩无关内容;(2) 使用更小的 Embedding 模型(如 text-embedding-3-small,性价比高);(3) 对历史对话做摘要压缩(Summarization Memory),而不是保留原始消息;(4) 如果用的是 ChatGPT,尽量用 gpt-4o(128K 上下文),而非 gpt-4o-mini。

Q8:如何确保 Agent 不会陷入无限循环?

A:这是 Agent 部署中最常见的坑。三道防线:(1) max_iterations 硬限制——设置一个合理的上限(通常 8-15 次),超过后强制终止并返回部分结果;(2) 循环检测——记录最近 N 次 Action 的哈希值,如果出现重复模式(连续 3 次相同 Action + Input),主动打断;(3) 渐进式信心衰减——在 System Prompt 中加入"如果连续 2 次尝试失败,请降级为不使用工具直接回答"的指令。LangSmith 中有一个专门的 loop_detected 事件可以订阅。

Q9:多 Agent 架构 vs 单 Agent + 多工具,什么时候该升级?

A:当出现以下任一信号时,就该考虑多 Agent 架构:(1) 工具集超过 8-10 个——单个 Agent 的选择困难导致频繁误用工具;(2) System Prompt 超过 500 字——提示词太长会稀释关键指令;(3) 不同任务需要的推理风格差异大——比如数据分析需要严谨性、客服需要亲和力,共用一个 System Prompt 难以兼顾;(4) 需要并行的子任务——比如同时在不同数据源搜索,单 Agent 只能串行。反之,如果工具少于 5 个、任务类型单一,单 Agent + 多工具就是最简方案。

Q10:RAG 系统的冷启动问题怎么解决——新知识库没有任何数据?

A:冷启动是每个 RAG 项目的第一道坎。建议:(1) 种子文档策略——手动准备 20-30 篇高质量 FAQ 或文档作为种子,不求多但求精;(2) 从用户日志中挖掘——如果有旧系统(如客服工单、论坛帖子),用 LLM 把"用户问题 + 最佳回复"转成 Q&A 文档对;(3) 渐进式上线——先在小范围(如内部团队)跑 1-2 周,收集 feedback loop,把用户问过但系统答不好的问题手工补充进知识库;(4) 不要一上来就追求全自动——第一版可以先做"检索推荐"模式,让人类审核检索结果后再生成,既保证质量又积累训练数据。


九、总结与展望

9.1 核心要点回顾

本文从 RAG 的四大痛点出发,系统性地介绍了 LangChain 生态中的高级解决方案:

  1. Multi-Query RAG 解决了"问题表述模糊"的问题——用 LLM 生成多角度变体查询,提高检索召回率
  2. Parent-Document RAG 解决了"上下文不完整"的问题——小块检索保精度,大块返回保上下文
  3. Contextual Compression 解决了"上下文窗口浪费"的问题——检索后自动提纯,只保留相关句子
  4. Self-Query RAG 解决了"元数据无法利用"的问题——让 LLM 自动将自然语言转成结构化过滤条件
  5. 工具增强 Agent 让 AI 从"只读"进化为"可执行"——搜索、计算、查询、操作,无所不能
  6. LangGraph 多 Agent 通过分工协作解决复杂任务——每个 Agent 专注自己的领域,通过状态图灵活编排

9.2 技能路径图

入门:基础 RAG(向量库 + LLM)
  ↓
进阶:高级 RAG 模式(Multi-Query, Parent-Doc, Compression)
  ↓
高级:工具增强 Agent(ReAct, Function Calling)
  ↓
专家:多 Agent 协作(LangGraph, 条件路由, 人机协作)
  ↓
大师:生产级部署(流式、缓存、监控、A/B 测试)

9.3 下一步行动

如果你想继续深入,建议按以下顺序学习:

  • 📖 阅读 LangChain 官方文档的 RAGAgents 教程
  • 🔧 用本文的代码模板搭建一个自己的多策略 RAG 系统,替换为你实际业务的知识库
  • 🧪 尝试 LangGraph 的 Multi-Agent Supervisor 官方示例
  • 📊 用 LangSmith 监控你的 Agent 调用链路,观察哪些工具用得最多、哪些步骤耗时最长
  • 🚀 将你的 RAG + Agent 系统部署到生产环境,收集真实用户反馈迭代优化

本文由 MarkShareX AI 自动创作,分类:Agent,方向:LangChain 实战(高级 RAG 与工具增强 Agent)