CodeGym /课程 /ChatGPT Apps /多步流程:由模型自动编排与循环控制

多步流程:由模型自动编排与循环控制

ChatGPT Apps
第 12 级 , 课程 3
可用

1. 什么是多步 run,它与“一次性”请求有何不同

当你只使用 ChatGPT App 和 MCP tools 时,交互通常很线性:用户发来请求 → GPT 决定调用一个或多个工具 → 你把结果回复给用户。即便工具内部做了更复杂的事,这依然可以视为“一个逻辑步骤”。

对于代理来说,run目标 + 一系列步骤。我们不再以“一个提示 — 一个回答”的方式思考,而是把任务视为一个由代理从头到尾推进的小型项目。

可以这样理解差异:

交互类型 模型做什么 逻辑在哪里
ChatGPT App 中的普通工具调用 判断是否调用工具、填充参数,并基于结果生成回答 主要的业务逻辑与动作顺序在一个工具或后端中
代理式 run(Agents SDK) 规划多个步骤,决定何时调用哪个 tool,分析中间结果,并可能修订计划 “如何朝目标前进”的逻辑部分在代理的 system 指令里,部分由模型在运行中生成

这里有个关键点:你不必把规划完全交给模型。通常是混合式:你把场景的主要阶段硬编码(例如“先收集需求,再挑选礼物,再准备卡片”),而在每个阶段内部,允许代理较自由地使用其工具。

小类比

一次性工具调用就像叫个快递:“取一份文件送到办公室。”

多步代理 run 则像个人助理:“帮我为同事准备生日礼物:了解他的喜好,挑几种方案,确认配送,并整理成一个漂亮的展示。”助理会自行决定沿途该做哪些动作。

稍后在本讲中,我们还会看看这些多步 run 如何嵌入你熟悉的 Apps SDK → MCP → 后端的栈中,让对 ChatGPT 和小部件而言,代理逻辑看起来就像一个普通而整洁的 MCP 工具。

2. 模型如何自行规划步骤:宏观视角

以 Agents SDK 的术语来说,把每个 run 想成三元组很方便:

  1. Goal(目标):以文本描述任务,进入代理的 system/user 指令。
  2. Tools:带有良好描述和 JSON Schema 的可用工具集合。
  3. State:你在外部维护的步骤历史与结构化状态(数据库、Redis 等)。

接着 run 循环开始:模型查看目标与可用工具,并在每一步做出决定:

  • “我现在信息足够了——可以向用户给出最终结果”;
  • 或者“我需要调用工具 X 并传入这些参数”;
  • 或者“我拿到了工具结果,现在要解释、筛选,可能还要调用其他工具”。

在伪代码层面,思路大致如下(提醒:这是心智模型,不是真实 API):

while (!done && steps < MAX_STEPS) {
  const modelResponse = await callModel({
    system: agentPolicy,
    messages: history,
    tools,
  });

  if (modelResponse.type === "tool_call") {
    const toolResult = await callTool(modelResponse.toolName, modelResponse.args);
    history.push({ role: "tool", content: toolResult });
  } else {
    // 最终答案
    done = true;
    return modelResponse.content;
  }

  steps++;
}

在真实的 Agents SDK 中,这个循环已由库实现并“封装”起来。你以声明方式描述代理,SDK 就会反复调用模型与工具,直到拿到最终答案或触达步骤/时间限制。

架构师的任务在于:

  • 制定合理的 goal 与 system 指令,引导模型规划出合适的步骤;
  • 构建无语义重叠的工具集合;
  • 设置步骤与时间的限制;
  • 考虑哪些步骤可以并行。

当我们有了目标、工具与状态的表示,下一个问题是——要以哪些步骤走向目标。并非所有步骤都一样:有些严格顺序依赖,有些可以并行。

3. 顺序步骤与并行步骤

现在我们已经理解了代理的 run 循环,接下来要弄清楚流程中的步骤类型。在代理的 workflow 中,主要有两类:顺序与并行。

