为工程Agents引入约束拓扑规划的概念

0 阅读11分钟

一、 核心架构与数据流

本系统的核心是 “一个规划器,一组专业智能体,一张约束拓扑图” 。目标是构建一个能够理解和执行团队规范、并能从错误中学习反哺的协作式AI系统。

拓扑图给了规划器"在约束内自由"的能力,既避免了无限探索,又保留了动态适应的灵活性

image.png

约束拓扑图不是限制,而是解放——它通过定义清晰的边界,让规划器在边界内可以安全、高效地探索最优路径。它把论文中的"动态规划"从理论变成了工程可实现的东西:

  1. 对规划器:它提供了决策的上下文和边界
  2. 对智能体:它定义了谁可以调用谁,避免无效调用
  3. 对研发:它让系统的行为变得可理解和可配置
  4. 对系统本身:它提供了错误恢复和追溯的蓝图

1、约束拓扑图到底是什么?

约束拓扑图是一个有向图 G = (V, Ê) ,其中:

  • V (节点) :代表智能体(如配置生成器、代码生成器等)
  • Ê (边) :定义允许的执行路径

但这是学术定义。工程上,约束拓扑图就是工作流的“交通规则” ——它规定了:

  1. 谁能接在谁后面(比如测试编码器必须在代码生成器之后)
  2. 谁可以重试谁(比如代码生成器失败后可以重新调用配置生成器)
  3. 谁能绕过谁(在特定条件下可以跳过某些步骤)

2、为什么需要约束拓扑图?——不这么做的后果

场景1:无约束的完全自由调用
# 伪代码:规划器可以随意调用任何智能体
planner.decide_next()  # 可能输出:code_generator
# 但此时连配置文件都没生成,代码生成器根本不知道要生成什么
# 结果:空跑、浪费token、无限循环
场景2:固定的顺序工作流
# 固定顺序:config → utils → template → code → test
# 但如果config生成失败,整个流程卡死
# 或者如果test失败需要修改code,但固定顺序不允许回溯
约束拓扑图的解决方案

它既不是完全自由,也不是完全固定,而是在约束内的动态规划

3、约束拓扑图应用

框架流程图:

image.png

会用到的agents定义

# topology.yaml - 

    agents:
      config_generator:
        description: "生成接口定义、数据库操作配置、缓存配置等"
        success_criteria: "生成的YAML/JSON配置完整且符合规范"
        
      utils_retriever:
        description: "从代码库检索工具类、Utils方法、常量等"
        success_criteria: "返回至少1个相关工具类"
        
      dto_generator:
        description: "生成Request/Response DTO类"
        success_criteria: "生成的DTO包含所有字段和注解"
        
      mapper_generator:
        description: "生成MyBatis Mapper接口和XML"
        success_criteria: "生成的Mapper包含必要的方法"
        
      service_generator:
        description: "生成Service层实现类"
        success_criteria: "生成的Service包含完整的业务逻辑"
        
      controller_generator:
        description: "生成Controller层接口"
        success_criteria: "生成的Controller包含正确的路由和参数"
        
      test_generator:
        description: "生成单元测试和集成测试"
        success_criteria: "测试代码可编译,覆盖率>80%"

    transitions:
      start:
        allowed_next: ["config_generator"]
        
      config_generator:
        allowed_next: ["utils_retriever", "dto_generator"]
        
      utils_retriever:
        allowed_next: ["dto_generator", "service_generator"]
        
      dto_generator:
        allowed_next: ["mapper_generator", "service_generator"]
        
      mapper_generator:
        allowed_next: ["service_generator"]
        
      service_generator:
        allowed_next: ["controller_generator", "test_generator", "config_generator"]
        
      controller_generator:
        allowed_next: ["test_generator"]
        
      test_generator:
        allowed_next: ["service_generator", "controller_generator"]

    error_recovery:
      service_generator_failed:
        retry_same: true
        fallback_to: ["config_generator", "dto_generator", "mapper_generator"]
        
      test_generator_failed:
        retry_same: true
        fallback_to: ["service_generator", "controller_generator"]

