规划 Agent 的工具集成与执行监控:让 Plan 真正落地执行

在上一篇《动态规划 Agent:执行中实时调整计划》中,我们已经把 Planning Agent 从静态计划升级到了动态计划。

也就是说,Agent 已经不再是:

text 复制代码
先生成一个固定 Plan
然后机械执行到底

而是开始具备:

text 复制代码
执行
↓
观察
↓
判断
↓
调整计划
↓
继续执行

这种能力。

但到这里为止,系统仍然缺少一个非常关键的工程能力:

Tool Integration

也就是:

text 复制代码
计划中的每个子任务,到底应该交给哪个工具执行?

这正是本篇要解决的问题。


一、为什么 Planning Agent 必须集成工具?

很多教程讲 Planning Agent,只讲到:

text 复制代码
Planner 生成任务列表
Executor 逐步执行任务

但这在真实生产环境中是不够的。

因为真实任务往往不是一句 LLM 推理就能完成的。

例如用户提出:

text 复制代码
帮我分析最近三个月 AI Agent 框架的发展趋势,
并生成一份技术选型建议。

这个任务至少需要:

text 复制代码
1. 搜索最新资料
2. 抓取网页内容
3. 总结关键观点
4. 对比不同框架
5. 生成最终报告

其中不同子任务应该使用不同工具:

text 复制代码
搜索资料       → Search Tool
读取网页       → Browser Tool
代码分析       → Code Tool
数据整理       → Python Tool
报告生成       → LLM Writer

所以,真正的 Planning Agent 不是简单的:

text 复制代码
Planner + LLM Executor

而应该是:

text 复制代码
Planner + Tool Router + Tool Executor + Monitor

这才是工程化 Agent 的形态。


二、本篇目标

这一篇我们实现一个更接近生产级的 Planning Agent:

text 复制代码
用户目标
↓
Planner 生成任务列表
↓
Tool Router 判断每个任务应该用什么工具
↓
Tool Executor 调用对应工具
↓
Monitor 记录执行状态
↓
失败时 Retry
↓
必要时触发 Re-Plan

重点包括:

text 复制代码
- 子任务分配给不同 Tool
- Tool Router 设计
- Tool Executor 设计
- Task Status 状态管理
- 初步失败重试机制
- 执行监控与日志
- LangGraph 中的条件流转

这篇开始,Agent 就不再只是"会想",而是开始真正"会干活"。


三、整体架构

先看完整架构图:

text 复制代码
                         ┌──────────────┐
                         │    User      │
                         └──────┬───────┘
                                │
                                ▼
                         ┌──────────────┐
                         │   Planner    │
                         └──────┬───────┘
                                │
                                ▼
                         ┌──────────────┐
                         │  Task Queue  │
                         └──────┬───────┘
                                │
                                ▼
                         ┌──────────────┐
                         │ Tool Router  │
                         └──────┬───────┘
                                │
          ┌─────────────────────┼─────────────────────┐
          ▼                     ▼                     ▼
   ┌──────────────┐      ┌──────────────┐      ┌──────────────┐
   │ Search Tool  │      │ Code Tool    │      │ Writer Tool  │
   └──────┬───────┘      └──────┬───────┘      └──────┬───────┘
          │                     │                     │
          └─────────────────────┼─────────────────────┘
                                ▼
                         ┌──────────────┐
                         │   Monitor    │
                         └──────┬───────┘
                                │
              ┌─────────────────┼─────────────────┐
              ▼                 ▼                 ▼
           Success            Retry             RePlan

这里最关键的不是某个工具本身,而是:

工具调用被纳入了 Agent 的 State Machine

这意味着:

text 复制代码
Tool 不再是零散函数
而是 Graph 工作流中的可监控执行单元

这就是 Agent Engineering 和 Prompt Demo 的区别。


四、从"任务列表"升级为"任务对象"

之前我们可能把计划设计成这样:

python 复制代码
plan = [
    "搜索 LangGraph 最新资料",
    "分析 LangGraph 的核心特性",
    "生成技术报告"
]

这对入门够用。

但生产环境不够。

因为每个任务都应该有状态。

例如:

