从单 Agent 到多 Agent
不知道怎么想的,居然用的一个LLMagent变量去invoke
改造前,整个系统只有 1 个 LLM 变量:
# ===== 旧代码 =====
llm = ChatOpenAI(model="deepseek-chat", base_url="https://api.deepseek.com",
api_key=os.getenv("DEEPSEEK_API_KEY"), temperature=0.7)
def supervisor_node(state):
resp = llm.invoke([SystemMessage(content=prompt), ...])
def route_agent_node(state):
resp = llm.invoke([SystemMessage(content=plan_prompt)])
def hotel_agent_node(state):
resp = llm.invoke([SystemMessage(content=compare_prompt)])
def food_agent_node(state):
resp = llm.invoke([SystemMessage(content=prompt)])
def info_agent_node(state):
resp = llm.invoke([SystemMessage(content=prompt)])
5 个函数,同一个 llm,只是传入的 prompt 不同。
这不是多 Agent,这是"单 LLM 多任务流水线"。
就像一个员工被分了 5 个任务,而不是 5 个员工各做一件事。
Agent 到底是什么?
Agent(智能体)= 角色 + 大脑 + 工具
| 组件 | 旧代码 | 新代码 |
|---|---|---|
| 角色(系统提示词) | 每次调用时临时拼在 prompt 里 | 每个 Agent 有专属常量 |
| 大脑(LLM 实例) | 共享 1 个 llm |
每个 Agent 独立的 LLM 实例 |
| temperature | 统一 0.7 | 按角色调优(0.5~0.9) |
| 工具 | 各自调不同的 API | 不变 |
改造后的代码
1. 每个 Agent 有独立的 LLM 实例
# ===== 新代码 =====
_api_key = os.getenv("DEEPSEEK_API_KEY")
_base_url = "https://api.deepseek.com"
# 主管:temperature=0.5(意图识别需要确定性)
supervisor_agent = ChatOpenAI(model="deepseek-chat", base_url=_base_url,
api_key=_api_key, temperature=0.5)
# 路线规划:temperature=0.5(结构化JSON输出)
route_agent = ChatOpenAI(model="deepseek-chat", base_url=_base_url,
api_key=_api_key, temperature=0.5)
# 酒店推荐:temperature=0.7(对比分析,适度多样性)
hotel_agent = ChatOpenAI(model="deepseek-chat", base_url=_base_url,
api_key=_api_key, temperature=0.7)
# 美食推荐:temperature=0.8(推荐需要创意)
food_agent = ChatOpenAI(model="deepseek-chat", base_url=_base_url,
api_key=_api_key, temperature=0.8)
# 文化讲解:temperature=0.9(故事化表达,最需要创意)
info_agent = ChatOpenAI(model="deepseek-chat", base_url=_base_url,
api_key=_api_key, temperature=0.9)
2. 每个 Agent 有专属的系统提示词
ROUTE_SYSTEM_PROMPT = "你是资深旅行路线规划师。根据提供的景点信息,规划详细的每日行程路线。返回结构化JSON。"
HOTEL_SYSTEM_PROMPT = "你是酒店对比分析专家。根据搜索到的酒店数据,给出性价比推荐。"
FOOD_SYSTEM_PROMPT = "你是本地美食达人,熟悉各地真实存在的知名餐厅。为旅行者推荐每日餐厅。"
INFO_SYSTEM_PROMPT = "你是旅行文化讲解员,擅长用生动有趣的故事化语言介绍目的地的文化、冷知识和礼仪。"
3. 各 Agent 节点使用自己的 LLM + 自己的提示词
def route_agent_node(state):
# route_agent 是路线规划专属的 LLM(temperature=0.5)
# ROUTE_SYSTEM_PROMPT 是路线规划专属的角色定义
resp = route_agent.invoke([
SystemMessage(content=ROUTE_SYSTEM_PROMPT),
HumanMessage(content=plan_prompt)
])
def hotel_agent_node(state):
# hotel_agent 是酒店推荐专属的 LLM(temperature=0.7)
resp = hotel_agent.invoke([
SystemMessage(content=HOTEL_SYSTEM_PROMPT),
HumanMessage(content=compare_prompt)
])
def food_agent_node(state):
# food_agent 是美食推荐专属的 LLM(temperature=0.8)
resp = food_agent.invoke([
SystemMessage(content=FOOD_SYSTEM_PROMPT),
HumanMessage(content=prompt)
])
def info_agent_node(state):
# info_agent 是文化讲解专属的 LLM(temperature=0.9)
resp = info_agent.invoke([
SystemMessage(content=INFO_SYSTEM_PROMPT),
HumanMessage(content=prompt)
])
为什么要这样做?
1. temperature 需要按角色调优
| Agent | temperature | 原因 |
|---|---|---|
| Supervisor | 0.5 | 意图识别要稳定,不能每次输出不同格式 |
| Route | 0.5 | JSON 输出要确定性,格式错误会解析失败 |
| Hotel | 0.7 | 对比分析需要一些表达多样性 |
| Food | 0.8 | 餐厅推荐需要创意,不能每次推一样的 |
| Info | 0.9 | 文化故事需要生动有趣,最需要创意 |
如果共享一个 LLM,只能取一个折中的 temperature(如 0.7), 对 Supervisor 来说太高(输出不稳定),对 Info 来说太低(故事太死板)。
2. 系统提示词是 Agent 的"身份"
旧代码中,系统提示词和用户内容混在同一个 prompt 里:
# 旧:提示词临时拼接
prompt = f"""你是资深旅行路线规划师。为{dest} {days}天行程规划详细路线。
要求:...
可用景点:{poi_text}
返回JSON:..."""
resp = llm.invoke([SystemMessage(content=prompt)])
新代码中,系统提示词是 Agent 的固有属性:
# 新:系统提示词是常量,用户内容是变量
resp = route_agent.invoke([
SystemMessage(content=ROUTE_SYSTEM_PROMPT), # 固有角色
HumanMessage(content=plan_prompt), # 具体任务
])
这让代码更清晰:角色定义和任务内容分离。
3. 为未来的工具调用做准备
真正的 Agent 应该能自主决定调用什么工具。当前的架构中, supervisor 决定调哪些 Agent,但每个 Agent 内部的工具调用是写死的。
有了独立的 LLM 实例后,未来可以给 Agent 绑定 tools:
# 未来的方向:Agent 自主决定调用什么工具
route_agent_with_tools = route_agent.bind_tools([
search_poi, search_flight, search_train
])
def route_agent_node(state):
# Agent 自己决定要不要调工具、调哪个
resp = route_agent_with_tools.invoke([
SystemMessage(content=ROUTE_SYSTEM_PROMPT),
HumanMessage(content=user_request)
])
# 如果 Agent 决定调工具,resp.tool_calls 会有内容
# 执行工具后,把结果再发给 Agent 让它继续
架构对比
改造前(单 LLM):
┌─────────────────────────────────────────────┐
│ llm (0.7) │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐│
│ │super- │ │ route │ │ hotel │ │ food ││
│ │visor │ │ agent │ │ agent │ │ agent ││
│ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘│
│ │invoke() │invoke() │invoke() │invoke()│
└──────┼──────────┼──────────┼──────────┼──────┘
└──────────┴──────────┴──────────┘
同一个 LLM
改造后(多 Agent):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│supervisor│ │ route │ │ hotel │ │ food │ │ info │
│agent │ │ agent │ │ agent │ │ agent │ │ agent │
│(0.5) │ │ (0.5) │ │ (0.7) │ │ (0.8) │ │ (0.9) │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│invoke() │invoke() │invoke() │invoke() │invoke()
▼ ▼ ▼ ▼ ▼
独立 LLM 独立 LLM 独立 LLM 独立 LLM 独立 LLM
多 Agent 的并行执行
LangGraph 的 Send() 机制让多个 Agent 并行工作:
def route_decision(state):
# supervisor 决定调哪些 Agent
return [Send(a, state) for a in state["agents_to_call"]]
# 如 [Send("route", state), Send("hotel", state), Send("food", state)]
# → 三个 Agent 同时执行
并行执行时,每个 Agent 分支收到 state 的副本。
Route Agent 写 route_result,Hotel Agent 写 hotel_result,互不干扰。
LangGraph 在 fan-in(汇聚到 aggregator)时自动合并各分支的更新。
什么时候用单 Agent,什么时候用多 Agent?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单问答 | 单 Agent | 一次 LLM 调用就够 |
| 多步骤串行任务 | 链式 Agent | 步骤间有依赖 |
| 多维度并行任务 | 多 Agent(本项目) | 各维度独立,并行更快 |
| 需要工具调用 | 带 tools 的 Agent | Agent 自主决定调用什么工具 |
一些误解
误解 1:"多 Agent 就是多个 LLM" 不完全对。Agent = 角色 + LLM + 工具。多个 Agent 可以共享底层模型(都是 DeepSeek),但每个是独立的实例,配不同的 temperature 和系统提示。
误解 2:"Agent 越多越好" 错。每个 Agent 增加一次 LLM 调用(延迟 + 成本)。只在真正需要并行或职责分离时才拆分。
误解 3:"共享 LLM 就是单 Agent" 共享底层模型 ≠ 共享实例。两个 Agent 都用 DeepSeek,但各自有独立的 ChatOpenAI 实例、独立的 temperature、独立的系统提示,这就是两个 Agent。
误解 4:"Send() 就是多线程" 不完全是。Send() 是 LangGraph 的调度机制,具体是多线程还是异步取决于执行器。但效果是并行的。
导航
← 上一篇:TravelAgent-5 记忆系统:三层记忆架构与上下文管理 → 下一篇:LangGraph Checkpoint:一次架构简化实践