复杂任务拆解:让AI像项目经理一样思考

24 阅读7分钟

为什么 AI 需要学会拆解任务?

一个让AI 崩溃的真实场景

用户说了一句看似简单的话:

“找出本月修改次数最多的文件,备份到桌面”

人类的直觉处理

  1. 我知道“本月”是什么意思(时间范围)。
  2. 我知道“修改次数最多”意味着要统计。
  3. 我知道“备份”是复制操作。
  4. 我凭经验知道执行顺序。

AI 面临的挑战

  • 没有“文件系统”的直觉。
  • 不知道“本月”在代码中怎么表示。
  • 不知道“修改次数”从哪获取。
  • 不知道“最多”需要比较。

核心矛盾

自然语言 vs 计算机指令

维度自然语言(用户)计算机指令(AI需要产出)
表达方式声明式(说什么)命令式(怎么做)
结构模糊、隐含精确、显式
依赖隐含依赖明确依赖
粒度粗粒度原子操作

解决方案:任务拆解

让 AI 像项目经理一样,把一句话的需求分解成多个可执行的任务列表。例如:用户的原始需求是:"找出本月修改次数最多的文件,备份到桌面":

  1. 获取当前目录所有文件列表。
  2. 获取每个文件的修改历史。
  3. 筛选本月内的修改记录。
  4. 统计每个文件的修改次数。
  5. 比较找出修改次数最多的文件。
  6. 复制该文件到桌面。
  7. 返回操作结果。

什么是复杂任务拆解?

拆解的关键特征

  • 原子性:每个步骤不可再分,可直接执行。
  • 有序性:步骤之间有明确的先后顺序。
  • 可验证性:每个步骤的输出可被验证。
  • 可组合性:子任务可以复用。

拆解的三个层次

层次1: 任务理解

  • 意图识别:用户想做什么?
  • 约束提取:有时间限制吗?范围是什么?
  • 隐含需求:用户没说但需要的(如错误处理、反馈)

层次2: 任务分解

  • 将目标拆分为 3-7 个逻辑子任务。
  • 标注子任务间的依赖关系。
  • 识别可并行执行的任务。

层次3: 原子操作

  • 将每个子任务映射到具体工具。
  • 确定工具参数和调用顺序。
  • 设计输入输出格式。

为什么拆解如此重要?

维度无拆解有拆解
成功率低(一步到位容易出错)高(每步可验证)
可调试性无法定位问题可定位到具体步骤
可复用性每个任务单独处理子任务可复用
可解释性黑盒每步可见
错误恢复全部重来从失败步骤恢复

任务拆解的5种策略模式

策略一:顺序拆解

适用于步骤有明确的前后依赖关系的场景。

顺序拆解示例

查询 -─▶ 统计 ──▶ 比较 ──▶ 操作

每个步骤的输出是下一步的输入!

代码示例

async function sequentialPlan() {
  const files = await listFiles()           // Step 1
  const stats = await countModifications(files) // Step 2
  const max = await findMax(stats)          // Step 3
  const result = await backup(max)          // Step 4
  return result
}

策略二:并行拆解

适用于存在独立、无依赖关系的子任务的场景。

并行拆解示例

graph TD
    Start([任务拆解]) --> Query1[查北京天气]
    Start --> Query2[查上海天气]
    Start --> Query3[查武汉天气]
    
    Query1 --> Merge[合并结果]
    Query2 --> Merge
    Query3 --> Merge
    
    Merge --> End([结束])

代码示例

async function parallelPlan() {
  const [beijing, shanghai, shenzhen] = await Promise.all([
    getWeather("北京"),
    getWeather("上海"),
    getWeather("武汉")
  ])
  return { beijing, shanghai, shenzhen }
}

策略三:条件拆解

适用于有决策点的任务的场景。

条件拆解示例

graph TD
    Start([获取天气]) --> Decision{判断天气}
    
    Decision -->|下雨| Rain[提醒带伞<br/>建议穿雨鞋]
    Decision -->|晴天| Sunny[推荐公园<br/>注意防晒]
    Decision -->|下雪| Snow[提醒保暖<br/>注意路滑]
    
    Rain --> End([结束])
    Sunny --> End
    Snow --> End