text 复制代码
任务是否完成?
失败了几次?
用哪个工具执行?
执行结果是什么?
错误信息是什么?

所以我们要把 Task 从字符串升级为结构化对象。


五、定义 Task 数据结构

python 复制代码
from typing import Literal, Optional
from pydantic import BaseModel, Field


class Task(BaseModel):
    id: int
    description: str

    tool_name: Optional[str] = None

    status: Literal[
        "pending",
        "running",
        "success",
        "failed",
        "skipped"
    ] = "pending"

    result: Optional[str] = None
    error: Optional[str] = None

    retry_count: int = 0
    max_retries: int = 2

这个 Task 对象已经比简单字符串强很多。

它包含了执行系统最关心的信息:

text 复制代码
- 任务描述
- 工具名称
- 执行状态
- 执行结果
- 错误信息
- 重试次数

这一步非常重要。

因为从这里开始,Planning Agent 才真正具备:

text 复制代码
可观测
可恢复
可重试
可调试

这些生产级能力。


六、定义 Agent State

接下来定义 LangGraph 的 State。

python 复制代码
from typing import TypedDict, List, Optional
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
from typing_extensions import Annotated


class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]

    user_input: str

    tasks: List[Task]
    current_task_id: Optional[int]

    observations: List[str]

    final_answer: Optional[str]

    need_replan: bool
    execution_log: List[str]

这里重点看几个字段。


tasks

这是完整任务队列。

每个任务都是结构化 Task。

python 复制代码
tasks: List[Task]

current_task_id

当前正在执行哪个任务。

python 复制代码
current_task_id: Optional[int]

为什么不直接保存 current_task?

因为任务对象会被不断更新。

用 id 更稳定。


execution_log

执行日志。

python 复制代码
execution_log: List[str]

这个字段非常重要。

生产环境中,如果 Agent 出问题,你必须知道:

text 复制代码
它执行了什么?
什么时候失败?
用了哪个工具?
失败原因是什么?
是否重试过?

没有日志的 Agent,基本不可维护。


七、Planner:生成结构化任务

Planner 不再返回字符串数组,而是返回结构化任务。


Planner Prompt

python 复制代码
PLANNER_PROMPT = """
你是一个专业 Planning Agent。

请将用户目标拆解为多个可执行子任务。

要求:
1. 每个任务必须具体
2. 每个任务必须可以被某类工具执行
3. 不要生成过度抽象的任务
4. 按执行顺序输出

用户目标:
{user_input}

请返回 JSON 数组,格式如下:
[
  {{
    "id": 1,
    "description": "搜索 LangGraph 最新版本变化"
  }},
  {{
    "id": 2,
    "description": "总结 LangGraph 的核心能力"
  }}
]
"""

Planner Node

python 复制代码
import json


def planner_node(state: AgentState):
    prompt = PLANNER_PROMPT.format(
        user_input=state["user_input"]
    )

    response = llm.invoke(prompt)
    raw_tasks = json.loads(response.content)

    tasks = [Task(**item) for item in raw_tasks]

    return {
        "tasks": tasks,
        "current_task_id": tasks[0].id if tasks else None,
        "execution_log": ["Planner generated tasks"]
    }

这里仍然使用了 json.loads,主要为了教学清晰。

但在生产环境中,更推荐:

python 复制代码
llm.with_structured_output(TaskPlan)

原因很简单:

text 复制代码
LLM 输出 JSON 不稳定

尤其当任务复杂时,裸解析 JSON 很容易失败。


八、Tool Router:决定任务交给哪个工具

这是本篇核心。

Planner 负责拆任务。

Tool Router 负责判断:

text 复制代码
当前任务应该由哪个 Tool 执行?

为什么需要 Tool Router?

很多简单 Agent 会让模型自己 tool calling。

这没问题。

但 Planning Agent 中,任务是一个队列。

每个任务都应该有明确执行器。

例如:

text 复制代码
搜索资料       → search
分析代码       → code
生成报告       → writer
数学计算       → calculator
读取文件       → file_reader

所以我们需要一个 Router。


工具枚举

先定义可用工具。

python 复制代码
AVAILABLE_TOOLS = [
    "search",
    "calculator",
    "code_interpreter",
    "writer",
    "summarizer"
]

