从单 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:一次架构简化实践