跳到主要内容

generate:模型调用与自动工具循环(agentic 引擎)

30 秒导读: 你调用一次 ai.generate({ prompt, tools }),Genkit 会替你反复跟模型 对话:模型说"我要调工具",Genkit 执行工具、把结果塞回去、再问模型,如此往复, 直到模型不再要工具为止——最多循环 maxTurns(默认 5)轮,超了就抛错。这一章讲清 这个"自动工具循环"从头到尾是怎么转的。

本章聚焦循环的控制流模型的调用。工具"内部怎么执行、如何中断做 human-in-the-loop" 留给 工具执行、中断与 human-in-the-loop;模型调用产出的结构化输出 细节见 Dotprompt 与结构化输出。想先弄懂"一切皆 Action" 和注册表,请回看 Action 原语与注册表


1. 这是什么(零基础也能懂)

一句话定义: generate 是 Genkit 的"AI 编排核心"——把你给的 prompt、工具、输出格式, 变成对模型的一次(或多次)调用,并自动帮你把模型要求的工具调用跑完

它解决什么问题? 现代模型不只会"回一段话",它还会说"我需要先查一下天气"(一次 tool call)。裸调 API 时,这套"模型 → 要工具 → 你执行 → 回给模型 → 模型再想 → ……" 的来回,得你自己手写循环。Genkit 把它做成了一个内建的、可控的闭环,你只管声明工具。

给谁用? 任何想让模型"能动手做事"的开发者——写 agent、写带函数调用的助手、写要吐 结构化 JSON 的抽取器的人。

用起来什么样? 一个最小的例子:

// 示意,非源码:声明一个工具,然后 generate 自动完成整个工具循环
const getWeather = ai.defineTool(
{ name: 'getWeather', inputSchema: z.object({ city: z.string() }) },
async ({ city }) => `${city} 晴,26°C` // 工具真正干的活
);

const { text } = await ai.generate({
prompt: '北京今天适合出门吗?',
tools: [getWeather], // 把工具交给模型
maxTurns: 5, // 最多来回 5 轮(默认就是 5)
});
// 你什么循环都没写:模型自己决定调 getWeather,Genkit 执行后把结果喂回,模型再总结成 text

一句话直觉:generate 想成一个尽职的中间人——它坐在你和模型之间,模型每次 "点单"要个工具,它就跑腿买回来递过去,来回跑腿,直到模型说"够了,这是最终答复"。


2. 顶层全景(它大概怎么转)

怎么读下面这张图: 从上到下是一次 ai.generate() 的生命周期;中间那个带箭头折回 的方框就是"自动工具循环"——它会一轮轮把自己再跑一遍。

ai.generate(options) generate.ts:490
│ 规整选项 → GenerateActionOptions toGenerateActionOptions

generateHelper (包一层 trace span) action.ts:127


generateActionImpl action.ts:232
│ 若有 generate 级中间件,先串成链

┌─────────────────────────────────────────┐
│ generateActionTurn = 跑「一轮」 │ action.ts:337
│ ① 解析 model / tools / format │ resolveParameters
│ ② 规整成 GenerateRequest │ actionToGenerateRequest
│ ③ 过 model 中间件链 → 调模型 │ dispatchModel
│ ④ 收集 toolRequests │
│ │
│ 没有工具? ── 是 ──► 返回最终响应 │ ← 循环出口
│ │ 有 │
│ ▼ │
│ currentTurn+1 > maxTurns? ─ 是 ─► 抛 ABORTED
│ │ 否 │
│ ▼ │
│ 执行工具 → 得到 toolMessage │ resolveToolRequests(第4章)
│ │ │
│ ▼ 把模型消息 + 工具结果追加进 messages
└────────┼─────────────────────────────────┘
│ 递归:currentTurn + 1
└──────────────► 回到 generateHelper 再跑一轮

部件一句话职责:

部件干什么在哪(文件:符号)
generate用户入口:规整选项、解析中间件/工具、发起循环generate.ts:490 generate
toGenerateActionOptions把好用的 GenerateOptions 压成可序列化的 GenerateActionOptionsgenerate.ts:619
defineGenerateAction把整个循环注册成一个名为 generateutil Actiongenerate/action.ts:80
generateHelper给每一轮套一个 trace span(可观测性)generate/action.ts:127
generateActionImpl若有 generate 级中间件,搭建中间件链;否则直接跑一轮generate/action.ts:232
generateActionTurn跑一轮:调模型 + 判断是否要继续循环generate/action.ts:337
resolveParameters把 model/tools/resources/format 从注册表解析成真 Actiongenerate/action.ts:170
dispatchModel把 model 中间件串成链,链尾调真正的模型 Actiongenerate/action.ts:454
defineModel把一个模型实现注册成 model 类型的 Actionmodel.ts:242

主线走一遍(高层): 你的 options →(规整)→ GenerateActionOptions →(套 trace)→ 跑第一轮:调模型 → 模型回了带 toolRequest 的消息 →(执行工具)→ 把结果追加进对话 → 递归跑第二轮 → 模型这次只回文本、没工具了 → 循环结束,返回最终 GenerateResponse


3. 核心原理(逐个机制,由浅入深)

3.1 选项规整:GenerateOptionsGenerateActionOptionsGenerateRequest

它要解决的小问题: 用户写的 GenerateOptions 很"人性化"——tools 可以是工具对象、 可以是字符串名;prompt 可以是字符串或多模态 parts;model 可以是引用或名字。但真正 喂给模型的东西必须是规整、可序列化的。所以中间要经过两层转换。

三层数据模型,各是什么:

类型特征谁生产
用户层GenerateOptions富对象:工具是 Action、prompt 是字符串/parts、有回调函数
动作层GenerateActionOptions扁平、可序列化:工具是字符串引用名、messages 已展开toGenerateActionOptions
请求层GenerateRequest模型真正收到的:messages + config + tools 定义 + output schemaactionToGenerateRequest

从 options 到 action options: toGenerateActionOptions(generate.ts:619)把 system / messages / prompt 合并成一个 messages 数组(messagesFromOptions,generate.ts:348), 把工具对象转成引用字符串(toolsToActionRefs,generate.ts:303,形如 /tool/getWeather), 并把 maxTurnsreturnToolRequeststoolChoice 等原样带上。

到请求层: 每一轮真正调模型前,actionToGenerateRequest(action.ts:572,循环内用的 那个)把动作层选项压成 ModelRequest:messages、config、tools(经 toToolDefinition 变成 给模型看的工具签名)、以及 output 约束。这里还会检查模型能力:如果你给了工具但模型 supports.tools 为假,只是 logger.warn 提醒,不报错(action.ts:583)。

另有一个独立toGenerateRequest(generate.ts:201),用途是"我只想看看请求长啥样" 而不真跑——它被 generate 在最后构造 GenerateResponse.request 时用来回填(generate.ts:537)。 别和循环内每轮用的 actionToGenerateRequest 搞混:一个是"预览请求",一个是"每轮真正发出去的请求"。

关键细节 —— 输出格式的默认值: 如果你设了 output.schema 但没显式设 format, toGenerateActionOptions 会默默把 format 设成 'json'(generate.ts:640);applyFormat (action.ts:186)则在每轮把格式化指令注入到 messages 里。为什么放在每轮?因为工具循环 里 messages 一直在变,格式指令得跟着新 messages 走。

3.2 一轮是什么:generateActionTurn

它要解决的小问题: "调一次模型"远不止"发 HTTP"。一轮里要解析参数、套格式、处理 resume、过中间件、构造响应、再判断"要不要继续"。generateActionTurn(action.ts:337) 就是这"一轮"的全部。

一轮的步骤(按代码顺序):