Router Prompt

python 复制代码
TOOL_ROUTER_PROMPT = """
你是一个 Tool Router。

你的任务是根据当前子任务,选择最合适的工具。

可用工具:
{tools}

工具说明:
- search:用于搜索公开信息、查询资料、查找事实
- calculator:用于数学计算、成本估算、数值分析
- code_interpreter:用于执行 Python 代码、处理数据、分析文件
- summarizer:用于总结已有观察信息
- writer:用于生成报告、文章、总结性输出

当前任务:
{task}

请只返回工具名称,不要输出其他内容。
"""

Router Node

python 复制代码
def get_current_task(state: AgentState) -> Task:
    current_id = state["current_task_id"]

    for task in state["tasks"]:
        if task.id == current_id:
            return task

    raise ValueError(f"Task not found: {current_id}")


def tool_router_node(state: AgentState):
    task = get_current_task(state)

    prompt = TOOL_ROUTER_PROMPT.format(
        tools=AVAILABLE_TOOLS,
        task=task.description
    )

    response = llm.invoke(prompt)
    tool_name = response.content.strip()

    task.tool_name = tool_name
    task.status = "running"

    return {
        "tasks": state["tasks"],
        "execution_log": state["execution_log"] + [
            f"Task {task.id} routed to tool: {tool_name}"
        ]
    }

这一步完成后,每个任务都会被分配到一个工具。

例如:

text 复制代码
Task 1: 搜索 LangGraph 最新资料 → search
Task 2: 统计不同框架 GitHub Star → calculator / code_interpreter
Task 3: 生成对比报告 → writer

九、实现几个示例 Tool

为了让教程可运行,我们先定义几个简化版工具。

生产环境中,你可以替换为真实工具。


python 复制代码
def search_tool(query: str) -> str:
    # 教学示例:真实项目中可以接入 Tavily、SerpAPI、Browser、企业内部搜索等
    return f"搜索结果:关于 {query} 的资料摘要。"

Calculator Tool

python 复制代码
def calculator_tool(expression: str) -> str:
    try:
        result = eval(expression)
        return str(result)
    except Exception as e:
        raise RuntimeError(f"计算失败: {e}")

注意:

生产环境中不建议直接使用 eval

更安全的方式是:

text 复制代码
- numexpr
- asteval
- sandboxed python
- 专用 calculator tool

Writer Tool

python 复制代码
def writer_tool(task: str, observations: list[str]) -> str:
    prompt = f"""
    请根据以下任务和观察结果生成输出。

    当前任务:
    {task}

    观察结果:
    {observations}
    """

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

Tool Registry

接下来把工具注册到一个字典中。

python 复制代码
TOOL_REGISTRY = {
    "search": search_tool,
    "calculator": calculator_tool,
    "writer": writer_tool,
}

这就是最简单的 Tool Registry。

生产环境中,它可以升级为:

text 复制代码
- Tool Metadata
- Tool Schema
- Tool Permission
- Tool Timeout
- Tool Retry Policy
- Tool Cost Model

这会在后续企业级篇继续展开。


十、Tool Executor:真正执行工具

现在我们写 Tool Executor。

它负责:

text 复制代码
1. 获取当前任务
2. 获取任务对应工具
3. 调用工具
4. 保存结果
5. 捕获异常
6. 更新任务状态

Executor Node

python 复制代码
def tool_executor_node(state: AgentState):
    task = get_current_task(state)

    tool_name = task.tool_name

    if tool_name not in TOOL_REGISTRY:
        task.status = "failed"
        task.error = f"Unknown tool: {tool_name}"

        return {
            "tasks": state["tasks"],
            "execution_log": state["execution_log"] + [
                f"Task {task.id} failed: unknown tool {tool_name}"
            ]
        }

    tool = TOOL_REGISTRY[tool_name]

    try:
        if tool_name == "writer":
            result = tool(task.description, state["observations"])
        else:
            result = tool(task.description)

        task.status = "success"
        task.result = result
        task.error = None

        return {
            "tasks": state["tasks"],
            "observations": state["observations"] + [result],
            "execution_log": state["execution_log"] + [
                f"Task {task.id} executed successfully by {tool_name}"
            ]
        }

    except Exception as e:
        task.status = "failed"
        task.error = str(e)

        return {
            "tasks": state["tasks"],
            "execution_log": state["execution_log"] + [
                f"Task {task.id} failed by {tool_name}: {e}"
            ]
        }