代码示例

async function conditionalPlan() {
  const weather = await getWeather("北京")
  
  if (weather.condition === "rain") {
    return "下雨了,记得带伞,建议穿雨鞋"
  } else if (weather.condition === "sunny") {
    const park = await recommendPark("北京")
    return `天气好,推荐去${park},注意防晒`
  } else if (weather.condition === "snowy") {
    return `下雪了,记得多穿点衣服,注意保暖,防止路滑`
  }
}

策略四:循环拆解

适用于需要批量处理相同操作的场景。

循环拆解示例

graph TD
    Start([获取文件列表]) --> Decision{还有文件<br/>未处理?}
    
    Decision -->|是| Process[处理文件]
    Decision -->|否| End([结束])
    
    Process --> Decision

代码示例

async function loopPlan(files) {
  const results = []
  for (const file of files) {
    const result = await processFile(file)
    results.push(result)
  }
  return results
}

策略五:聚合拆解

适用于需要汇总多个结果的场景。

聚合拆解示例

graph TD
    File1[文件1统计] --> Merge[比较聚合]
    File2[文件2统计] --> Merge
    File3[文件3统计] --> Merge
    
    Merge --> Output[输出结果]

代码示例

async function aggregatePlan() {
  // 并行执行多个统计
  const stats = await Promise.all(
    files.map(file => countModifications(file))
  )
  
  // 聚合比较
  const max = stats.reduce((max, current) => 
    current.count > max.count ? current : max
  )
  
  return max
}

让 AI 学会拆解的 Prompt 技术

技术一:显式要求拆解

在回答问题前,请先将任务拆解为具体步骤:

1. 列出所有需要的子任务
2. 说明每个子任务的作用
3. 标注子任务间的依赖关系
4. 确认所有子任务都能执行

任务: {task}

技术二:提供拆解模板

请使用以下模板拆解任务:

### 任务分析
- 目标: [一句话总结]
- 约束: [时间、范围、条件]
- 隐含需求: [用户没明说但需要的]

### 子任务列表
| ID | 子任务 | 依赖 | 工具 | 输出 |
|----|--------|------|------|------|
| 1 | ... | 无 | ... | ... |
| 2 | ... | 1 | ... | ... |
| 3 | ... | 1,2 | ... | ... |

### 异常处理
- 如果步骤1失败,则...
- 如果步骤2无结果,则...

技术三:Few-shot 示例

## 示例1:文件备份任务

用户输入:找出本月修改次数最多的文件,备份到桌面

拆解结果:
1. [查询] 获取当前目录所有文件列表
2. [筛选] 获取每个文件的修改历史,筛选本月记录
3. [统计] 统计每个文件的修改次数
4. [比较] 找出修改次数最多的文件
5. [处理并列] 如果有多个并列,询问用户选择
6. [执行] 复制文件到桌面
7. [反馈] 返回备份结果

## 示例2:代码质量分析任务

用户输入:分析项目代码,找出圈复杂度超过10的函数

拆解结果:
1. [扫描] 扫描项目所有代码文件
2. [解析] 解析每个文件的AST
3. [提取] 提取所有函数定义
4. [计算] 计算每个函数的圈复杂度
5. [筛选] 筛选复杂度>10的函数
6. [生成] 生成报告(文件、函数名、复杂度值)

## 当前任务
{task}

技术四:思维链引导

请按以下思路思考任务拆解:

1. **目标分析**:用户想要达到什么目的?
2. **信息需求**:完成这个目的需要哪些信息?
3. **信息获取**:如何获取这些信息?(需要哪些工具)
4. **信息处理**:获取信息后如何处理?(计算、比较、筛选)
5. **结果呈现**:最后如何呈现结果?
6. **异常处理**:什么情况会出错?如何处理?

任务: {task}

实战:任务拆解 Agent 实现

完整的拆解 Agent 代码

import axios from 'axios'
import dotenv from 'dotenv'

dotenv.config()

// ==================== 类型定义 ====================

interface SubTask {
  id: string
  description: string
  tool?: string
  dependsOn: string[]
  fallback?: string
  output?: string
}

