头图:规划、执行、评审三角色与反思回流

前几篇我们一直在打磨"单个 Agent":01 篇搭好统一的 LLM 底座与重试策略,02 篇补上对话记忆,03 篇让模型学会调工具,04 篇用检索增强让它有据可答。但真实任务往往一句话里塞了三件事——“调研竞品,输出对比报告,给出落地建议”。让一个 Agent 一口气全做完,中间步骤会被压缩,错了也不知道错在哪。这一篇把镜头拉远:在 HarmonyOS 7 上用三个各司其职的角色——规划、执行、评审——把"任务分解 + 自我反思"跑成一条可测试的流水线。

先把边界划清楚:本篇讲的是单设备上的多角色任务分解与反思,不是跨设备的多智能体协商。系列后面讲 A2A 时,智能体之间会对等交换意图、互相协商;这里的三种角色只是同一个应用里的三段纯逻辑,彼此不通信、不协商,只有编排器单向调度它们。这个区分在架构评审里经常被追问,值得先讲明白。

一、为什么"分解 + 反思"是 Agent 的基本功

大模型直接面对复杂目标的典型症状是"一步到位":想一次性给出最终答案,过程不可见,结果不可纠。把目标拆成子任务再逐个执行,有两个直接收益:

  1. 可观测:每一步的输入输出都是独立字符串,哪个子任务出了问题一目了然,页面可以逐条渲染;
  2. 可纠错:评审角色对结果打分,不满意就把反馈回灌给规划器重新拆解——这就是"反思",也是本篇标题里"反思"二字的来源。

拿开头那个目标举例。直连模式下,模型被要求一次输出"竞品调研 + 对比报告 + 落地建议",往往前两部分写得认真,第三部分开始敷衍,而且你没法只重做第三部分。分解之后,三个子任务各自独立执行、独立留痕,评审发现"落地建议"太薄,可以精准地只对它重规划。粒度带来的不只是质量,还有可运维性——这是把"大任务"变"小任务"之后最容易被低估的收益。

本篇的三个角色全部是确定性纯函数:不联网、不依赖设备、没有隐藏状态。这带来一个很实际的工程好处——同样的逻辑可以在 Node 里镜像跑断言做预检,也可以在 DevEco 里用 Hypium 做回归,两边结论互为印证。这是纯逻辑分层从 01 篇延续下来的红利。

二、三个角色,各管一段

架构:规划到执行到评审的有界循环

PlannerAgent(规划):decompose(goal) 把目标按中英文标点(,。、;;,)切成子任务;整句里一个分隔符都没有时,回退到固定三步模板,保证任何输入都有一条可执行的计划。

export interface SubTask {
  id: number;
  description: string;
}

const FALLBACK_TEMPLATE: string[] = [
  '明确目标的边界与成功标准',
  '拆解为核心可执行步骤',
  '执行并汇总自检结果'
];

export class PlannerAgent {
  decompose(goal: string): SubTask[] {
    const descs: string[] = this.hasDelimiter(goal)
      ? this.splitGoal(goal)
      : FALLBACK_TEMPLATE.slice();
    const finalDescs: string[] = descs.length > 0 ? descs : FALLBACK_TEMPLATE.slice();
    const tasks: SubTask[] = [];
    for (let i = 0; i < finalDescs.length; i++) {
      tasks.push({ id: i + 1, description: finalDescs[i] });
    }
    return tasks;
  }
  // hasDelimiter:目标中是否出现过任意分隔符
  // splitGoal:逐字符扫描,遇分隔符收尾一段,空段丢弃,首尾空白去除
}

注意 hasDelimiter 的判断维度:目标里是否出现过分隔符,而不是"切完剩几段"。一句没有标点的话切完只有一段,但它需要的不是单步执行,而是模板兜底的三步走。这个细节曾让首版实现把"做个年度计划"拆成了一步,单测立刻抓住了。

WorkerAgent(执行):execute(subtask, context) 对单个子任务产出一段结果。演示里返回确定性的 完成[编号]: 描述 -> 产出编号,真实项目把这一行换成工具调用或模型推理即可,编排层完全不用改。