resolveParameters 解析 model / tools / resources / format action.ts:359
│ 追加中间件注入的 tools action.ts:366
applyFormat 把输出格式指令注入 messages action.ts:368
applyResources 把 resource 占位符展开成真实内容 action.ts:369
assertValidToolNames 生成前先查:工具名有没有撞车 action.ts:372
resolveResumeOption 处理"中断后恢复"(细节见第4章) action.ts:378
actionToGenerateRequest 规整成给模型的 GenerateRequest action.ts:421
dispatchModel 过 model 中间件链 → 调真正的模型 action.ts:477
GenerateResponse 包装模型返回 action.ts:492
response.assertValid 响应不可用就抛错 action.ts:502
── 收集 toolRequests,决定去留 ── action.ts:505+

收集工具请求: 模型返回的消息里,凡是带 toolRequest 的 part 就是"模型要调的工具":

// 真实源码 action.ts:505
const toolRequests = generatedMessage.content.filter(
(part) => !!part.toolRequest
);

背景模型的短路: 如果模型是 background-model 类型(异步长任务),这一轮不进工具循环, 直接把包着 operation 的响应返回(action.ts:483action.ts:497)——它的结果得靠轮询。

3.3 循环的三个出口(最重要的控制流)

它要解决的小问题: 一轮跑完,拿到 toolRequests 后,generate 要在三条路里选一条。 搞懂这三个出口,就搞懂了整个 agentic 引擎。

怎么读: 下面是 action.ts:509562 的判断顺序,从上往下,命中即走

一轮结束,拿到 toolRequests

├─ returnToolRequests==true 或 没有工具请求?
│ └──► 出口A:直接返回响应,不执行任何工具 action.ts:509

├─ currentTurn + 1 > maxTurns(默认5)?
│ └──► 出口B:抛 GenerationResponseError('ABORTED') action.ts:515

└─ 否则:执行工具
├─ 工具触发中断(interrupt)?
│ └──► 出口C:返回 finishReason='interrupted' action.ts:532
└─ 否则:追加消息,递归跑下一轮(currentTurn+1) action.ts:562

出口 A —— returnToolRequests 短路(action.ts:509):

// 真实源码 action.ts:509
if (rawRequest.returnToolRequests || toolRequests.length === 0) {
if (toolRequests.length === 0) response.assertValidSchema(request);
return response.toJSON();
}

两种情况都在这返回:①你显式要"把工具请求还给我自己处理"(returnToolRequests: true); ②模型压根没要工具——这是正常收尾的出口。注意只有"没工具"时才校验输出 schema (assertValidSchema),因为带工具请求的中间消息不该被当成最终结构化结果。

出口 B —— 循环上限 maxTurns(action.ts:514):

// 真实源码 action.ts:514
const maxIterations = rawRequest.maxTurns ?? 5;
if (currentTurn + 1 > maxIterations) {
throw new GenerationResponseError(
response,
`Exceeded maximum tool call iterations (${maxIterations})`,
'ABORTED',
{ request }
);
}

这是防失控的护栏:模型可能陷入"无限调工具"。currentTurn 从 0 起,每递归一轮 +1。 把上限设为默认 5,展开来看究竟能跑几次:

轮次 currentTurncurrentTurn+1 > 5?这一轮做什么
01 > 5 否调模型 → 有工具 → 执行 → 递归到 1
12 > 5 否调模型 → 有工具 → 执行 → 递归到 2
23 > 5 否…执行 → 递归到 3
34 > 5 否…执行 → 递归到 4
45 > 5 否…执行 → 递归到 5
56 > 5 若模型要工具 → 抛 ABORTED

所以默认下模型最多被调 6 次(轮 0–5),而工具最多被执行 5 轮(轮 0–4);第 6 次(轮 5) 若模型还想要工具,就在执行工具之前抛错。抛的是 GenerationResponseError(generate.ts:283), status'ABORTED',detail.response 里带着触发它的那次响应,方便你事后检查。

出口 C —— 中断(interrupt)(action.ts:532): 执行工具时,若某个工具是 interrupt (human-in-the-loop),resolveToolRequests 会返回 revisedModelMessage,循环停下,返回 finishReason: 'interrupted'。这条路的具体机制是第 4 章的主题,这里只需知道它是循环的 第三个出口。