顺序步骤

当步骤 A 的结果对步骤 B 至关重要时。例如,在我们的教学项目 GiftGenius 中:

  1. 先明确礼物收件人:同事/亲友、年龄、兴趣等。
  2. 然后通过 tool search_gifts 挑选候选项。
  3. 接着按预算与约束进行筛选。
  4. 再为小部件把卡片精美化。
  5. 最后再考虑引导到结算(checkout)。

每个后续步骤都依赖前一步的数据,因此执行是严格顺序的。

在代理行为的伪“内部计划”中可能类似这样:

1. 询问用户关于收件人与预算的信息
2. 调用 tool search_gifts(profile, budget)
3. 调用 tool filter_by_constraints(gifts, constraints)
4. 生成最终列表与描述

模型并不会真的用代码写下这类清单,但我们可以通过 system 指令、对话示例与工具描述来引导它遵循类似结构。

并行步骤

有时步骤可以相互独立地执行。例如,我们想同时对比三个商店的礼物候选:

  • search_gifts_amazon
  • search_gifts_etsy
  • search_gifts_local_store

对代理而言,这是三个独立的工具调用,可以并行启动以缩短总体响应时间。

在 Agents SDK(以及现代代理框架)中,通常支持如果模型在同一步提出多个工具调用,就并行执行这些调用。典型场景:模型在回复中列出了调用清单,SDK 并发调用、收集结果,并把它们作为一组 tool 消息提供给模型的下一步。

从规划角度看大概是这样:

// 代理的一个步骤:模型决定调用三个工具
const calls = [
  { name: "search_gifts_amazon", args: {...} },
  { name: "search_gifts_etsy", args: {...} },
  { name: "search_gifts_local_store", args: {...} },
];

const results = await Promise.all(
  calls.map(c => callTool(c.name, c.args))
);

// 之后,所有结果会在进入下一步前作为一组 tool 消息加入上下文

如果你写过 JS/TS 前端,应该熟悉并行请求的思路:比如用 Promise.all 同时发起多个 fetch()。现在同样的理念出现在代理的 run 循环中,只不过对哪些动作可以并行的决定,更多由模型来做出。

4. GiftGenius 的 workflow 示例:步骤、目标与工具

在“顺序步骤”部分,我们已直观地把 GiftGenius 的行为拆成了阶段。现在把这个多步场景正式化为一个代理 workflow:描述目标、步骤,并把它们绑定到工具与代理配置上。先不绑定具体 Agents SDK 的 API,我们用结构化的描述与少量 TypeScript 伪代码来巩固理解。

目标(goal)

目标可以这样表述:

帮助用户为特定收礼人挑选 3–5 个礼物方案,考虑预算、场合与配送限制,并输出 GiftGenius 小部件所需的结构化礼物卡片列表。

主要步骤

给出一个最小的 4 步版本:

  1. 收集收件人上下文
    目标:收集被赠与者的信息(年龄、性别、兴趣、与赠与者关系),以及预算与日期。
    工具:可能完全不需要工具,纯模型 ↔ 用户的对话。
  2. 搜索与初筛礼物
    目标:获取“原始”的礼物候选集合。
    工具:search_gifts(profile, budget)——访问我们的目录/搜索系统并返回候选列表的 tool。
  3. 过滤与排序
    目标:剔除不合适的方案(无法配送到地区、超预算、不满足限制等),并按相关性排序。
    工具:filter_and_score_gifts(candidates, constraints)——纯函数、幂等的工具。
  4. 为小部件格式化结果
    目标:把数据整理为 UI 友好格式:标题、简述、图片、价格、CTA。
    工具:format_gift_cards(gifts)——既可为代码型工具(结构生成),也可为 LLM 工具(美化文案)。

在代理配置中可能是这样

设想我们有一个代理构造器(伪代码):

