Python & AI Tutorials Logo
LangChain & LangGraph

5. 代理预览 — "顿悟"时刻

到目前为止,你构建的系统中控制着流程。你编写提示词,调用LLM,并处理响应。LLM很强大,但它仍然只是遵循你的指令。

本章介绍一个根本性的转变:如果LLM决定接下来发生什么呢?

为什么存在本章

问题:如果你在没有这个预览的情况下继续构建聊天机器人直到第11章,你可能会认为"代理(agent)只是更好的聊天机器人"。

真相:代理(agent)从根本上是不同的。它们做出控制你程序的决策,而不仅仅是生成文本。

解决方案:本章向你展示我们要去哪里,这样当你在接下来的章节中构建管道和状态管理时,你会理解为什么每个部分都很重要。

你将学到什么(以及不会学到什么)

本章(第5章):

  • 15分钟的代理思维最小化预览
  • LLM输出如何触发不同的代码路径
  • 路由和代理循环之间的概念差异

后续章节(从第12章开始):

  • 具有工具、循环和自我纠正的真实代理系统
  • 生产就绪的实现
  • 高级模式,如多代理协调

为什么有差距?在构建代理之前,你需要基础:提示工程(第4章)、执行管道(第6章)、结构化输出(第7章)、状态管理(第8章)和知识集成(第9-11章)。

关于学习方法的说明:本章侧重于概念,而非实现细节。实际的设计和开发技术将在第12章及以后出现。如果你不知道如何实现这里看到的所有内容,不要担心——这是有意为之。现在理解代理在概念上是什么就足够了。

让我们开始吧。

5.1) 从聊天机器人到决策者

概念:LLM不是回答问题,而是决定使用哪个工具

在第3章和第4章中,你构建了LLM生成文本响应的聊天系统。流程很简单:

  1. 用户提出问题
  2. LLM生成答案
  3. 你显示答案

这是一个静态链(chain):流程是预先确定的。LLM的唯一工作是生成文本。

现在让我们改变请求的类型。用户问:"847 × 923等于多少?"

你在第3章构建的聊天机器人会尝试回答这个问题,但LLM实际上不进行计算——它们预测看起来合理的标记(token)。对于"2 + 2",答案"4"在训练数据中出现得如此频繁,以至于LLM能答对。但对于"847 × 923"——LLM从未见过的计算——它会生成看起来像数字但可能是错误的东西。

你真正想要的是:

  1. LLM识别:"这是一个数学问题"
  2. LLM决定:"使用计算器工具,而不是我的标记预测"
  3. Python执行: 847 × 923 = 781,781
  4. 系统返回:正确答案

LLM的工作不是计算——而是路由到能够计算的工具。

这里有一个关键见解:LLM不需要解决数学问题——它需要决定使用计算器。

这就是从聊天机器人到决策者的转变。LLM检查请求并将其路由到适当的工具。它正在做出关于程序流程的决策,而不仅仅是生成文本。

让我们可视化这种差异:

静态链(聊天机器人)

用户: 847 × 923等于多少?

LLM生成文本

显示: '大约780,000...'

动态路由(决策者)

用户: 847 × 923等于多少?

LLM决定: 需要计算器

执行: Python计算器

显示: 781,781

在静态链中,LLM尝试直接回答所有问题。在动态路由系统中,LLM将请求路由到正确的工具。

可视化:比较"线性链"(静态)与"路由器"(动态)

以下是静态链如何处理任何用户请求:

用户输入

LLM

文本响应

显示给用户

LLM尝试直接回答所有问题。无论你问"法国的首都是什么?"还是"240的15%是多少?",LLM都会生成文本响应。它可能会答对首都(它在训练数据中见过"巴黎"很多次),但它可能会算错百分比。

问题:LLM对所有问题使用相同的方法——标记预测——即使存在更好的工具。


现在让我们引入一个可以以不同方式处理不同类型请求的系统:

  1. 事实性问题:"法国的首都是什么?" → 使用搜索工具
  2. 数学问题:"240的15%是多少?" → 使用计算器
  3. 对话性:"你好吗?" → LLM直接响应

以下是使这成为可能的动态路由器:

事实性

数学

对话性

用户输入

LLM决策层

搜索工具

计算器工具

直接响应

格式化结果

显示给用户

LLM检查输入并根据请求类型选择路径。这是路由逻辑,LLM充当路由器。

关键区别:LLM不再总是生成文本,而是决定如何处理每个请求——通过将其路由到适当的工具。

关键要点:从生成答案到选择行动