这里已经开始具备基本生产能力。

因为所有工具调用都被包装在:

python 复制代码
try:
    ...
except Exception:
    ...

中。

这看起来简单,但非常重要。

真实 Agent 项目中,Tool 失败是常态,不是异常。


十一、失败重试机制初步引入

接下来引入 Retry。

这是生产级 Agent 的必备能力。


为什么 Tool 一定会失败?

因为工具面对的是外部世界。

常见失败包括:

text 复制代码
- 网络超时
- API 限流
- 参数格式错误
- 返回为空
- 权限不足
- 文件不存在
- 第三方服务异常

所以如果 Agent 没有 Retry,稳定性会非常差。


Retry 的基本策略

最简单的策略:

text 复制代码
如果任务失败
并且 retry_count < max_retries
则重新执行

否则:

text 复制代码
标记为最终失败
进入下一个任务或触发 RePlan

Retry Node

python 复制代码
def retry_node(state: AgentState):
    task = get_current_task(state)

    task.retry_count += 1
    task.status = "pending"

    return {
        "tasks": state["tasks"],
        "execution_log": state["execution_log"] + [
            f"Retry task {task.id}, count={task.retry_count}"
        ]
    }

判断是否需要 Retry

python 复制代码
def should_retry(state: AgentState):
    task = get_current_task(state)

    if task.status == "failed" and task.retry_count < task.max_retries:
        return "retry"

    if task.status == "failed":
        return "failed"

    return "success"

Retry 的工程注意点

不要无限重试。

一定要限制:

text 复制代码
- 最大重试次数
- 最大执行时间
- 最大工具调用次数

否则 Agent 很容易陷入:

text 复制代码
失败
↓
重试
↓
失败
↓
重试
↓
无限循环

这在真实系统中非常常见。


十二、任务推进:选择下一个 Task

当当前任务成功后,需要进入下一个 pending task。


Next Task Node

python 复制代码
def next_task_node(state: AgentState):
    tasks = state["tasks"]

    for task in tasks:
        if task.status == "pending":
            return {
                "current_task_id": task.id,
                "tasks": tasks,
                "execution_log": state["execution_log"] + [
                    f"Move to next task: {task.id}"
                ]
            }

    return {
        "current_task_id": None,
        "tasks": tasks,
        "execution_log": state["execution_log"] + [
            "All tasks completed"
        ]
    }

是否结束?

python 复制代码
def has_next_task(state: AgentState):
    if state["current_task_id"] is None:
        return "finish"

    return "continue"

十三、失败后的处理:跳过还是重规划?

一个任务失败,并且超过最大重试次数后,系统有几种选择。


方案一:直接终止

适合强依赖任务。

例如:

text 复制代码
读取用户上传的合同文件

如果读不到,后面都没法做。


方案二:跳过任务

适合弱依赖任务。

例如:

text 复制代码
补充搜索某个资料来源

失败后可以继续执行其他任务。


方案三:触发 RePlan

这是 Planning Agent 更推荐的方式。

因为 Agent 可以尝试:

text 复制代码
换一个工具
换一个搜索关键词
换一个执行路径
降低任务要求

Failure Handler Node

python 复制代码
def failure_handler_node(state: AgentState):
    task = get_current_task(state)

    task.status = "failed"

    return {
        "tasks": state["tasks"],
        "need_replan": True,
        "execution_log": state["execution_log"] + [
            f"Task {task.id} reached max retries, trigger replanning"
        ]
    }

这里我们选择触发 RePlan。

这比直接终止更加智能。


十四、Monitor:执行监控节点

现在引入 Monitor。

Monitor 的职责不是执行任务,而是观察系统状态。


Monitor 应该看什么?

至少包括:

text 复制代码
- 当前执行到哪个任务
- 每个任务状态
- 工具调用是否成功
- 是否超过重试次数
- 是否出现连续失败
- 是否需要 RePlan
- 是否应该提前终止