import { createAgent } from "@acme/agents-sdk";
import { tools } from "./gift-tools";

export const giftAgent = createAgent({
  name: "gift-guru",
  system: `
    你是 GiftGenius 代理,帮助挑选礼物。
    目标:给出 3–5 个可以真实购买的选项,
    并考虑收礼人画像、预算与配送限制。
    先澄清关键细节,再使用搜索与筛选工具。
    如果还不知道预算或关键兴趣点,不要调用工具。
    当你有清晰的礼物卡片列表时再结束工作。
  `,
  tools, // 这里会有 search_gifts、filter_and_score_gifts、format_gift_cards
  maxSteps: 12,
  timeoutMs: 15000,
});

请注意以下几点:

  • 在 system 指令中我们明确要求代理先澄清细节,再调用搜索工具。这能减少模型在上下文过于模糊时就过早调用工具的风险。
  • 我们限制了 maxSteps,以防代理陷入无限循环。
  • timeoutMs 是为了避免整个 run 花费用户半天时间。

5. 由模型自动编排:哪些交给模型,哪些要硬性固定

代理是在模型自由度与你设定的硬性结构之间寻求平衡。

如果给模型太多自由、边界不清,你会得到“创造性混乱”:多余的 tool 调用、重复步骤、隐性循环。反之,如果把一切都在后端按有限状态机硬编码,模型就沦为文本装饰器,而非智能的任务执行者。

通常交给模型的内容

GiftGenius 等类似场景中,合理交给模型:

  • 向用户提问的表述(如何澄清兴趣、如何恰当地询问预算);
  • 判断何时信息已足够,可以启动搜索;
  • 在同一阶段内决定用哪些具体工具(例如有多个商店搜索工具时选择其一);
  • 生成描述文案、说明与对比文字。

最好硬性固定的内容

与此同时,建议预先固定:

  • 场景的大阶段(“信息收集” → “搜索” → “筛选” → “格式化” → “完成”);
  • 步骤与时间的限制;
  • 在某些条件下代理必须“停下”并坦诚告知用户任务不可解(例如预算 5 美元却要明天送达的昂贵电子产品);
  • 工具的幂等策略与重试策略。

混合示例:用阶段当状态,细节交由模型

可以在代理的 state 中设置一个 phase 字段,它取值为 "collect_profile" | "search" | "filter" | "format" | "done"。这样你的后端(或 Agents SDK 本身,如果支持自定义状态机)就能控制在不同阶段开放的工具集合。

伪代码:

type Phase = "collect_profile" | "search" | "filter" | "format" | "done";

interface GiftAgentState {
  phase: Phase;
  profile?: UserProfile;
  candidates?: GiftCandidate[];
  finalGifts?: GiftCard[];
}

可以在代理的 system 指令里加入对阶段的简述,而你在代码中根据当前阶段限制暴露给模型的工具清单。这就是 tool gating 的例子,关于 workflow 的模块里会更详细展开。

6. 控制无限循环与无用重复

如果任由代理在 run 循环中自由发挥,它迟早会像临近 ddl 的学生那样:无止境地“再确认与重写”,就是不交差。我们的目标是防止它挂起。

常见的三类循环来源:

  1. 模型对答案不自信,持续用微小改动反复向同一工具发起请求。
  2. 工具稳定返回错误或空结果,而代理执意“再试一次”。
  3. 代理在两个工具之间来回震荡,调用这个又调用那个,却不向最终答案推进。

步骤上限(maxSteps)

最简单且必不可少的机制是限制步骤数量。在多数 Agents SDK 实现中,你可在启动 run 或代理配置中指定 maxSteps。一旦触达上限,SDK 会以特殊状态结束 run(例如 aborted_by_max_steps)。之后由你决定如何告知用户。

GiftGenius 中,我们可以认为一次合理的礼物挑选可在约 10 步内完成(若干澄清、几次搜索、一次筛选与格式化)。可设置 12–15 步并留出余量,并妥善处理触达上限的情况:

const run = await giftAgent.run({
  input: userGoal,
  maxSteps: 12, // 覆盖默认值
});

if (run.status === "max_steps_exceeded") {
  // 向用户展示坦诚的提示
}

时间上限(timeout)

有时问题不在步骤数量,而在总时长。工具可能较慢、网络可能不稳。因此既要在单个 tool 调用层面,也要在整个 run 层面设置 timeoutMs

例如,你可以规定:

  • 每次外部 API 调用(向伙伴搜索礼物)不应超过 3–5 秒;
  • 整个礼物挑选的 run 应控制在 15 秒内。

若触发了超时,你应温和地结束 run,可以向用户展示部分结果,并坦诚说明“部分来源未及时响应”。

重复调用检测

更进阶(但很有价值)的模式是检测以相同参数重复调用的工具。如果你发现代理连续三次调用了相同参数的 search_gifts(profile, budget),这就说明它卡住了。

你可以在 state 中为键 (toolName, argsHash) 维护一个计数器,当计数超过阈值时,选择:

  • 中止 run 并返回清晰的错误;
  • 或向模型追加指令:“你已三次用相同参数调用该工具,请改变策略或向用户发问”。

伪代码:

function shouldAbortToolCall(toolName: string, args: unknown, state: GiftAgentState) {
  const key = `${toolName}:${hashArgs(args)}`;
  const count = state.toolCallCounts[key] ?? 0;

  if (count >= 3) return true;

  state.toolCallCounts[key] = count + 1;
  return false;
}

这里的 hashArgs 可以是任意确定性参数序列化函数(例如带键排序的 JSON.stringify)。

7. 明确的任务完成标准

“玩具级”代理和生产级代理的关键差异之一是是否有清晰的完成标准。如果没有,模型可能过早收工(“给点礼物,剩下你们自己看”),或相反地无休止“打磨结果”。

GiftGenius 中,可以设定简单规则:

  • 当代理拥有 3 至 5 个礼物,且字段: idtitleshortDescriptionpriceimageUrlpurchaseUrl 均已填充,并通过了预算与配送过滤,即可结束。
  • 若在最多 N 次搜索与筛选后仍少于 3 个合格礼物,代理应坦诚告知无合适结果,并建议提升预算或放宽限制。

这些标准既可写进代理的 system 指令,也可在 run 结束后由你的代码进行校验。

示例:在 run 结束后校验结果:

if (run.status === "completed") {
  const gifts = run.output.gifts; // 假设我们的代理返回结构化 JSON

  if (!gifts || gifts.length < 3) {
    // 代理“完成”了,但结果不理想——你可以:
    // 1) 展示坦诚说明,
    // 2) 建议用户调整条件。
  } else {
    // 一切正常——展示礼物小部件
  }
}

不要寄希望于模型“魔法般理解业务成功”。作为开发者,你需要显式制定“足够好”的条件并进行校验。

8. 编排究竟落在哪里:代理、后端、小部件

前面提到,编排可以存在于不同层次:代理、后端、小部件。

就多步流程而言,逻辑大致如下。

代理(Agents SDK)负责“思维层”的 workflow:

  • 如何把目标拆成步骤;
  • 以何种顺序调用哪些工具;
  • 向用户追加哪些问题。

后端(Backend)通常负责:

  • 工具实现(搜索、过滤、交易等);
  • 状态与检查点的存储;
  • 硬性的业务限制(预算上限、权限、地域可用性)。

小部件(Apps SDK)负责:

  • 进度显示(步骤条、进度条,“第 2/4 步”);
  • 输入表单;
  • UX 细节,例如数据未完整时禁用按钮。

一个好的思路是:代理导演工具与对话的工作流,UI 小部件导演用户的可视化体验。二者通过结构化数据(ToolOutput、agent run output)进行协作。

9. 最小代码示例:从 MCP 工具启动 GiftGenius 多步代理

