一、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)

更多推荐