☰
Genkit Dart 1.0 实战:Flutter 原生 AI Agent 编排指南
2026/10/13 13:56:55 网站建设 项目流程

做 Flutter 开发的这两年,我几乎每个 AI 功能都是这么过来的:先手写 HTTP 请求调大模型 API,再自己拼 prompt、解析 JSON、处理流式输出,遇到 Agent 多轮工具调用还要自己维护状态机。所以看到 Genkit Dart 1.0 发布的消息时,我的第一反应是——Flutter 原生的 AI Agent 终于不是"硬凑"出来的了。这篇内容适合所有想用 Dart 在 Flutter 里正经做 AI 应用、又不愿意维护一堆胶水代码的开发者,也适合那些一直在观望、不知道 Genkit 到底能干什么的人。我会结合自己跑通一个完整 Agent 的过程,把核心概念、最小接入代码、以及官方文档里不太容易注意到的坑一起讲清楚。

1. 为什么 Flutter 开发者等这个版本等了很久

1.1 在此之前,Flutter 做 AI 应用只能靠"手搓"

有一段时间我接到过不少 AI 相关的 Flutter 需求,最开始大家都是直接对着各家模型接口写 HTTP 调用。看起来只是"发个请求拿个文本",真做起来才发现要补的东西特别多。

第一层是基础的请求工程。鉴权头怎么拼、超时怎么设置、请求失败要不要重试、响应里的 usage 信息有没有用,这些每个项目都要重新来一遍。第二层是流式输出。很多模型走的是 SSE,也就是按数据块一行行推过来。你需要在 Flutter 里手动拆行、去掉前后空白、累积半行文本,还要把不同模型返回格式的差异兜住。第三层是上下文管理。所谓多轮对话不是每次都把整段历史砸给模型,而是要把 messages 按角色拼装、控制 token 长度,超了还要想办法截断或者做摘要。第四层是最容易劝退人的:工具调用。模型回答里出现一个"我想调用 getWeather 函数,参数是杭州",你的代码就得去解析这个意图、调用真实函数、再把函数结果拼回消息列表,然后重新请求模型。这个过程往往要循环好几轮,直到模型认为不需要再调用工具为止。

说白了,一个真正能用的 AI 功能背后,是大量和"AI"没关系、但绕不开的胶水代码。这些东西每个项目都不同,却又高度相似。Flutter 生态里一直没有一套统一的框架把这些事抽象好,于是每个团队都在重复造轮子,而且造出来的轮子还都不太圆。

1.2 Genkit 补的是"模型之外"的那块拼图

Genkit 不是某个大模型,而是一个 AI 编排框架。它真正处理的是模型调用之外、但你又不得不做的那些事。

第一,它做了一个统一的生成器接口。不管是接哪家云模型还是本地模型,业务代码里都是一个generate调用,模型路由通过配置切换。第二,它把"工具"抽象成了定义好的函数。模型需要查数据、调外部接口时,走的是固定的工具调用协议,框架帮你处理协商过程。第三,它提供了 Flow 的概念。一个 Flow 就是一个有输入、有输出、可观测、可部署的 AI 业务函数,你可以把它想象成一条流水线上的工位:入料是用户请求,出料是 AI 结果,中间可以有多个步骤。第四是贯穿始终的可观测性。框架会记录每一次模型调用的模型名称、提示词、耗时、token 消耗,甚至每一步 Flow 的入参出参,这让调试 AI 应用从"黑盒猜谜"变成了"打开仪表盘看日志"。

我习惯用一个类比来解释:大模型像一位能力很强但记性很差的厨师,Genkit 像后厨管理系统。你不用每次亲自帮他备菜(拼接上下文)、递调料(塞系统提示词)、等他喊人时再去叫帮厨(工具调用)。你只需要把菜单和食材安排清楚,后厨自己会把流程跑完,而且每道菜做了什么都记录在案。

Dart 1.0 的发布,等于把这一整套后厨搬进了 Flutter 应用内部。以前 Flutter 开发者想在业务里用 Genkit,只能通过它的 JS 版搭一个后端服务,Flutter 这边用 HTTP 去调;现在可以在 Dart 代码里直接定义 Flow、直接调用工具、直接接入模型,端侧和编排层是同一个语言。对原型验证、小型项目,以及不想为一个小功能单独养一个后端的团队来说,这个转变的意义比表面看起来大得多。

