跳到主要内容

openai-agents-js — 本课题摘录

读了哪几篇: 01-agent-loop(Runner 的 agent 循环)、04-human-in-the-loop(人审与运行状态序列化)。 其余三篇(工具与 agent-as-tool、交接与护栏、模型抽象与流式)本轮没读——属工具层、多 agent 与界面圈。

这是 OpenAI 官方的 TypeScript SDK,跟我们要写的技术栈同语言,所以它的形状值得逐字看。

它对本课题回答了什么

决定四:什么时候停 —— 用一个「下一步」标签当控制流词汇表

这是这一家最值得抄的一条:循环不靠一堆 if 判断该不该继续,而是每拍产出一个标签,循环顶部只读这个标签。

标签循环怎么做
再来一拍继续转
最终输出跑输出护栏,返回结果
交接换一个 agent,再转
中断直接返回,把待审批的事情交还给调用方

(依据:前沿库 · OpenAI Agents SDK (JS) · Runner 的 agent 循环 —— 四个 next_step_* 标签定义成一个 zod discriminated union,是整个引擎的控制流词汇表,循环顶部读 state._currentStep 分发)

好处很实在: 新增一种"停下来的理由"不用改循环正文,只要多一个标签。人审就是这么加进去的——它不是循环里的特判,是第四个标签。

决定四补充:一个容易踩的语义坑

同一拍里如果有工具调用,就不能把同拍的助手文本当最终答案。 模型经常边说话边调工具,不挡住就会过早结束。 (依据:前沿库 · OpenAI Agents SDK (JS) · Runner 的 agent 循环 —— turnResolution 里显式注释说明「同一拍里有工具调用就不能把同拍 assistant 文本当最终答案」,并用 hasToolsOrApprovalsToRun 挡住)

判定"这一拍到底有没有动作要执行"被收敛成一个方法,而不是散在各处。 (依据:前沿库 · OpenAI Agents SDK (JS) · Runner 的 agent 循环 —— processModelResponse 把模型输出归类成 functions/handoffs/computerActions/shellActions/mcpApprovalRequests 等数组,并提供 hasToolsOrApprovalsToRun() 统一回答「这一拍有没有动作」)

决定二:怎么认出模型要调工具

分类而不是判断。 模型这一拍的输出被遍历一遍,按类型归到不同的桶里,再统一决议。

一个值得记住的细节:函数调用和"交接"在线上长得一模一样,都是同一种输出类型,靠名字区分——先查交接表再查函数表,都没有就报"找不到"。 (依据:前沿库 · OpenAI Agents SDK (JS) · Runner 的 agent 循环 —— 函数调用与 handoff 都是 function_call 类型,resolveFunctionOrHandoff 先查 handoffMap 再查 functionMap,都没有报 not_found)

决定三:结果怎么回填 —— 什么并行、什么必须串行

互不依赖的并行,会打架的串行,而且顺序按模型给的来。

动作怎么跑为什么
函数工具 + 计算机动作并行二者互不依赖
shell 与打补丁按模型给的顺序串行都改同一个沙箱文件系统

(依据:前沿库 · OpenAI Agents SDK (JS) · Runner 的 agent 循环 —— 函数工具与计算机动作用 Promise.all 并行,shell/apply_patch 因都改沙箱文件系统必须按模型给的顺序串行)

这条比"全并行"或"全串行"都准:并发与否取决于动作碰不碰同一份状态。

决定一相关:结构化输出的校验发生在循环里

给 agent 设了输出类型时,模型的文本会在循环内被 schema 校验,校验失败抛错而不是静默返回脏数据。 (依据:前沿库 · OpenAI Agents SDK (JS) · Runner 的 agent 循环 —— 有 outputType 时模型返回文本经 agent.processFinalOutput 走 Zod/标准 schema 校验,失败抛 ModelBehaviorError)

它的做法(可以抄的部分)

把「一次运行」整个存成字符串,再还原着跑

这是最有分量的一条。人审的难点不在"暂停",在于暂停可能跨进程——人可能十分钟后在另一台机器上点同意。

第一次运行 → 撞到需审批的工具 → 贴「中断」标签 → 返回
→ 把运行状态转成字符串存进库
⋯ 人在界面上点同意 ⋯
→ 从库里取出、还原成运行状态 → 标记这一项已批准
→ 把同一个状态再传回去跑 → 从中断处接着跑

(依据:前沿库 · OpenAI Agents SDK (JS) · 人审与 RunState 序列化 —— RunState 可 toString() 存库、fromString() 还原,approve/reject 后把同一个 state 再传回 run() 即从中断处续跑)

审批不是"执行前弹个窗",而是先查审批表,没批过就根本不执行,产出一个待审项;批准记录默认只对当前这一次调用生效,也可以让整个工具长期批准。 (依据:前沿库 · OpenAI Agents SDK (JS) · 人审与 RunState 序列化 —— handleFunctionApproval 先查 isToolApproved,未批准则不执行而产出 RunToolApprovalItem;批准默认只对当前调用生效,传 options 可长期批准)

拒绝时还能带一句话,当作"被拒绝"的工具结果回喂模型。

流式和非流式是两条平行的循环,但共用同三个函数

(依据:前沿库 · OpenAI Agents SDK (JS) · Runner 的 agent 循环 —— 流式循环与非流式循环高度对称,共用 prepareTurn / resolveTurnAfterModelResponse / applyTurnResult 三个函数以保证行为一致)

这条对我们有直接用处: 以后接界面时,流式不该是另一套逻辑,而应该是同一套决议函数外面换一层壳。

它没回答什么

  • 系统提示怎么拼、超长怎么裁——这一篇只说"拼历史、组装 system prompt",没讲细节。要看 headroomhermes-agentkunonyx
  • 不用原生工具调用怎么办——它假设模型支持原生 function calling。文本协议那条路看 crewaiagenticseeksmolagents
  • 工具太多怎么办——工具清单是给定的。要看 cherry-studioqwen-codewhale

坑与代价

  • 只有第一个 agent 的输入护栏会跑,交接之后的新 agent 不重跑输入护栏。这是它自己写在文档里的行为,不是 bug——但换成我们自己的场景要想清楚。 (依据:前沿库 · OpenAI Agents SDK (JS) · Runner 的 agent 循环 —— 文档明说 only the first agent's input guardrails are run,交接后的新 agent 不重跑输入护栏)
  • 状态可序列化的代价是"状态里的一切都得能序列化"。 想在运行状态里塞一个数据库连接或者文件句柄,这条路就断了。

    判断(无锚): 这个代价对我们最小原型不构成问题,因为一轮里能带的本来就只有消息与工具结果。 如果错,会错在: 如果以后工具需要持有长连接(比如一个开着的浏览器会话),那"状态可整个序列化"就得降级成"状态里存一个句柄 id,句柄本身另存"。