CriticAgent(评审):review(goal, results) 用启发式打分——每条结果非空且长度达到 MIN_LEN(本例 8 字符)记为达标,全部达标给满分 1.0;否则按达标比例给分,低于 0.6 触发 needsReplan。完整实现如下:

export class CriticAgent {
  review(goal: string, results: string[]): CriticVerdict {
    if (results.length === 0) {
      return { score: 0, needsReplan: true, feedback: '没有任何执行结果,需要重规划' };
    }
    let okCount: number = 0;
    for (const r of results) {
      if (r.trim().length >= MIN_LEN) {
        okCount++;
      }
    }
    if (okCount === results.length) {
      return { score: 1.0, needsReplan: false, feedback: '全部子任务产出达标' };
    }
    const score: number = okCount / results.length;
    return {
      score: score,
      needsReplan: score < REPLAN_THRESHOLD,
      feedback: score < REPLAN_THRESHOLD ? '部分产出不足,建议重规划' : '大部分达标,可交付'
    };
  }
}

分数不是重点,feedback 才是:它会被回灌给规划器,成为下一轮拆解的输入。评审器的判决结构(分数、是否重规划、反馈文本)是一个纯数据接口,今天用启发式实现,明天换成大模型评审,编排器一行代码都不用动。

三、Orchestrator:把三段串成有界循环

export class Orchestrator {
  run(goal: string, maxReplans: number = 1): OrchestratorResult {
    let plan: SubTask[] = this.planner.decompose(goal);
    let results: string[] = this.worker.executeAll(plan, '');
    let verdict: CriticVerdict = this.critic.review(goal, results);
    let replanned: boolean = false;
    let replanCount: number = 0;

    while (verdict.needsReplan && replanCount < maxReplans) {
      replanned = true;
      replanCount++;
      const newGoal: string = goal + ';反思反馈:' + verdict.feedback;
      plan = this.planner.decompose(newGoal);
      results = this.worker.executeAll(plan, verdict.feedback);
      verdict = this.critic.review(goal, results);
    }
    return { plan, results, score: verdict.score, replanned };
  }
}

三个关键设计:

  • 终止性:replanCount < maxReplans 是循环唯一的继续条件,评审器再怎么打低分,循环也最多转 maxReplans 圈。只要这一条在,编排器就不可能死循环;
  • 反馈回灌:重规划时把 feedback 追加到目标字符串里,规划器下一轮"看得见"上次差在哪,这就是反思闭环的物理载体;
  • 依赖注入:三个角色都从构造参数传入,缺省才用默认实现。单测时可以注入一个永远打低分的 mock 评审器,精确验证"恰好重规划一次后收敛",也能验证 maxReplans=3 时评审恰好被调用 4 次。

四、在 DevEco 里跑起来

演示页只做一件事:输入目标,点"运行编排",看到计划、逐条产出、评审分、是否发生过重规划。页面同时接了一个 MockLlmClient——继承 01 篇的 LlmClient 底座,超时、重试与响应解析照常复用,只是不联网:

export class MockLlmClient extends LlmClient {
  protected async sendOnce(_req: ChatRequest): Promise<string> {
    return JSON.stringify({
      choices: [{ message: { content: '(演示)已基于编排给出结论' }, finish_reason: 'stop' }]
    });
  }
  // buildPayload / buildHeaders / classifyError 按底座契约同样实现
}

它把编排结论转成一句"LLM 旁注",证明大模型通道随时可以接进来。真实项目里换成 LlmClientFactory.create('openai', {...}) 即可,页面与编排层的代码零改动。

const orch: Orchestrator = new Orchestrator();
const res = orch.run(this.goal, 1);
this.planLines = res.plan.map((t): string => t.id + '. ' + t.description);
this.resultLines = res.results;
this.replanText = res.replanned ? '本次发生了重规划' : '一次通过,未触发重规划';

验证分两层。Node 镜像层跑 22 条断言:分隔符切分计数、无分隔符走模板、空目标兜底、好评分不重规划、低分恰好一次重规划、maxReplans=3 时评审恰好 4 次后终止、聚合结果与计划一一对应。Hypium 层共 29 个用例,覆盖同一批场景外加底座回归(RetryPolicy 分类与退避、ResponseParser 解析、ConversationStore 裁剪、Agent 编排、ChatUIMessage 映射)。两层都绿,才敢说这条流水线是稳的。

