1. 代理到底是什么,以及为什么需要它
代理并不是 ChatGPT App 的必需部分:不使用 LLM 代理,你也可以创建很多很棒的应用。不过,我有三条充分的理由向你介绍它们。
代理是为你的应用后端添加智能的极佳方式。比如智能礼物推荐、解析用户的文字偏好;又如复杂的检索、分析、处理与总结场景——用 LLM 代理都能很轻松地实现。
ChatGPT 发布了自家的 AgentsSDK,支持 TS 与 Python。它非常好用,自带代理编排。面对一个复杂任务,不再只是一个代理在工作,而是一个团队。这是非常有前景的方向。
同时,它还有教学意义。ChatGPT 调用 mcp-tools 的方式,与 LLM 代理调用自己的 tools 的方式完全一致。一旦你理解了 LLM 代理的工作机制,你就会明白如何在应用中把状态机放到模型侧。此外,学习 AgentsSDK 还能帮助你理解将来 ChatGPT SDK 的工作方式。
那就开始吧。
什么是 LLM 代理
如果说 ChatGPT App 是你在 ChatGPT 内的服务“前台”,而 MCP 服务器是携带工具和业务逻辑的“引擎”,那么代理就好比一个聪明的调度员,能够:
- 读懂目标;
- 自行决定调用哪些工具以及顺序;
- 必要时询问补充信息;
- 在出现错误时重试步骤;
- 最终达成清晰的结果。
用接近 Agents SDK 官方文档的表述,代理就是一个程序。它访问 LLM 和一组工具,能自主规划实现目标的步骤,并且通过 tool‑calls 执行这些步骤。
与现有内容类比:
- 在普通的 ChatGPT App 中,ChatGPT 模型会直接编排对你 MCP 工具的调用。
- 后端的 LLM 代理同样具备任务描述和一组工具,它会自行决定调用哪些 tools、走多少步、何时停止,以及返回何种结果。
在我们的 GiftGenius 场景中,这可能是这样的:
- 没有代理的应用: 模型直接调用 searchGifts,然后是 filterByBudget,接着是 getDetails——每次都要重新思考。
- 有代理的应用: ChatGPT 调用 mcp‑tool,而后端给代理下达任务:“为该画像找出前 5 个礼物。” 代理会走若干步骤:收集补充信息,调用不同的搜索工具,筛选、排序,构建最终卡片,并返回结构化的成品答案。
ChatGPT 与后端的 LLM 代理就像公司老板与员工。ChatGPT 拥有更大的自由度:它与用户对话,并决定要发起哪些战略性任务(调用 mcp 工具)。而 LLM 代理仅在后端工作,不直接与用户交互,但它也会“思考”,并可调用自己的 tool。可以理解为“迷你版 ChatGPT”。
2. 代理由哪些部分构成:LLM、指令、工具与状态
把代理看成若干层会更容易理解。
首先,底层依然是 LLM。可以是 GPT‑5.1 或 Agents SDK 使用的其他模型。它负责生成文本、规划步骤、选择工具——总之在你的编排上下文中负责“思考”。
其次,模型之上是指令。这是代理的 system 提示词,规定它的角色、边界、交流风格、工具的使用方式。你在 ChatGPT App 中已经做过类似的事,不过现在这是针对一个独立代理来设置的。
第三,是一组代理工具。可以是:
- TypeScript 函数(经典的 function calling);
- HTTP/REST 工具;
- 对你 MCP 工具的封装,以便代理能访问与 ChatGPT App 相同的后端;
- OpenAI 自带的“托管”工具(例如,如果你启用的话,Web 搜索)。
最后,是状态与步骤的工作规则:会话状态(session state)如何存储、如何保存中间结果、如何限制循环。更深入的内容我们会放在下一讲(关于记忆与状态),但此刻你应牢记:代理不是“一次性请求”,而是一个可能会保存进度的长流程。
如果从 TypeScript 开发者的视角看,脑海中会浮现出类似下面这样的对象(伪代码,接近 Agents SDK TS):
const giftAgent = new Agent({
model: "gpt-5.1",
systemPrompt: giftAgentPrompt,
tools: { searchGifts, filterGifts, checkoutDraft },
// 此处还可以配置内存、步数上限等
});
这里先不深入具体 API,关键是这个概念:模型、指令、工具与行为配置集中在一个地方。
3. 消息角色:在代理世界中的 system / user / assistant / tool
你已经熟悉 Chat Completions 中经典的 system、user、assistant、tool 角色。在 Agents SDK 里它们依然存在,但含义更贴近实操。
system 角色定义了代理的个性与使命。比如对 GiftGenius 代理,它可以是:“你是一名礼物推荐代理。你的任务是在尽量少的步骤内,基于收礼人画像和预算,挑选出 3–7 个合适选项,然后为小部件准备结构化 JSON。”你也会在这里写明限制:不该做什么(例如,未经单独确认不要执行真实购买)以及该如何使用工具。
在代理语境中,user 并不一定是“活生生的人”。更多情况下,它是给代理的“任务”:由你的 App、服务或其他代理形成的目标。例如,ChatGPT App 可以用 user 消息来调用代理:“为一位同事开发者挑选 5 个礼物,预算 50 美元,场合是生日。”
assistant 表示代理内部模型“说的话”。这里既可能包含中间的推理与计划,也可能是最终答案。你的目标是配置好 system 提示词,让这些消息有用,并在需要时可以记录日志。
tool(或对应 SDK 中的类似角色)描述工具调用的结果:“通过 MCP 找到 50 个商品”、“API 返回超时错误”、“数据库返回了用户画像”。这些消息与 assistant 消息一起构成代理 run‑循环的历史。
我们可以把它们汇总成一张小表:
| 角色 | 说话者 | GiftGenius 场景中的示例 |
|---|---|---|
|
你(代理的开发者) | “你是礼物推荐代理……” |
|
外部调用(App、其他代理) | “挑选 5 个不超过 50 美元的礼物……” |
|
代理内部的模型 | “计划:1)询问细节……” |
|
被调用工具的结果 | “searchGifts 返回了 20 个选项……” |
这套结构很重要,因为它正是构建run‑循环的基础——也是本讲的主角。
4. LLM 如何调用你后端的函数
当你习惯了“问答模式”,会觉得 LLM 的工作流程很简单:输入文本 → 模型输出文本。实际上底层比这复杂一些,这也正是 function calling 能成立的原因。
模型拿到的不是一个问题,而是一串消息——对话历史。其中包含所有过往的发言:system 指令(“你是谁、能/不能做什么”)、你的消息、模型此前的回答、工具的结果。每一步,模型都会把整条对话时间线当作聊天日志来观察,并决定:“我应该在末尾追加哪条下一条消息?”。
这就是关键点:LLM 始终只做一个动作——在历史末尾追加下一条消息。它不会“修改过去”,而是仅仅继续列表。你写一个问题,模型回答;你再追加第二个问题,模型仍会结合完整对话历史(所有消息)来回答。
Function calling 也是基于同样的原则。模型不会“直接启动函数”,而是会:
- 在对话历史中看到可用的工具/tools及其描述;
- 做出决定:“现在先别直接回文本,先调用某个工具更合理。”;
- 并将下一条消息追加为一种特殊消息: “我想调用某个 tool,参数如下。”
接下来就不是模型了,而是你的后端读取这条历史末尾的新消息,理解到这是函数调用请求,于是实际调用目标工具。然后再向历史中追加另一条消息——工具的结果,并再次把完整消息列表发给模型。模型再看一遍整条时间线,继续追加下一步:要么再发起一次调用,要么产出最终的可读答案。
也就是说:
- 对于普通 Q&A:“下一条消息”= 文本回答;
- 对于 function calling:“下一条消息”= 调用函数的指令 或 使用函数后的回答。
并不存在什么单独的“调用函数的魔法指令”——它只是模型在链尾追加的一种特殊消息而已。
模型不会通过公共 API 调用你后端的函数。它只是“在聊天中写下”要调用某个函数以及参数;真正的函数调用由你的后端完成,并把结果写回聊天。然后流程再次开始。
5. 代理的 run‑循环:它如何一步一步“思考”
实际上,LLM 代理就是你服务器上的一个对象/算法,它会运行代理的 run‑循环——这是“问题 → 思考 → 可能执行动作 → 再思考 → … → 最终回答”的扩展循环。在 OpenAI 文档中,这有时被称为agent loop,或 ReAct 模式(Reason + Act + Observe)。
从概念层面来看,一个代理的单次 run 大致如下:
- 代理接收输入:system 指令、任务(user 消息),以及(可选)当前状态。
- 模型生成一步:可能是文本回答,也可能是计划与调用一个或多个工具的决定。
- 如果模型选择了 tool‑call,代理在代码中调用相应的工具(这可能是本地函数、MCP 工具、HTTP 请求、访问数据库等)。
- 工具的结果以 tool 消息的形式加入历史。
- 循环带着新上下文回到模型。模型再决定下一步:继续规划、调用其他工具,或以最终回答结束任务。
- 当模型显式结束或达到停止条件时,本次 run 结束,代理将最终结果返回给调用方。
我们用一个小图表示如下:
flowchart TD
A[开始 run:目标 + system] --> B[调用模型]
B --> C{模型要
直接文本回答
还是调用 tool?}
C --> D["文本回答
(assistant)"]
D --> E{任务已完成?}
E -->|是| F[最终结果]
E -->|否| B
C --> G["Tool-call
(调用描述)"]
G --> H[调用函数 / MCP / HTTP]
H --> I["Tool 结果
(tool message)"]
I --> B
把它翻译成简化的 TypeScript 伪代码(与真实 API 有差距,但逻辑正确),大概是这样:
async function runAgent(goal: string) {
let context = buildInitialContext(goal);
while (!isFinished(context)) {
const decision = await callLLM(context); // 代理的一个步骤
if (decision.type === "tool_call") { // 调用函数?
const toolResult = await callTool(decision.tool, decision.args); // 调用本地函数
context = appendToolResult(context, toolResult); // 将结果追加到消息末尾
} else {
context = appendAssistantMessage(context, decision.message);
}
enforceLimits(context); // 步数/时间/循环的限制
}
return extractFinalResult(context);
}
Agents SDK 会替你承担大部分样板工作:历史存储、tool‑call 的编解码、重试逻辑等等。你需要做的是配置与实现工具本身。
Run 与 step 的区别
需要区分两个概念:
- run——针对某个目标的一次代理启动,例如“为这个场景挑礼物”;
- step——run‑循环中的一步:一次具体的模型调用,可能产生文本回答或 tool‑call。
在监控中你会看到一个 run 内的多步;而安全与成本的限制,往往会设在“每个 run”或“每步”上。
现在你已经了解了代理在一次 run 内如何沿着 run‑循环前进,接下来看看:在哪些地方值得上代理,哪些地方一个简单的 tool 就足够。
5. 在 GiftGenius 中哪里需要代理,哪里不需要
在把所有事情都交给代理之前,请先问自己一个问题:“这里真的需要代理吗?”
代理的好场景,是带有分支、重试、逻辑较复杂的多步骤任务,而这些逻辑仅靠提示词很难维护。
在 GiftGenius 中,一个合适的任务是“智能礼物推荐向导”,它可以:
- 会补问关键细节(收礼人性别、爱好、亲密度等);
- 可以查询多个商品来源(通过不同供应商的 MCP 工具);
- 过滤与排序结果;
- 在来源报错时重试或走备选路径;
- 返回的不只是文本列表,而是带解释与 product feed 中 SKU 链接的结构化候选列表。
在这里,代理作为“编排器”很有价值,尤其当你之后想补充语音/Realtime 场景或更复杂的商业流程(ACP)时。
而对于简单的 getGiftDetails(giftId) 调用就无需代理:直接从 ChatGPT 调用一个 MCP 工具足以。同样地,那些“单步”的场景,比如“根据商品卡文本生成礼物描述”,也不需要上代理。
总体经验:如果可以把场景描述为“一个合格的 tool”,大概率不需要代理;如果你开始明确写一个带检查与重试的多步骤工作流,那么代理很可能能帮上忙。
6. 确定性:如何让代理更可预测
在 LLM 代理的世界里,确定性是个麻烦事。理论上,你希望在相同的输入与设置下,得到相同的行动计划与相同的 tool‑call 序列。实际上,模型具备随机性,但你可以用几种方式提高可预测性。
首先,经典手段:温度等生成参数。温度越低,越少“发挥”,越“听话”。对于礼物推荐代理,你大概率希望既不是零自由度,也不要太高,否则模型会每天发明一种新方式去调用同一个工具。
其次,清晰的 system 指令。如果你含糊其辞地描述“你可以调用各种工具、做任何事”,那就别惊讶代理一会儿乱跳 API,一会儿凭空编造答案。更好的做法是明确描述:何时调用工具、允许的参数有哪些、如何解读错误、什么时候应该结束任务。
例如,GiftGenius 代理的 system 提示词可以包含:
如果你没有完整的收礼人画像(年龄、性别、场合、大致预算),
先通过面向用户的渠道提出澄清问题,并等待回答。
之后再调用 search_gifts 工具,并提供完整的画像。
不要凭空编造商品,务必以工具结果为依据。
这样的指示可以降低决策的发散性,让行为更确定。
第三,工具本身的设计。如果你有三个“差不多都能搜礼物”的工具,模型不可避免地会时而选这个、时而选那个。最好把工具设计为职责边界清晰、互不重叠,并把这一点写在描述里。
最后,你可以使用guardrails——用于校验代理行为与模型结果的规则与模式。Agents SDK 内置了对校验与限制的支持,包括对输出数据结构的约束。如果模型尝试生成不符合模式的内容,你可以温和地纠正它,甚至重试该步骤。
小示例:固定结果格式
假设你需要代理总是返回包含 gifts 字段的 JSON,且内部对象包含 id、title 与 score。你可以:
- 在代理层定义这份模式;
- 要求最终输出必须符合该模式;
- 若违反——重试该步或返回安全错误。
伪代码:
const giftResultSchema = z.object({
gifts: z.array(z.object({
id: z.string(),
title: z.string(),
score: z.number().min(0).max(1),
}))
});
// 在代理配置中
const agent = new Agent({
/* ... */
outputSchema: giftResultSchema,
});
当模型试图返回奇怪内容时,runner 会报告校验错误,你可以选择重新请求模型,或记录该事件。
7. 幂等性:为什么代理可能会把你的 API 调两次
如果说确定性是“相同输入得出相同计划”,那么幂等性就是“重复更安全”。在代理环境中,它至关重要,原因有二。
首先,你会多出一层重试:不仅是 HTTP 客户端与负载均衡器,代理本身也可能因错误或结果不完整而决定重试某个工具调用。其次,在真实生产场景中还会有 webhook、队列、流式通道等——你可能会意外地把同一个逻辑步骤处理多次。
你已经在 MCP 工具层谈过幂等性:避免重复扣款、避免重复创建订单、在请求中使用幂等键(idempotency key)。现在在代理的多步骤特性之上,这一点更重要。
设想 GiftGenius 提供了一个 create_checkout_session 工具,它会根据选中的礼物列表在 ACP/Stripe 中创建一个 draft 结账。如果代理因为网络错误决定重试这次调用,你当然不想得到两笔订单与两次扣款。
因此需要:
- 为每个逻辑动作设计一个外部幂等键(例如 runId + stepIndex 或显式生成的 checkoutDraftId);
- 把它传入你的后端/ACP 端点;
- 在后端检查该键是否已处理过,如已处理则返回保存的结果,而不是再次执行。
TypeScript 伪例:
async function createCheckoutDraft(runId: string, payload: DraftPayload) {
const key = `gift-checkout-${runId}`;
const existing = await findDraftByKey(key);
if (existing) return existing;
const draft = await stripe.checkout.sessions.create({
/* ... */,
idempotencyKey: key, // 或者在其之上实现自有的幂等层
});
await saveDraftWithKey(key, draft);
return draft;
}
这样一来,即使代理因为某种原因用同一个 runId 调用该工具两次,你的代码仍是幂等的:同一逻辑步骤 → 同一实际结果。
“先检查,后执行”
另一个常见的幂等模式是:先检查状态,再执行动作。比如,在创建订单之前,先检查是否已经存在具有相同 clientReferenceId 或相同参数集的订单。这在长工作流中特别有用,因为代理可能“忘记”它在上一步已经做过某事。
Safe‑mode/Fake‑mode
在开发阶段,为危险工具提供“安全模式”很有用:它们不做真实动作,只记录“将会做什么”,并返回伪结果。对代理而言,这是在生产环境跑一遍 run‑循环而不冒金钱或数据风险的方便方式。
8. 小练习:用自然语言描述 GiftGenius 代理
我们已经讨论了 run‑循环、确定性与工具幂等性。现在从代码抽离一分钟,看看它们如何在真实场景中组合起来。
此刻做一个小练习(纸上或脑中均可),无需写代码。
设想你要描述一个简单代理:
-
: 你是礼物推荐助手;总是先澄清重要细节,不凭空编造商品,只使用工具的结果。system -
: 我想给同事买不超过 50 美元的礼物。user
请用文字描述,这样的代理应完成哪些步骤。
一个典型流程可能是这样:
- 代理先检查信息是否充分。若不足,它会提出澄清问题:同事大致从事什么工作(设计、开发、管理)、是否有禁忌(酒类、玩笑礼物)、是否有配送限制。答案会进入 session state,或用于工具调用的参数。
- 然后代理用完整画像调用 search_gifts:例如“同事—开发者、预算 50、美学偏好——小玩意和办公用品”。工具返回候选列表,包含价格、类别与商品 ID。
- 接着,如果发现部分商品无法送达目标区域,代理可以再调用 filter_gifts_by_constraints,或在提示词中自行过滤。随后按相关性与性价比排序,可能附上简短评语(“适合爱咖啡的人”、“远程办公的好选择”)。
- 最后,代理为 ChatGPT App 准备最终的结构化输出:列出 5–7 个礼物,附简要描述、使用提示与 Checkout 链接(或下一步——创建 draft 结账)。
哪些地方需要 tool‑call?显然在商品搜索与过滤、可达性检查、以及创建 draft 结账。哪些步骤必须幂等?首先是所有与订单和资金相关的操作——创建 draft 结账,以及可能的数据库记账等。
9. 使用代理的初学者常见错误
常见错误 1:把代理当作“第二个无限制的 ChatGPT”。
有时人们只想再给模型一个提示词,并把它称为“代理”。结果就是它疯狂生成文本、随意调用工具、难以控制。为避免这种情况,你需要在 system 中清晰描述代理的角色,限制工具列表,并把它视为一个带明确使命的“编排器”,而非“第二个文本生成宇宙”。
常见错误 2:工具缺乏幂等性。
开发者常把旧的 HTTP 处理器不加改造地放到代理下面,忽略了 runner 现在可能会自动重试调用。对支付和订单而言,这会很危险。正确做法是从一开始就让工具是幂等的:用同一个逻辑键重复调用不会导致重复动作。
常见错误 3:模型参数过于“有创意”。
高温度适合写祝词和诗,但对需要可靠编排多步骤流程的代理来说,这会导致不可预测的行为:模型每次选择不同的工具、生成不同的计划,甚至忘记自己有 tools。要把代理视为“服务型”实体,用更严格的设置。
常见错误 4:一个“万能工具”。
有时人们想做一个万能 tool,比如 execute_any_sql 或 do_anything_with_orders,然后交给代理。在 LLM 的创造性加持下,这几乎是注定的安全隐患。更好的做法是多个职责明确、契约清晰、权限边界良好的窄工具,而不是一个“全能神”工具。
常见错误 5:缺少明确的 run 结束标准。
如果不告诉代理何时该停,它可能进入无限或半无限循环:再检查一次结果、再问一次用户、在相同错误下再试一次。这往往只会在负载下暴露,比如依赖项不稳定时。正确方式是对步数、run 时长、同类错误重试次数设定上限,并在 system 中说明代理在用尽合理选项时要“坦诚放弃”。
常见错误 6:把所有东西都塞进代理状态。
由于 Agents SDK 简化了会话状态的使用,你可能会把大文档、原始日志、敏感数据统统放进去。这会膨胀上下文、增加成本并引发安全风险。代理状态应只存放继续工作所必需的数据;其余内容应放在数据库、日志与其他层中,并注意隐私。
常见错误 7:在只需要一个简单 MCP 工具的地方使用代理。
有时开发者一开始就用代理,即使任务只是调用一个函数并返回结果。这会无谓地增加复杂度:引入 run‑循环、状态、额外日志与潜在故障点。如果场景可以用一次 tool‑call 搞定,就保持这样;只有当出现真正的多步骤工作流时再上代理。
GO TO FULL VERSION