interface TaskPlan {
  originalTask: string
  goal: string
  constraints: string[]
  implicitNeeds: string[]
  steps: SubTask[]
  exceptions: Array<{
    condition: string
    action: string
  }>
}

interface TaskUnderstanding {
  goal: string
  constraints: string[]
  implicitNeeds: string[]
  complexity: 'simple' | 'moderate' | 'complex'
  estimatedSteps: number
}

interface LLMResponse {
  choices: Array<{
    message: {
      content: string
    }
  }>
}

// ==================== 任务拆解Agent ====================

class TaskDecompositionAgent {
  private llm: any
  private apiKey: string
  private apiUrl: string
  
  constructor() {
    this.apiKey = process.env.DEEPSEEK_API_KEY || ''
    this.apiUrl = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions'
    
    if (!this.apiKey) {
      throw new Error('请设置 DEEPSEEK_API_KEY 环境变量')
    }
  }
  
  /**
   * 核心方法:将用户输入拆解为可执行计划
   */
  async decompose(userInput: string): Promise<TaskPlan> {
    try {
      // 步骤1:任务理解
      const understanding = await this.understandTask(userInput)
      
      // 步骤2:任务分解
      const plan = await this.decomposeIntoSteps(understanding, userInput)
      
      // 步骤3:验证计划
      const validatedPlan = await this.validatePlan(plan)
      
      // 步骤4:优化计划(并行化、缓存等)
      const optimizedPlan = await this.optimizePlan(validatedPlan)
      
      return optimizedPlan
    } catch (error) {
      console.error('任务拆解失败:', error)
      // 返回降级计划
      return this.createFallbackPlan(userInput)
    }
  }
  
  private async understandTask(userInput: string): Promise<TaskUnderstanding> {
    const prompt = `
你是一个任务分析专家。请分析用户的任务,提取关键信息。

用户输入: ${userInput}

请输出JSON格式,不要包含其他文字:
{
  "goal": "一句话总结任务目标",
  "constraints": ["时间约束", "范围约束", "条件约束"],
  "implicitNeeds": ["用户没说但需要的", "如错误处理、反馈等"],
  "complexity": "simple | moderate | complex",
  "estimatedSteps": 预计需要多少步骤
}
`
    const response = await this.callLLM(prompt)
    
    try {
      // 清理响应,提取JSON部分
      const jsonMatch = response.match(/\{[\s\S]*\}/)
      if (!jsonMatch) {
        throw new Error('无法从LLM响应中提取JSON')
      }
      return JSON.parse(jsonMatch[0]) as TaskUnderstanding
    } catch (error) {
      console.error('解析任务理解失败:', error)
      // 返回默认理解
      return {
        goal: userInput,
        constraints: [],
        implicitNeeds: ['错误处理', '结果反馈'],
        complexity: 'moderate',
        estimatedSteps: 3
      }
    }
  }
  
  private async decomposeIntoSteps(
    understanding: TaskUnderstanding, 
    originalTask: string
  ): Promise<TaskPlan> {
    const prompt = `
基于以下任务分析,将其拆解为可执行的子任务列表。

任务分析:
- 目标: ${understanding.goal}
- 约束: ${understanding.constraints.join(', ')}
- 隐含需求: ${understanding.implicitNeeds.join(', ')}

## 拆解要求
1. 每个子任务应该是原子操作(不可再分)
2. 使用数字ID(1, 2, 3...)标识子任务
3. 明确标注子任务间的依赖关系
4. 为关键子任务设计异常处理
5. 考虑可并行执行的子任务

## 输出格式(JSON)
{
  "steps": [
    {
      "id": "1",
      "description": "子任务描述",
      "tool": "推荐使用的工具名",
      "dependsOn": [],
      "fallback": "失败时的备选方案",
      "output": "预期输出"
    }
  ],
  "exceptions": [
    {
      "condition": "异常条件",
      "action": "处理动作"
    }
  ]
}

请直接输出JSON,不要添加其他文字:
`
    const response = await this.callLLM(prompt)
    
    try {
      const jsonMatch = response.match(/\{[\s\S]*\}/)
      if (!jsonMatch) {
        throw new Error('无法从LLM响应中提取JSON')
      }
      
      const parsed = JSON.parse(jsonMatch[0])
      
      return {
        originalTask: originalTask,
        goal: understanding.goal,
        constraints: understanding.constraints,
        implicitNeeds: understanding.implicitNeeds,
        steps: parsed.steps || [],
        exceptions: parsed.exceptions || []
      }
    } catch (error) {
      console.error('解析任务分解失败:', error)
      // 返回默认计划
      return this.createFallbackPlan(originalTask)
    }
  }
  
