1. 从"能跑"到"好用":Genkit Dart 1.0 到底补上了哪块拼图
如果你最近一年在用 Flutter 做移动端或者桌面端应用,大概率动过"把 AI 能力塞进 App"的念头。但真正动手之后你会发现,事情远没有想象中那么顺:调用大模型接口的代码散落在各个页面里,提示词硬编码在业务逻辑中间,多轮对话的状态管理全靠自己手搓,流式输出和 UI 刷新之间的配合更是让人头大。更别提工具调用、结构化输出、多 Agent 协作这些稍微进阶一点的需求了,基本上等于从零造轮子。
Genkit Dart 1.0 的发布,解决的正是这个"最后一公里"的问题。它不是又一个模型 API 的封装库,而是一套面向 Dart 和 Flutter 生态的 AI 应用开发框架。你可以把它理解成 AI 应用领域的"脚手架 + 标准件仓库":脚手架帮你把项目结构搭好,标准件让你不用每次都重新发明提示词管理、工具注册、流程编排这些东西。
这个版本之所以值得单独拿出来说,核心在于"1.0"这个信号。在此之前,Genkit 的 Dart 支持一直处于实验阶段,API 不稳定、文档零散、Flutter 端的适配更是语焉不详。1.0 意味着接口冻结、语义化版本承诺、以及官方明确把 Flutter 作为一等公民来对待。对于正在做技术选型的团队来说,这是一个可以放心押注的时间点。
这篇文章适合三类人看:第一类是有 Flutter 开发经验、想往 AI 方向延伸的移动端工程师;第二类是在做 AI 产品、纠结前端技术栈的全栈开发者;第三类是对 Agent 架构感兴趣、想找一个轻量级落地框架的技术负责人。我会从实际使用的角度出发,把 Genkit Dart 1.0 的核心能力拆开讲清楚,包括它解决了什么问题、关键 API 怎么用、Flutter 集成时有哪些坑、以及我在实际项目中总结出来的一些经验。
需要提前说明的是,Genkit 本身是一个跨语言的框架,JavaScript/TypeScript 版本已经相对成熟,Dart 版本在 1.0 之后才真正具备生产可用性。所以本文的视角会聚焦在 Dart 和 Flutter 这一侧,不会花太多篇幅去对比其他语言生态。
2. Genkit Dart 的核心抽象:为什么是这四个概念
2.1 Flow:把 AI 调用从"散装代码"变成"可测试单元"
在没有框架的情况下,一个典型的 AI 功能调用长这样:在某个 Widget 的按钮回调里,拼好提示词,调用 HTTP 接口,拿到结果,setState 刷新 UI。这段逻辑和 UI 耦合在一起,没法单独测试,没法复用,改一个提示词要翻半天代码。
Genkit 的 Flow 抽象就是冲着这个问题来的。一个 Flow 本质上是一个有名字、有输入输出类型定义的函数,它把 AI 调用的完整链路封装成一个独立单元。你可以把它想象成后端开发里的 Service 层,只不过这个 Service 专门处理 AI 相关的逻辑。
final recipeFlow = ai.defineFlow( name: 'recipeFlow', inputSchema: Schemas.string(), outputSchema: Schemas.string(), fn: (topic, context) async { final response = await ai.generate( prompt: '用简单易懂的方式介绍$topic的做法', ); return response.text; }, );这段代码看起来简单,但背后的价值在于:Flow 可以被单独调用、单独测试、单独部署。在 Flutter 里,你的 Widget 只需要调用recipeFlow.call(input),完全不关心内部用的是哪个模型、提示词怎么写的。这种解耦在项目规模变大之后会救命。
2.2 Tool:让模型知道"它能做什么"
工具调用是 Agent 能力的基石。简单说,就是你把一些函数注册给模型,模型在需要的时候会主动要求调用这些函数。比如你注册一个"查询天气"的工具,用户问"明天出门要带伞吗",模型会判断需要先查天气,然后返回一个工具调用请求,你的代码执行完再把结果喂回给模型。
Genkit 的 Tool 定义方式很直观:
final weatherTool = ai.defineTool( name: 'getWeather', description: '查询指定城市的当前天气', inputSchema: Schemas.object({ 'city': Schemas.string(), }), outputSchema: Schemas.string(), fn: (input, context) async { // 实际调用天气服务的逻辑 return '晴,25摄氏度'; }, );这里有个容易被忽略的细节:description字段不是给人看的注释,而是给模型看的。模型会根据这个描述来判断什么时候该调用这个工具。描述写得含糊,模型就可能在该调用的时候不调用,或者在不该调用的时候乱调用。我在实际项目里踩过这个坑,后面会详细说。
2.3 Retriever 与 Indexer:RAG 不是可选项,是标配
检索增强生成(RAG)现在已经不是什么新鲜概念了,但真正把它做扎实的项目并不多。Genkit 把 RAG 拆成了两个部分:Indexer 负责把文档切块、向量化、存进向量库;Retriever 负责根据查询语句召回相关文档块。
这个拆分的好处是职责清晰。Indexer 通常在数据准备阶段跑一次,Retriever 在每次用户查询时跑。两者可以独立替换实现,比如你从内存向量库换成外部向量数据库,只需要改 Indexer 和 Retriever 的实现,上层 Flow 不用动。
2.4 Plugin:模型和平台的适配层
Genkit 本身不绑定任何模型提供商。你想用哪家的模型,就装对应的 Plugin。这种设计在 Dart 生态里尤其重要,因为移动端和桌面端对模型的选择可能完全不同:移动端可能更倾向于端侧小模型或者云端轻量模型,桌面端可能可以用更大的模型。
Plugin 体系还负责处理不同模型之间的 API 差异。比如有的模型支持结构化输出,有的不支持;有的支持流式,有的只支持一次性返回。Genkit 在 Plugin 层做了归一化,上层代码不用关心这些差异。
3. Flutter 集成实战:从零搭一个带工具调用的对话页面
3.1 环境准备与依赖选择
先说版本要求。Genkit Dart 1.0 需要 Dart SDK 3.0 以上,Flutter 版本建议 3.10 以上。如果你还在用更老的版本,升级是第一步,没有商量余地。
依赖方面,核心包是genkit,然后根据你用的模型选对应的 Plugin 包。以常见的云端模型为例:
dependencies: genkit: ^1.0.0 genkit_google_genai: ^1.0.0 flutter_markdown: ^0.6.0这里我特意加了flutter_markdown,因为 AI 返回的内容经常带 Markdown 格式,直接显示纯文本会很难看。这个细节在官方文档里不会强调,但实际做产品的时候是必须处理的。
初始化代码通常放在main.dart或者一个单独的ai_config.dart里:
import 'package:genkit/genkit.dart'; import 'package:genkit_google_genai/genkit_google_genai.dart'; final ai = Genkit( plugins: [ googleAI(apiKey: const String.fromEnvironment('GOOGLE_AI_KEY')), ], model: googleAI.gemini15Flash, );注意:API Key 千万不要硬编码在代码里。用
String.fromEnvironment配合编译期变量,或者用flutter_dotenv从.env文件读取。我见过不止一个项目把 Key 直接写在源码里然后提交到了公开仓库。
3.2 对话状态管理:别用 setState 硬扛
对话类界面的状态比普通页面复杂得多。你需要管理消息列表、当前是否在生成中、流式输出的增量内容、工具调用的中间状态等等。用setState不是不行,但代码会迅速变得难以维护。
我的建议是把对话状态抽成一个独立的ChangeNotifier或者用 Riverpod 的StateNotifier。核心状态大概长这样:
class ChatState { final List<ChatMessage> messages; final bool isGenerating; final String? streamingText; final List<ToolCallRecord> toolCalls; }streamingText和messages分开存是有原因的。流式输出的时候,每个 token 都会触发 UI 刷新,如果每次都往messages列表里塞,列表会频繁重建,性能很差。正确做法是流式过程中只更新streamingText,等生成结束后再把它作为一个完整的消息追加到messages里。
3.3 流式输出的 UI 处理
Genkit 的流式 API 返回的是一个 Stream,你可以用StreamBuilder或者手动监听。手动监听更灵活,推荐这种方式:
final stream = ai.generateStream( prompt: userInput, tools: [weatherTool, searchTool], ); await for (final chunk in stream) { if (chunk.text != null) { state = state.copyWith( streamingText: state.streamingText + chunk.text!, ); } if (chunk.toolRequests.isNotEmpty) { // 处理工具调用 } }这里有个性能优化点:不要每个 chunk 都触发一次notifyListeners()。可以做一个简单的节流,比如每 50 毫秒合并一次更新。在低端安卓设备上,这个优化能让滚动流畅度提升一个档次。
3.4 工具调用的完整链路
工具调用是 Agent 能力的核心,也是集成时最容易出问题的地方。完整的链路是这样的:
- 用户发送消息
- 模型判断需要调用工具,返回工具调用请求
- 你的代码执行工具函数,拿到结果
- 把工具结果连同之前的对话历史一起发回给模型
- 模型根据工具结果生成最终回复
Genkit 在 Flow 层面可以自动处理这个循环,但如果你用的是底层的generateStream,就需要自己管理这个循环。我的建议是:简单场景用 Flow 自动处理,复杂场景(比如需要人工确认工具调用)才手动管理。
final response = await ai.generate( prompt: userInput, tools: [weatherTool], toolChoice: ToolChoice.auto, ); if (response.toolRequests.isNotEmpty) { for (final request in response.toolRequests) { final result = await executeTool(request); // 把结果喂回去 } }提示:工具函数的执行时间要控制好。如果一个工具需要几秒钟才能返回,用户会看到界面卡住。建议给工具执行加超时,超时后返回一个友好的错误信息,让模型基于错误信息继续对话。
4. 那些文档里不会写的坑:我在实际项目中踩过的
4.1 工具描述写得太"技术",模型根本不调用
这是我踩的第一个坑,也是最隐蔽的一个。我写了一个查询订单状态的工具,描述写的是"根据订单 ID 查询订单当前状态"。测试的时候发现,用户问"我的快递到哪了",模型压根不调用这个工具,而是直接编了一个回答。
问题出在描述上。"订单"这个词在模型看来和"快递"不是一回事。后来我把描述改成"查询用户的包裹配送状态,包括已发货、运输中、已签收等信息",调用率立刻上来了。
经验总结:工具描述要用用户可能说的词,而不是你系统内部的术语。多写几个同义词,把使用场景描述清楚。这个投入产出比极高,改几个字就能显著提升 Agent 的可用性。
4.2 结构化输出在流式模式下会"断片"
Genkit 支持让模型返回结构化的 JSON 数据,这在需要把结果直接映射成 Dart 对象时非常有用。但如果你同时开了流式输出,会发现一个问题:流式返回的 JSON 片段是不完整的,你没法在每个 chunk 到达时就解析。
正确的做法是:流式模式下只做展示,等流结束后再解析完整 JSON。或者干脆结构化输出的场景就别用流式,两者本身就有矛盾。我在一个表单填充功能里遇到过这个问题,用户看到的是半截 JSON 字符串,体验很糟糕。后来改成非流式,加一个 loading 动画,反而更清爽。
4.3 多轮对话的历史管理比想象中复杂
模型本身是无状态的,每次调用你都需要把完整的历史传进去。但历史不能无限增长,否则 token 消耗会爆炸,而且模型对超长上下文的处理能力也会下降。
我的做法是维护一个滑动窗口:保留最近 N 轮对话,同时把更早的对话做摘要压缩。Genkit 没有内置这个机制,需要自己实现。一个简单的策略是:
- 最近 5 轮:完整保留
- 5 到 20 轮:只保留用户消息和模型的最终回复,去掉工具调用等中间过程
- 20 轮以上:生成一段摘要,替换掉原始消息
这个策略在大多数场景下够用了。如果你的应用对上下文连续性要求特别高,可以考虑用向量检索的方式召回相关历史,但这会显著增加复杂度。
4.4 Flutter Web 上的 CORS 问题
如果你用 Flutter Web 做调试,直接调用模型 API 会遇到跨域问题。这不是 Genkit 的问题,是浏览器的安全策略。解决方案有两个:一是用 Genkit 的服务端部署能力,把 Flow 部署成一个 HTTP 服务,Flutter Web 调用自己的服务;二是在开发阶段用浏览器的跨域插件临时绕过。
生产环境肯定要用第一种方案。这也引出一个架构建议:不要把模型 API Key 放在客户端。正确的做法是客户端调用你自己的后端服务,后端再调用模型。Genkit 的 Flow 可以很方便地部署成 HTTP 端点,这个能力在 1.0 里已经比较完善了。
5. 从单 Agent 到多 Agent:Genkit 的编排能力边界
5.1 什么时候需要多 Agent
单 Agent 能做的事情有限。当你的应用需要处理多种类型的任务时,比如一个客服系统既要回答产品问题、又要处理退换货、还要做技术支持,把所有工具都塞给一个 Agent 会导致两个问题:一是工具太多,模型选择困难;二是提示词太长,成本和延迟都上去了。
多 Agent 的思路是分工:一个路由 Agent 负责判断用户意图,然后把请求分发给对应的专业 Agent。每个专业 Agent 只带自己需要的工具和提示词,职责单一,效果更好。
5.2 Genkit 里的多 Agent 实现方式
Genkit 没有提供现成的"多 Agent 框架",但用 Flow 组合可以实现。基本模式是:
final routerFlow = ai.defineFlow( name: 'router', inputSchema: Schemas.string(), outputSchema: Schemas.string(), fn: (input, context) async { final intent = await classifyIntent(input); switch (intent) { case 'product': return await productAgent.call(input); case 'refund': return await refundAgent.call(input); default: return await generalAgent.call(input); } }, );这种方式的优点是简单直接,每个 Agent 都是独立的 Flow,可以单独测试和优化。缺点是路由判断本身也是一次模型调用,会增加延迟。优化思路是用一个轻量模型做路由,或者用规则引擎先做一层粗筛。
5.3 多 Agent 的常见陷阱
第一个陷阱是路由错误。如果路由 Agent 判断错了意图,后面的专业 Agent 再强也没用。我的经验是给路由 Agent 留一个"不确定"的出口,当置信度低的时候走通用 Agent,而不是强行分类。
第二个陷阱是上下文丢失。用户在跟退款 Agent 对话的过程中突然问了一个产品问题,如果直接切到产品 Agent,之前的退款上下文就丢了。解决方案是在 Agent 之间传递一个共享的上下文对象,或者让路由 Agent 在切换时生成一个上下文摘要。
第三个陷阱是无限循环。Agent A 调用 Agent B,Agent B 又调用 Agent A,如果没有终止条件就会死循环。一定要设置最大调用深度,超过就返回兜底回复。
6. 性能与成本:移动端 AI 应用的现实约束
6.1 延迟优化的几个实操手段
移动端用户对延迟的容忍度很低。一个 AI 功能如果响应超过 3 秒,用户就会觉得卡。优化延迟的手段按性价比排序:
第一,用流式输出。首 token 到达时间通常比完整响应时间短得多,用户看到内容开始出现,感知延迟会大幅降低。这是投入产出比最高的优化。
第二,精简提示词。提示词越长,模型处理时间越长。定期审查你的提示词,删掉冗余的描述和示例。我见过一个项目提示词写了 2000 多字,精简到 500 字后效果没变,延迟降了将近一半。
第三,工具调用并行化。如果模型一次请求了多个工具,而这些工具之间没有依赖关系,就并行执行。Genkit 的Future.wait可以搞定。
第四,缓存。对于相同或相似的查询,缓存模型响应。注意缓存 key 的设计,不能只用用户输入做 key,还要考虑对话历史的影响。
6.2 Token 消耗的控制策略
Token 就是钱,移动端应用的用户量大,成本控制不好会很难受。几个实用的策略:
- 设置
maxTokens上限,防止模型生成过长的回复 - 对话历史做摘要压缩,不要无限制传递
- 简单任务用轻量模型,复杂任务才用大模型
- 工具返回的结果要精简,不要把整个数据库记录都塞回去
我在一个项目里做过统计,光是对话历史压缩这一项,就把平均 token 消耗降低了 40% 左右。这个优化不需要改任何业务逻辑,纯粹是历史管理策略的调整。
6.3 离线与弱网场景的处理
移动端绕不开弱网问题。Genkit 本身不提供离线能力,但你可以做几件事来改善体验:
一是本地缓存常见问题的回答。用户问"怎么退货",这种高频问题可以直接从本地缓存返回,不用调模型。
二是请求失败时的降级策略。网络超时后,给用户一个明确的提示,而不是一直转圈。同时把用户输入暂存,等网络恢复后自动重试。
三是考虑端侧小模型。对于一些简单的分类、抽取任务,端侧模型完全够用,而且没有网络延迟。Genkit 的 Plugin 体系理论上支持接入端侧模型,但这块生态还在发展中,需要自己做一些适配工作。
7. 我的选型建议与后续演进方向
如果你正在评估要不要在 Flutter 项目里用 Genkit Dart,我的判断是这样的:如果你的 AI 功能只是偶尔调用一下模型 API,逻辑简单,那直接写 HTTP 请求也没什么问题,引入框架反而增加复杂度。但如果你的应用涉及多轮对话、工具调用、RAG 检索、或者多 Agent 协作中的任何一项,Genkit 带来的结构化和可维护性提升是值得的。
1.0 版本之后,Genkit Dart 的 API 已经稳定,社区也在逐渐活跃。我比较期待的几个方向:一是端侧模型的官方支持,现在这块还需要自己折腾;二是更完善的调试工具,目前排查 Agent 行为问题还是靠打日志,效率不高;三是和 Flutter 状态管理库的深度集成,现在还是需要自己写胶水代码。
最后分享一个我在实际使用中的小习惯:每次调整提示词或者工具描述后,不要只看一两个 case 就下结论。准备一组 20 到 30 个典型输入,跑一遍看整体表现。模型的行为对措辞非常敏感,单点测试很容易产生误导。这个习惯帮我避免了好几次"改了一个地方,修好了两个 case,弄坏了五个 case"的情况。