Monitor Node

python 复制代码
def monitor_node(state: AgentState):
    tasks = state["tasks"]

    total = len(tasks)
    success = len([t for t in tasks if t.status == "success"])
    failed = len([t for t in tasks if t.status == "failed"])
    pending = len([t for t in tasks if t.status == "pending"])

    summary = (
        f"Monitor: total={total}, "
        f"success={success}, failed={failed}, pending={pending}"
    )

    return {
        "execution_log": state["execution_log"] + [summary]
    }

这个 Monitor 很简单。

但已经有了雏形。

生产环境中,Monitor 可以升级为:

text 复制代码
- 写入数据库
- 上报 Prometheus
- 发送 LangSmith Trace
- 记录工具延迟
- 记录 Token 成本
- 记录失败分布

后面企业级篇会继续展开。


十五、把所有节点编排进 LangGraph

现在开始写 Graph。


Graph 节点

python 复制代码
from langgraph.graph import StateGraph, START, END

builder = StateGraph(AgentState)

builder.add_node("planner", planner_node)
builder.add_node("tool_router", tool_router_node)
builder.add_node("tool_executor", tool_executor_node)
builder.add_node("retry", retry_node)
builder.add_node("failure_handler", failure_handler_node)
builder.add_node("monitor", monitor_node)
builder.add_node("next_task", next_task_node)
builder.add_node("final_writer", final_writer_node)

Final Writer Node

最后生成最终答案。

python 复制代码
def final_writer_node(state: AgentState):
    prompt = f"""
    请根据以下观察结果,为用户生成最终回答。

    用户需求:
    {state["user_input"]}

    观察结果:
    {state["observations"]}

    执行日志:
    {state["execution_log"]}
    """

    response = llm.invoke(prompt)

    return {
        "final_answer": response.content,
        "execution_log": state["execution_log"] + [
            "Final answer generated"
        ]
    }

Graph Edge

python 复制代码
builder.add_edge(START, "planner")
builder.add_edge("planner", "tool_router")
builder.add_edge("tool_router", "tool_executor")

Executor 后的条件判断

python 复制代码
builder.add_conditional_edges(
    "tool_executor",
    should_retry,
    {
        "retry": "retry",
        "failed": "failure_handler",
        "success": "monitor"
    }
)

Retry 回到 Router

python 复制代码
builder.add_edge("retry", "tool_router")

为什么回到 Router,而不是直接回到 Executor?

因为失败后可能需要重新选择工具。

例如:

text 复制代码
search 工具失败
可以改用 browser 工具

这就是动态工具选择。


Monitor 后推进任务

python 复制代码
builder.add_edge("monitor", "next_task")

判断是否继续执行

python 复制代码
builder.add_conditional_edges(
    "next_task",
    has_next_task,
    {
        "continue": "tool_router",
        "finish": "final_writer"
    }
)

结束

python 复制代码
builder.add_edge("final_writer", END)

graph = builder.compile()

完整流程如下:

text 复制代码
START
  ↓
Planner
  ↓
Tool Router
  ↓
Tool Executor
  ↓
成功?失败?
  ├── Retry → Tool Router
  ├── Failure Handler → RePlan
  └── Monitor
        ↓
     Next Task
        ├── Tool Router
        └── Final Writer

十六、完整可运行示例骨架

下面给出一个教学版完整代码骨架。

真实项目中,你需要替换真实 LLM 和真实 Tool。

python 复制代码
import json
from typing import TypedDict, List, Optional, Literal
from typing_extensions import Annotated
from pydantic import BaseModel

from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages


class Task(BaseModel):
    id: int
    description: str
    tool_name: Optional[str] = None
    status: Literal["pending", "running", "success", "failed", "skipped"] = "pending"
    result: Optional[str] = None
    error: Optional[str] = None
    retry_count: int = 0
    max_retries: int = 2


class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    user_input: str
    tasks: List[Task]
    current_task_id: Optional[int]
    observations: List[str]
    final_answer: Optional[str]
    need_replan: bool
    execution_log: List[str]


AVAILABLE_TOOLS = [
    "search",
    "calculator",
    "writer"
]