  private async validatePlan(plan: TaskPlan): Promise<TaskPlan> {
    // 检查完整性
    if (!plan.steps || plan.steps.length === 0) {
      throw new Error('计划为空')
    }
    
    // 检查步骤ID唯一性
    const ids = plan.steps.map(s => s.id)
    if (new Set(ids).size !== ids.length) {
      throw new Error('步骤ID不唯一')
    }
    
    // 检查依赖关系
    const allIds = new Set(plan.steps.map(s => s.id))
    for (const step of plan.steps) {
      if (step.dependsOn) {
        for (const dep of step.dependsOn) {
          if (!allIds.has(dep)) {
            throw new Error(`步骤 ${step.id} 依赖不存在的步骤 ${dep}`)
          }
        }
      }
    }
    
    // 检查循环依赖
    this.checkCyclicDependency(plan.steps)
    
    // 检查原子性(只警告,不抛出错误)
    for (const step of plan.steps) {
      if (this.isTooCoarse(step.description)) {
        console.warn(`⚠️ 步骤 ${step.id} 可能过粗: ${step.description}`)
      }
    }
    
    // 为没有fallback的步骤添加默认fallback
    for (const step of plan.steps) {
      if (!step.fallback) {
        step.fallback = `重试步骤 ${step.id} 或跳过并记录错误`
      }
    }
    
    return plan
  }
  
  private checkCyclicDependency(steps: SubTask[]): void {
    const graph = new Map<string, string[]>()
    for (const step of steps) {
      graph.set(step.id, step.dependsOn || [])
    }
    
    const visited = new Set<string>()
    const recursionStack = new Set<string>()
    
    const hasCycle = (node: string): boolean => {
      visited.add(node)
      recursionStack.add(node)
      
      const deps = graph.get(node) || []
      for (const dep of deps) {
        if (!visited.has(dep)) {
          if (hasCycle(dep)) return true
        } else if (recursionStack.has(dep)) {
          return true
        }
      }
      
      recursionStack.delete(node)
      return false
    }
    
    for (const step of steps) {
      if (!visited.has(step.id)) {
        if (hasCycle(step.id)) {
          throw new Error(`检测到循环依赖,涉及步骤: ${step.id}`)
        }
      }
    }
  }
  
  private isTooCoarse(description: string): boolean {
    // 启发式判断:如果描述中包含“处理”、“分析”等模糊词汇,可能过粗
    const coarseWords = ['处理', '分析', '操作', '管理', '进行', '执行']
    return coarseWords.some(word => description.includes(word))
  }
  
  private async optimizePlan(plan: TaskPlan): Promise<TaskPlan> {
    // 识别可并行的步骤
    const parallelGroups = this.identifyParallelGroups(plan.steps)
    
    // 为并行组添加元数据(不修改tool字段,避免破坏原有逻辑)
    const optimizedSteps = plan.steps.map(step => ({
      ...step,
      parallelWith: parallelGroups.find(group => group.includes(step.id))?.filter(id => id !== step.id) || []
    }))
    
    // 重新排序步骤:将可并行的步骤放在相邻位置
    const reorderedSteps = this.reorderStepsForParallel(optimizedSteps, parallelGroups)
    
    return {
      ...plan,
      steps: reorderedSteps
    }
  }
  