"恰好一次重规划"这条最值得展开,它靠注入 mock 评审器实现:

class LowScoreCritic extends CriticAgent {
  calls: number = 0;
  review(goal: string, results: string[]): CriticVerdict {
    this.calls++;
    return { score: 0.3, needsReplan: true, feedback: 'mock 不达标' };
  }
}

it('低分:恰好发生一次重规划后终止', 0, () => {
  const critic = new LowScoreCritic();
  const res = new Orchestrator({ critic }).run('目标一,目标二。目标三', 1);
  expect(critic.calls).assertEqual(2); // 初评 + 重规划后复评,恰好一轮
  expect(res.replanned).assertTrue();
});

评审器被调用两次,意味着初评一次、重规划后复评一次,然后循环条件 replanCount < maxReplans 失败退出。终止性不是靠"希望它停下来",而是靠一个可以被断言的结构性质——这是编排器区别于"随手写个 while"的地方。

运行效果截图:
在这里插入图片描述

五、从演示到生产:把 Worker 换成真活

演示版的 Worker 返回占位字符串,迁到生产只需三步,方法签名一律不动:

  1. 接入真实执行:在 execute 里调用 LlmClient.chat 或某个工具,把子任务描述组装进提示词;
  2. 串起上下文:context 参数此时派上用场——把前序子任务的产出拼接进去,子任务之间就有了依赖链,不再是孤岛;
  3. 升级评审:把启发式的 MIN_LEN 换成大模型评审,让它输出结构化的分数与反馈,判决接口 CriticVerdict 保持不变。

唯一要留意的结构性变化:Worker 变成异步后,run 也随之变成 async,但循环上界 maxReplans 的写法原样保留。换句话说,无论三个角色内部多复杂,"有界反思循环"这个骨架是整个编排器的不变量——改血肉,别改骨架。

六、避坑指南

避坑:四个常见雷区

结合源码,四个最容易踩的雷:

  1. 重规划死循环:忘了 maxReplans 上界,评审器持续打低分时页面直接卡死。循环条件必须是 replanCount < maxReplans,且计数器在进入循环体时自增,别放在循环末尾;
  2. 反馈空泛:feedback 写"结果不好,重新想想",规划器收到的等于没有。哪怕启发式评审,也要输出"哪几条不达标、差在哪"级别的具体描述,LLM 评审时同理;
  3. 子任务爆炸:用户目标里有十个逗号就会拆出十步。工程上应给单条计划设上限(比如 8 步),超出就合并相邻步骤,否则执行成本和 UI 都撑不住;
  4. 评审无标准:MIN_LEN 与 0.6 阈值要和业务对齐。阈值定 0.9,两步里错一步就永远重规划;定 0.3,评审形同虚设。标准先于代码定下来,写进测试。

还有一条工程习惯值得坚持:三个角色保持纯函数。别在 Planner 里偷偷发请求,别在 Critic 里读数据库。纯函数才能在 Node 里秒级镜像、在 Hypium 里稳定回归——这也是本系列一以贯之的分层原则:底座、编排、UI 各管各的,谁也不越界。

六、和"多智能体协商"的区别,再强调一次

维度本篇:任务分解与反思系列后续:A2A 多智能体协商
角色关系编排器单向调度三段逻辑智能体对等协商
通信方式同进程函数调用跨设备、跨进程消息
终止条件maxReplans 数值上界协商轮次或共识达成
典型场景拆解、执行、自检意图交换、能力发现

一句话概括:本篇的三个角色是同一张流程图上的三站,协商场景里的多个智能体是各自流程图的主人。搜"多 Agent"资料时两者经常混在一起讲,动手前先问自己要的是哪一种。


参考与出处

  • HarmonyOS 开发者官网主页:https://developer.huawei.com/consumer/cn/doc/harmonyos-home
  • ArkTS 语言介绍(官方指南):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-overview
  • UIAbility 与页面生命周期(官方参考):https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-app-ability-uiability
  • ArkUI 声明式开发范式(官方指南):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-ui-development
  • 应用测试框架 Hypium(官方指南):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/application-test-framework

最后一句:让规划的有章法、执行的有产出、评审的有标准,Agent 才算真正学会了"想清楚再做,做完再想"。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