2. Genkit Dart 1.0 的核心能力拆解:生成、流式与工具调用

2.1 模型调用:从 Genkit 实例到一次 generate

不管之后要接多少能力,起点都是先创建一个 Genkit 实例,然后注册模型插件。下面是我在 Dart 侧第一个跑通的最小例子。

import 'package:genkit/genkit.dart'; import 'package:genkit_googleai/googleai.dart'; final ai = Genkit( plugins: [ googleAI(), ], ); Future<String> suggestMenu(String theme) async { final result = await ai.generate( model: 'googleai/gemini-2.0-flash', prompt: '给一个以$theme为主题的晚餐菜单,包含三道菜,用中文回答。', ); return result.text ?? ''; }

这段代码看着简单,但有一个设计值得注意:model 参数不是只写一个模型名,而是采用了"插件名/模型名"的命名空间形式。googleai是插件标识,gemini-2.0-flash是模型标识。这样做的直接好处是,一个项目里同时接入多家模型时不会撞名,切换模型时也只需要改字符串,业务代码不用动。

ai.generate返回的结果里,除了text,还带了生成过程的信息,比如消耗的 token、完成的请求元数据。这些信息在开发阶段用处很大,后面讲到可观测性时会体现出来。

2.2 流式输出:Flutter 的 Stream 与生成式 UI 是天生一对

大模型应用里,流式输出基本是标配。用户先看到一个光标的跳动,再看到文字逐字出现,体验上和"转圈等最终结果"完全不同。Genkit Dart 1.0 对这一点的处理非常贴合 Flutter 的编程习惯,它直接把流式生成暴露成了 Dart 的 Stream。

final stream = ai.generateStream( model: 'googleai/gemini-2.0-flash', prompt: '用三句话介绍 Flutter', ); await for (final chunk in stream) { print(chunk.text); }

在页面上,我可以用一个StreamBuilder把增量文本直接接到 UI 上。

StreamBuilder<String>( stream: outputStream, builder: (context, snapshot) { return Text(snapshot.data ?? ''); }, )

这里最省心的地方在于:你不需要在自己的代码里维护一个"已收到的完整文本"的缓冲变量,Stream 天然自带增量语义,每个 chunk 到来时都只处理当前这一段。StreamBuilder会在每次事件到来时重建文本组件,用户看到的文字会像打字机一样持续增长。

2.3 工具调用:让模型学会"先查一下,再回答"

模型本身的知识是有截止时间的,也不可能知道你的业务数据。要让 Agent 真正可用,必须让它具备"调用你提供的函数"的能力。Genkit Dart 里定义工具的方式如下。

