LangChain 工具调用与 RAG 深度融合实战:打造智能知识型 Agent

Agent 0 次阅读
LangChain 工具调用与 RAG 深度融合实战:打造智能知识型 Agent

当 LLM 不再只会「聊天」,而是能够主动查资料、调工具、做决策,AI Agent 的威力才真正被释放。本文将带你从工具调用机制到 RAG 深度融合,构建一个能搜、能算、能调用外部 API 的生产级智能 Agent。

架构全景图

目录

  1. 背景与概念:为什么 Tool Calling + RAG 是 Agent 的核心
  2. 核心知识:LangChain Tool Calling 机制详解
  3. 核心知识:RAG 原理与检索优化策略
  4. 实战演练:构建知识型 Agent
  5. 进阶技巧:多工具编排与智能路由
  6. 进阶技巧:混合检索与重排序实战
  7. 生产级考虑:性能、成本与安全
  8. 常见问题 FAQ
  9. 总结与展望

一、背景与概念:为什么 Tool Calling + RAG 是 Agent 的核心

2024 年以来,LLM 的能力边界已经从「纯文本生成」扩展到了「具身智能」。GPT-4 的函数调用(Function Calling)、Claude 的工具使用(Tool Use)、以及开源模型通过格式约束实现的工具调用能力,让大语言模型从「聊天机器人」进化为「智能代理」(Agent)。

但工具调用只是硬币的一面。LLM 受限于训练数据的截止时间和参数化的知识存储方式,在处理实时信息、私有数据、精确事实时往往力不从心。这就是 RAG(Retrieval-Augmented Generation,检索增强生成) 的价值所在——它让 Agent 在需要时「翻阅资料」,用外部知识弥补模型的短板。

为什么两者必须结合?

单一的 RAG 系统只能做「问答」——用户提问,系统检索,模型回答。它无法胜任需要多步推理、跨系统交互的复杂任务。比如:

  • 「帮我查一下上周的销售数据,和上个月同期做个对比,然后把结果发到 Slack 通知团队」——这需要数据库查询工具 + 计算工具 + 消息工具 + RAG 检索
  • 「阅读这份合同 PDF,找出违约条款,对照《民法典》相关法条,给出修改建议」——这需要文件工具 + RAG 知识库 + 推理链

Tool Calling 赋予 Agent 行动能力,RAG 赋予 Agent 知识底座。两者的深度融合,才是构建实用 AI Agent 的正解。

概念关系图

本文主要内容

我们将以 LangChain 框架为核心,从零构建一个「知识型 Agent」:

  • 理解 LangChain 的工具系统设计哲学(@tool 装饰器、StructuredTool、BaseTool)
  • 深入 AgentExecutor 的推理-行动循环(ReAct / Tool Calling Agent)
  • 实现向量化 RAG 引擎(ChromaDB + 嵌入模型 + 多策略检索)
  • 将 RAG 与工具调用深度整合:把「检索」本身变成一个 Tool
  • 进阶:多工具编排、智能路由、混合检索、重排序
  • 生产部署:流式输出、缓存优化、Token 成本控制、安全防护

在开始之前,请确保你已经安装了以下依赖:

pip install langchain langchain-openai langchain-community chromadb tiktoken pypdf

二、核心知识:LangChain Tool Calling 机制详解

2.1 Tool 的本质:函数的「结构化描述」

在 LangChain 中,一个 Tool 本质上是对 Python 函数的封装,附加了 LLM 可理解的描述信息。当 Agent 决定使用某个工具时,它实际上是在生成一个结构化的函数调用请求。

LangChain 提供了三种定义工具的方式:

方式一:@tool 装饰器(最简洁)

from langchain.tools import tool

@tool
def search_knowledge_base(query: str) -> str:
    """
    在内部知识库中搜索相关信息。
    当用户询问公司政策、产品文档、历史记录时使用此工具。
    
    Args:
        query: 搜索查询字符串
    """
    # 实际检索逻辑
    results = vector_store.similarity_search(query, k=5)
    return "\n\n".join([doc.page_content for doc in results])

@tool
def calculate(expression: str) -> str:
    """
    执行数学计算。支持加减乘除、幂运算、三角函数等。
    当需要进行精确数值计算时使用此工具。
    
    Args:
        expression: 数学表达式,如 '2 + 3 * 4'
    """
    import math
    try:
        result = eval(expression, {"__builtins__": {}}, {"math": math})
        return f"计算结果: {result}"
    except Exception as e:
        return f"计算错误: {str(e)}"