def get_current_task(state: AgentState) -> Task:
    current_id = state["current_task_id"]

    for task in state["tasks"]:
        if task.id == current_id:
            return task

    raise ValueError(f"Task not found: {current_id}")


def search_tool(query: str) -> str:
    return f"搜索结果:{query} 的相关资料摘要。"


def calculator_tool(expression: str) -> str:
    try:
        return str(eval(expression))
    except Exception as e:
        raise RuntimeError(f"计算失败: {e}")


def writer_tool(task: str, observations: list[str]) -> str:
    prompt = f"""
    当前任务:{task}
    观察结果:{observations}
    请生成总结。
    """
    response = llm.invoke(prompt)
    return response.content


TOOL_REGISTRY = {
    "search": search_tool,
    "calculator": calculator_tool,
    "writer": writer_tool,
}


def planner_node(state: AgentState):
    prompt = f"""
    请将用户目标拆解为 3 到 5 个可执行任务。
    只返回 JSON 数组。

    用户目标:{state["user_input"]}

    格式:
    [
      {{"id": 1, "description": "..."}},
      {{"id": 2, "description": "..."}}
    ]
    """

    response = llm.invoke(prompt)
    raw_tasks = json.loads(response.content)
    tasks = [Task(**item) for item in raw_tasks]

    return {
        "tasks": tasks,
        "current_task_id": tasks[0].id if tasks else None,
        "observations": [],
        "need_replan": False,
        "execution_log": ["Planner generated tasks"]
    }


def tool_router_node(state: AgentState):
    task = get_current_task(state)

    prompt = f"""
    你是 Tool Router。

    可用工具:{AVAILABLE_TOOLS}

    工具说明:
    - search:搜索资料
    - calculator:数学计算
    - writer:生成报告或总结

    当前任务:{task.description}

    请只返回工具名称。
    """

    response = llm.invoke(prompt)
    tool_name = response.content.strip()

    task.tool_name = tool_name
    task.status = "running"

    return {
        "tasks": state["tasks"],
        "execution_log": state["execution_log"] + [
            f"Task {task.id} routed to {tool_name}"
        ]
    }


def tool_executor_node(state: AgentState):
    task = get_current_task(state)
    tool_name = task.tool_name

    if tool_name not in TOOL_REGISTRY:
        task.status = "failed"
        task.error = f"Unknown tool: {tool_name}"
        return {
            "tasks": state["tasks"],
            "execution_log": state["execution_log"] + [
                f"Task {task.id} failed: unknown tool {tool_name}"
            ]
        }

    tool = TOOL_REGISTRY[tool_name]

    try:
        if tool_name == "writer":
            result = tool(task.description, state["observations"])
        else:
            result = tool(task.description)

        task.status = "success"
        task.result = result
        task.error = None

        return {
            "tasks": state["tasks"],
            "observations": state["observations"] + [result],
            "execution_log": state["execution_log"] + [
                f"Task {task.id} success by {tool_name}"
            ]
        }

    except Exception as e:
        task.status = "failed"
        task.error = str(e)

        return {
            "tasks": state["tasks"],
            "execution_log": state["execution_log"] + [
                f"Task {task.id} failed by {tool_name}: {e}"
            ]
        }


def should_retry(state: AgentState):
    task = get_current_task(state)

    if task.status == "failed" and task.retry_count < task.max_retries:
        return "retry"

    if task.status == "failed":
        return "failed"

    return "success"


def retry_node(state: AgentState):
    task = get_current_task(state)

    task.retry_count += 1
    task.status = "pending"

    return {
        "tasks": state["tasks"],
        "execution_log": state["execution_log"] + [
            f"Retry task {task.id}, retry_count={task.retry_count}"
        ]
    }


def failure_handler_node(state: AgentState):
    task = get_current_task(state)

    return {
        "need_replan": True,
        "tasks": state["tasks"],
        "execution_log": state["execution_log"] + [
            f"Task {task.id} reached max retries"
        ]
    }


def monitor_node(state: AgentState):
    tasks = state["tasks"]

    total = len(tasks)
    success = len([t for t in tasks if t.status == "success"])
    failed = len([t for t in tasks if t.status == "failed"])
    pending = len([t for t in tasks if t.status == "pending"])

    return {
        "execution_log": state["execution_log"] + [
            f"Monitor: total={total}, success={success}, failed={failed}, pending={pending}"
        ]
    }