4、约束拓扑图在规划器决策中的作用

现在看规划器如何利用这个图做决策。以下是规划器的核心决策代码:

# planner_with_topology.py - 展示拓扑图如何指导决策

    class TopologyAwarePlanner:
        def __init__(self, topology_config: Dict, llm_client):
            self.topology = topology_config
            self.llm = llm_client
            
        async def decide_next_agent(self, 
                                    current_state: Dict,
                                    last_agent: Optional[str],
                                    failed_agents: List[str]) -> Dict:
            """
            基于拓扑图决定下一步
            
            Args:
                current_state: 当前所有智能体的状态
                last_agent: 上一步执行的智能体
                failed_agents: 当前任务中失败的智能体列表
            """
            
            # 1. 从拓扑图获取允许的下一个智能体
            if last_agent is None:
                # 初始状态
                allowed_next = self.topology["transitions"]["start"]["allowed_next"]
            else:
                allowed_next = self.topology["transitions"][last_agent]["allowed_next"]
                conditions = self.topology["transitions"][last_agent].get("conditions", {})
            
            # 2. 过滤掉不需要的选项
            filtered_next = self._filter_by_context(allowed_next, current_state, failed_agents)
            
            # 3. 如果有失败的智能体,检查错误恢复路径
            if failed_agents:
                recovery_options = self._get_recovery_options(failed_agents[-1], filtered_next)
                if recovery_options:
                    filtered_next = recovery_options
            
            # 4. 如果过滤后没有选项,考虑HITL
            if not filtered_next:
                return {"type": "hitl", "reason": "无可用路径,需要人工介入"}
            
            # 5. 让LLM从过滤后的选项中选择最优的
            decision = await self._llm_select_best(
                options=filtered_next,
                conditions=conditions,
                state=current_state
            )
            
            return decision
        
        def _filter_by_context(self, allowed_next: List[str], 
                              state: Dict, failed_agents: List[str]) -> List[str]:
            """根据上下文过滤选项"""
            filtered = []
            
            for agent in allowed_next:
                # 检查该智能体需要的输入是否已存在
                agent_spec = self.topology["agents"].get(agent, {})
                required_inputs = agent_spec.get("input_required", [])
                
                # 检查状态中是否已有这些输入
                has_inputs = all(
                    inp in state.get("artifacts", {}) or 
                    inp in state.get("context", {})
                    for inp in required_inputs
                )
                
                if has_inputs:
                    filtered.append(agent)
                else:
                    # 如果缺少输入,但允许从失败中恢复,可能仍保留
                    if agent in failed_agents:
                        # 检查是否允许重试相同智能体
                        recovery = self.topology["error_recovery"].get(f"{agent}_failed", {})
                        if recovery.get("retry_same", False):
                            filtered.append(agent)
            
            return filtered
        
        def _get_recovery_options(self, failed_agent: str, 
                                 current_options: List[str]) -> List[str]:
            """获取错误恢复选项"""
            recovery_key = f"{failed_agent}_failed"
            recovery = self.topology["error_recovery"].get(recovery_key, {})
            
            fallback = recovery.get("fallback_to", [])
            # 只返回同时在当前选项和fallback中的
            return [opt for opt in fallback if opt in current_options]
        
        async def _llm_select_best(self, options: List[str], 
                                  conditions: Dict, state: Dict) -> Dict:
            """让LLM从多个合法选项中选择最优的"""
            
            prompt = f"""
    你需要在以下合法的智能体中选择下一个要调用的:

    【可用选项】
    {json.dumps(options, indent=2)}

    【各选项的适用条件】
    {json.dumps(conditions, indent=2)}

    【当前任务状态】
    - 已完成: {state.get('completed_steps', [])}
    - 失败: {state.get('failed_agents', [])}
    - 现有产物: {list(state.get('artifacts', {}).keys())}

    【当前错误】
    {json.dumps(state.get('errors', [])[-2:], indent=2)}

    请基于以下原则选择最优的下一步:
    1. **最小化迭代次数**:优先选择能直接推进任务的
    2. **最大化成功率**:优先选择当前上下文最完备的
    3. **错误恢复优先**:如果有失败的智能体,优先选择能修复错误的

    输出格式:
    {{
        "actor": "选择的智能体名称",
        "reason": "为什么选择这个",
        "planner_input": "给该智能体的具体指令"
    }}
    """
            
            response = await self.llm.complete(prompt, temperature=0.1)
            return json.loads(response)