这是代理系统的根本转变:

传统LLM使用:你提出问题,LLM写出答案。

代理式LLM使用:你提出问题,LLM决定做什么,你的代码执行该决策。

LLM的输出不再只是给用户的文本——它是给你程序的指令

这样想:在传统聊天机器人中,LLM是回答客户问题的员工。在代理系统中,LLM是决定哪个部门应该处理每个请求的经理。

这种决策能力是代理系统的本质。LLM不仅仅是响应——它控制你的程序接下来做什么。根据输入,它可以触发计算器、调用搜索API或直接响应。程序的行为根据LLM的决策而改变。

但我们如何实际实现这一点?让我们看看使这一切工作的最小代码。

5.2) 最小化执行

触发器:强制结构化输出的系统提示词

要使LLM充当路由器,我们需要约束其输出。我们不希望它生成完整的答案,而是希望它输出一个结构化决策——一个JSON对象,告诉我们的代码要做什么以及使用什么数据。

以下是实现这一点的系统提示词:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
import json
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
system_prompt = """你是一个路由助手。分析用户的请求并用JSON响应。
 
输出格式:
{
  "action": "CALC" | "SEARCH" | "CHAT",
  "input": "为工具清理的输入"
}
 
示例:
用户: "计算15乘以7"
输出: {"action": "CALC", "input": "15 * 7"}
 
用户: "日本的首都是什么?"
输出: {"action": "SEARCH", "input": "日本的首都"}
 
用户: "你好!"
输出: {"action": "CHAT", "input": "你好!"}
 
提取关键信息并为适当的工具格式化。
只输出有效的JSON,不要输出其他内容。"""
 
def get_routing_decision(user_input: str) -> dict:
    messages = [
        SystemMessage(content=system_prompt),
        HumanMessage(content=user_input)
    ]
    response = llm.invoke(messages)
    
    # 解析JSON响应
    try:
        decision = json.loads(response.content.strip())
        return decision
    except json.JSONDecodeError:
        # 如果LLM没有输出有效的JSON,则回退
        return {"action": "CHAT", "input": user_input}
 
# 用与示例不同的输入测试它
print(get_routing_decision("25 * 48等于多少?"))  
# 输出: {'action': 'CALC', 'input': '25 * 48'}
 
print(get_routing_decision("谁写了《哈姆雷特》?"))  
# 输出: {'action': 'SEARCH', 'input': '《哈姆雷特》的作者'}
 
print(get_routing_decision("你今天好吗?"))  
# 输出: {'action': 'CHAT', 'input': '你今天好吗?'}

这个提示词是故意限制性的。我们不是要求LLM有创意——我们要求它对输入进行分类并提取相关信息,采用结构化格式。

注意这里发生了什么:

  1. LLM读取用户的问题
  2. 它确定意图(计算、事实搜索、对话)
  3. 它提取并清理关键信息
  4. 它输出一个包含动作和清理后输入的JSON对象
  5. 我们的代码接收这个结构化数据并相应地路由

LLM不是在回答问题——它在决定什么应该回答问题

桥接:将LLM决策连接到工具执行

现在我们需要将LLM的决策连接到实际的工具执行。以下是最小化桥接:

python
def safe_calculator(expression: str) -> float:
    try:
        # 使用受限命名空间的eval以确保安全
        # 注意: eval()存在安全风险。
        # 在生产环境中,使用数学解析库如sympy或基于ast的求值。
        result = eval(expression, {"__builtins__": {}}, {})
        return float(result)
    except:
        raise ValueError(f"无法计算: {expression}")
 
def search(query: str) -> str:
    """占位符搜索函数"""
    # 实际上,这会调用搜索API
    # 现在接收清理后的查询,如"《哈姆雷特》的作者"
    return f"搜索结果: {query}"
 
def route_and_execute(user_input: str) -> str:
    """获取LLM决策并执行适当的工具"""
    decision = get_routing_decision(user_input)
    
    action = decision["action"]
    tool_input = decision["input"]  # LLM清理后的输入
    
    if action == "CALC":
        try:
            result = safe_calculator(tool_input)
            return f"计算结果: {result}"
        except ValueError as e:
            return f"无法执行计算: {e}"
    
    elif action == "SEARCH":
        result = search(tool_input)
        return result
    
    else:  # CHAT
        # 对于对话查询,让LLM直接响应
        response = llm.invoke([HumanMessage(content=tool_input)])
        return response.content
 
# 测试完整流程
print(route_and_execute("25 * 48等于多少?"))
# LLM提取"25 * 48" → 计算器接收清理后的输入
# 输出: 计算结果: 1200.0
 
