从去年开始,我一直在琢磨怎么把 Flutter 和 LLM 真正揉到一起做点能跑的东西。市面上聊 Flutter 的教程不少,聊 LLM 的也一堆,但把 Flutter + LLM + 多智能体这三件事串成一条完整学习路径的内容,确实少得可怜。要么只讲 Flutter UI 怎么画,要么只甩一堆 Python 后端的接口文档,中间那层"客户端怎么和大模型对话、多智能体到底在客户端侧怎么落地"的断层,几乎没人填。这篇就把我这段实战入门的学习笔记整理出来,围绕 Flutter 作为前端载体、LLM 作为能力核心、多智能体作为协作架构这三条主线,把从环境搭建、原生通信、知识库增强到多智能体编排的完整链路讲清楚。适合已经会用 Flutter 写基础页面、但对 LLM 应用开发还比较陌生的朋友参考,也适合想从零搭一个 LLM 客户端壳子的同学照着抄作业。
1. 项目整体设计与思路拆解
先聊清楚我为什么选 Flutter 来搞 LLM 应用。当时摆在面前的选项其实不少:Web 页面、原生 App、小程序、Electron。但我的核心诉求很明确——跨端一致性要高、UI 灵活度要大、后续要能接原生能力。Flutter 在这几个维度上的平衡性是最好的,一套 Dart 代码跑 Android、iOS、Windows、macOS、Web,UI 是自绘的,不像 WebView 那套方案在不同平台上有明显的渲染差异。更重要的是,Flutter 的异步模型和 LLM 流式输出的场景非常契合,Dart 的 Stream API 天生就是干这个的,后面我会详细说。
1.1 为什么不是"Flutter 套壳 WebView"
很多做 LLM 聊天的团队偷懒,直接用 WebView 嵌一个 Chatbot 页面进去。这套方案上线最快,但坑也很深:WebView 的 JS 桥和原生通信会带来明显的性能损耗,流式输出(LLM 逐字吐 token)的时候尤其卡顿;Web 页面在移动端的键盘处理、手势交互、内存回收都不如原生顺手;更别提多智能体场景里,你需要在界面层同时渲染多个角色的状态面板,WebView 里做这种复杂布局本身就是给自己找麻烦。
Flutter 自绘引擎的好处在这里就体现出来了。整个 UI 不依赖系统控件,所有平台跑的是同一套渲染逻辑,哪怕是 Windows 和 Linux 桌面端、Web 端叠加部署,都不会出现"安卓好好的,iOS 上样式崩了"这种破事。再加上 Flutter 最近的 Impeller 渲染引擎逐渐覆盖全平台,Skia 那套旧的渲染后端正在被替换,实际体感是大量交互动画(比如 TabBar 点击、列表滚动、卡片翻转这种高频操作)的帧率和稳定性都上来了。
1.2 多智能体到底解决了什么问题
先给没接触过的朋友一句话解释:多智能体不是玄幻的 AI 概念,本质就是让多个分工明确的 LLM 实例(或角色)协同处理一个复杂任务。你可以把每个智能体想象成一个专职助手——有的负责检索知识库、有的负责写代码、有的负责审阅答案,最后由一个调度者汇总输出。这个架构比"一个 Prompt 走天下"的粗暴聊天机器人强在三点:任务拆解更清晰、上下文更聚焦、单点失误可以被其他人兜底。
选型和设计阶段,我花了不少时间纠结"多智能体到底在客户端实现还是服务端实现"。结论是:调度逻辑放客户端,计算逻辑放服务端。客户端负责编排流程、管理会话状态、渲染多角色消息流;服务端只暴露最小粒度的模型调用接口,每个接口对应一个智能体的单次决策单元。这样做的原因是客户端能拿到完整的交互上下文,用户中途插话、切换任务、撤回上一步,客户端处理起来最自然。
注意:别把多智能体的"多"理解成"多开几个 API 线程"。真正的多智能体必须有角色分工、有消息路由、有上下文隔离,后面第 4 节我会给出一套可落地的最小实现方案。
1.3 学习路径的整体地图
我给这个项目定的学习路径是四层递进:第一层,把 Flutter 和 LLM 的基础能力打通,包括环境搭建、HTTP/SSE 通信、流式渲染;第二层,解决"LLM 上下文不够用"的问题,引入知识库、RAG、GraphRAG、本体(Ontology)这些增强手段,让模型能回答私有领域问题;第三层,做多智能体编排,让多个角色协作处理任务;第四层,把工程化问题补齐——原生嵌入、状态管理、性能优化、打包发布的坑。
这四层每层都有大量值得记录的细节。下面我按实际推进的顺序,把每层的核心知识点和实操经验展开讲。
2. Flutter 端关键技术点拆解
这一节先讲纯 Flutter 侧的准备工作。LLM 应用对客户端的要求跟普通业务 App 不太一样——高频网络请求、长连接、实时渲染、复杂状态,这些都会放大普通项目的隐性坑。
2.1 Impeller、Flutter 版本与 Web 端兼容
Flutter 3.16 以上版本已经默认开启 Impeller 渲染引擎(Android 上从 3.10 开始探索,3.16 之后 iOS 全面切换)。Impeller 最大的变化是告别了 Skia 在运行时编译着色器的机制,改用预编译的着色器,这直接解决了老版本上"滑动页面突然掉帧"和"首次进入动画卡顿"的问题。但注意,Impeller 目前对 Web 端(CanvasKit)和部分老 GPU 的支持仍然不够好。如果你要跑 Web 端,建议在 Web 上显式关闭 Impeller,用 CanvasKit 的 Skia 后端保底。
版本选择上,Flutter 3.x 最近的几个稳定版都值得关注。3.22 之后的 Dart 3.4 对宏(Macro)和扩展类型有了更好的支持,虽然这些特性在 LLM 客户端里不一定直接用上,但工具链的稳定度确实在提升。3.27 和 3.29 版本在 Windows 桌面端的字体渲染和输入法兼容上修复了很多问题——如果你要做桌面端 LLM 工具,这两个版本是分水岭。
我个人的建议是:直接用你本地 Flutter 最新稳定版,不要为了某个老插件死守旧版本。2024 到 2025 年这段时间,Flutter 官方和社区都在快速向 Impeller、单一工具链方向收敛,老版本的坑是修不完的。
2.2 EventChannel 与原生通信的正确姿势
LLM 应用经常需要调用原生能力:读取剪贴板、调系统分享、做语音输入、监控网络状态。Flutter 和 Android/iOS 原生之间的通信通道有三条——MethodChannel(方法调用)、EventChannel(事件流)、BasicMessageChannel(基础消息),但很多教程只讲了 MethodChannel,EventChannel 一提而过。
EventChannel 在设计上是为"原生侧主动向 Flutter 侧推送事件"准备的。比如你做了一个录音按钮,原生侧负责录音并持续返回音量分贝、音频片段,Flutter 侧通过 EventChannel 的receiveBroadcastStream监听事件流。这个能力在 LLM 对话场景里很有用——语音输入时实时把语音转文字的结果回传,或者原生网络库上报连接状态变化,EventChannel 比 MethodChannel 频繁主动调用要优雅得多。
实操里踩过的坑:EventChannel 的 Stream 必须由原生侧在所有逻辑处理完后手动endOfStream,否则 Flutter 侧会一直等事件;另外,EventChannel 的事件回调是跑在平台线程的,不能在回调里直接更新 Flutter widget 状态,必须切回 UI 线程或用compute处理后再通过 State 更新。
2.3 PlatformView 在 LLM 场景的取舍
PlatformView 解决的是"Flutter 里嵌原生视图"的问题,典型场景是嵌入地图、视频播放器、WebView。LLM 应用里最常见的 PlatformView 需求就是富文本编辑器——很多 AI 写作工具的编辑器原生的,不想用 Flutter 重写一个。
我的经验是:除非万不得已,别用 PlatformView。它最大的痛点是触摸事件处理、键盘弹出、滚动联动的成本极高,每次 Flutter 版本升级都可能有兼容性问题。如果你只是需要展示 Markdown 渲染结果,用flutter_markdown或markdown_widget这些纯 Dart 包就够了;真正需要富文本编辑的时候,优先考虑用文本编辑器包(如appflowy_editor、super_editor)而不是嵌原生。
真正的 PlatformView 使用场景是视频会议、实时摄像头预览这种 Flutter 生态实在搞不定的领域。LLM 应用里如果要做"边对话边视频"的功能,才值得上。
2.4 Cubit 状态管理:LLM 对话流的最佳拍档
Flutter 生态的状态管理方案很多,Riverpod、Bloc、Provider、GetX 各有拥趸。LLM 对话场景有一个特殊需求——状态变化极其频繁,因为流式输出时每秒可能有几十个 token 到达,如果每次到达都重建整棵 widget 树,性能必崩。我发现Cubit(Bloc 的轻量版)在这种场景下最顺手:它比 Bloc 少了一堆 Event 类,直接通过方法调用来emit新状态,样板代码少,逻辑又足够清晰。
对话页面状态我用 Cubit 管理,核心状态就三类:会话列表(ListView)、当前会话消息数组(List)、以及每个消息的流式输出进度(可以用一个 String 或独立的流控制字段)。关键设计是:流式输出阶段,Cubit 内部维护一个StreamSubscription,每收到一个新 token 就emit一次最新的完整消息列表。这个方案的缺点是状态对象频繁重建,但配合Equatable做浅比较后,实际性能开销完全可控。
注意:流式输出时不要用
setState直接改某个消息文本,而是把"追加 token"这个操作收敛到 Cubit 内部。这样才能保证多智能体切换、用户撤回到上一步等操作时,状态仍然是唯一可信源。
2.5 TabBar 与页面切换的几个细节
"TabBar 点击取消动画"这个热词背后是个真实痛点:Flutter 的 TabBar 默认自带切换动画,但在 LLM 应用的多标签页(比如"对话""知识库""智能体管理"三个 Tab)场景下,动画会导致两个问题——切换时页面状态被短暂重建的错觉、以及频繁点击时的动画堆积卡顿。
如果你不需要 TabBar 的滑动切换动画,可以在TabBar上设置animationDuration: Duration.zero,同时配合physics: NeverScrollableScrollPhysics()禁止手势滑动。但更好的方案是:你的应用如果 Tab 页之间不需要联动滑动,直接用BottomNavigationBar+IndexedStack组合来切换页面。IndexedStack能保证所有 Tab 页面的状态常驻内存——这正好解决热词里那个问题:Flutter Navigator 切换页面后状态丢失。Navigator push 会销毁原页面(除非你用PageRouteBuilder保留底层页面),用IndexedStack则天然规避这个问题。
3. LLM 与知识库增强实战
这一节是很多人容易"知道概念但不会落地"的部分。LLM 不是数据库,你不可能让它准确回答你私有知识库里的事实;但你可以通过 RAG(检索增强生成)给它外挂一个事实库。更进阶的,还有 GraphRAG 和本体驱动的知识库。下面从最基础的 Token 机制开始讲。
3.1 Token 机制与上下文窗口的真相
先破除一个迷信:Token 不是字数,是模型处理文本的最小单位。中文场景下一个大字(汉字)通常要拆成 1~2 个 token,英文一个单词通常 1 个 token,代码和 Markdown 的 token 消耗会非常高(一个json代码块能吃掉几百 token)。市面上的模型按"上下文窗口"来标称能力,比如 128K、200K,但这不代表你每次对话都能填满——上下文越长,单次请求的延迟越高、费用越高、模型注意力越不集中。
用生活类比解释:Token 就像一张草稿纸的格子。你把整篇《红楼梦》塞进去当背景,剩下的格子就不够写回复了;你只塞相关段落,模型答复的质量会显著提升。所以控制 Token 消耗是 LLM 客户端开发者的基本功:- 系统级 Prompt(设定角色、行为准则)常驻上下文,但要精简到几百 token 以内;- 对话历史要按滑动窗口裁剪(比如只保留最近 10 轮);- 知识库检索只把命中的片段塞进上下文,而不是把整篇文档都塞进去。
3.2 RAG:让模型学会查资料
RAG 的核心是:先把你私有文档(Word、PDF、Wiki 页面)切成小块,做向量化(embedding)存入向量数据库;用户提问时,先把问题也做向量化,然后在向量库里做相似度检索,找到最相关的若干碎片;最后把"用户问题 + 检索到的碎片"一起塞给 LLM 让它生成答案。这相当于给模型配了一个搜索引擎。
落地时最容易忽略的三件事:切块策略、索引结构、检索时的重排序。切块不能太粗(半个文档一个块,检索精度差),也不能太细(一句话一块,上下文碎片化),经验值是 300~500 个汉字一块,块与块之间保留 50 字重叠。索引结构上,除了向量相似度,还建议给每块附带元数据(来源文档、章节、标签),这样检索时可以按标签过滤。重排序是在向量检索之后再做一遍精确打分(可以用cross-encoder模型,或简单的 BM25 关键词加权),能显著提升命中质量。
3.3 GraphRAG 与本体(Ontology):更聪明的知识组织
传统 RAG 是"平面化"的——文档被切成向量块,互相独立。但知识往往有强关联:一个概念的解释依赖另一个概念,一个流程的步骤横跨多个章节。GraphRAG 的思路是:从文档里抽取实体(如"Flutter""Impeller""渲染引擎")和关系("Flutter 使用 Impeller 作为渲染引擎"),构建成知识图谱;检索时沿着图结构跳转,把相关节点和一跳邻居都捞出来喂给模型。这个方案适合做LLM Wiki 知识库——它能把几十篇零散文档组织成一张互联的知识网,回答"索引从哪来""渲染流程是怎样的"这类跨文档问题。
本体是更严格的知识建模:它不仅抽实体,还定义类、属性、关系类型,让知识图谱变得"可推理"。比如你定义"智能体"和"工具"两个类,再定义"智能体调用工具"这条关系,那系统就能自动推理出"哪个智能体能执行哪些工具"。热词里提到的llm ontology、rag graphrag llm wiki 本体 rag,其实就是这条技术线的关键词。初学者建议从 RAG 入手,跑通了再上 GraphRAG,本体是进阶玩法——它需要你花时间建模,短期内收益不明显。
3.4 LLM 框架选型与服务端设计
热词里反复出现llm 框架,这通常指 LangChain、LlamaIndex、Semantic Kernel 这类 LLM 编排框架。我的观点是:客户端侧的 LLM 框架需求很弱,服务端才是框架发挥价值的地方。在 Flutter 端,你只需要维护一个干净的 API client,封装 HTTP/SSE 请求;真正的 Prompt 模板、工具调用(Function Calling)、多智能体编排,放在服务端用 Python 或 Node 去实现更合理。
服务端设计时,网关(Gateway)这个角色值得你留意。LLM 网关统一管理模型路由、Key 池、限流、审计、缓存,避免每个客户端直连各个模型厂商。你只需要在 Flutter 端维护一个 base URL,这个 URL 指向自己的网关,后期换模型提供商(从 GPT 切到 Claude、切到本地模型)时客户端零改动。
4. 多智能体架构:从概念到最小实现
这一节是标题的核心落点。多智能体的资料网上大多是学术味的白皮书,要么讲得太玄,要么上来就甩 LangGraph 的大规模示例。我想分享的是:怎么在客户端里构建一套看得见、摸得着、能调试的多智能体系统。
4.1 智能体的本质:一个带角色和工具的循环单元
不要被术语唬住。一个最小智能体就是四件事:人设(角色 Prompt)、工具集(能调用的接口/函数)、记忆(对话历史或知识库)、决策循环(决定下一步是直接回答还是调用工具)。多个智能体组合时,再加一个第五件事:通信机制(A 智能体的输出如何交给 B 智能体)。
这个抽象一旦建立,多智能体就不难了。比如我做一个"技术写作助手"项目,拆了三个智能体:调研员(负责检索知识库,输出资料摘要)、写手(根据摘要生成初稿)、审阅人(检查逻辑漏洞和格式问题)。用户输入一个主题后,调度者按"调研员 → 写手 → 审阅人"的顺序依次调用;审阅人发现问题时,会把反馈发回给写手,触发一轮修订。
4.2 上下文隔离:多智能体最容易踩的坑
多个智能体共用一个上下文是最常见的错误。假设调研员读了十篇文档、写手拿到三页摘要,如果你把调研员的所有检索结果都放进同一份上下文,那写手很容易被无关信息干扰(模型会关注大量非核心细节)。正确做法是:每个智能体有独立的上下文窗口,相互之间只通过结构化消息(JSON 或专门的函数返回值)传递必要信息。
在客户端实现时,我为每个智能体建立一个消息队列(一个 DartList<ChatMessage>),调度时切换不同的队列。用户的总聊天界面只展示汇总后的消息流,具体每个智能体内部的心路历程(它的检索过程、中间推理)不直接展示,必要时用调试模式以折叠卡片展示。
4.3 工具调用(Function Calling)的落地细节
多智能体几乎必然涉及工具调用。LLM 不是一个"只输出文字"的系统,它能按你提供的 JSON Schema 声明式地请求调用工具,比如"查询天气""搜索知识库"。客户端要做的:
- 在请求体里附带
tools数组(包含工具名、描述、参数 Schema); - 收到模型返回的
tool_calls时,不要去渲染这个内容,而是本地执行真实工具; - 把工具执行结果作为一条消息追加回上下文,再发起下一轮模型请求。
这中间有一个很实际的坑:工具调用的结果过大(比如检索返回了 50 条资料)会撑爆上下文,方案是只回传摘要和前 3 条命中详情。还有一个坑是"工具调用循环失控"——模型可能会反复调用同一个工具,需要设置最大迭代轮数(比如 8 轮),超出即强制生成最终答案。热词里那个报错llm request failed: provider rejected the request schema or tool payload,就是因为你传给模型的 tools Schema 不符合要求——常见原因是 Schema 里缺type: "object"或properties为空对象,模型厂商校验严格后会直接拒收。
5. 客户端与服务端的完整联动实操
前面的知识点单独都能讲一篇,但项目真正跑起来靠的是把它们串成一条完整的调用链。这一节我按实际的调用流程,从 Flutter 页面发起请求到流式渲染完整展示一遍。
5.1 HTTP + SSE:LLM 流式输出的接入方式
大模型的流式输出用的是 SSE(Server-Sent Events)协议——服务端持续向客户端推送事件,每个事件包含一小段文本增量。Flutter 里接入 SSE 不要用 WebSocket,因为 SSE 是单向的且实现更轻。
我的实现是基于http包的StreamedResponse:发起 POST 请求时把response.stream转成utf8.decoder,按行解析 SSE 事件。Dart 的 Stream API 在这里如鱼得水,你可以用transform、where、listen来做管道式处理。
服务端返回的数据格式各家略有不同,但 OpenAI 兼容协议已经成了事实标准。每条 SSE 事件形如:
data: {"choices":[{"delta":{"content":"你好"}}]}注意:每条 data 后面跟一个空行(\n\n),解析时要按这个分隔符切分。还要处理[DONE]结束标记。如果遇到provider rejected这类错误,服务端会推一个带error字段的事件,Flutter 端要把它当成 UI 状态而不是字符串拼接。
5.2 Flutter 端请求层设计:拦截器、错误处理与重试
LLM 请求的特点是长耗时、易超时、偶尔 5xx。请求层如果裸写,很快会被业务代码里四处散落的网络处理搞崩溃。我的做法是封装一个LlmApiClient类,所有请求统一走这个类,内部做好四件事:-timeout设得比普通接口长(流式请求 30~60 秒,首 token 的等待时间其实只要求小于 10 秒);- 对网络错误做分级(超时、连接失败、HTTP 错误、业务错误),向 UI 层抛一个带错误码的异常;- 流式请求支持中途取消(StreamSubscription.cancel()),取消时要向服务端发送一个abort请求释放服务端资源;- 对 429 限流做指数退避重试(首次 1 秒、第二次 2 秒、第三次 4 秒,最多 3 次)。
注意:重试只对幂等请求做——即服务端实现了"生成完整快照"的机制。如果服务端已经生成了半份回答,重试会造成内容割裂。稳妥的做法是重试时使用新的会话 ID,让服务端从零开始生成。
5.3 前端 UI 与状态的映射关系
流式输出时,UI 必须实时刷新。我把对话消息设计成三级模型:
Message:最终展示的消息(含完整文本、发送者、时间戳);StreamingMessage:正在流式输出的消息,内含一个StringBuffer,每来一个增量就往 buffer 里追加;MessageStatus:消息状态(sending、streaming、completed、failed、aborted)。
UI 层对StreamingMessage做特殊渲染:用动画光标提示"生成中",并给出一个"停止生成"按钮。这里最容易犯的错误是每个 token 都重建 Text widget——性能杀手。解法是用SelectableText或RichText,配合CustomPaint和局部重绘标记,或者退一步用AnimatedBuilder和RepaintBoundary控制重绘范围,保证只有当前消息这棵子树在刷新。
5.4 安卓原生项目嵌入 Flutter 页面的工程化
热词里有一个很现实的工程场景:安卓原生项目嵌入 Flutter 页面。很多团队现有一部原生 App,想逐步引入 Flutter 来做 LLM 对话页,而不是全部重写。这种混合开发模式有几件必须提前确认的事:
- 集成方式:官方推荐用
FlutterFragment嵌入,而不是FlutterView硬拼。FlutterFragment自带生命周期管理,能避免"Activity 销毁但 Flutter engine 还活着"的内存泄漏。 - Engine 复用:多个 Flutter 页面要共用一个
FlutterEngine,否则每个页面都新起一个引擎,内存很快吃满。用FlutterEngineCache缓存预热的引擎,启动速度能快一个量级。 - 通信链路:原生工程和 Flutter 页面的通信通道要设计成"统一 EventBus"模式——原生侧发出事件,Flutter 侧监听并刷新 UI;反向用 MethodChannel 回传。别让每个 Flutter 页面各自维护私有通道,后续页面多了会疯。
6. 环境搭建与报错排查实录
最后这部分是给所有打算自己动手复现的朋友的。环境搭建报错排查往往是最劝退的环节,我把实际踩过、以及在社区里高频出现的问题整理成速查表,按"症状 → 原因 → 解法"的格式排出来。
6.1 Flutter 环境搭建与版本匹配
先说结论:Flutter SDK 用最新稳定版,Android Studio 的 Android SDK 保持 compileSdk 33 以上,Xcode 必须和 Flutter 版本匹配。
热词里多次出现xcode27很多flutter包报版本低,这是典型的 Flutter 和 Xcode 版本不匹配问题。苹果发布新 Xcode 后,Flutter 的 iOS 构建工具链往往没跟上,导致大量插件包因为最低部署版本(IPHONEOS_DEPLOYMENT_TARGET)低于 Xcode 的默认值而报错。解法:升级 Flutter SDK 到支持该 Xcode 的版本(一般 Xcode 大版本刚发布后两周内 Flutter 会有对应更新);如果仍报低版本,手动在 Podfile 里把所有 pod 的platform :ios改成 '13.0' 或更高。
flutter环境搭建的整体顺序是:装 Flutter SDK → 跑flutter doctor→ 按提示安装缺失的 Android SDK/Xcode/Visual Studio → 配置 IDE 插件 → 建项目。这里有一个人人都会踩的坑:Windows 上用 Android Studio 但没装 Visual Studio,桌面端(Windows/macOS)构建会直接失败,flutter doctor会明确列出这条,装完重启即可。
6.2 Gradle 与构建期常见报错
You are applying Flutter's main Gradle plugin imperatively using the apply script这个警告,在新版 Flutter 里几乎人人都会遇到。简单解释:Flutter 早期要求老式 Gradle 写法的项目用apply flutter plugin命令式应用插件,新版则推荐在settings.gradle里用声明式插件机制。这个警告本身通常不会阻止构建,但会提示你迁移。最干净的解法是在 Android 工程的settings.gradle.kts(或.gradle)里,把:
plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.1.0" apply false // ... }换成按最新模板配置的写法,使settings.gradle负责加载 Flutter 插件,而不是在app/build.gradle里直接apply。
flutter打包 java.lang.assertionerror: java.lang.exception: could not close i...这个报错常见于打包 AAB/APK 时遇到资源或签名异常。多数情况是flutter clean+ 删除build/和.gradle/缓存能解决;如果还不行,检查是否使用了太大而无法打包的资源文件(比如十几兆的自定义字体、动图),压缩或转为网络加载即可。
The current configured Flutter SDK is not known to be fully supported提示意味着当前 Flutter 版本和项目要求有一点偏差。通常直接flutter upgrade到最新稳定版就能消掉;如果项目是别人给的,还要执行flutter pub get重新拉取版本对应的包依赖。
6.3 运行期与模型调用报错
最典型的是llm request failed: provider rejected the request schema or tool payload。这个问题的根因是你传给模型的functions/tools参数没被模型厂商的校验器接受。排查顺序:先用官方调试工具(如 OpenAI 的 playground)手动传一次同样的 payload,确认接口正常;再检查自己的 tools 定义是否缺少必要的字段。最容易被漏掉的是:参数 Schema 必须是完整 JSON Schema,且至少包含type字段;另外,description字段如果为空字符串,某些提供商会直接拒绝,建议每个字段都写上描述。
还有一类运行期问题不是报错,而是表现异常——比如热词里的flutter tabbar点击取消动画效果、flutter navigator切换页面后,会丢失状态吗。这类问题本质上不是 LLM 的问题,而是 Flutter 页面生命周期管理的问题。如果你做的是多标签页的 LLM 工具,强烈建议用IndexedStack保活所有 Tab 页面的状态,同时用AutomaticKeepAliveClientMixin让滚动位置不丢失。如果你用 Navigator push 打开对话详情页,进详情再返回列表时列表滚动位置默认会丢,需要在列表PageStorageKey或接管RouteAware来保存状态。
6.4 常用 LLM 榜单与模型选型参考
热词里出现了open llm leaderboard 等公开榜单,选型确实要看榜,但不能唯榜是从。公开榜单考察的是模型在通用测试集上的能力,比如推理、编码、多语言,但这些测试题目和你的业务场景差异很大。我的参考维度是:单 token 成本、上下文窗口长度、中文能力、工具调用(Function Calling)的稳定性、流式输出的速度。以目前主流模型为例:
| 模型 | 上下文 | 中文能力 | Function Calling | 适合场景 |
|---|---|---|---|---|
| GPT-4o 系列 | 128K | 强 | 稳定 | 通用对话、复杂推理 |
| Claude 3.5 Sonnet | 200K | 较强 | 非常稳定 | 长文档、多工具调用 |
| 国产开源系列(Qwen 等) | 128K+ | 强 | 较强 | 本地部署、私有化 |
| 轻量模型(如开源 7B 级别) | 32K | 中 | 一般 | 简单任务、成本敏感 |
选型结论:如果你做的是私有化知识库+多智能体协作项目,国产开源模型 + 本地部署(比如用 vLLM 或 Ollama)是性价比最高的路径,离线可用且数据不出内网;如果需要极强的函数调用和复杂工作流,Claude 系列和 GPT-4o 系列是首选。多智能体场景对 Function Calling 的稳定性要求极高,一定要选在tool use评测上有充足分数的模型,否则你会被"模型把工具参数填错、调了半天调不出来"的反复折磨耗死。
7. 最后的建议与扩展方向
我个人在实际操作中体会到,Flutter+LLM 的组合最大的价值不是"把聊天机器人包进 App",而是把大模型的能力拆成若干个可嵌入到任何页面角落的工具。比如你们的产品里有一个"合同审核"功能,你可以在任意表单页旁边挂一个"AI 助手"浮动面板,这个面板本质就是一个单智能体;当你同时开"合同审核""条款对比""风险提示"三个面板协作处理同一份合同时,多智能体的意义才真正体现出来。
另外一个小技巧分享给准备动手的朋友:刚开始不要追求架构完美,先把最简单的"Flutter 页面 → 直连一个 LLM API → 流式显示回答"跑通,再把请求层抽象出来、引入知识库、最后把单智能体扩展成多智能体。一步一步来,每层都确保可运行、可回退,比一口气搭一个宏大架构要稳健得多。
这个项目后续可以扩展的方向还很多:接入语音输入做语音对话、把 GraphRAG 换成更轻量级的图谱检索、在桌面端做本地模型调用(通过 Ollama 拉起本地模型,再用 FastAPI 包装一层 OpenAI 兼容接口)、或者把多智能体的编排逻辑做成可视化工作流——让用户在界面上拖拽定义智能体之间的协作关系。每一个方向都值得单独开一篇实战记录。如果你也在折腾 Flutter 和 LLM 的结合,欢迎在这些方向上踩坑后一起交流。