方式二:StructuredTool(带结构化参数)

from langchain.tools import StructuredTool
from pydantic import BaseModel, Field

class WeatherInput(BaseModel):
    city: str = Field(description="城市名称,如 '北京'、'上海'")
    date: str = Field(default="today", description="日期,格式 YYYY-MM-DD")

def get_weather(city: str, date: str = "today") -> str:
    """获取指定城市和日期的天气信息"""
    # 调用天气 API
    return f"{city} {date} 天气:晴,22°C~30°C"

weather_tool = StructuredTool.from_function(
    func=get_weather,
    name="get_weather",
    description="获取指定城市的天气信息",
    args_schema=WeatherInput
)

方式三:BaseTool 子类(最灵活)

from langchain.tools import BaseTool
from typing import Optional, Type

class DatabaseQueryTool(BaseTool):
    name: str = "database_query"
    description: str = """
    执行 SQL 查询。仅支持 SELECT 语句。
    表结构:orders(id, customer, amount, date), products(id, name, price)
    """
    args_schema: Type[BaseModel] = DatabaseQueryInput
    
    def _run(self, sql: str, limit: int = 10) -> str:
        # 安全检查:只允许 SELECT
        if not sql.strip().upper().startswith("SELECT"):
            return "错误:仅允许 SELECT 查询"
        # 执行查询
        return execute_sql(sql, limit)

2.2 Tool Calling 的底层流程

当 Agent 接到用户请求后,LangChain 的 AgentExecutor 进入一个推理-行动循环(Reasoning-Action Loop):

工具调用流程图

步骤详解:

  1. 构建 Prompt:将系统提示、可用工具列表(含名称、描述、参数 schema)、对话历史一并组装成 prompt
  2. LLM 推理:模型分析用户意图,决定「直接回答」还是「调用工具」
  3. 解析输出:如果是工具调用,LangChain 解析模型输出的结构化参数(JSON/function call)
  4. 执行工具:调用对应的 Python 函数,获取结果
  5. 反馈观察:工具执行结果作为新的「观察」(Observation)注入上下文
  6. 循环判断:Agent 判断是否还需要更多信息,如果是则回到步骤 1;否则生成最终回答
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

# 初始化 LLM 和工具
llm = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [search_knowledge_base, calculate, weather_tool]

# 创建 Agent
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个智能助手,可以搜索知识库、计算和查天气。"),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=5,  # 最多循环 5 次
    handle_parsing_errors=True,
)

# 执行
result = executor.invoke({
    "input": "北京的天气怎么样?如果温度超过25度就帮我算一下空调一天的电费"
})

2.3 关键设计决策:工具粒度与描述策略

工具设计有两个核心原则:

原则一:单一职责 每个工具只做一件事。避免「万能工具」——它会让 LLM 困惑。

# ❌ 不好的设计:一个工具做太多事
@tool
def do_everything(action: str, data: str) -> str:
    """可以搜索、计算、发邮件、查数据库"""
    ...

# ✅ 好的设计:原子化工具
@tool
def search_docs(query: str) -> str: ...
@tool
def send_email(to: str, subject: str, body: str) -> str: ...
@tool
def query_database(sql: str) -> str: ...

原则二:描述即文档 LLM 完全依赖工具的 description 字段来理解何时调用。描述应该:

  • 说明什么时候使用这个工具
  • 说明什么时候不该用
  • 包含参数含义返回值格式
@tool
def get_stock_price(symbol: str) -> str:
    """
    获取股票实时价格。
    
    使用场景:
    - 用户询问某只股票的当前价格
    - 需要计算投资组合价值时
    
    不适用场景:
    - 历史价格走势(请使用 get_stock_history)
    - 公司基本面分析(请使用 get_company_info)
    
    Args:
        symbol: 股票代码,如 AAPL、TSLA、600519.SH
    Returns:
        JSON 格式:{"symbol": "...", "price": ..., "change": "..."}
    """

三、核心知识:RAG 原理与检索优化策略

3.1 RAG 的工作流程

RAG 的核心思想是「先检索,再生成」。整个流程分为离线(索引构建)和在线(查询处理)两个阶段:

离线阶段:

  1. 文档加载:从 PDF、网页、数据库等来源读取文档
  2. 文本分割:将长文档切分为适当大小的「块」(chunk)
  3. 向量化:使用嵌入模型(如 text-embedding-3-small)将每个 chunk 转为向量
  4. 索引存储:将向量存入向量数据库(如 ChromaDB、Pinecone、Qdrant)