def next_task_node(state: AgentState):
    for task in state["tasks"]:
        if task.status == "pending":
            return {
                "current_task_id": task.id,
                "execution_log": state["execution_log"] + [
                    f"Move to task {task.id}"
                ]
            }

    return {
        "current_task_id": None,
        "execution_log": state["execution_log"] + [
            "No pending tasks"
        ]
    }


def has_next_task(state: AgentState):
    if state["current_task_id"] is None:
        return "finish"
    return "continue"


def final_writer_node(state: AgentState):
    prompt = f"""
    用户需求:{state["user_input"]}

    观察结果:{state["observations"]}

    请生成最终回答。
    """

    response = llm.invoke(prompt)

    return {
        "final_answer": response.content,
        "execution_log": state["execution_log"] + [
            "Final answer generated"
        ]
    }


builder = StateGraph(AgentState)

builder.add_node("planner", planner_node)
builder.add_node("tool_router", tool_router_node)
builder.add_node("tool_executor", tool_executor_node)
builder.add_node("retry", retry_node)
builder.add_node("failure_handler", failure_handler_node)
builder.add_node("monitor", monitor_node)
builder.add_node("next_task", next_task_node)
builder.add_node("final_writer", final_writer_node)

builder.add_edge(START, "planner")
builder.add_edge("planner", "tool_router")
builder.add_edge("tool_router", "tool_executor")

builder.add_conditional_edges(
    "tool_executor",
    should_retry,
    {
        "retry": "retry",
        "failed": "failure_handler",
        "success": "monitor"
    }
)

builder.add_edge("retry", "tool_router")
builder.add_edge("failure_handler", "monitor")
builder.add_edge("monitor", "next_task")

builder.add_conditional_edges(
    "next_task",
    has_next_task,
    {
        "continue": "tool_router",
        "finish": "final_writer"
    }
)

builder.add_edge("final_writer", END)

graph = builder.compile()

十七、运行示例

python 复制代码
result = graph.invoke({
    "user_input": "分析 LangGraph、AutoGen、CrewAI 三个 Agent 框架,并给出技术选型建议",
    "messages": [],
    "tasks": [],
    "current_task_id": None,
    "observations": [],
    "final_answer": None,
    "need_replan": False,
    "execution_log": []
})

print(result["final_answer"])
print(result["execution_log"])

你可以看到类似日志:

text 复制代码
Planner generated tasks
Task 1 routed to search
Task 1 success by search
Monitor: total=4, success=1, failed=0, pending=3
Move to task 2
Task 2 routed to search
Task 2 success by search
...
Final answer generated

这就是执行监控的最小形态。


十八、调试技巧:重点看 execution_log

对于 Planning Agent,最重要的调试入口不是最终答案。

而是:

python 复制代码
execution_log

因为最终答案错了,只是结果。

真正要排查的是:

text 复制代码
Planner 有没有拆错任务?
Router 有没有选错工具?
Executor 有没有调用失败?
Retry 有没有生效?
Monitor 有没有发现异常?

常见问题 1:Router 选错工具

例如任务是:

text 复制代码
生成最终报告

结果 Router 选择了:

text 复制代码
search

这说明 Router Prompt 不够明确。

解决方案:

text 复制代码
- 给每个工具写清楚适用场景
- 增加反例
- 使用 structured output
- 使用规则优先,LLM 兜底

常见问题 2:任务描述太模糊

例如 Planner 生成:

text 复制代码
分析资料

这就很糟糕。

因为 Router 不知道该用哪个工具。

更好的任务应该是:

text 复制代码
搜索 LangGraph 最近版本变化
总结 LangGraph 在状态管理上的核心能力
对比 LangGraph 与 AutoGen 在工作流控制上的差异

所以 Planner Prompt 一定要强调:

text 复制代码
任务必须具体,可执行,可分配工具

常见问题 3:Retry 没有意义

有些失败不是临时失败。

例如:

text 复制代码
Unknown tool: browser_search_v2

这种失败重试 100 次也没用。