print(route_and_execute("谁写了《哈姆雷特》?"))
# LLM重新格式化为"《哈姆雷特》的作者" → 更好的搜索查询
# 输出: 搜索结果: 《哈姆雷特》的作者
 
print(route_and_execute("你今天好吗?"))
# 输出: 我很好,谢谢你的询问!

这是最简单形式的最小化路由模式:

  1. LLM决策:从LLM获取路由决策
  2. 代码执行:Python代码检查决策
  3. 工具调用:调用适当的函数
  4. 结果返回:格式化并返回输出

关键见解:LLM的输出不再是给用户的文本——它是控制程序行为的结构化数据。action字段告诉你的代码采取哪条路径,input字段为该工具提供清理后的数据。这个JSON响应成为可执行逻辑。

这与聊天机器人有根本区别。在聊天机器人中,LLM的输出直接发送给用户。在这里,LLM的输出发送给你的代码,然后代码决定执行什么。

我们在本节中构建的是一个路由器——完整代理的前身。它展示了基本原理(LLM控制流程),但缺乏定义真正代理循环的迭代和自我纠正。

这个简单的路由系统有一个主要限制:它无法纠正自己的错误。让我们探讨为什么这很重要以及接下来会发生什么。

5.3) 理解术语:路由器与代理

我们刚刚构建的是一个路由器。但它与完整代理相比如何?让我们建立清晰的定义:

路由器(我们刚刚构建的)

  • 每个请求做出一个分类决策
  • 选择执行哪个工具/路径
  • 执行一次并返回
  • 没有迭代、没有状态、没有自我纠正
  • 示例:电子邮件分类器、意图检测器、工具选择器

工具调用助手(第13章)

  • LLM可以通过函数调用API直接调用工具
  • 通常仍然是单轮(一个请求 → 一个响应)
  • 比路由更复杂,但不一定是迭代的
  • 示例:"搜索天气并总结"在一次调用中完成

代理(完整循环)(第14-17章)

  • 通过思考 → 行动 → 观察循环迭代
  • 在迭代之间携带状态
  • 可以根据结果修改决策
  • 实现自我纠正
  • 示例:调试助手,尝试修复直到代码工作

代理系统(总称)

  • 任何LLM输出影响控制流的系统
  • 包括路由器、工具调用助手和完整代理
  • "代理性(Agentic)"描述属性;"代理(agent)"描述特定架构
  • 示例:以上所有都表现出代理行为

我们在5.2中构建的是一个路由器——它做出一个决策并执行它。这对于简单的分类任务很有效,但它无法处理需要多个步骤、验证或纠正路线的情况。如果初始决策不是最优的,或者任务比预期更复杂,路由器没有机制来适应。

代理通过引入迭代来解决这个限制。它们可以观察行动的结果,重新考虑它们的方法,然后再试一次。这使它们适合复杂的多步骤任务,其中解决方案的路径从一开始就不清楚。

在第14-17章中,你将学习如何构建这些迭代代理循环。

注意:最近,像o1和o3这样的推理模型可以在内部处理一些多步骤任务,减少了在某些场景中对显式循环的需求。当你在第四部分构建真实代理时,我们将探讨何时使用循环与推理模型。


你现在应该理解的内容:

  1. 代理系统从控制流开始:LLM的输出决定你的代码接下来做什么
  2. 路由是最简单的形式:一个决策、一个工具、一个结果——我们在5.2中构建的
  3. 真正的代理需要循环:处理多步骤任务、验证和错误恢复(第14-17章)
  4. 自我纠正是关键区别:代理可以观察结果并调整它们的方法

我们没有涵盖的内容(直到后续章节才会涵盖):

  • 如何组合执行管道(第6章)
  • 如何处理结构化输出(第7章)
  • 如何管理对话记忆(第8章)
  • 如何正确定义工具(第12章)
  • 如何实现具有自我纠正的代理循环(第14-17章)
  • 如何使代理生产就绪(第18-26章)

本章是关于概念转变——理解什么使系统表现出代理行为。实现细节稍后出现。

在下一章中,我们将继续构建实用基础:使用LangChain表达式语言(LCEL)组合执行管道。这些是你在第四部分实现真实代理时将使用的构建块。

"顿悟"时刻完成了。你现在理解代理系统是那些LLM决定,你的代码执行的系统。其他一切——工具、循环、状态管理——都是为了使这个决策-执行循环更加健壮和强大。

让我们继续构建基础。