继续循环(action.ts:550562): 没命中前三个出口,就把"模型这条消息"和"工具结果 消息"都追加进 messages,构造 nextRequest,递归 generateHelper,currentTurn + 1:

// 真实源码 action.ts:550
const messages = [...rawRequest.messages, generatedMessage.toJSON()];
if (toolMessage) {
messages.push(toolMessage);
}
let nextRequest = { ...rawRequest, messages };
// 递归再来一轮
return await generateHelper(registry, {
rawRequest: nextRequest,
currentTurn: currentTurn + 1,
messageIndex: messageIndex + 1,
/* …streamingCallback / middleware / abortSignal… */
});

注意这是递归而非 while 循环——每一轮都重新进 generateHelper → 套一层新的 trace span (action.ts:142)。所以在 Dev UI 里,一次多工具调用的 generate 会呈现为嵌套的多层 span,一目了然地看到"第几轮、调了什么"。

3.4 两条中间件链:generate 级 vs model 级

它要解决的小问题: 中间件(middleware,拦截器)要能插在两个不同的粒度上—— "每一整轮"和"每一次模型调用"。Genkit 因此有两条独立的洋葱链

中间件层hook 名包住什么在哪搭链典型用途
generate 级.generate一整轮(含工具循环的这一层)generateActionImpldispatchGenerate,action.ts:259注入请求参数、改最终响应、catch 错误
model 级.model单次模型调用generateActionTurndispatchModel,action.ts:454模型级缓存、重试、prompt/响应改写

两个 hook 的签名在 GenerateMiddlewareDef(generate/middleware.ts:97)里定义;.generate 拿到的是 { request, currentTurn, messageIndex } 信封,.model 拿到的是 GenerateRequest。 中间件还能通过 .tools 字段静态注入工具——这些工具在 generateActionTurn 里被追加 进 tools 数组(action.ts:366)。

洋葱式 dispatch 长什么样(model 级为例):

// 真实源码(节选)action.ts:454
const dispatchModel = async (index, req, actionOpts) => {
if (!middleware || index === middleware.length) {
return await model(req, actionOpts); // 链尾:调真正的模型
}
const currentMiddleware = middleware[index];
if (currentMiddleware.model) {
return currentMiddleware.model(req, actionOpts,
async (modifiedReq, opts) => // next():交给下一层
dispatchModel(index + 1, modifiedReq || req, opts || actionOpts));
}
return dispatchModel(index + 1, req, actionOpts); // 这层没有 model hook,跳过
};

每层中间件拿到 req 和一个 next,可以改请求、调 next 进入下一层、再改返回值——典型的 "洋葱模型"。generate 级的 dispatchGenerate(action.ts:259)是同样的结构,只不过链尾是 generateActionTurn 而不是 model

还有第三层:模型自带的内建中间件。 别忘了模型 Action 自身在定义时就挂了 use 中间件——getModelMiddleware(model.ts:362)会自动加两个:augmentWithContext() (注入检索到的文档,当模型不原生支持 context 时)和 simulateConstrainedGeneration() (模型不支持"受约束生成"时,用 prompt 模拟出结构化输出)。这些在 model Action 内部执行, 对上面两条链是透明的。这类"补能力"的中间件实现见 model/middleware.ts

3.5 流式:每一轮的 chunk 怎么透传

它要解决的小问题: 工具循环有很多轮,用户却希望看到一条连续的流。所以流式回调 必须跨轮共享上下文,让每个 chunk 知道"我是第几条消息、前面有哪些内容"。

关键载体是 sharedPreviousChunks —— 一个跨轮累积的数组(action.ts:256)。makeChunk (action.ts:431)用它给每个 chunk 打上 index(=messageIndex)、role、以及 previousChunks 快照:

// 真实源码(节选)action.ts:431
const makeChunk = (role, chunk) => {
if (role !== chunkRole && sharedPreviousChunks.length) messageIndex++; // 角色变了→新消息
chunkRole = role;
const prevToSend = [...sharedPreviousChunks]; // 给这条 chunk 的"前情"快照
sharedPreviousChunks.push(chunk); // 再把自己累积进去
return new GenerateResponseChunk(chunk, {
index: messageIndex, role, previousChunks: prevToSend,
parser: format?.handler(request.output?.schema).parseChunk,
});
};