在线阶段:

  1. 查询改写:对用户问题进行优化(扩展、纠错、多角度重写)
  2. 向量检索:将改写后的查询转为向量,在向量数据库中搜索相似 chunk
  3. 重排序:对检索结果进行二次排序,提高相关性
  4. 上下文组装:将检索到的文档片段拼接进 LLM 的 prompt
  5. 生成回答:LLM 综合上下文和用户问题生成最终答案

RAG流程图

3.2 向量存储与嵌入模型选型

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader, PyPDFLoader

# 1. 加载文档
loader = PyPDFLoader("company_policy.pdf")
documents = loader.load()

# 2. 文本分割
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,        # 每块 1000 字符
    chunk_overlap=200,      # 块间重叠 200 字符
    separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""],
)
chunks = text_splitter.split_documents(documents)
print(f"共生成 {len(chunks)} 个文本块")

# 3. 向量化 + 存储
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vector_store = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db",
)

嵌入模型对比:

模型 维度 最大输入 价格(每1M token) 适合场景
text-embedding-3-small 512 8191 $0.02 通用检索,成本敏感
text-embedding-3-large 3072 8191 $0.13 高精度语义匹配
bge-large-zh-v1.5 1024 512 免费(本地) 中文场景
m3e-base 768 512 免费(本地) 中文,轻量级

3.3 关键词检索的补充价值

纯向量检索虽然强大,但有其固有的盲区。嵌入模型将文本映射为语义向量,在这个过程中会丢失精确的关键词匹配能力。例如,用户搜索「SKU-2024-Q3-88492」时,向量检索可能返回包含「SKU-2024-Q2-88493」的文档——语义接近但并非用户要的产品编号。

这就是为什么生产级 RAG 系统通常采用混合检索策略——同时运行向量检索和关键词检索(如 BM25),然后通过融合算法合并结果。BM25 是经典的 TF-IDF 变体,擅长精确字符串匹配和罕见词检索。LangChain 的 EnsembleRetriever 可以无缝整合两种检索器。

在实践中,推荐的初始权重配比是向量检索 0.7、BM25 0.3。对于包含大量技术编号、API 端点、错误码的知识库,可以适当提高 BM25 的权重到 0.4-0.5。

3.4 Chunk 策略:RAG 性能的关键杠杆

Chunk 的大小和重叠度直接影响检索质量:

策略 Chunk 大小 重叠 适用场景 优点 缺点
固定大小 500 100 通用场景 简单可靠 可能截断语义
语义分割 按段落 0 结构化文档 保留语义完整 依赖文档格式
小 Chunk 256 64 精准问答 精确定位 上下文不足
大 Chunk 2000 400 复杂推理 丰富上下文 噪声增加

实践建议:对同一份文档,使用多种 chunk 策略生成不同粒度的索引(小块用于精准检索,大块用于上下文补充),即「父子文档」(Parent Document)模式。

# 父子文档模式
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore

# 子文档(用于检索)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
# 父文档(用于上下文)
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)

store = InMemoryStore()
retriever = ParentDocumentRetriever(
    vectorstore=vector_store,
    docstore=store,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)

四、实战演练:构建知识型 Agent

现在我们将 Tool Calling 和 RAG 结合,构建一个完整的知识型 Agent。这个 Agent 可以:

  • 在本地知识库中搜索信息(RAG)
  • 调用外部 API(天气、股价)
  • 进行数学计算
  • 根据检索结果做决策

代码执行流程图

4.1 项目结构

knowledge_agent/
├── main.py              # Agent 入口
├── tools/
│   ├── __init__.py
│   ├── search_tool.py   # RAG 检索工具
│   ├── api_tools.py     # 外部 API 工具
│   └── calc_tool.py     # 计算工具
├── rag/
│   ├── loader.py        # 文档加载
│   ├── indexer.py       # 向量索引
│   └── retriever.py     # 检索器
└── config.py            # 配置文件

4.2 将 RAG 封装为 Tool

这是深度融合的关键步骤——把检索能力变成一个标准 Tool:

# tools/search_tool.py
from langchain.tools import tool
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from typing import Optional

# 全局变量(生产环境用依赖注入)
_vector_store = None
_llm = None

def init_search_tool(persist_dir: str = "./chroma_db"):
    """初始化检索工具"""
    global _vector_store
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    _vector_store = Chroma(
        persist_directory=persist_dir,
        embedding_function=embeddings,
    )

