技术深度:buddyMe 框架任务拆解的 "盲拆" 问题与技能感知优化方案

admin · 2026-05-10 · skill

在 AI 智能体开发中,任务拆解的质量直接决定了复杂任务的执行效率和最终效果。一个优秀的规划器不仅要能把大任务拆成可执行的小步骤,更重要的是要知道自己有什么工具可用,并据此做出最优的拆解决策。

然而在 buddyMe 框架的早期实现中,我们发现了一个普遍存在于绝大多数开源 Agent 项目中的设计缺陷:规划器在进行任务拆解时,完全不知道系统有哪些专业技能可用。

问题本质:"盲拆" 导致的效率与质量双重损失

一般的 Skill 模块只负责子任务的执行,不参与项目的拆解与规划。这种设计在简单场景下工作良好,但在处理复杂专业任务时会暴露出严重问题:

规划器由于上下文限制,只能根据大模型自身的通用能力进行任务拆解,完全不会考虑系统中已经存在的专业技能。这导致拆解出的子任务往往不够专业,甚至会产生大量不必要的重复工作。

典型案例:当用户要求 "设计一个响应式 HTML 登录页面" 时,规划器可能会将其拆分为:

  1. 编写 HTML 结构
  2. 添加基础 CSS 样式
  3. 调整移动端适配
  4. 优化按钮样式
  5. 修复布局问题
  6. 再次调整颜色搭配

而实际上,buddyMe 框架中已经有一个专门的web_page_generator技能,可以一次性完成从需求到完整页面的所有工作。规划器的 "盲拆" 不仅浪费了 token 资源,还导致执行过程中反复进行样式修正,大幅延长了任务完成时间。

根因分析:buddyMe 当前的任务拆解与技能匹配流程

让我们深入代码层面,看看这个问题是如何产生的。

1. 任务拆解阶段(todo_manager.py)

当前plan_task函数的实现非常简洁,但也存在明显的局限性:

try:
    messages = [
        {"role": "system", "content": "你是一个务实的任务分解助手。"},
        {"role": "user", "content": plan_prompt}
    ]

    response = await client.chat(messages=messages)
    # 提取所有 type 为 "text" 的内容块并拼接
    texts = [b["text"] for b in response["content"] if b["type"] == "text"]
    plan_text = "".join(texts).strip()
except Exception:
    # 调用失败时降级为只包含原任务
    return [user_input]

# 解析:按行分割,过滤空行,去除可能的编号前缀(如 "1. "、"- " 等)
steps = []
for line in plan_text.splitlines():
    line = line.strip()
    if not line:
        continue
    # 去除常见编号前缀
    if line[0].isdigit() and len(line) > 2 and line[1] in ".、) ":
        parts = line.split(maxsplit=1)
        if len(parts) > 1:
            line = parts[1]
    elif line[0] in "-*•▪▫":
        line = line[1:].strip()
    steps.append(line)

return steps if steps else [user_input]


核心问题:

2. 技能匹配阶段(agent.py:888-912)

技能匹配发生在每个子任务执行之前,而不是规划阶段:

matched_instructions = self._skill_loader.get_matched_instructions(item["text"], max_skills=1)


get_matched_instructions内部调用match_skills()函数,使用关键词重叠评分算法(技能名称权重 3,描述权重 1)来匹配子任务描述。匹配成功后,才会将 Level 2 完整指令体注入到执行上下文中。

流程总结

当前流程:先盲目拆任务 → 再逐个子任务匹配技能

这种 "先拆后配" 的模式会导致两个严重后果:

  1. 规划器可能把一个 Skill 就能覆盖的工作拆成多个子任务
  2. 规划器完全不知道某个 Skill 存在,导致子任务需要从零开始实现

解决方案:让规划器 "看见" 所有可用技能

要解决这个问题,我们需要让规划阶段就感知到系统的技能能力。幸运的是,这个改动非常小,只需要修改两个文件。

方案核心思想

在任务分解之前,将所有可用技能的 Level 1 元数据注入到规划器的 Prompt 中。这样规划器在拆解任务时,就可以:

具体实现步骤

1. 修改 todo_manager.py:增加 skill_metadata 参数

