在上一篇《动态规划 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
为了让教程可运行,我们先定义几个简化版工具。
生产环境中,你可以替换为真实工具。
Search 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