@tool
def search_knowledge_base(query: str, k: int = 5) -> str:
    """
    在内部知识库中搜索信息。用于回答公司政策、产品文档、技术规范等问题。
    
    检索策略:
    - 对用户查询自动进行同义词扩展
    - 使用 MMR(最大边际相关性)算法去重
    - 返回 Top-K 最相关文档片段
    
    Args:
        query: 搜索查询
        k: 返回结果数量(默认 5)
    """
    if _vector_store is None:
        return "错误:知识库未初始化"
    
    # 使用 MMR 检索(兼顾相关性和多样性)
    results = _vector_store.max_marginal_relevance_search(
        query, k=k, fetch_k=20, lambda_mult=0.7
    )
    
    if not results:
        return "未找到相关信息。建议尝试不同的关键词。"
    
    formatted = []
    for i, doc in enumerate(results, 1):
        source = doc.metadata.get("source", "未知来源")
        formatted.append(f"[{i}] (来源: {source})\n{doc.page_content}")
    
    return "\n\n---\n\n".join(formatted)

4.3 创建外部 API 工具

# tools/api_tools.py
from langchain.tools import tool
import requests
from datetime import datetime

@tool
def get_current_time(timezone: str = "Asia/Shanghai") -> str:
    """获取当前日期和时间"""
    now = datetime.now()
    return f"当前时间: {now.strftime('%Y-%m-%d %H:%M:%S')} (时区: {timezone})"

@tool
def web_search_tool(query: str) -> str:
    """
    搜索互联网获取最新信息。
    适用于知识库中没有的实时信息。
    
    Args:
        query: 搜索查询
    """
    # 这里接入实际的搜索 API(如 Tavily、SerpAPI)
    return f"[模拟搜索结果] 关于 '{query}' 的最新信息..."

4.4 组装 Agent

# main.py
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from tools.search_tool import search_knowledge_base, init_search_tool
from tools.api_tools import get_current_time, web_search_tool
from tools.calc_tool import calculate

# 初始化
init_search_tool("./chroma_db")
llm = ChatOpenAI(model="gpt-4o", temperature=0)

tools = [
    search_knowledge_base,
    get_current_time,
    web_search_tool,
    calculate,
]

# 精心设计的系统提示
system_prompt = """你是一个知识型 AI Agent,具备以下能力:

1. **知识检索**:可以搜索内部知识库获取公司政策、产品文档等信息
2. **联网搜索**:对于知识库中没有的实时信息,可以搜索互联网
3. **数学计算**:可以进行精确的数值计算
4. **时间感知**:知道当前日期和时间

工作原则:
- 优先使用内部知识库,找不到再联网搜索
- 当用户问题涉及多个步骤时,逐步执行
- 计算结果要给出推导过程
- 如果信息不足以回答问题,诚实说明"""

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

agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=8,
    handle_parsing_errors=True,
    return_intermediate_steps=True,
)

# 测试运行
if __name__ == "__main__":
    response = executor.invoke({
        "input": "公司今年的休假政策是什么?帮我算一下如果我有15天年假,平均每个月可以休几天?"
    })
    print(f"\n最终回答:\n{response['output']}")

4.5 运行轨迹分析

让我们看看一个典型的运行轨迹:

> Entering new AgentExecutor chain...

Invoking: search_knowledge_base with {'query': '公司休假政策 年假'}
[返回 3 条文档片段]

Invoking: calculate with {'expression': '15 / 12'}
计算得到: 1.25

根据公司休假政策,员工每年享有 15 天带薪年假...
平均到每个月,您可以休 15/12 = 1.25 天...

这个例子展示了 Agent 如何自主决策工具调用顺序:先检索政策文档获取事实依据,再调用计算工具进行数值推导,最后综合两个结果生成回答。


五、进阶技巧:多工具编排与智能路由

当一个 Agent 拥有越来越多的工具时,简单地把所有工具都扔给 LLM 会带来两个问题:

  1. 性能下降:prompt 中工具描述越多,推理越慢,Token 消耗越大
  2. 选择失误:工具越多,LLM 选错工具的概率越高

解决方案是工具路由(Tool Routing)——根据用户意图动态选择工具子集。

5.1 意图识别 + 工具分组

from typing import Literal
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field

class Intent(BaseModel):
    """用户意图分类"""
    category: Literal["knowledge_search", "calculation", "api_call", "general"]
    confidence: float = Field(ge=0, le=1)

