生产环境里的 AI 应用,最让人头疼的不是模型本身,而是围绕模型调用的一堆"外围问题"——调用失败要不要重试?工具执行结果要不要做安全过滤?生成循环跑了 50 步还没停,谁来掐断?Google 最近为 Genkit 引入了 Middleware 架构,正是瞄准这些痛点:在模型调用、工具执行、生成循环这三条关键路径上,加了一层可编程的拦截层,让开发者用代码而不是配置文件来掌控可靠性、安全性和编排逻辑。
拦截层拦截的是什么
Genkit 的中间件不是传统 Web 框架里那种"请求→响应"的管道。它拦截的是 AI 应用内部的三个核心动作:
- 模型调用(Model Call)——发送 prompt 到 LLM、接收生成文本的过程
- 工具执行(Tool Execution)——Agent 调用外部工具(搜索、数据库查询、API 调用等)的过程
- 生成循环(Generation Loop)——Agent 反复"思考→行动→观察"的迭代过程
每一条路径都可以被中间件"包住",在动作发生前和发生后各插入一段逻辑。这意味着你可以在模型返回结果之后、但结果还没交给下游之前,做一轮敏感信息扫描;也可以在工具执行之前校验参数合法性,避免 Agent 把畸形参数丢给你的生产数据库。
写一个最简中间件
Genkit 中间件的核心 API 风格接近 Express.js 的 use 模式——定义一个函数,接收上下文和 next,在合适的位置调用 next() 让流程继续。下面是一个 TypeScript 示例,展示如何给模型调用加一层计时与日志:
import { genkit, z } from 'genkit';
import { googleAI, gemini15Flash } from '@genkit-ai/googleai';
const ai = genkit({
plugins: [googleAI()],
model: gemini15Flash,
});
// 定义中间件:记录每次模型调用的耗时和输入 token 数
const timingMiddleware = ai.middleware({
// 拦截模型调用
onModelCall: async (ctx, next) => {
const start = Date.now();
console.log(`[middleware] 模型调用开始, prompt 长度: ${ctx.prompt.length}`);
// 调用 next() 让模型真正执行
const result = await next(ctx);
const elapsed = Date.now() - start;
console.log(`[middleware] 模型调用完成, 耗时 ${elapsed}ms, 输出长度: ${result.text.length}`);
// 可以修改结果后再返回
return result;
},
});
// 把中间件挂载到 flow 上
const chatFlow = ai.defineFlow(
{
name: 'chat',
inputSchema: z.string(),
outputSchema: z.string(),
middleware: [timingMiddleware], // 关键:注册中间件
},
async (input) => {
const { text } = await ai.generate({ prompt: input });
return text;
}
);
// 本地运行测试
async function main() {
const response = await chatFlow('用三句话解释什么是向量数据库');
console.log('最终输出:', response);
}
main();
运行前需要安装依赖并配置 Google AI API Key:
npm install genkit @genkit-ai/googleai
export GOOGLE_API_KEY="your-api-key-here"
npx genkit start # 启动本地开发 UI,或在代码中直接调用 main()
这段代码的关键在于 middleware: [timingMiddleware]——中间件不是全局生效的,而是挂载在具体的 flow 上,粒度可控。
三个实战拦截模式
单纯的计时只是入门。真正在生产里发挥作用的是以下三种模式:
模式一:安全过滤——拦截模型输出中的敏感信息
const safetyMiddleware = ai.middleware({
onModelCall: async (ctx, next) => {
const result = await next(ctx);
// 对模型输出做敏感信息扫描
const sensitivePatterns = [/密码是\s*\S+/gi, /API[_-]?key\s*[:=]\s*\S+/gi];
let filteredText = result.text;
for (const pattern of sensitivePatterns) {
filteredText = filteredText.replace(pattern, '[已过滤]');
}
// 返回过滤后的结果,下游完全不知道原始输出包含敏感内容
return { ...result, text: filteredText };
},
});
这种模式的价值在于:安全逻辑和业务逻辑解耦。你不需要在每个 flow 里手写过滤代码,中间件自动生效。
模式二:工具执行守卫——防止 Agent 调用危险工具或传入非法参数
const toolGuardMiddleware = ai.middleware({
onToolExecution: async (ctx, next) => {
// ctx 包含工具名称和输入参数
const blockedTools = ['dropDatabase', 'deleteAllUsers'];
if (blockedTools.includes(ctx.toolName)) {
throw new Error(`工具 ${ctx.toolName} 已被安全策略禁止调用`);
}
// 对参数做基本校验
if (ctx.toolName === 'queryDatabase' && ctx.args.sql?.includes('DROP')) {
throw new Error('SQL 参数包含危险操作,已拦截');
}
console.log(`[tool-guard] 允许执行工具: ${ctx.toolName}`);
return await next(ctx);
},
});
Agent 的自主性越强,工具调用的风险越高。中间件让你在不修改工具定义的前提下,从外部加一层防护网。
模式三:生成循环限流——防止 Agent 无限迭代
const loopLimitMiddleware = ai.middleware({
onGenerationLoop: async (ctx, next) => {
// ctx 中可以获取当前迭代步数
const MAX_STEPS = 20;
if (ctx.stepCount >= MAX_STEPS) {
console.warn(`[loop-limit] Agent 已迭代 ${ctx.stepCount} 步,强制终止`);
return { text: '任务执行超时,已自动终止。请简化需求或检查工具返回。', stop: true };
}
return await next(ctx);
},
});
这是生产环境里最实用的拦截之一。Agent 在复杂任务中陷入循环是常见故障,中间件比在 flow 逻辑里硬编码 while (step < 20) 更干净——限流策略可以独立测试、独立替换。
组合使用与优先级
中间件可以组合,按数组顺序依次执行,形成洋葱模型:
const secureChatFlow = ai.defineFlow(
{
name: 'secureChat',
inputSchema: z.string(),
outputSchema: z.string(),
middleware: [loopLimitMiddleware, toolGuardMiddleware, safetyMiddleware, timingMiddleware],
},
async (input) => {
const { text } = await ai.generate({
prompt: input,
tools: [queryDatabaseTool, searchTool], // Agent 可调用工具
maxTurns: 30, // 生成循环最大轮次(中间件可以进一步收紧)
});
return text;
}
);
执行顺序:请求阶段从外到内(loopLimit → toolGuard → safety → timing),响应阶段从内到外。这和 Express/Koa 的洋葱模型一致,有 Web 开发经验的人上手很快。
采用建议与边界
适合用中间件的场景: - 需要统一的安全策略(敏感信息过滤、工具调用权限) - 需要可观测性(调用日志、耗时统计、token 用量追踪) - 需要可靠性兜底(重试、限流、熔断) - 多个 flow 共享相同的前置/后置逻辑
不适合用中间件的场景: - 只在单个 flow 里出现一次的简单逻辑——直接写在 flow 里更直观 - 需要根据用户输入动态决定是否拦截的场景——中间件更适合静态策略,动态判断会让代码难读
需要注意的边界: - 中间件目前是 Genkit 的新特性,API 可能还在迭代,生产使用前务必锁定版本号 - 拦截模型输出做修改时,要考虑下游是否依赖原始格式(比如 JSON 结构化输出被中间件改了文本,下游解析可能出错) - 工具守卫中间件抛出异常后,Agent 的错误恢复策略需要提前设计——是重试、换工具、还是直接终止?
Genkit 中间件解决的不是"模型不够聪明"的问题,而是"聪明模型在生产环境里不可控"的问题。如果你正在用 Genkit 构建 Agent 系统,建议先从 timingMiddleware 入手跑通管道,再逐步叠加安全和限流逻辑——每加一层中间件,就多一层可独立测试的防护,这才是它最大的工程价值。