运维Agent(Python)
一、Plan-Execute-Replan
Plan-Execute-Replan是一种Multi-Agent(多智能体)协作的任务执行模式,核心思路是:先规划、再执行、随时调整。Multi-Agent(多智能体)就是不是一个AI在单打独斗,而是多个AI各司其职、分工协作。每个Agent负责一个专门的职责,就像公司里不同岗位的同事配合完成一—个项目。
1.1、Planner
Planner是整个模式的起点。它接收用户的目标(比如"排查CPU飙高的原因"),然后生成一份结构化的执行计划。每一步做什么、用什么工具、传什么参数,都规划好。这就像项目经理给你排的Task列表,清晰明了。
1.2、Executor
Executor是真正动手的那个。它拿到计划中的当前第一步,然后去调用对应的工具完成任务。比如计划说"调用日志工具查error日志",Executor就真的去调日志API,传入服务器IP、时间范围、日志级别这些参数,拿到返回结果。Executor只专注做好当前这一步,不操心全局。
1.3、Replanner
Replanner是这个模式里最关键的角色——它让整个系统从"死板执行"变成了"智能适应”。
每次Executor完成一步之后,Replanner都会介入,做三件事:
情况一:步骤完成,结果有效→继续推进
“日志查到异常了,接下来按计划执行第二步。“这种情况最简单,照着计划往下走就行。
情况二:结果不符合预期→调整计划
“日志查了,啥异常都没有。那原计划的排查顺序可能不太对,得先看监控定位是哪个进程的问题。“这时候Replanner会修改后续步骤的内容或顺序。
情况三:所有步骤完成→终止任务,输出结论
"根因已经定位了——是全量同步任务没做分页,导致CPU打满。任务结束,输出报告。"
Replanner的关键能力:评估执行结果、判断任务进度、识别问题并动态优化计划。
1.4、与ReAct的区别
打个比方来区分
Plan-Execute-Replan像是按导航开车:
出发前先看好路线(Plan),然后按导航走(Execute),遇到堵车了导航会重新规划路线(Replan)。你始终知道”我现在在第几步,还有几步到终点”。
ReAct像是凭感觉找路:
到了一个路口,想一下”往左好像对”,就往左走;走到下一个路口再想一下“好像该右转了”.每一步都是现想、现走,没有全局路线图。
二、过程解析
运维Agent的核心目标是将运维人员的告警处理经验转化为自动化流程,通过计划生成 -> 工具执行 -> 动态调整的闭环,替代人工完成重复性告警排查工作。

2.1、计划生成(Planner)
目标:基于告警类型和召回的运维手册,生成可执行的多步骤排查计划,替代人工凭经验梳理流程的过程。
核心逻辑:
输入:告警信息(alertname、description)+召回的处理文档(来自于知识库RAG)
输出:结构化计划(JSON格式),包含步骤描述、工具调用参数、预期结果
2.2、计划执行(Executor)
目标:通过集成监控/日志工具,自动执行计划中的步骤,替代人工查日志/查监控的操作。
核心工具示例:
1.告警查询工具(query_prometheus_alerts):从 Prometheus API获取当前活跃告警详情
2.日志查询工具(query_log):通过腾讯云CLS的MCP服务,用自然语言查询日志(如搜索ad_app 最近1小时的 response error 日志)
2.3、动态重规划(Replanner)
目标:根据Executor的执行结果,判断是否需要调整计划(如增加步骤、终止排查),替代人工持续关注/判断是否需进一步处理的决策过程。
评估逻辑:
成功条件:当前步骤结果符合预期(如步骤1返回error日志,步骤2明确原因为contextcancel),执行下一步
调整条件:结果不符合预期(如日志查询无结果),触发计划修正(如扩大时间范围、更换关键词)
终止条件:达到目标(如确认下游发版导致临时超时,无需处理)或无法继续(如需人工介入下游沟通)
三、代码设计
class PlanExecuteState(TypedDict):
"""Plan-Execute-Replan 状态"""
# 用户输入(任务描述)
input: str
# 执行计划(步骤列表)
plan: List[str]
# 已执行的步骤历史
# 使用 operator.add 实现追加式更新(而非覆盖)
past_steps: Annotated[List[tuple], operator.add]
# 最终响应/报告
response: str
async def planner(state: PlanExecuteState) -> Dict[str, Any]:
"""
规划节点:根据用户输入生成执行计划
流程:
1. 先查询内部文档,获取相关经验和最佳实践
2. 基于经验文档和可用工具制定执行计划
"""
logger.info("=== Planner:制定执行计划 ===")
input_text = state.get("input", "")
logger.info(f"用户输入: {input_text}")
try:
# 步骤1: 查询内部文档获取相关经验
logger.info("查询内部文档,寻找相关经验...")
experience_docs = ""
try:
# retrieve_knowledge 使用 response_format="content_and_artifact"
# ainvoke() 只返回 content(字符串),不是元组
context_str = await retrieve_knowledge.ainvoke({"query": input_text})
if context_str and context_str.strip():
experience_docs = context_str
logger.info(f"找到相关经验文档,长度: {len(experience_docs)}")
else:
logger.info("未找到相关经验文档")
except Exception as e:
logger.warning(f"查询内部文档失败: {e}")
# 步骤2: 获取可用工具列表
# 获取本地工具
local_tools = [
get_current_time,
retrieve_knowledge
]
# 获取 MCP 工具
mcp_client = await get_mcp_client_with_retry()
mcp_tools = await mcp_client.get_tools()
# 合并所有工具
all_tools = local_tools + mcp_tools
logger.info(f"可用工具数量: 本地 {len(local_tools)} + MCP {len(mcp_tools)}")
# 格式化工具描述
tools_description = format_tools_description(all_tools)
# 步骤3: 格式化经验文档上下文
if experience_docs:
experience_context = dedent(f"""
## 相关经验文档
以下是从知识库中检索到的相关经验和最佳实践,请参考这些经验制定执行计划:
{experience_docs}
---
""").strip()
else:
experience_context = ""
# 步骤4: 创建 LLM 并生成计划
llm = ChatQwen(
model=config.rag_model,
api_key=config.dashscope_api_key,
temperature=0
)
# planner_prompt,给 AI 的指令手册
# | 是管道符:把前面和后面串起来、
# 强制 AI 输出固定格式(你定义的 Plan 结构体)
planner_chain = planner_prompt | llm.with_structured_output(Plan)
# 调用 LLM 生成计划
plan_result = await planner_chain.ainvoke({
"messages": [("user", input_text)],
"tools_description": tools_description,
"experience_context": experience_context
})
# 提取步骤列表
if isinstance(plan_result, Plan):
plan_steps = plan_result.steps
else:
# 如果返回的是字典,提取 steps 字段
plan_steps = plan_result.get("steps", []) # type: ignore
logger.info(f"计划已生成,共 {len(plan_steps)} 个步骤")
for i, step in enumerate(plan_steps, 1):
logger.info(f" 步骤{i}: {step}")
return {"plan": plan_steps}
except Exception as e:
logger.error(f"生成计划失败: {e}", exc_info=True)
# 返回一个默认计划
return {
"plan": [
"收集相关信息",
"分析数据",
"生成报告"
]
}
async def executor(state: PlanExecuteState) -> Dict[str, Any]:
"""
执行节点:执行计划中的下一个步骤
使用 LangGraph 的 ToolNode 自动处理工具调用
"""
logger.info("=== Executor:执行步骤 ===")
plan = state.get("plan", [])
# 如果计划为空,不执行
if not plan:
logger.info("计划为空,跳过执行")
return {}
# 取出第一个步骤
task = plan[0]
logger.info(f"当前任务: {task}")
try:
# 获取本地工具
local_tools = [
get_current_time,
retrieve_knowledge
]
# 获取 MCP 工具
mcp_client = await get_mcp_client_with_retry()
mcp_tools = await mcp_client.get_tools()
logger.info(f"可用工具数量: 本地 {len(local_tools)} + MCP {len(mcp_tools)}")
# 合并所有工具
all_tools = local_tools + mcp_tools
# 创建 LLM(绑定工具)
llm = ChatQwen(
model=config.rag_model,
api_key=config.dashscope_api_key,
temperature=0
)
llm_with_tools = llm.bind_tools(all_tools)
# 创建工具节点(自动执行工具调用)
tool_node = ToolNode(all_tools)
# 构建消息(只包含当前步骤,避免原始任务干扰)
messages = [
SystemMessage(content="""你是一个能力强大的助手,负责执行具体的任务步骤。
你可以使用各种工具来完成任务。对于每个步骤:
1. 理解步骤的目标
2. 选择合适的工具,如果已经指定了工具,则使用指定的工具
3. 调用工具获取信息
4. 返回执行结果
注意:
- 如果工具调用失败,请说明失败原因
- 不要编造数据,只返回实际获取的信息
- 执行结果要清晰、准确
- 专注于当前步骤,不要考虑其他任务"""),
HumanMessage(content=f"请执行以下任务: {task}")
]
# 第一步:LLM 决定是否调用工具
llm_response = await llm_with_tools.ainvoke(messages)
logger.info(f"LLM 响应类型: {type(llm_response)}")
# 第二步:如果有工具调用,执行工具
if hasattr(llm_response, "tool_calls") and llm_response.tool_calls:
logger.info(f"检测到 {len(llm_response.tool_calls)} 个工具调用")
# 使用 ToolNode 自动执行工具
messages.append(llm_response)
tool_messages = await tool_node.ainvoke({"messages": messages})
# 第三步:将工具结果返回给 LLM 生成最终答案
messages.extend(tool_messages["messages"])
final_response = await llm_with_tools.ainvoke(messages)
result = final_response.content if hasattr(final_response, 'content') else str(final_response)
else:
# 没有工具调用,直接使用 LLM 的输出
logger.info("LLM 未调用工具,直接返回结果")
result = llm_response.content if hasattr(llm_response, 'content') else str(llm_response)
logger.info(f"步骤执行完成,结果长度: {len(result)}")
# 返回更新:移除已执行的步骤,添加执行历史
return {
"plan": plan[1:], # 移除第一个步骤
"past_steps": [(task, result)], # 使用 operator.add 追加
}
except Exception as e:
logger.error(f"执行步骤失败: {e}", exc_info=True)
return {
"plan": plan[1:],
"past_steps": [(task, f"执行失败: {str(e)}")],
}
class Response(BaseModel):
"""最终响应的格式"""
response: str = Field(description="对用户的最终响应")
class Act(BaseModel):
"""重新规划的输出格式"""
action: str = Field(
description="""下一步的行动,必须是以下三种之一:
- 'continue': 当前计划合理,继续执行下一个步骤
- 'replan': 当前计划需要调整,提供新的步骤列表
- 'respond': 计划已完成且信息充足,生成最终响应"""
)
# action 为 'replan' 时,新的步骤列表(会替换当前剩余计划)
new_steps: List[str] = Field(
default_factory=list,
description="新的步骤列表(如果 action 是 'replan',这些步骤会替换剩余计划)"
)
# Replanner 提示词
replanner_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
dedent("""
作为一个重新规划专家,你需要根据已执行的步骤决定下一步行动。
可用工具列表(用于制定计划时参考):
{tools_description}
注意:你的职责是制定或调整计划,实际的工具调用由 Executor 负责执行。
你有三个选择(按优先级排序):
**1. 'respond' - 信息充足,立即生成最终响应** 【最高优先级】
- 使用场景:当前信息已经足够回答用户问题
- 决策标准:
* 已执行步骤 >= 3 且获取了关键信息
* 或者已执行步骤 >= 5(无论结果如何)
* 或者当前信息完全满足任务需求
- ⚠️ 不要等到"完美"才响应,"足够好"就应该立即 respond
**2. 'continue' - 当前计划合理,继续执行** 【次优先级】
- 使用场景:剩余计划合理且必要
- 决策标准:剩余步骤确实能提供关键信息
- ⚠️ 如果剩余步骤不是"必需"的,应选择 respond
**3. 'replan' - 当前计划有严重问题** 【最低优先级,谨慎使用】
- 使用场景:原计划明显错误或遗漏关键步骤
- ⚠️ **严格限制**:
* 新步骤数量必须 <= 当前剩余步骤数
* 优先简化计划,不要添加不必要的步骤
* 总步骤数已执行 >= 5 次时,禁止 replan,只能 respond
评估标准:
- 当前信息是否已经足够解决用户问题?【最关键】
- 已执行步骤是否成功获取了核心信息?
- 剩余步骤是否真的"必需"?
- 已执行步骤数是否过多(>= 5)?如果是,立即 respond
**决策优先级口诀:**
"优先结束 > 保持不变 > 调整计划"
"信息足够就响应,不要追求完美"
""").strip(),
),
("placeholder", "{messages}"),
]
)
# 最终响应生成提示词
response_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
dedent("""
根据原始任务和已执行步骤的结果,生成一个全面的最终响应。
响应要求:
- 清晰、结构化
- 基于实际数据,不要编造
- 如果某些步骤失败,要诚实说明
- 使用 Markdown 格式
""").strip(),
),
("placeholder", "{messages}"),
]
)
async def replanner(state: PlanExecuteState) -> Dict[str, Any]:
"""
重新规划节点:决定是继续、调整计划还是生成最终响应
三种决策:
1. continue - 继续执行当前计划
2. replan - 调整计划(替换剩余步骤)
3. respond - 生成最终响应
"""
logger.info("=== Replanner:重新规划 ===")
input_text = state.get("input", "") # 任务 告警
plan = state.get("plan", []) # 剩余没执行的步骤
past_steps = state.get("past_steps", []) # 已经执行过的步骤
logger.info(f"剩余计划步骤: {len(plan)}")
logger.info(f"已执行步骤: {len(past_steps)}")
# ⚠️ 强制限制:如果已执行步骤过多,直接生成响应
MAX_STEPS = 8
if len(past_steps) >= MAX_STEPS: # 防止 AI 无限循环
logger.warning(f"已执行 {len(past_steps)} 个步骤,超过最大限制 {MAX_STEPS},强制生成最终响应")
llm = ChatQwen(
model=config.rag_model,
api_key=config.dashscope_api_key,
temperature=0
)
return await _generate_response(state, llm)
# 获取可用工具列表
try:
# 获取本地工具
local_tools = [
get_current_time,
retrieve_knowledge
]
# 获取 MCP 工具
mcp_client = await get_mcp_client_with_retry()
mcp_tools = await mcp_client.get_tools()
# 合并所有工具
all_tools = local_tools + mcp_tools
logger.info(f"可用工具数量: 本地 {len(local_tools)} + MCP {len(mcp_tools)}")
# 格式化工具描述
tools_description = format_tools_description(all_tools)
except Exception as e:
logger.warning(f"获取工具列表失败: {e}")
tools_description = "无法获取工具列表"
# 创建 LLM
llm = ChatQwen(
model=config.rag_model,
api_key=config.dashscope_api_key,
temperature=0
)
# 格式化已执行的步骤
steps_summary = "\n".join([
f"步骤: {step}\n结果: {result[:300]}..."
for step, result in past_steps
])
# 如果还有剩余计划,进行决策
if plan:
logger.info("还有剩余计划,评估下一步行动")
# 给 AI 绑定规则,让它只能输出 3 种动作:respond /continue/replan
replanner_chain = replanner_prompt | llm.with_structured_output(Act)
try:
messages = [
("user", f"原始任务: {input_text}"),
("user", f"已执行的步骤:\n{steps_summary}"),
("user", f"剩余计划: {', '.join(plan)}"),
("user", f"⚠️ 重要提示:已执行 {len(past_steps)} 个步骤,请优先考虑是否信息已足够生成响应(respond)")
]
act = await replanner_chain.ainvoke({
"messages": messages,
"tools_description": tools_description
})
# 处理返回结果
if isinstance(act, Act):
action = act.action
new_steps = act.new_steps
else:
# 如果返回的是字典
action = act.get("action", "continue") # type: ignore
new_steps = act.get("new_steps", []) # type: ignore
logger.info(f"Replanner 决策: {action}")
if action == "respond":
logger.info("决定生成最终响应")
return await _generate_response(state, llm)
elif action == "replan":
# ⚠️ 强制限制:新步骤数不能超过当前剩余步骤数
if len(new_steps) > len(plan):
logger.warning(
f"新步骤数 {len(new_steps)} > 剩余步骤数 {len(plan)},"
f"强制截断为 {len(plan)} 个步骤"
)
new_steps = new_steps[:len(plan)]
# ⚠️ 二次检查:如果已执行步骤 >= 5,禁止 replan
if len(past_steps) >= 5:
logger.warning(f"已执行 {len(past_steps)} 个步骤,禁止重新规划,强制生成响应")
return await _generate_response(state, llm)
logger.info(f"决定调整计划,新步骤数量: {len(new_steps)}")
if new_steps:
# 替换剩余计划
return {"plan": new_steps}
else:
logger.warning("replan 但未提供新步骤,继续执行原计划")
return {}
else: # action == "continue"
logger.info("决定继续执行当前计划")
return {} # 不修改状态,继续执行
except Exception as e:
logger.error(f"重新规划失败: {e}, 继续执行剩余计划")
return {}
else:
# 没有剩余计划,生成最终响应
logger.info("计划已执行完毕,生成最终响应")
return await _generate_response(state, llm)
async def _generate_response(state: PlanExecuteState, llm: ChatQwen) -> Dict[str, Any]:
"""生成最终响应"""
logger.info("生成最终响应...")
input_text = state.get("input", "")
past_steps = state.get("past_steps", [])
# 格式化执行历史
execution_history = "\n\n".join([
f"### 步骤: {step}\n**结果:**\n{result}"
for step, result in past_steps
])
response_gen = response_prompt | llm.with_structured_output(Response)
try:
messages = [
("user", f"原始任务: {input_text}"),
("user", f"执行历史:\n{execution_history}"),
("user", "请基于以上信息生成全面的最终响应")
]
# 让阿里云通义千问大模型,现场生成 “处理建议” 的那一行!
response_obj = await response_gen.ainvoke({"messages": messages})
# 处理返回结果
if isinstance(response_obj, Response):
final_response = response_obj.response
else:
# 如果返回的是字典
final_response = response_obj.get("response", "") # type: ignore
logger.info(f"最终响应生成完成,长度: {len(final_response)}")
return {"response": final_response}
except Exception as e:
logger.error(f"生成响应失败: {e}")
# 生成简单的后备响应
fallback_response = f"""# 任务执行结果
## 原始任务
{input_text}
## 执行的步骤
{_format_simple_steps(past_steps)}
## 说明
由于系统异常,无法生成完整响应。以上是已收集的信息。
"""
return {"response": fallback_response}
def _format_simple_steps(past_steps: list) -> str:
"""格式化步骤列表(简单版)"""
if not past_steps:
return "无"
formatted = []
for i, (step, result) in enumerate(past_steps, 1):
result_preview = result[:200] + "..." if len(result) > 200 else result
formatted.append(f"{i}. **{step}**\n {result_preview}\n")
return "\n".join(formatted)
更多推荐

所有评论(0)