# 意图识别 prompt
intent_prompt = ChatPromptTemplate.from_messages([
    ("system", """分析用户消息,判断意图类别:
    - knowledge_search: 需要查询知识库、文档、政策
    - calculation: 需要数学计算、数据分析
    - api_call: 需要调用外部 API(天气、股价等)
    - general: 普通对话,不需要工具
    
    返回 JSON 格式。"""),
    ("human", "{input}"),
])

# 带结构化输出的 LLM
intent_chain = intent_prompt | llm.with_structured_output(Intent)

# 工具分组
tool_groups = {
    "knowledge_search": [search_knowledge_base, web_search_tool],
    "calculation": [calculate],
    "api_call": [get_current_time, weather_tool, get_stock_price],
}

def select_tools(user_input: str):
    """根据意图选择工具子集"""
    intent = intent_chain.invoke({"input": user_input})
    print(f"识别意图: {intent.category} (置信度: {intent.confidence})")
    
    selected = tool_groups.get(intent.category, [])
    # 始终包含通用工具
    selected.append(get_current_time)
    return selected

5.2 多 Agent 协作模式

对于更复杂的任务,可以使用「路由 Agent + 专家 Agent」的模式:

进阶架构图

from langchain.agents import AgentExecutor

# 创建多个专家 Agent
knowledge_agent = AgentExecutor(
    agent=create_tool_calling_agent(llm, [search_knowledge_base], prompt),
    tools=[search_knowledge_base],
)

calc_agent = AgentExecutor(
    agent=create_tool_calling_agent(llm, [calculate], prompt),
    tools=[calculate],
)

# 路由 Agent(Supervisor)
supervisor_tools = [
    StructuredTool.from_function(
        func=lambda query: knowledge_agent.invoke({"input": query})["output"],
        name="delegate_to_knowledge_expert",
        description="将知识检索类问题委派给知识专家处理"
    ),
    StructuredTool.from_function(
        func=lambda expr: calc_agent.invoke({"input": expr})["output"],
        name="delegate_to_calc_expert",
        description="将计算类问题委派给计算专家处理"
    ),
]

supervisor = AgentExecutor(
    agent=create_tool_calling_agent(llm, supervisor_tools, prompt),
    tools=supervisor_tools,
)

5.3 动态工具注册

生产环境中,工具可能需要动态加载(如按用户权限加载不同工具集):

class ToolRegistry:
    """工具注册中心"""
    
    def __init__(self):
        self._tools: Dict[str, BaseTool] = {}
        self._permissions: Dict[str, List[str]] = {}  # 用户 -> 工具列表
    
    def register(self, tool: BaseTool, required_role: str = "user"):
        self._tools[tool.name] = tool
        if required_role not in self._permissions:
            self._permissions[required_role] = []
        self._permissions[required_role].append(tool.name)
    
    def get_tools_for_user(self, user_role: str) -> List[BaseTool]:
        allowed = self._permissions.get(user_role, [])
        return [self._tools[name] for name in allowed if name in self._tools]

# 使用
registry = ToolRegistry()
registry.register(search_knowledge_base, required_role="user")
registry.register(query_database, required_role="admin")
registry.register(send_email, required_role="user")

# 不同用户看到不同的工具集
user_tools = registry.get_tools_for_user("user")     # 不含 query_database
admin_tools = registry.get_tools_for_user("admin")   # 包含所有

六、进阶技巧:混合检索与重排序实战

6.1 为什么需要混合检索?

纯向量检索在某些场景下效果不佳:

  • 精确关键词匹配:如产品编号 "SKU-88492",向量检索可能返回语义相近但编号不同的结果
  • 专有名词:公司内部术语、缩写,嵌入模型可能无法准确理解
  • 数字/日期查询:「2024年Q3财报」——向量检索可能返回 Q2 或 Q4 的内容

混合检索 = 向量检索 + 关键词检索,取二者的交集或加权融合。

6.2 实现 BM25 + 向量检索的混合引擎

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

# 1. BM25 关键词检索器(基于文档集合)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5

# 2. 向量检索器
vector_retriever = vector_store.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 5, "fetch_k": 20, "lambda_mult": 0.7}
)

# 3. 集成检索器(RRF 融合)
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.3, 0.7],  # BM25 权重 30%,向量检索 70%
)

6.3 重排序(Re-ranking)提升精度

检索到的文档可能相关但排序不佳。重排序模型可以对初检结果精排:

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

# 使用 Cross-Encoder 进行重排序
model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-large")
compressor = CrossEncoderReranker(model=model, top_n=5)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=ensemble_retriever,
)