流的组成: 每轮模型产出的 chunk 经 sendChunk(action.ts:450)→ makeChunk('model', …) 透传;若这轮要继续循环,工具结果也会作为一条 'tool' 角色的 chunk 流出去(action.ts:542)。 于是消费方(generateStream,generate.ts:766,用一个 Channel 把回调变成 AsyncIterable) 看到的是跨所有轮次拼接起来的、带正确 index 的连续流

parser 让流可结构化: 每个 chunk 都带一个 parser(来自 format handler),所以即使是流式, 消费方也能边收边把 JSON 拼成部分结构化对象——这块的格式机制细看 结构化输出


4. 深入实现:一次带一轮工具调用的完整时序

怎么读: 竖向是时间,左到右是参与者。这是"prompt 触发一次工具调用后收尾"的真实路径。

你 generate() generateActionTurn model 中间件链 模型
│ options │ │ │ │
├────────────►│ toGenerateActionOptions │ │
│ ├── generateHelper ─┤(套 span) │ │
│ │ ├ resolveParameters │ │
│ │ ├ actionToGenerateRequest │
│ │ ├───── dispatchModel ─►│──── model(req) ►│
│ │ │ │◄── 含 toolRequest│
│ │ ◄─────────────────────┤ │
│ │ toolRequests.length > 0 │ │
│ │ currentTurn+1(=1) > 5? 否 │ │
│ │ resolveToolRequests → 执行工具 → toolMessage(第4章) │
│ │ messages += [模型消息, 工具结果] │ │
│ ├── generateHelper(currentTurn=1)─┐(第二轮:新 span) │
│ │ dispatchModel ─────────────┼──── model(req) ────────►│
│ │ │◄── 只有 text,无 toolReq │
│ │ toolRequests.length == 0 → 出口A:返回 │
│◄────────────┤ GenerateResponse(最终 text) │ │

入口注册(defineGenerateAction,action.ts:80): 整个循环被注册成一个名为 generateutil 类型 Action。它先建一个子注册表 Registry.withParent(action.ts:91),规整并解析 中间件(normalizeMiddleware / resolveMiddleware),再决定"要不要流式"地调 generateActionImpl。 把循环做成 Action 的好处:天然可被 trace、可被别的 Action 复用——呼应第 1 章"一切皆 Action"。

用户入口(generate,generate.ts:490) 与这个 util Action 的关系:generate便捷函数, 它自己规整选项、解析中间件与工具引用,然后直接调 generateHelper(generate.ts:530)。也就是说 generate.ts:490generatedefineGenerateAction 注册的那个 Action 走的是同一条 generateHelper 主干,只是入口包装不同。

中间件归一化(normalizeMiddleware,generate.ts:386): 用户能传的中间件形态五花八门 ——裸函数、中间件实例、MiddlewareRef。这个函数把它们统一成 MiddlewareRef[],并把匿名函数 动态注册进注册表(起个 dynamic-middleware-… 的随机名,generate.ts:415),后续才能按名查回。


5. 巧妙之处(可借鉴的技术)

  • 用递归而非 while 表达多轮循环。 每轮重进 generateHelper 套新 span(action.ts:562action.ts:142),让"第几轮调了什么"在 trace 里天然嵌套可读——把控制流和可观测性 用同一结构表达出来。

  • 三条出口顺序精心排列。 returnToolRequests/无工具的正常收尾在最前,maxTurns 护栏 居中,中断在后(action.ts:509532)。护栏放在"执行工具之前"检查(action.ts:515), 意味着超限时绝不会再多跑一次昂贵的工具。

  • currentTurn + 1 > maxTurns 的边界很微妙但正确。 让"模型调用次数"比"工具执行轮数" 多一次:最后一轮允许模型再说话(可能收尾),只有它还要工具才判失控。

  • 跨轮共享的 sharedPreviousChunks + 每 chunk 快照 previousChunks 既让流连续 (action.ts:256action.ts:438),又让每个 chunk 自带"前情",消费方无需自己维护状态。

  • 两层 dispatch 复用同一个洋葱套路。 dispatchGeneratedispatchModel(action.ts:259action.ts:454)结构一模一样,只是链尾不同——一个 pattern,两个粒度。


