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 压成可序列化的 GenerateActionOptions | generate.ts:619 |
defineGenerateAction | 把整个循环注册成一个名为 generate 的 util Action | generate/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 从注册表解析成真 Action | generate/action.ts:170 |
dispatchModel | 把 model 中间件串成链,链尾调真正的模型 Action | generate/action.ts:454 |
defineModel | 把一个模型实现注册成 model 类型的 Action | model.ts:242 |
主线走一遍(高层): 你的 options →(规整)→ GenerateActionOptions →(套 trace)→
跑第一轮:调模型 → 模型回了带 toolRequest 的消息 →(执行工具)→ 把结果追加进对话 →
递归跑第二轮 → 模型这次只回文本、没工具了 → 循环结束,返回最终 GenerateResponse。
3. 核心原理(逐个机制,由浅入深)
3.1 选项规整:GenerateOptions → GenerateActionOptions → GenerateRequest
它要解决的小问题: 用户写的 GenerateOptions 很"人性化"——tools 可以是工具对象、
可以是字符串名;prompt 可以是字符串或多模态 parts;model 可以是引用或名字。但真正
喂给模型的东西必须是规整、可序列化的。所以中间要经过两层转换。
三层数据模型,各是什么:
| 层 | 类型 | 特征 | 谁生产 |
|---|---|---|---|
| 用户层 | GenerateOptions | 富对象:工具是 Action、prompt 是字符串/parts、有回调函数 | 你 |
| 动作层 | GenerateActionOptions | 扁平、可序列化:工具是字符串引用名、messages 已展开 | toGenerateActionOptions |
| 请求层 | GenerateRequest | 模型真正收到的:messages + config + tools 定义 + output schema | actionToGenerateRequest |
从 options 到 action options: toGenerateActionOptions(generate.ts:619)把 system /
messages / prompt 合并成一个 messages 数组(messagesFromOptions,generate.ts:348),
把工具对象转成引用字符串(toolsToActionRefs,generate.ts:303,形如 /tool/getWeather),
并把 maxTurns、returnToolRequests、toolChoice 等原样带上。
到请求层: 每一轮真正调模型前,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:483、action.ts:497)——它的结果得靠轮询。
3.3 循环的三个出口(最重要的控制流)
它要解决的小问题: 一轮跑完,拿到 toolRequests 后,generate 要在三条路里选一条。
搞懂这三个出口,就搞懂了整个 agentic 引擎。
怎么读: 下面是 action.ts:509–562 的判断顺序,从上往下,命中即走。
一轮结束,拿到 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,展开来看究竟能跑几次:
轮次 currentTurn | currentTurn+1 > 5? | 这一轮做什么 |
|---|---|---|
| 0 | 1 > 5 否 | 调模型 → 有工具 → 执行 → 递归到 1 |
| 1 | 2 > 5 否 | 调模型 → 有工具 → 执行 → 递归到 2 |
| 2 | 3 > 5 否 | …执行 → 递归到 3 |
| 3 | 4 > 5 否 | …执行 → 递归到 4 |
| 4 | 5 > 5 否 | …执行 → 递归到 5 |
| 5 | 6 > 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:550–562): 没命中前三个出口,就把"模型这条消息"和"工具结果
消息"都追加进 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 | 一整轮(含工具循环的这一层) | generateActionImpl 的 dispatchGenerate,action.ts:259 | 注入请求参数、改最终响应、catch 错误 |
| model 级 | .model | 单次模型调用 | generateActionTurn 的 dispatchModel,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): 整个循环被注册成一个名为 generate
的 util 类型 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:490 的 generate 和 defineGenerateAction 注册的那个 Action 走的是同一条
generateHelper 主干,只是入口包装不同。
中间件归一化(normalizeMiddleware,generate.ts:386): 用户能传的中间件形态五花八门
——裸函数、中间件实例、MiddlewareRef。这个函数把它们统一成 MiddlewareRef[],并把匿名函数
动态注册进注册表(起个 dynamic-middleware-… 的随机名,generate.ts:415),后续才能按名查回。
5. 巧妙之处(可借鉴的技术)
-
用递归而非 while 表达多轮循环。 每轮重进
generateHelper套新 span(action.ts:562→action.ts:142),让"第几轮调了什么"在 trace 里天然嵌套可读——把控制流和可观测性 用同一结构表达出来。 -
三条出口顺序精心排列。
returnToolRequests/无工具的正常收尾在最前,maxTurns护栏 居中,中断在后(action.ts:509–532)。护栏放在"执行工具之前"检查(action.ts:515), 意味着超限时绝不会再多跑一次昂贵的工具。 -
currentTurn + 1 > maxTurns的边界很微妙但正确。 让"模型调用次数"比"工具执行轮数" 多一次:最后一轮允许模型再说话(可能收尾),只有它还要工具才判失控。 -
跨轮共享的
sharedPreviousChunks+ 每 chunk 快照previousChunks。 既让流连续 (action.ts:256、action.ts:438),又让每个 chunk 自带"前情",消费方无需自己维护状态。 -
两层 dispatch 复用同一个洋葱套路。
dispatchGenerate和dispatchModel(action.ts:259、action.ts:454)结构一模一样,只是链尾不同——一个 pattern,两个粒度。
6. 边界与局限(诚实)
-
maxTurns是硬上限,超了直接抛ABORTED(action.ts:515),不是"截断返回最后结果"。 想要"跑满就优雅收尾"得自己 catchGenerationResponseError并读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)编排 |
| 中断/HITL | interrupt 作为循环的第三出口(见第4章) | 有的靠"暂停并持久化整段会话状态" |
| 扩展点 | 两层洋葱中间件(generate 级 / model 级) | 有的只在"每次模型调用"一个粒度上给 hook |
细节参见本章的兄弟章节:Action 原语与注册表、 Plugin 系统与门面、工具执行 与中断、 结构化输出、会话与可观测性。
8. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 用户入口:发起循环 | js/ai/src/generate.ts | generate |
| 选项类型定义 | js/ai/src/generate.ts | GenerateOptions |
| options → action options | js/ai/src/generate.ts | toGenerateActionOptions |
| system/messages/prompt 合并 | js/ai/src/generate.ts | messagesFromOptions |
| 工具对象 → 引用字符串 | js/ai/src/generate.ts | toolsToActionRefs |
| 独立"预览请求"构造 | js/ai/src/generate.ts | toGenerateRequest |
| 循环上限错误类型 | js/ai/src/generate.ts | GenerationResponseError |
| 中间件归一化 | js/ai/src/generate.ts | normalizeMiddleware |
| 流式:回调转 AsyncIterable | js/ai/src/generate.ts | generateStream |
| 把循环注册成 util Action | js/ai/src/generate/action.ts | defineGenerateAction |
| 每轮套 trace span | js/ai/src/generate/action.ts | generateHelper |
| 搭建 generate 级中间件链 | js/ai/src/generate/action.ts | generateActionImpl / dispatchGenerate |
| 跑一轮 + 三出口判断 | js/ai/src/generate/action.ts | generateActionTurn |
| 解析 model/tools/format | js/ai/src/generate/action.ts | resolveParameters |
| 注入输出格式指令 | js/ai/src/generate/action.ts | applyFormat |
| model 级中间件链 | js/ai/src/generate/action.ts | dispatchModel |
| 每轮构造模型请求 | js/ai/src/generate/action.ts | actionToGenerateRequest |
| 流式 chunk 组装 | js/ai/src/generate/action.ts | makeChunk |
| 定义模型 Action | js/ai/src/model.ts | defineModel / model |
| 模型内建中间件注入 | js/ai/src/model.ts | getModelMiddleware |
| 中间件 hook 签名 | js/ai/src/generate/middleware.ts | GenerateMiddlewareDef |
| 中间件解析 | js/ai/src/generate/middleware.ts | resolveMiddleware |
| 能力补齐中间件实现 | js/ai/src/model/middleware.ts | downloadRequestMedia 等 |