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 想成三元组很方便:
- Goal(目标):以文本描述任务,进入代理的 system/user 指令。
- Tools:带有良好描述和 JSON Schema 的可用工具集合。
- 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 中:
- 先明确礼物收件人:同事/亲友、年龄、兴趣等。
- 然后通过 tool search_gifts 挑选候选项。
- 接着按预算与约束进行筛选。
- 再为小部件把卡片精美化。
- 最后再考虑引导到结算(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 步版本:
- 收集收件人上下文
目标:收集被赠与者的信息(年龄、性别、兴趣、与赠与者关系),以及预算与日期。
工具:可能完全不需要工具,纯模型 ↔ 用户的对话。 - 搜索与初筛礼物
目标:获取“原始”的礼物候选集合。
工具:search_gifts(profile, budget)——访问我们的目录/搜索系统并返回候选列表的 tool。 - 过滤与排序
目标:剔除不合适的方案(无法配送到地区、超预算、不满足限制等),并按相关性排序。
工具:filter_and_score_gifts(candidates, constraints)——纯函数、幂等的工具。 - 为小部件格式化结果
目标:把数据整理为 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 的学生那样:无止境地“再确认与重写”,就是不交差。我们的目标是防止它挂起。
常见的三类循环来源:
- 模型对答案不自信,持续用微小改动反复向同一工具发起请求。
- 工具稳定返回错误或空结果,而代理执意“再试一次”。
- 代理在两个工具之间来回震荡,调用这个又调用那个,却不向最终答案推进。
步骤上限(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 个礼物,且字段: id、title、shortDescription、price、imageUrl、purchaseUrl 均已填充,并通过了预算与配送过滤,即可结束。
- 若在最多 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:缺少步骤与时间上限。
如果不设置 maxSteps 与 timeout,在生产环境很快就会出现“游走的”run,既耗资源又不给用户反馈。上限不是“可选项”,而是基本卫生。同时要合理处理超限情形,而非直接无提示 500。
错误 3:没有明确的完成标准。
模型会在它觉得“够了”时结束 run,但它的“够了”常与业务要求相距甚远。若不形式化成功标准(礼物数量、字段要求、通过哪些过滤)并进行校验,你会得到不稳定的体验:今天 5 个很棒的方案,明天 1 个一般般再加三个重复项。
错误 4:不跟踪重复的工具调用。
代理可能卡在“收到错误 → 把请求改两个字 → 又调用同一工具”的循环中。若你不按 (toolName, args) 跟踪重复调用,这些循环会在你查看日志前都不可见。简单的计数器与参数哈希能大幅改观。
错误 5:把编排与业务实现混在一个工具里。
有时会尝试把整个 workflow 藏进一个 MCP 工具或函数里:搜索、过滤、格式化与决策全部打包。结果代理失去意义——模型无法按步骤掌控过程,你也失去了透明度与复用性。更好的做法是把阶段拆成独立 tools,并让代理做组合。
错误 6:缺少与状态/检查点的连接。
没有中间状态与检查点的多步流程会变得脆弱:一旦中途出错,用户只能从头再来。对需要在步骤间往返或隔段时间返回的场景尤为致命。使用状态存储,持久化阶段、画像与候选项,让代理能从正确位置继续。
错误 7:忽视 UX 层。
有时开发者沉浸于代理的内部 workflow,而忘了用户只看到小部件与聊天消息。如果 UI 中没有清晰的进度、状态“正在搜索礼物…”、“正在筛选方案…”,用户会以为 App “卡住了”或“什么都没做”,即使代理正在编排复杂流程。规划多步 run 时,同时考虑它在界面上的呈现。
GO TO FULL VERSION