  private identifyParallelGroups(steps: SubTask[]): string[][] {
    const groups: string[][] = []
    const processed = new Set<string>()
    let remainingSteps = [...steps]
    
    while (remainingSteps.length > 0) {
      // 找出所有依赖都已满足的步骤
      const parallelGroup: string[] = []
      
      for (const step of remainingSteps) {
        const hasUnmetDeps = step.dependsOn.some(dep => !processed.has(dep))
        if (!hasUnmetDeps) {
          parallelGroup.push(step.id)
        }
      }
      
      if (parallelGroup.length === 0) {
        // 如果没有可执行的步骤,说明有循环依赖或依赖错误
        console.warn('无法识别并行组,可能存在依赖问题')
        break
      }
      
      groups.push(parallelGroup)
      for (const id of parallelGroup) {
        processed.add(id)
      }
      
      // 移除已处理的步骤
      remainingSteps = remainingSteps.filter(step => !processed.has(step.id))
    }
    
    return groups
  }
  
  private reorderStepsForParallel(steps: SubTask[], parallelGroups: string[][]): SubTask[] {
    const reordered: SubTask[] = []
    const stepMap = new Map(steps.map(s => [s.id, s]))
    
    for (const group of parallelGroups) {
      // 按原顺序添加同组的步骤
      for (const id of group) {
        const step = stepMap.get(id)
        if (step) {
          reordered.push(step)
        }
      }
    }
    
    return reordered
  }
  
  private createFallbackPlan(userInput: string): TaskPlan {
    return {
      originalTask: userInput,
      goal: userInput,
      constraints: [],
      implicitNeeds: ['错误处理'],
      steps: [
        {
          id: '1',
          description: `执行任务: ${userInput}`,
          dependsOn: [],
          fallback: '任务失败,请手动处理',
          output: '执行结果'
        }
      ],
      exceptions: [
        {
          condition: '任何步骤失败',
          action: '记录错误并终止执行'
        }
      ]
    }
  }
  