现在,按照开头的承诺,把新概念与 Apps SDK → MCP → 后端的栈打通,示例展示 MCP 工具如何调用代理的 run

设想在你的 app/mcp/route.ts 中有一个 tool run_gift_workflow,它:

  • 接收用户的文本请求(他的目标);
  • 启动代理 giftAgent
  • 返回供小部件使用的结构化结果。

代码做了简化,仅用于说明这层连接关系:

// app/mcp/route.ts
import { server } from "@modelcontextprotocol/sdk/server";
import { z } from "zod";
import { giftAgent } from "@/agents/giftAgent";

server.registerTool(
  "run_gift_workflow",
  {
    title: "挑选礼物",
    description: "启动多步礼物挑选代理",
    inputSchema: {
      userGoal: z
        .string()
        .describe("用户的任务,例如:想给同事选一份不超过 $50 的礼物"),
    },
  },
  async ({ userGoal }) => { 		
    const run = await giftAgent.run({		// 在这里以 12 步与 15 秒超时启动代理
      input: userGoal,
      maxSteps: 12,
      timeoutMs: 15000,
    });

    return {
      status: run.status,
      gifts: run.output?.gifts ?? [],
      debug: run.debugInfo, // 之后可以去掉
    };
  }
);

接下来 ChatGPT App 可以像调用其他 MCP 工具一样调用它,而你的小部件 GiftGenius 则基于 gifts 构建 UI。对外部的 ChatGPT 而言,一切看起来像一个整洁的单一 tool,但你在“幕后”获得了多步 workflow。

10. 设计多步流程的常见错误

错误 1:“让模型自己搞定,我把所有工具都给它就行。”
当代理能访问十来个语义重叠的 tools 且缺少清晰的 system 指令与阶段划分时,模型会四处乱撞:用不同方式重复调用、重复请求并进入循环。最好花时间做设计:把场景拆成阶段,限制各阶段可用工具清单,并在 system 提示中明确策略。

错误 2:缺少步骤与时间上限。
如果不设置 maxStepstimeout,在生产环境很快就会出现“游走的”run,既耗资源又不给用户反馈。上限不是“可选项”,而是基本卫生。同时要合理处理超限情形,而非直接无提示 500。

错误 3:没有明确的完成标准。
模型会在它觉得“够了”时结束 run,但它的“够了”常与业务要求相距甚远。若不形式化成功标准(礼物数量、字段要求、通过哪些过滤)并进行校验,你会得到不稳定的体验:今天 5 个很棒的方案,明天 1 个一般般再加三个重复项。

错误 4:不跟踪重复的工具调用。
代理可能卡在“收到错误 → 把请求改两个字 → 又调用同一工具”的循环中。若你不按 (toolName, args) 跟踪重复调用,这些循环会在你查看日志前都不可见。简单的计数器与参数哈希能大幅改观。

错误 5:把编排与业务实现混在一个工具里。
有时会尝试把整个 workflow 藏进一个 MCP 工具或函数里:搜索、过滤、格式化与决策全部打包。结果代理失去意义——模型无法按步骤掌控过程,你也失去了透明度与复用性。更好的做法是把阶段拆成独立 tools,并让代理做组合。

错误 6:缺少与状态/检查点的连接。
没有中间状态与检查点的多步流程会变得脆弱:一旦中途出错,用户只能从头再来。对需要在步骤间往返或隔段时间返回的场景尤为致命。使用状态存储,持久化阶段、画像与候选项,让代理能从正确位置继续。

错误 7:忽视 UX 层。
有时开发者沉浸于代理的内部 workflow,而忘了用户只看到小部件与聊天消息。如果 UI 中没有清晰的进度、状态“正在搜索礼物…”、“正在筛选方案…”,用户会以为 App “卡住了”或“什么都没做”,即使代理正在编排复杂流程。规划多步 run 时,同时考虑它在界面上的呈现。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION