5. 代理预览 — "顿悟"时刻
到目前为止,你构建的系统中你控制着流程。你编写提示词,调用LLM,并处理响应。LLM很强大,但它仍然只是遵循你的指令。
本章介绍一个根本性的转变:如果LLM决定接下来发生什么呢?
为什么存在本章
问题:如果你在没有这个预览的情况下继续构建聊天机器人直到第11章,你可能会认为"代理(agent)只是更好的聊天机器人"。
真相:代理(agent)从根本上是不同的。它们做出控制你程序的决策,而不仅仅是生成文本。
解决方案:本章向你展示我们要去哪里,这样当你在接下来的章节中构建管道和状态管理时,你会理解为什么每个部分都很重要。
你将学到什么(以及不会学到什么)
本章(第5章):
- 15分钟的代理思维最小化预览
- LLM输出如何触发不同的代码路径
- 路由和代理循环之间的概念差异
后续章节(从第12章开始):
- 具有工具、循环和自我纠正的真实代理系统
- 生产就绪的实现
- 高级模式,如多代理协调
为什么有差距?在构建代理之前,你需要基础:提示工程(第4章)、执行管道(第6章)、结构化输出(第7章)、状态管理(第8章)和知识集成(第9-11章)。
关于学习方法的说明:本章侧重于概念,而非实现细节。实际的设计和开发技术将在第12章及以后出现。如果你不知道如何实现这里看到的所有内容,不要担心——这是有意为之。现在理解代理在概念上是什么就足够了。
让我们开始吧。
5.1) 从聊天机器人到决策者
概念:LLM不是回答问题,而是决定使用哪个工具
在第3章和第4章中,你构建了LLM生成文本响应的聊天系统。流程很简单:
- 用户提出问题
- LLM生成答案
- 你显示答案
这是一个静态链(chain):流程是预先确定的。LLM的唯一工作是生成文本。
现在让我们改变请求的类型。用户问:"847 × 923等于多少?"
你在第3章构建的聊天机器人会尝试回答这个问题,但LLM实际上不进行计算——它们预测看起来合理的标记(token)。对于"2 + 2",答案"4"在训练数据中出现得如此频繁,以至于LLM能答对。但对于"847 × 923"——LLM从未见过的计算——它会生成看起来像数字但可能是错误的东西。
你真正想要的是:
- LLM识别:"这是一个数学问题"
- LLM决定:"使用计算器工具,而不是我的标记预测"
- Python执行: 847 × 923 = 781,781
- 系统返回:正确答案
LLM的工作不是计算——而是路由到能够计算的工具。
这里有一个关键见解:LLM不需要解决数学问题——它需要决定使用计算器。
这就是从聊天机器人到决策者的转变。LLM检查请求并将其路由到适当的工具。它正在做出关于程序流程的决策,而不仅仅是生成文本。
让我们可视化这种差异:
在静态链中,LLM尝试直接回答所有问题。在动态路由系统中,LLM将请求路由到正确的工具。
可视化:比较"线性链"(静态)与"路由器"(动态)
以下是静态链如何处理任何用户请求:
LLM尝试直接回答所有问题。无论你问"法国的首都是什么?"还是"240的15%是多少?",LLM都会生成文本响应。它可能会答对首都(它在训练数据中见过"巴黎"很多次),但它可能会算错百分比。
问题:LLM对所有问题使用相同的方法——标记预测——即使存在更好的工具。
现在让我们引入一个可以以不同方式处理不同类型请求的系统:
- 事实性问题:"法国的首都是什么?" → 使用搜索工具
- 数学问题:"240的15%是多少?" → 使用计算器
- 对话性:"你好吗?" → LLM直接响应
以下是使这成为可能的动态路由器:
LLM检查输入并根据请求类型选择路径。这是路由逻辑,LLM充当路由器。
关键区别:LLM不再总是生成文本,而是决定如何处理每个请求——通过将其路由到适当的工具。
关键要点:从生成答案到选择行动
这是代理系统的根本转变:
传统LLM使用:你提出问题,LLM写出答案。
代理式LLM使用:你提出问题,LLM决定做什么,你的代码执行该决策。
LLM的输出不再只是给用户的文本——它是给你程序的指令。
这样想:在传统聊天机器人中,LLM是回答客户问题的员工。在代理系统中,LLM是决定哪个部门应该处理每个请求的经理。
这种决策能力是代理系统的本质。LLM不仅仅是响应——它控制你的程序接下来做什么。根据输入,它可以触发计算器、调用搜索API或直接响应。程序的行为根据LLM的决策而改变。
但我们如何实际实现这一点?让我们看看使这一切工作的最小代码。
5.2) 最小化执行
触发器:强制结构化输出的系统提示词
要使LLM充当路由器,我们需要约束其输出。我们不希望它生成完整的答案,而是希望它输出一个结构化决策——一个JSON对象,告诉我们的代码要做什么以及使用什么数据。
以下是实现这一点的系统提示词:
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有创意——我们要求它对输入进行分类并提取相关信息,采用结构化格式。
注意这里发生了什么:
- LLM读取用户的问题
- 它确定意图(计算、事实搜索、对话)
- 它提取并清理关键信息
- 它输出一个包含动作和清理后输入的JSON对象
- 我们的代码接收这个结构化数据并相应地路由
LLM不是在回答问题——它在决定什么应该回答问题。
桥接:将LLM决策连接到工具执行
现在我们需要将LLM的决策连接到实际的工具执行。以下是最小化桥接:
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("你今天好吗?"))
# 输出: 我很好,谢谢你的询问!这是最简单形式的最小化路由模式:
- LLM决策:从LLM获取路由决策
- 代码执行:Python代码检查决策
- 工具调用:调用适当的函数
- 结果返回:格式化并返回输出
关键见解:LLM的输出不再是给用户的文本——它是控制程序行为的结构化数据。action字段告诉你的代码采取哪条路径,input字段为该工具提供清理后的数据。这个JSON响应成为可执行逻辑。
这与聊天机器人有根本区别。在聊天机器人中,LLM的输出直接发送给用户。在这里,LLM的输出发送给你的代码,然后代码决定执行什么。
我们在本节中构建的是一个路由器——完整代理的前身。它展示了基本原理(LLM控制流程),但缺乏定义真正代理循环的迭代和自我纠正。
这个简单的路由系统有一个主要限制:它无法纠正自己的错误。让我们探讨为什么这很重要以及接下来会发生什么。
5.3) 理解术语:路由器与代理
我们刚刚构建的是一个路由器。但它与完整代理相比如何?让我们建立清晰的定义:
路由器(我们刚刚构建的)
- 每个请求做出一个分类决策
- 选择执行哪个工具/路径
- 执行一次并返回
- 没有迭代、没有状态、没有自我纠正
- 示例:电子邮件分类器、意图检测器、工具选择器
工具调用助手(第13章)
- LLM可以通过函数调用API直接调用工具
- 通常仍然是单轮(一个请求 → 一个响应)
- 比路由更复杂,但不一定是迭代的
- 示例:"搜索天气并总结"在一次调用中完成
代理(完整循环)(第14-17章)
- 通过思考 → 行动 → 观察循环迭代
- 在迭代之间携带状态
- 可以根据结果修改决策
- 实现自我纠正
- 示例:调试助手,尝试修复直到代码工作
代理系统(总称)
- 任何LLM输出影响控制流的系统
- 包括路由器、工具调用助手和完整代理
- "代理性(Agentic)"描述属性;"代理(agent)"描述特定架构
- 示例:以上所有都表现出代理行为
我们在5.2中构建的是一个路由器——它做出一个决策并执行它。这对于简单的分类任务很有效,但它无法处理需要多个步骤、验证或纠正路线的情况。如果初始决策不是最优的,或者任务比预期更复杂,路由器没有机制来适应。
代理通过引入迭代来解决这个限制。它们可以观察行动的结果,重新考虑它们的方法,然后再试一次。这使它们适合复杂的多步骤任务,其中解决方案的路径从一开始就不清楚。
在第14-17章中,你将学习如何构建这些迭代代理循环。
注意:最近,像o1和o3这样的推理模型可以在内部处理一些多步骤任务,减少了在某些场景中对显式循环的需求。当你在第四部分构建真实代理时,我们将探讨何时使用循环与推理模型。
你现在应该理解的内容:
- 代理系统从控制流开始:LLM的输出决定你的代码接下来做什么
- 路由是最简单的形式:一个决策、一个工具、一个结果——我们在5.2中构建的
- 真正的代理需要循环:处理多步骤任务、验证和错误恢复(第14-17章)
- 自我纠正是关键区别:代理可以观察结果并调整它们的方法
我们没有涵盖的内容(直到后续章节才会涵盖):
- 如何组合执行管道(第6章)
- 如何处理结构化输出(第7章)
- 如何管理对话记忆(第8章)
- 如何正确定义工具(第12章)
- 如何实现具有自我纠正的代理循环(第14-17章)
- 如何使代理生产就绪(第18-26章)
本章是关于概念转变——理解什么使系统表现出代理行为。实现细节稍后出现。
在下一章中,我们将继续构建实用基础:使用LangChain表达式语言(LCEL)组合执行管道。这些是你在第四部分实现真实代理时将使用的构建块。
"顿悟"时刻完成了。你现在理解代理系统是那些LLM决定,你的代码执行的系统。其他一切——工具、循环、状态管理——都是为了使这个决策-执行循环更加健壮和强大。
让我们继续构建基础。