5、约束拓扑图的核心价值

保证工程可靠性没有拓扑图时可能发生的灾难planner: "调用 testcase_coder!"但此时 code_generator 还没成功,没有代码可测试结果:testcase_coder 不知道测试什么,空转​有拓扑图时if"code_generator"notinstate["success"]:    拓扑图不允许从其他节点直接到 testcase_coder    planner: "不能调用 testcase_coder,先完成代码生成"
实现错误隔离和追溯向研发人员解释为什么系统做了某个决定explanation=f"""当前步骤: {last_agent}根据拓扑图,允许的下一步: {allowed_next}基于当前上下文({context_summary}),选择了 {chosen_agent}原因: {reason}"""研发人员可以理解系统的行为逻辑
提供可解释性向研发人员解释为什么系统做了某个决定explanation=f"""当前步骤: {last_agent}根据拓扑图,允许的下一步: {allowed_next}基于当前上下文({context_summary}),选择了 {chosen_agent}原因: {reason}"""研发人员可以理解系统的行为逻辑
支持渐进式团队适配团队A的拓扑testcase_generator: allowed_next: ["testcase_coder"]  必须实现测试才能继续​团队B的拓扑 testcase_generator: allowed_next: ["testcase_coder","code_generator"]  可以跳过测试先出代码

6、拓扑图的学习和动态演化

最后,拓扑图本身不是静态的,它可以学习和演化

class TopologyLearner:
        def __init__(self, base_topology):
            self.topology = base_topology
            self.execution_stats = {}
            
        def record_execution(self, task_id: str, path: List[str], success: bool):
            """记录一次执行路径和结果"""
            for i in range(len(path)-1):
                from_agent = path[i]
                to_agent = path[i+1]
                
                key = f"{from_agent}->{to_agent}"
                if key not in self.execution_stats:
                    self.execution_stats[key] = {"success": 0, "total": 0}
                
                self.execution_stats[key]["total"] += 1
                if success:
                    self.execution_stats[key]["success"] += 1
        
        def suggest_topology_update(self) -> Dict:
            """基于历史数据建议拓扑图更新"""
            suggestions = {}
            
            for transition, stats in self.execution_stats.items():
                success_rate = stats["success"] / stats["total"]
                if stats["total"] > 10 and success_rate < 0.3:
                    # 这条路径成功率太低,建议移除或加条件
                    from_agent, to_agent = transition.split("->")
                    suggestions[transition] = {
                        "action": "remove_or_restrict",
                        "reason": f"成功率仅{success_rate:.1%}",
                        "alternative": self._find_alternative_path(from_agent, to_agent)
                    }
            
            return suggestions

二、 关键模块工程化实现方案

模块一:环境定义与知识库构建

这是系统能够“个性化”的基础,需要工程化地表达团队知识。

  • 拓扑图即代码 (Topology as Code) : 将论文中的约束拓扑图实现为一个可配置的、版本化的YAML或JSON文件。这个文件定义了智能体间的依赖关系,是整个工作流的“宪法”。
    • 示例结构 ( agent_topology.yaml ) :
 agents:
      - name: config_generator
        allowed_next: [utils_retriever, code_template_generator]
        max_retries: 3
        success_criteria: "生成YAML可通过Pydantic模型校验"
      - name: code_generator
        allowed_next: [testcase_generator, code_template_generator, HITL]
        max_retries: 5
        success_criteria: "生成的PySpark代码在测试数据集上执行无错误"
  • 静态知识库向量化:
    • 输入: 团队现有的代码库(如GitLab/GitHub仓库)、README文档、Wiki、最佳实践文档、以及历史的“特征规范配置(FSC)”和“数据框注册表(DFR)”
    • 工程动作: 将这些静态文档切片、清洗,使用Embedding模型存入向量数据库。当智能体(如工具检索器)被调用时,规划器提供的“用户任务”会被向量化,从库中检索最相关的代码片段或文档,作为上下文发送给LLM。这是实现检索增强生成的关键。