所以生产环境中要区分:

text 复制代码
可重试错误:timeout、rate limit、temporary unavailable
不可重试错误:unknown tool、permission denied、invalid schema

教学版先统一 Retry。

生产版必须细分错误类型。


十九、生产级升级方向

现在这个版本只是 Tool Integration 的最小可用版本。

要进入生产环境,还需要继续升级。


1. Tool Metadata

每个工具应该带元信息:

python 复制代码
class ToolSpec(BaseModel):
    name: str
    description: str
    input_schema: dict
    timeout: int
    max_retries: int
    cost_level: str

这样 Router 才能更准确地选择工具。


2. Retry Policy

不同工具应该有不同重试策略。

例如:

text 复制代码
Search Tool:最多重试 3 次
Calculator:不重试
Browser Tool:超时可重试
Writer Tool:失败可重试 1 次

不要所有工具使用同一个 Retry 策略。


3. Timeout 控制

所有 Tool 都必须设置 timeout。

因为外部工具可能卡住。

生产系统中,最怕:

text 复制代码
一个工具调用阻塞整个 Agent

4. Cost Tracking

每个工具都应该记录成本。

例如:

text 复制代码
LLM 调用 Token
搜索 API 次数
浏览器调用次数
代码执行时间

否则 Agent 成本很难控制。


5. Tool Permission

不是所有任务都应该能调用所有工具。

比如:

text 复制代码
文件删除工具
邮件发送工具
支付工具
数据库写入工具

这些高风险工具必须有权限控制。

后面讲 Human-in-the-loop 时会重点展开。


二十、最佳实践

最后总结一些工程经验。


1. 不要让 Planner 直接调用工具

Planner 只负责拆任务。

不要让它既规划又执行。

否则很容易变成:

text 复制代码
职责混乱
状态不可控
调试困难

推荐结构:

text 复制代码
Planner → Router → Executor

2. Tool Router 要尽量稳定

Router 是系统中非常关键的节点。

如果 Router 不稳定,后面全部会错。

生产环境中推荐:

text 复制代码
规则匹配优先
LLM Router 兜底
结构化输出校验
未知工具拦截

3. 每个任务必须有状态

不要只保存字符串任务列表。

至少保存:

text 复制代码
status
result
error
retry_count
tool_name

否则你无法调试复杂 Agent。


4. Retry 不是越多越好

Retry 只能解决临时问题。

不能解决设计问题。

如果工具选择错了,重试没有意义。

如果参数错了,重试也没有意义。

所以 Retry 应该结合:

text 复制代码
错误类型判断
工具重新选择
RePlan

5. Monitor 不要等到最后才做

很多人一开始只关心 Agent 能不能跑通。

但真正工程化时,Monitor 必须早早加入。

至少要记录:

text 复制代码
每一步执行日志
每个任务状态
每次工具调用结果
每次失败原因

否则后期排查会非常痛苦。


二十一、本篇总结

这一篇非常关键。

因为我们把 Planning Agent 从:

text 复制代码
会规划

升级到了:

text 复制代码
会分配工具
会执行工具
会监控执行
会失败重试

这已经非常接近真实 Agent 系统。


你现在已经掌握了什么?

1. 子任务工具分配

通过 Tool Router:

text 复制代码
Task → Tool

实现任务到工具的映射。


2. 结构化 Task State

每个任务都有:

text 复制代码
status
result
error
retry_count
tool_name

这让 Agent 可调试、可监控。


3. Tool Executor

工具调用不再是零散函数。

而是 LangGraph 中的节点。


4. Retry 机制

失败后可以自动重试。

并且通过最大重试次数避免死循环。


5. Monitor 节点

Agent 执行过程开始变得可观测。

这是真正工程化的开始。


最终认知升级

到这里,你应该形成一个非常重要的认知:

text 复制代码
Planning Agent 不是一个"大模型提示词"

而是一个:

任务系统
+ 工具系统
+ 状态系统
+ 执行系统
+ 监控系统

也就是:

Agent Runtime

这正是后续 Multi-Agent、Deep Research Agent、企业级 Agent 的基础。

更完整的企业级架构代码可参考:https://github.com/flower-trees/regnexe-py