在plan_task函数中增加skill_metadata参数,并将其注入到规划 Prompt 中:

async def plan_task(user_input: str, client, skill_metadata: str = "") -> list:
    """
    将用户输入的复杂任务分解为可执行的子任务列表
    新增:支持传入技能元数据,让规划器了解可用技能
    """
    plan_prompt = f"""
    将以下任务分解为清晰、可执行的步骤:
    {user_input}
    
    可用技能列表:
    {skill_metadata}
    
    分解规则:
    1. 如果某个步骤可以使用上述可用技能完成,请用 [SKILL:skill-name] 标注该步骤
    2. 每个步骤应该是一个独立的、可完成的小任务
    3. 步骤数量控制在3-10个之间
    4. 保持语言简洁明了
    """
    
    try:
        messages = [
            {"role": "system", "content": "你是一个务实的任务分解助手,擅长利用已有技能高效完成任务。"},
            {"role": "user", "content": plan_prompt}
        ]

        response = await client.chat(messages=messages)
        # 提取所有 type 为 "text" 的内容块并拼接
        texts = [b["text"] for b in response["content"] if b["type"] == "text"]
        plan_text = "".join(texts).strip()
    except Exception:
        # 调用失败时降级为只包含原任务
        return [user_input]

    # 解析:按行分割,过滤空行,去除可能的编号前缀(如 "1. "、"- " 等)
    steps = []
    for line in plan_text.splitlines():
        line = line.strip()
        if not line:
            continue
        # 去除常见编号前缀
        if line[0].isdigit() and len(line) > 2 and line[1] in ".、) ":
            parts = line.split(maxsplit=1)
            if len(parts) > 1:
                line = parts[1]
        elif line[0] in "-*•▪▫":
            line = line[1:].strip()
        steps.append(line)

    return steps if steps else [user_input]


关键改进:

2. 修改 agent.py:调用时传入技能元数据

在 Agent 调用plan_task函数时,传入从 SkillLoader 获取的技能元数据:

plans = await todo_manager.plan_task(
    enriched_input,
    client=self._client,
    skill_metadata=self._skill_loader.get_metadata_prompt()
)


配套实现:SkillLoader 的 get_metadata_prompt 方法

为了让这个方案完整工作,我们还需要在SkillLoader类中实现get_metadata_prompt方法,用于生成格式化的技能元数据:

def get_metadata_prompt(self) -> str:
    """生成用于规划器的技能元数据Prompt"""
    if not self.skills:
        return "暂无可用技能"
    
    metadata = []
    for skill_name, skill in self.skills.items():
        # 只返回Level 1元数据(名称和简短描述),避免token浪费
        metadata.append(f"- {skill_name}: {skill.description}")
    
    return "\n".join(metadata)


优化效果与后续扩展

立即见效的优化

  1. 任务拆解更专业:规划器会优先使用已有技能,避免重复工作
  2. 执行效率提升:一个技能就能完成的工作不会被拆成多个子任务
  3. token 成本降低:减少了不必要的 LLM 调用和上下文传递
  4. 错误率下降:使用经过验证的专业技能,比大模型从零开始实现更可靠

后续扩展方向

  1. 技能依赖管理:让规划器了解技能之间的依赖关系,生成更合理的执行顺序
  2. 技能能力评估:在元数据中加入技能的能力范围和限制,帮助规划器做出更准确的判断
  3. 动态技能加载:根据任务类型动态加载相关技能,减少规划 Prompt 的长度
  4. 技能使用统计:记录每个技能的使用频率和成功率,用于后续优化


总结

这个看似微小的改动,实际上解决了 Agent 开发中一个非常普遍的架构问题。绝大多数开源 Agent 项目都采用了 "先拆后配" 的模式,导致规划器无法充分利用系统的能力。

通过让规划器 "看见" 所有可用技能,我们不仅提升了 buddyMe 框架的执行效率和任务质量,也为后续的技能生态建设打下了坚实的基础。这也再次印证了工程开发中的一个真理:好的架构不是一蹴而就的,而是在解决实际问题的过程中不断演进的。


一句话引用:技术深度:buddyMe 框架任务拆解的 "盲拆" 问题与技能感知优化方案