# 现在检索的结果经过了 BM25 → 向量 → 重排序 三阶段过滤
results = compression_retriever.invoke("公司今年的休假政策")

6.4 查询优化:让检索更聪明

在检索前对用户查询进行处理,可以显著提升检索质量:

from langchain_core.prompts import ChatPromptTemplate

# 查询扩展:将短查询扩展为多个相关查询
query_expansion_prompt = ChatPromptTemplate.from_messages([
    ("system", """将用户的查询扩展为3个不同角度的搜索查询。
    原始查询: {query}
    
    输出每行一个查询,不要编号。"""),
])

def expand_query(query: str) -> list[str]:
    response = llm.invoke(
        query_expansion_prompt.format_messages(query=query)
    )
    queries = [q.strip() for q in response.content.split("\n") if q.strip()]
    return queries[:3]  # 最多 3 个

# 多查询融合检索
from langchain.retrievers.multi_query import MultiQueryRetriever

multi_query_retriever = MultiQueryRetriever.from_llm(
    retriever=vector_store.as_retriever(),
    llm=llm,
)

6.5 带来源引用的回答

高质量 RAG 的标配是回答带上来源引用:

def format_with_citations(query: str, retriever) -> str:
    docs = retriever.invoke(query)
    
    # 构建带编号的上下文
    context_parts = []
    sources = {}
    for i, doc in enumerate(docs, 1):
        context_parts.append(f"[{i}] {doc.page_content}")
        sources[i] = doc.metadata.get("source", "未知")
    
    context = "\n\n".join(context_parts)
    
    prompt = f"""基于以下检索到的文档回答问题。在回答中引用来源编号(如 [1]、[2])。

文档:
{context}

问题:{query}

要求:
- 每个关键事实都要标注来源编号
- 如果文档之间信息有冲突,请指出
- 如果没有足够信息,请明确说明"""

    response = llm.invoke(prompt)
    return response.content, sources

七、生产级考虑:性能、成本与安全

7.1 流式输出与用户体验

from langchain.callbacks import StreamingStdOutCallbackHandler
from langchain_core.runnables import RunnableConfig

async def stream_agent_response(user_input: str):
    """流式返回 Agent 响应"""
    async for event in executor.astream_events(
        {"input": user_input},
        version="v2",
    ):
        kind = event["event"]
        
        if kind == "on_tool_start":
            print(f"\n🔧 调用工具: {event['name']}")
        elif kind == "on_tool_end":
            print(f"✅ 工具完成: {event['name']}")
        elif kind == "on_chat_model_stream":
            content = event["data"]["chunk"].content
            if content:
                yield content  # 实时返回给前端

7.2 Token 成本优化策略

策略 效果 实现难度
压缩工具描述 节省 20-30% 系统 prompt token ⭐ 低
对话历史摘要 节省 50%+ 上下文 token ⭐⭐ 中
工具结果截断 节省 30-50% 观察 token ⭐ 低
缓存常用检索 减少重复 API 调用 ⭐⭐⭐ 高
意图路由减少工具集 节省 40-60% 工具描述 token ⭐⭐ 中
# 对话历史摘要
from langchain.memory import ConversationSummaryMemory

memory = ConversationSummaryMemory(
    llm=llm,
    max_token_limit=500,  # 摘要最大 token 数
    return_messages=True,
)

# 工具结果截断
@tool
def search_with_truncation(query: str) -> str:
    results = vector_store.similarity_search(query, k=10)
    formatted = []
    total_chars = 0
    for doc in results:
        chunk = doc.page_content[:500]  # 每段最多 500 字符
        formatted.append(chunk)
        total_chars += len(chunk)
        if total_chars > 3000:  # 总字符数上限
            formatted.append("... (更多结果已截断)")
            break
    return "\n\n".join(formatted)

7.3 安全防护

# SQL 注入防护
def sanitize_sql(sql: str) -> bool:
    dangerous = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "TRUNCATE", "EXEC"]
    sql_upper = sql.upper()
    for keyword in dangerous:
        if keyword in sql_upper:
            return False
    return sql_upper.strip().startswith("SELECT")

# 工具执行沙箱
@tool
def safe_python_exec(code: str) -> str:
    """在受限环境中执行 Python 代码"""
    allowed_builtins = {
        "abs": abs, "len": len, "range": range,
        "int": int, "float": float, "str": str,
        "list": list, "dict": dict, "sum": sum,
        "min": min, "max": max, "sorted": sorted,
    }
    try:
        result = eval(code, {"__builtins__": allowed_builtins}, {})
        return str(result)
    except Exception as e:
        return f"代码执行错误: {str(e)}"