final weatherTool = ai.defineTool( name: 'getWeather', description: '查询指定城市的实时天气,只在用户询问天气时使用。', inputSchema: { 'type': 'object', 'properties': { 'city': {'type': 'string'}, }, 'required': ['city'], }, handler: (city) async { // 这里调用你自己封装的天气服务,返回 JSON 友好的数据 return {'city': city, 'temperature_celsius': 26, 'condition': '晴'}; }, );

重点说一下机制。当你在generate里把tools: [weatherTool]传给模型后,模型本身并不会真的去查天气。它会根据自己的判断,返回一个类似"我需要调用 getWeather,参数是 {'city': '杭州'}"的请求。Genkit 框架收到这个请求后,才会去执行你定义的 handler,再把执行结果作为一条新的消息返回给模型,让模型继续组织回答。对业务代码来说,你只需要写好 handler,中间这轮协商流程框架都处理掉了。

这里有一个我在早期踩过的小坑:工具 handler 的返回值最好保持 JSON 友好的扁平结构。如果你把一个包含特殊类型对象的 Dart 实例返回给框架,某些模型的解析环节会出问题。后来我养成了习惯,凡是工具返回,一律组装成 Map 或 String,宁可多写两行转换代码,也不给后续序列化留隐患。

3. 从零跑通一个 Flutter AI 项目:最小示例全流程

3.1 依赖配置与密钥处理

在 pubspec.yaml 里,我加的是这三个依赖。

dependencies: genkit: ^1.0.0 genkit_googleai: ^1.0.0 genkit_flutter: ^1.0.0

genkit是核心库,genkit_googleai提供模型接入,genkit_flutter负责 Flutter 侧的集成支持,比如调试面板和连接管理。如果你用的是其他模型服务,可以把后两个换成对应的插件包。

密钥这块,我强烈建议不要写在代码里,也不要写进配置文件后提交到仓库。Flutter 项目里最省事的做法是用--dart-define在启动时注入。

flutter run --dart-define=GOOGLE_API_KEY=你的密钥

代码里通过String.fromEnvironment('GOOGLE_API_KEY', defaultValue: '')读取。这样密钥不会进仓库,也不怕被反编译翻出来,至少在常规的安全审查层面算及格。

3.2 页面上跑通一次"输入问题,输出回答"

下面是我用来做冒烟测试的完整流程。核心就三步:定义一个 Flow、在按钮回调里调用它、把结果展示到页面。

class _ChatPageState extends State<ChatPage> { final _controller = TextEditingController(); String _answer = ''; late final Genkit _ai = createGenkit(); late final _quickFlow = _ai.defineFlow( 'quickAnswer', (String question) async { final result = await _ai.generate( model: 'googleai/gemini-2.0-flash', prompt: question, ); return result.text ?? ''; }, ); Future<void> _ask() async { setState(() => _answer = '思考中...'); final answer = await _quickFlow(_controller.text); setState(() => _answer = answer); } // build 方法里放输入框、按钮和 _answer 的展示 }

这个例子故意没有拆得太细,目的是先把"Flow 从定义到调用"的闭环跑通。你在 real world 里会发现,Flow 这个抽象最好的地方在于:定义的时候看起来只是一个函数,但它天然带上了可观测、可部署的属性。同一个 Flow,你在 Flutter 里可以像本地函数一样调用,未来也可以搬到服务端跑,接入层不用改。

3.3 Genkit 实例的初始化:别一股脑塞进 main()

有同学会习惯把所有东西都放到 main() 里初始化,然后全局变量到处用。Genkit 的初始化本身不重,但它牵扯到插件加载、模型鉴权,在 Flutter 的多页面场景里,我更建议用一个服务类或依赖注入容器来持有 Genkit 实例。

原因有两个。第一,测试的时候方便替换。写单测时你不会真想去打一次模型接口,如果把 Genkit 实例放在全局变量里,想 mock 就很难;通过构造参数传入一个 fake,测试就好写得多。第二,不同页面可能需要不同的模型或工具集,全局只有一个实例反而不好扩展。我自己习惯把模型选择、插件列表都收敛在配置层,页面代码只依赖一个已经配好的实例,同时通过构造函数传入,保持可替换性。

4. 把演示变成真 Agent:多轮工具调用的完整实现

4.1 场景设计:一个会查数据的售前助手

前面那些最小示例还只是"模型单次回答",离 Agent 还有一段距离。我这次跑通的是一个售前助手场景:用户会问"某商品有货吗?今天下单能发货吗?适不适合当前季节用?"这类问题。模型本身不知道库存、不知道物流规则,所以它必须借助两个工具:一个查库存,一个查配送政策,拿到数据后再给出最终答复。

这个场景看起来不大,但它覆盖了 Agent 的核心形态:模型不再是一次性生成最终答案,而是先尝试规划——"我需要查一下库存,再查一下配送政策,然后结合两者回答用户"。

4.2 用循环处理 toolCalls:让模型"边查边想"

在 Dart 侧,多轮工具调用的核心逻辑并不复杂:把用户消息放进消息列表,循环调用模型,只要模型返回了工具调用请求,就执行对应工具并把结果追加回消息列表,直到模型不再要求调用工具为止。下面是我抽出来的核心代码,类名和调用方式按 1.0 版本的语义做了简化示意,具体以你安装的包为准,重点是理解这个循环结构。

Future<String> runAgent(Genkit ai, String question) async { final messages = <ModelMessage>[ ModelMessage.user(question), ]; for (var turn = 0; turn < 6; turn++) { final response = await ai.generate( model: 'googleai/gemini-2.0-flash', messages: messages, tools: [checkStockTool, getShippingPolicyTool], ); final calls = response.output?.toolCalls ?? []; if (calls.isEmpty) { return response.text ?? ''; } for (final call in calls) { final result = await call.tool.handler(call.input); messages.add(ModelMessage.toolResult(call.name, result)); } } return '处理次数超过上限,请简化问题后重试'; }

这段代码里有几个容易忽略的细节。第一,我把最大轮数限制成 6 次。别小看这个数字,没有上限的话,一旦模型陷入"反复调用工具却不给结论"的循环,会浪费大量 token 和时间。第二,每轮执行完工具,一定要把工具结果以toolResult的形式追加回 messages。这是模型继续推理的关键,很多人第一次实现 Agent 会漏掉这一步,导致模型完全没有上下文继续。第三,工具 handler 执行本身也可能出错,比如查库存接口超时,这种情况最好返回一个明确的错误信息字符串给模型,让模型知道"查询失败"而不是凭空想象一个库存数字。

4.3 对话历史的持久化与 token 控制

演示版本里把 messages 放在内存里没问题,但真实产品里用户很可能隔一天回来继续问。我的做法是:需要长期保存的轮次写入后端数据库,只存关键字段(角色、内容、时间戳、token 数),本地只保留一个用于快速展示的缓存。

更关键的是 token 长度控制。多轮对话历史是文本增长最快的部分,如果每次请求都把全部历史发给模型,很快会顶到上下文上限。我现在用的是"最近 10 轮完整消息 + 一份压缩摘要"的策略:超过 10 轮后,把更早的对话用模型压缩成一段摘要,作为一条 system 消息放在最前面。这样做既保留了长期记忆,又不会让请求体无限膨胀。

5. 与 JS 版对比,以及我踩过的几个坑

5.1 Dart 1.0 已覆盖哪些能力,还缺哪些

Genkit 最早从 JS 生态起家,Dart 1.0 在设计上其实是"对标主流能力"而非"全量复刻"。我梳理了一下两者在我实际使用中的差别。

能力JS 版Dart 1.0
模型生成与流式完整完整
Flow 定义与调用完整完整
工具定义与调用完整完整
RAG / Retriever完整已支持
评测 Evaluator丰富起步阶段
服务端部署成熟需要额外配置
插件生态多主流模型够用

如果你是 Flutter 团队,主战场在端侧,Dart 1.0 已经覆盖了日常最需要的部分。如果要做非常复杂的评测流水线、或者要重度扩展第三方插件,JS 版仍然是功能更全的地方。这不是说 Dart 版本弱,而是它选择优先把端侧体验做扎实。

5.2 坑一:工具入参 schema 的 JSON 兼容性

我在定义第二个工具时就遇到了问题。第一次写工具时,我把入参定义成了一个嵌套比较深的对象,结果模型调用工具时传过来的参数有时候缺字段,有时候多出奇怪类型。排查半天,根源出在 Dart 侧的 JSON Schema 自动生成上:框架会尽量把你的 schema 声明转成模型要求的标准格式,但嵌套结构在个别模型上解析并不兼容。

我的解决办法有两个。第一,把工具的入参设计成扁平的简单类型,能用字符串就绝不用嵌套对象。第二,工具需要的数据如果比较复杂,不要让模型来提供,而是让 handler 自己去数据层查。比如"查商品"这个工具,模型只需要传一个商品 ID,其余信息由 handler 内部去数据库补齐。

5.3 坑二:模型名写错,会得到一个绕远路的错误

model参数必须带插件前缀,这个我一开始没在意。某次我把googleai/gemini-2.0-flash写成了gemini-2.0-flash,得到的是一个相对绕的错误信息,不是直白的"模型不存在"。如果你也遇到模型相关但提示又不太对劲的报错,先查一下模型名是不是带上了插件前缀。

有时问题不在前缀,而在模型权限。同样的模型名,在不同账号、不同套餐下可访问范围不一样,运行时表现可能是 404,也可能是配额不足。排查这类问题最有效的路径就是打开调试面板,看真实请求里到底发给了谁。

5.4 坑三:Flutter 里的流式输出生命周期

这可能是 Flutter 特有的坑。generateStream返回的 Stream 在页面销毁后不会自动取消,如果不处理,轻则内存泄漏,重则出现"组件销毁后还在 setState"的警告。我在项目里把生成流统一封装在页面级的控制器里,页面销毁时主动 cancel,按钮在生成过程中也置灰,防止用户重复点击开多条流。

另一个相关问题是 App 切后台。移动端进入后台后,网络流可能被系统挂起,导致已经开始的流式输出迟迟不结束。如果业务要求 Agent 在后台也能继续跑,建议把执行放到服务端 Flow 上,Flutter 端只是订阅结果;前台短会话才适合直接在端侧跑完整循环。

5.5 坑四:本地调试面板怎么连

用过 JS 版 Genkit 的同学会习惯用命令打开一个本地调试界面,看每一次调用的耗时、输入输出。Dart 侧也有类似能力,但连接的配置比 JS 版要多几步。我这次的体会是:先在 Flutter 里把genkit_flutter的调试组件开启,确认能收集到调用记录,再决定是否要把它暴露给远程调试。如果配置不对,最常见的结果是页面一片空白,这时候先检查本机网络和调试端口的绑定,而不是怀疑框架本身。

6. 落地路线与我的个人建议

6.1 我建议的三周上手路径

如果你现在准备在 Flutter 项目里引入 Genkit Dart 1.0,我建议按照这个节奏来,别急着一步到位。

第一周,只做"生成 + 流式 + 一个工具"。选一个业务里最不起眼的场景,比如关键词回复助手,把generate、generateStream、defineTool三个核心 API 跑通。第二周,做完整的 Agent 循环。把多轮工具调用的循环结构写出来,至少覆盖两个工具,并且把对话历史持久化策略定下来。第三周,接入 RAG 和部署。把项目里的常见问题文档做分块和向量化,用检索器增强模型回答;同时评估哪些 Flow 需要放到服务端,把端侧和部署的边界划清楚。

这三周不要反过来。第一周就上 RAG,或者一上来就写五个工具,出问题时根本分不清是模型的问题、工具的问题还是上下文的问题。从最小闭环逐步叠加,排查起来会非常有条理。

6.2 什么时候不该用 Genkit Dart

框架再好也不能包打天下。如果你的需求只是"调一次模型拿一段文字",直接写个 HTTP 调用反而更轻,没必要为一次调用引入整套编排框架。如果你的团队已经有一套成熟的 Python 或 Node AI 服务,业务里绝大部分 Agent 逻辑都在服务端,那 Flutter 端保留轻量客户端即可,没必要为了"统一语言"强行把编排迁到 Dart。最后一个提醒:高并发、严苛稳定的服务端生产场景,Dart 侧目前的生态和部署经验还比不上 JS 或 Go,这时候服务端仍是正确的主场。

6.3 把提示词从代码里拆出来

Genkit Dart 1.0 用起来舒服,但有一个习惯我必须建议你早点养成:把 prompt 模板从代码里抽出去,放到单独的文件或配置里维护。我做过的 AI 应用里,几乎所有迭代都发生在提示词的调整上,很少发生在模型调用代码本身。提示词一旦和业务代码混在一起,每次调一个词都要改代码、走发布,效率和风险都很差。

我现在的做法是:每个 Flow 配一个提示词模板文件,模板里用占位符标注动态内容,比如"商品信息:{{productInfo}}"。需要调整时直接改模板,甚至可以做不同版本的对比测试。这也是后面做模型评测的基础——先有一个稳定的输入工程层,才谈得上系统评估效果。

最后说点个人体会。我在实际接入 Genkit Dart 1.0 时最舒服的一点,不是某个 API 多好用,而是终于有一个稳定的框架帮我把"一次模型调用"和"一个业务能力"对应起来。我建议你先别急着做完整 Agent,先拿一个 Flow 接一个工具跑通闭环,跑通了再叠加多轮循环、再叠加检索。这种从简单到复杂的节奏,会比你一口气堆完所有能力舒服得多,也更容易在出错时定位问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询