模块二:规划器的工程实现

规划器是系统的“大脑”,它是一个特殊的、有状态的LLM智能体。

  • 状态管理: 规划器需要维护一个短期记忆,记录当前任务中所有智能体的执行状态(成功/失败/重试次数)、产生的中间产物(配置、代码、错误日志)。
    • 工程实现: 可以使用一个简单的内存对象Redis缓存来存储任务的状态机。每次迭代,规划器都会收到这个状态的摘要。
  • 决策引擎:
    • 输入: 当前状态s_t + 约束拓扑图G + 全局任务描述T
    • 处理: 构造一个高级提示词(如论文附录F所示),要求LLM基于拓扑图的约束和当前状态,选择下一个最优的智能体,并生成该智能体所需的上下文指令(planner_input)。
    • 关键提示词工程: 提示词必须包含:
      1. 角色设定: “你是一个资深机器学习工程师的规划代理...”
      2. 约束规则: “你只能从拓扑图中定义的 allowed_next 列表中选择...”
      3. 状态摘要: “当前状态:config_generator (成功),utils_retriever (失败,错误信息:找不到工具) ...”
      4. 决策与输出: 要求LLM以严格的JSON格式输出,包含 call_type, actor, reason, args,便于后续程序解析。
    • 人工介入(HITL)接口: 当规划器连续重试失败、或置信度低时,应能生成一个暂停信号,并通过即时通讯工具(如Slack、钉钉)向指定的研发人员发送一个带有明确上下文和选项的“审批/澄清请求” 。研发人员的反馈将被作为下一轮规划的输入。

模块三:专业智能体的开发

这些是执行具体任务的“手”,可以是调用LLM的函数,也可以是执行传统脚本的工具

  • 智能体抽象化: 为所有智能体定义一个统一的接口。例如:
    • 输入: 一个字典,包含 task_context (从规划器来的 planner_input), static_knowledge (从向量库检索到的信息)。
    • 处理: 调用LLM(如Claude)并执行必要的代码逻辑。
    • 输出: 一个标准化的响应,包含 status (成功/失败), output_data (生成的代码/配置), error_log (错误信息), metadata (如生成的token数)。
  • 关键智能体
规划器基于LLM的决策核心,根据当前任务状态和拓扑图约束,选择下一步调用的最优智能体
配置生成器解析需求文档,生成接口定义、数据库操作配置、缓存策略等元数据
工具检索器核心是一个代码解析+语义搜索组件。它接收 planner_input 描述的任务,去向量数据库中搜索最相关的工具函数,并返回其名称、签名、导入路径和使用示例。供后续代码生成使用。
代码生成器提示词包含:任务描述、代码模板、选定的工具函数签名、单元测试用例、以及团队编码规范, 根据配置和工具,生成Controller/Service/DTO/Mapper等工程代码。
测试生成器基于业务逻辑和代码,生成单元测试和集成测试用例
  • 错误处理与自省: 当智能体执行失败时,它的输出必须包含结构化的失败原因分析和修复建议(如论文中的 <reason><fix> 标签),这为规划器提供了关键的“下游失败”信号,用于追溯上游问题。

参考的资料:

Towards Reliable ML Feature Engineering via Planning in Constrained-Topology of LLM Agents

Himanshu Thakur,Anusha Kamath,Anurag Muthyala,Dhwani Sanmukhani,Smruthi Mukund,Jay Katukuri