# 内容过滤
def filter_sensitive_info(text: str) -> str:
    """过滤敏感信息(手机号、身份证、银行卡)"""
    import re
    text = re.sub(r'1[3-9]\d{9}', '[手机号已隐藏]', text)
    text = re.sub(r'\d{17}[\dXx]', '[身份证号已隐藏]', text)
    text = re.sub(r'\d{16,19}', '[银行卡号已隐藏]', text)
    return text

7.4 监控与可观测性

在生产环境中运行 Agent,看不见的问题才是最危险的。建议对每个 Agent 请求记录以下指标:

import time
from langchain.callbacks import BaseCallbackHandler

class MetricsCallback(BaseCallbackHandler):
    """收集 Agent 运行指标"""
    
    def __init__(self):
        self.tool_calls = []
        self.total_tokens = 0
        self.start_time = None
    
    def on_agent_start(self, *args, **kwargs):
        self.start_time = time.time()
    
    def on_tool_start(self, serialized, input_str, **kwargs):
        self.tool_calls.append({
            "tool": serialized.get("name", "unknown"),
            "input": input_str,
            "start": time.time(),
        })
    
    def on_tool_end(self, output, **kwargs):
        if self.tool_calls:
            self.tool_calls[-1]["end"] = time.time()
            self.tool_calls[-1]["duration"] = (
                self.tool_calls[-1]["end"] - self.tool_calls[-1]["start"]
            )
    
    def get_report(self) -> dict:
        elapsed = time.time() - self.start_time if self.start_time else 0
        return {
            "total_duration": f"{elapsed:.2f}s",
            "tool_calls": len(self.tool_calls),
            "tool_details": [
                f"{tc['tool']}: {tc.get('duration', 0):.2f}s"
                for tc in self.tool_calls
            ],
        }

# 使用
callback = MetricsCallback()
executor.invoke({"input": "..."}, config={"callbacks": [callback]})
print(callback.get_report())

7.5 错误处理与降级策略

Agent 在生产中最常见的失败模式是工具调用超时或返回异常。一个健壮的 Agent 应该实现优雅降级:

@tool
def resilient_search(query: str) -> str:
    """带降级的搜索工具"""
    try:
        # 主路径:向量检索
        return vector_store.similarity_search(query, k=5)
    except Exception as e:
        try:
            # 降级路径 1:简单关键词匹配
            return keyword_search(query)
        except Exception:
            # 降级路径 2:返回预设的兜底回答
            return "搜索服务暂时不可用,请稍后重试或尝试其他关键词。"

对于外部 API 工具,建议加上超时和重试:用 tenacity 库实现指数退避重试(首次 1s、二次 2s、三次 5s),最多重试 3 次后返回友好的降级信息。这样即使部分依赖不可用,Agent 仍能给出部分回答而非完全报错。


八、常见问题 FAQ

Q1: Tool Calling 和 Function Calling 有什么区别?

A: Tool Calling 是 LangChain 的概念,Function Calling 是 OpenAI 的原生能力。两者本质相同:让 LLM 输出结构化参数来调用外部函数。LangChain 的 Tool Calling Agent 在底层可以适配多种 LLM 的工具调用机制(OpenAI function calling、Anthropic tool use、Ollama JSON mode),提供统一的接口。

Q2: 如何选择嵌入模型?中文场景用哪个?

A: 推荐选择顺序:

  • 有预算且追求精度 → text-embedding-3-large(OpenAI)
  • 成本敏感但需要好效果 → bge-large-zh-v1.5(开源,中文 SOTA)
  • 本地部署、轻量级 → m3e-basebge-small-zh-v1.5

中文场景优先选择在中文数据集上微调过的模型,如 BGE 系列、M3E 系列。

Q3: Agent 频繁调用同一个工具怎么办?

A: 这通常是工具描述不够精确导致的。解决方案:

  • 设置 max_iterations 限制最大循环次数
  • return_intermediate_steps=True 模式下分析哪些步骤是冗余的
  • 优化工具描述,明确「何时停止」
  • 添加一个 final_answer 工具让 LLM 显式声明完成

Q4: 向量数据库怎么选?ChromaDB、Pinecone、Qdrant 哪个好?

A: 取决于场景:

向量数据库 类型 适用规模 优势
ChromaDB 嵌入式 <100K 向量 零配置,适合原型和中小项目
Qdrant 独立服务 100K-10M 高性能,过滤功能强
Pinecone 云服务 任意规模 全托管,无需运维
Milvus 分布式 10M+ 大规模生产环境首选
FAISS <1M 极快,适合离线批处理

Q5: 如何处理文档更新?RAG 索引如何保持同步?

A: 常见策略:

  1. 全量重建:适合文档量小(<1000)的场景,定时全量重建索引
  2. 增量更新:使用文档 ID 追踪,只更新变化的文档。ChromaDB 支持 update_document
  3. 版本管理:为索引打版本号,每次更新切换版本(蓝绿部署)
  4. 实时同步:通过 Webhook 或消息队列监听文档变更事件

Q6: Tool Calling 的延迟太大了,怎么优化?

A: 延迟来源主要是:LLM 推理 + 工具执行。优化方向:

  • 使用更快的模型(如 GPT-4o-mini 代替 GPT-4o 做意图路由)
  • 并行执行独立的工具调用(LangChain 支持 RunnableParallel
  • 缓存常见的工具调用结果
  • 对知识检索类工具使用更小的向量维度

Q7: 如何防止 Agent 执行危险操作?

A: 多层防护:

  1. 工具层:每个工具内部做参数校验和权限检查
  2. 执行层:设置 max_iterations、超时时间
  3. 内容层:敏感信息过滤、输出审计
  4. 人机协同:高风险操作(如发送邮件、修改数据)增加人工确认步骤

Q8: 使用 LangChain 的 Agent 和使用 LangGraph 有什么区别?

A: LangChain Agent 适合简单的「单轮推理-行动」循环,代码量少、上手快。LangGraph 提供了图结构编排能力,支持条件分支、并行执行、循环、人工介入等复杂工作流。如果你的 Agent 需要:

  • 多步骤且有条件分支 → 使用 LangGraph
  • 需要并行调用多个工具 → LangGraph 的 Send API 更方便
  • 需要持久化状态和断点续跑 → LangGraph 内置 checkpointer
  • 简单的工具调用问答 → LangChain Agent 完全够用

建议:从 LangChain Agent 开始,当发现 AgentExecutor 难以表达你的工作流时再迁移到 LangGraph。

Q9: 本地部署 RAG 的最低硬件配置是什么?

A: 取决于嵌入模型和向量数据库:

  • 嵌入模型bge-small-zh-v1.5(384 维)在 CPU 上即可运行,内存 < 500MB
  • 向量数据库:ChromaDB 在 10 万条向量以下仅需 < 2GB 内存
  • LLM:7B 量化模型(如 Qwen2.5-7B-Instruct-GGUF q4)需 6-8GB 显存/内存
  • 推荐最低配置:16GB RAM + 8GB VRAM 的消费级 GPU(或 Apple Silicon 16GB 统一内存)

九、总结与展望

核心要点回顾

本文从零构建了一个 LangChain 驱动的知识型 Agent,涵盖了:

  1. Tool Calling 机制:三种定义方式、推理-行动循环、工具设计原则
  2. RAG 深度整合:向量存储、Chunk 策略、父子文档模式
  3. 工具路由与编排:意图识别、动态工具加载、多 Agent 协作
  4. 混合检索优化:BM25 + 向量融合、Cross-Encoder 重排序、查询扩展
  5. 生产级考量:流式输出、成本优化、安全防护、可观测性

下一步学习建议

  • LangGraph:LangChain 的图编排框架,适合复杂的多步骤 Agent 工作流
  • CrewAI / AutoGen:多 Agent 框架,构建 Agent 团队协作
  • Agent 记忆系统:长短期记忆管理,让 Agent 记住用户偏好和上下文
  • RLHF 对齐:通过人类反馈强化学习优化 Agent 的行为策略

技术趋势展望

2025 年,Agent 技术正在向以下方向发展:

  • MCP 协议(Model Context Protocol):Anthropic 提出的标准化工具接口协议,让 Agent 可以即插即用地连接各种工具
  • Agent-to-Agent 通信:多个 Agent 之间通过标准协议协作,形成 Agent 网络
  • 本地优先:更多工具和模型支持本地运行,降低延迟和成本,保护数据隐私

AI Agent 的黄金时代才刚刚开始。掌握 Tool Calling + RAG 这两项核心能力,你就拥有了构建智能应用的最强武器。


本文由 MarkShareX AI 自动创作,分类:Agent,方向:LangChain 工具调用与 RAG 深度融合实战