6. 边界与局限(诚实)

  • maxTurns 是硬上限,超了直接抛 ABORTED(action.ts:515),不是"截断返回最后结果"。 想要"跑满就优雅收尾"得自己 catch GenerationResponseError 并读 detail.response

  • 背景(异步)模型不进工具循环。 background-model 在第一轮就带 operation 返回 (action.ts:497),自动工具循环对它不生效——工具编排要靠调用方轮询后自理。

  • returnToolRequests: true 时你自己负责一切。 循环在出口 A 立即返回工具请求 (action.ts:509),不执行、不校验最终 schema——把控制权全交给你。

  • 重启(restart)期间的工具中断不被支持。 若一次"恢复执行"中又触发 interrupt, 直接抛 FAILED_PRECONDITION(action.ts:383),代码注释明说这是当前有意的限制。

  • 模型不支持工具时只 warn 不报错(action.ts:583)。你可能拿到一个"忽略了工具"的 普通回答,而不是显式失败——排查时要留意这条日志。


7. 横向对比

同 shelf 的 agent 框架各自怎么处理"模型 ↔ 工具"的自动循环,取舍不同:

关切Genkit 的做法可对比的兄弟取舍
循环上限maxTurns 默认 5,超限抛 ABORTED有的框架用"步数预算 + 回调决定是否继续"
循环载体把整轮注册成 util Action,递归 + trace span有的用显式状态机 / 图(node-edge)编排
中断/HITLinterrupt 作为循环的第三出口(见第4章)有的靠"暂停并持久化整段会话状态"
扩展点两层洋葱中间件(generate 级 / model 级)有的只在"每次模型调用"一个粒度上给 hook

细节参见本章的兄弟章节:Action 原语与注册表Plugin 系统与门面工具执行与中断结构化输出会话与可观测性


8. 代码地图(导航索引)

主题文件路径符号名
用户入口:发起循环js/ai/src/generate.tsgenerate
选项类型定义js/ai/src/generate.tsGenerateOptions
options → action optionsjs/ai/src/generate.tstoGenerateActionOptions
system/messages/prompt 合并js/ai/src/generate.tsmessagesFromOptions
工具对象 → 引用字符串js/ai/src/generate.tstoolsToActionRefs
独立"预览请求"构造js/ai/src/generate.tstoGenerateRequest
循环上限错误类型js/ai/src/generate.tsGenerationResponseError
中间件归一化js/ai/src/generate.tsnormalizeMiddleware
流式:回调转 AsyncIterablejs/ai/src/generate.tsgenerateStream
把循环注册成 util Actionjs/ai/src/generate/action.tsdefineGenerateAction
每轮套 trace spanjs/ai/src/generate/action.tsgenerateHelper
搭建 generate 级中间件链js/ai/src/generate/action.tsgenerateActionImpl / dispatchGenerate
跑一轮 + 三出口判断js/ai/src/generate/action.tsgenerateActionTurn
解析 model/tools/formatjs/ai/src/generate/action.tsresolveParameters
注入输出格式指令js/ai/src/generate/action.tsapplyFormat
model 级中间件链js/ai/src/generate/action.tsdispatchModel
每轮构造模型请求js/ai/src/generate/action.tsactionToGenerateRequest
流式 chunk 组装js/ai/src/generate/action.tsmakeChunk
定义模型 Actionjs/ai/src/model.tsdefineModel / model
模型内建中间件注入js/ai/src/model.tsgetModelMiddleware
中间件 hook 签名js/ai/src/generate/middleware.tsGenerateMiddlewareDef
中间件解析js/ai/src/generate/middleware.tsresolveMiddleware
能力补齐中间件实现js/ai/src/model/middleware.tsdownloadRequestMedia