  private async callLLM(prompt: string): Promise<string> {
    if (!this.apiKey) {
      throw new Error('请设置 DEEPSEEK_API_KEY 环境变量')
    }
    
    try {
      // 修复:添加必要的headers
      const response = await axios.post<LLMResponse>(
        this.apiUrl,
        {
          model: 'deepseek-chat',
          messages: [{ role: 'user', content: prompt }],
          temperature: 0.3, // 低温度,保证拆解稳定
          max_tokens: 2000
        },
        {
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${this.apiKey}`
          },
          timeout: 30000 // 30秒超时
        }
      )
      
      const content = response.data.choices[0]?.message?.content
      if (!content) {
        throw new Error('LLM返回空响应')
      }
      
      return content
    } catch (error) {
      if (axios.isAxiosError(error)) {
        if (error.response) {
          // 服务器返回了错误状态码
          throw new Error(`API调用失败 [${error.response.status}]: ${error.response.data?.error?.message || error.message}`)
        } else if (error.request) {
          // 请求已发送但没有收到响应
          throw new Error(`API调用失败: 无响应 (${error.message})`)
        } else {
          // 请求配置出错
          throw new Error(`API调用失败: ${error.message}`)
        }
      }
      throw error
    }
  }
  
  /**
   * 获取任务的执行顺序(拓扑排序)
   */
  getExecutionOrder(plan: TaskPlan): string[][] {
    return this.identifyParallelGroups(plan.steps)
  }
  
  /**
   * 获取任务的统计信息
   */
  getStats(plan: TaskPlan): {
    totalSteps: number
    parallelGroups: number
    maxParallelism: number
    hasFallbacks: number
  } {
    const parallelGroups = this.identifyParallelGroups(plan.steps)
    const maxParallelism = Math.max(...parallelGroups.map(g => g.length), 0)
    const hasFallbacks = plan.steps.filter(s => s.fallback).length
    
    return {
      totalSteps: plan.steps.length,
      parallelGroups: parallelGroups.length,
      maxParallelism,
      hasFallbacks
    }
  }
}

// ==================== 使用示例 ====================

async function main() {
  // 检查环境变量
  if (!process.env.DEEPSEEK_API_KEY) {
    console.error('❌ 错误: 请设置 DEEPSEEK_API_KEY 环境变量')
    console.error('示例: export DEEPSEEK_API_KEY="your-api-key"')
    process.exit(1)
  }
  
  console.log('🚀 启动任务拆解Agent...\n')
  
  const agent = new TaskDecompositionAgent()
  
  const tasks = [
    "找出本月修改次数最多的文件,备份到桌面",
    "分析项目代码,找出圈复杂度超过10的函数",
    "将下载文件夹中超过30天未使用的文件移动到归档文件夹"
  ]
  
  for (const task of tasks) {
    console.log('\n' + '='.repeat(60))
    console.log(`📝 原始任务: ${task}`)
    console.log('='.repeat(60))
    
    try {
      const plan = await agent.decompose(task)
      
      console.log('\n🎯 目标:', plan.goal)
      console.log('\n📋 执行计划:')
      plan.steps.forEach(step => {
        const deps = step.dependsOn.length > 0 
          ? ` (依赖: ${step.dependsOn.join(', ')})` 
          : ''
        console.log(`   ${step.id}. ${step.description}${deps}`)
        if (step.tool) {
          console.log(`      🔧 工具: ${step.tool}`)
        }
        if (step.fallback) {
          console.log(`      🔄 备选: ${step.fallback}`)
        }
      })
      
      if (plan.exceptions.length > 0) {
        console.log('\n⚠️ 异常处理:')
        plan.exceptions.forEach(ex => {
          console.log(`   如果 ${ex.condition},则 ${ex.action}`)
        })
      }
      
      // 显示执行顺序
      const executionOrder = agent.getExecutionOrder(plan)
      console.log('\n⚡ 执行顺序(可并行组):')
      executionOrder.forEach((group, idx) => {
        console.log(`   组${idx + 1}: [${group.join(', ')}] ${group.length > 1 ? '(可并行)' : '(串行)'}`)
      })
      
      // 显示统计信息
      const stats = agent.getStats(plan)
      console.log('\n📊 统计信息:')
      console.log(`   总步骤数: ${stats.totalSteps}`)
      console.log(`   并行组数: ${stats.parallelGroups}`)
      console.log(`   最大并行度: ${stats.maxParallelism}`)
      console.log(`   有备选方案: ${stats.hasFallbacks}`)
      
    } catch (error) {
      console.error(`❌ 处理任务失败:`, error)
    }
  }
}

main().catch(console.error)

运行输出示例

============================================================
📝 原始任务: 找出本月修改次数最多的文件,备份到桌面
============================================================

🎯 目标: 找出本月修改最多的文件并备份

📋 执行计划:
   1. 获取当前目录所有文件列表
   2. 获取每个文件的修改历史记录
   3. 筛选本月内的修改记录
   4. 统计每个文件的本月修改次数
   5. 比较找出修改次数最多的文件 (依赖: 4)
   6. 处理并列情况,询问用户选择 (依赖: 5)
   7. 复制文件到桌面 (依赖: 6)
   8. 返回备份结果 (依赖: 7)
      备选: 如果备份失败,提示用户手动检查

⚠️ 异常处理:
   如果 步骤3 无结果,则 告知用户本月无修改记录
   如果 步骤6 用户取消,则 终止操作并告知

拆解质量评估

质量评估清单

维度检查项通过标准
完整性是否覆盖所有用户需求?无遗漏
粒度每个步骤是否原子操作?不可再分
可执行性每个步骤是否有对应工具?工具存在
依赖正确步骤顺序是否合理?数据流正确
异常处理是否考虑失败场景?有备选方案
可验证性是否有中间结果检查点?关键步骤可验证

常见问题与优化

问题表现优化方法
拆解过粗步骤如"处理文件"细化:打开→读取→修改→保存
遗漏步骤忘记备份后的验证增加验证步骤
依赖错误步骤顺序不对检查数据流向,绘制依赖图
无异常处理文件不存在直接崩溃增加条件判断和fallback
粒度不一致有的步骤过粗,有的过细统一粒度标准

最佳实践清单

  • 先理解后拆解:明确目标、约束、隐含需求。
  • 原子化:每个步骤应该是可执行的最小单元。
  • 标注依赖:明确步骤间的数据依赖。
  • 设计异常:为关键步骤准备fallback。
  • 保留中间结果:便于调试和恢复。
  • 可验证:关键步骤后设置检查点。
  • 迭代优化:根据执行结果持续优化拆解。

结语

你在使用 AI 处理复杂任务时,遇到过哪些“拆解失败”的案例?欢迎在评论区分享,一起分析优化!

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!