ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter客户端中的LLM多智能体协作:轻量编排架构实战

Flutter客户端中的LLM多智能体协作:轻量编排架构实战 如果你以为“多智能体”一定得跑分布式系统、上消息队列、搞Agent框架那这期内容可能会颠覆你的第一印象。我要讲的是怎么用Flutter做客户端把LLM大语言模型当作一个个独立“角色”在手机App里完成一场多智能体协作。这个组合我玩了差不多一个多月从最初的懵圈到现在能跑通一个三Agent协作的产品策划Demo踩了不少坑也总结出一套适合新手入门的轻量架构。这篇东西不是PPT是能照着重现的实操笔记适合那些已经会一点Dart、想快速把LLM和多智能体概念落到项目里的人。1. 这个组合到底在做什么Flutter、LLM和多智能体的交集1.1 先卸下“多智能体”的神秘感很多人一听“多智能体”就想到AutoGPT那种社区开源项目或者一整套Agent编排框架。实际上在这个场景里多智能体就是让多个LLM角色在一个工作流里按顺序或并行地执行各自的任务然后共享中间结果最终汇总出一个更完整的结果。打个比方你要上新一款产品。一个人问ChatGPT“帮我做策划”它能给你一份清单但往往不够细、不够落地。如果你让“产品经理GPT”先拆需求再让“技术GPT”评估可行性再让“运营GPT”想推广策略最后让“汇总GPT”整合成报告这就是一个多智能体流程。每个“GPT”就是一个小智能体它们的共同点是都调用同一个LLM接口不同点是System Prompt和输入上下文不一样。在Flutter端做这件事本质上是写一个编排器Orchestrator。编排器是核心它决定哪个智能体先跑、跑完的结果传给谁、什么时候停止。这个编排器完全可以用Dart写不需要引入外部Agent框架。这么做的好处很明显客户端直接控制界面状态每个Agent的思考过程能实时展示在页面上用户能看着几个“角色”挨个发言体验很直观。1.2 Flutter在里面的角色Flutter在这里不是用来训练模型也不负责跑大模型推理。模型在云端API那边Flutter负责三件事提供交互界面用户输入任务、选择模式、查看中间结果。扮演进程编排的角色用Dart的异步机制调度多个Agent。管理会话状态整个多Agent协作中的数据流、消息列表、Agent状态都保存在客户端状态层。所以你的Flutter工程本质上是一个“多智能体工作台”而不是单个聊天框。这也是为什么我说不要把它当成普通聊天App来做。一旦你接受这个定位后续代码架构就清晰了。1.3 前置知识清单如果你是纯Flutter零基础建议先补几个点Dart的异步编程Future、Stream、async/await。这一篇里会频繁用到不懂真的没法写编排器。HTTP请求至少会配Dio或HttpClient能POST JSON。状态管理基本功不用学太深理解Cubit或ChangeNotifier的“监听-通知”机制就够。一点Prompt工程常识至少知道System Prompt和User Prompt的关系。我这个系列是“实战入门”所以不会从Dart语法开始讲。但你只要满足上述前提跟着下来基本不会卡壳。2. 环境准备这一节不讲怎么装SDK只讲怎么选版本2.1 Flutter SDK版本选择的现实问题我没法替你装SDK但可以负责任地说千万别追最新稳定版前面加“beta”或“dev”的东西。网上经常能看到“Flutter 3.44”“Flutter Windows 3.47.5”之类的字眼混在一起其实是不同渠道的版本号。确认版本的方式只有一个——打开终端执行flutter --version输出会带类似Flutter 3.24.0 • channel stable的信息。只有stable渠道才适合我们做生产型Demo。如果你本机之前装过版本又出现“The current configured Flutter SDK is not known to be fully supported”这种提示不要慌。这句话一般出现在新建项目后说明你项目里的pubspec.yaml或IDE插件认为你的SDK版本太新/太旧。解决办法很简单把本机SDK升级或降级到一个常见的稳定版本或者忽略该警告但跑一遍flutter doctor确认环境没毛病。我的建议是固定使用一个你能稳定复现的版本例如3.22.x或3.24.x并记住它。以后排查莫名其妙的构建问题很多都是版本浮动造成的。2.2 创建项目和依赖设计创建项目不用命令行也行Android Studio或VS Code里都有“New Flutter Project”。注意指定的是项目存放路径和项目名包名要选一个你自己域名反写。我习惯用Android Studio操作路径是File New New Flutter Project。这个流程对应热搜词里的“如何as创建flutter项目”其实就是一个GUI操作不用手敲flutter create。项目创建好后pubspec.yaml里需要加这些依赖dependencies: flutter: sdk: flutter dio: ^5.4.0 flutter_riverpod: ^2.4.0 google_fonts: ^6.1.0 dev_dependencies: flutter_test: sdk: flutter这里解释一下选型dio比http包好用拦截器、超时、取消请求都比较顺手。多智能体编排中经常要处理多个并发请求取消机制非常重要。flutter_riverpod我用来管理会话状态。不是因为它比Bloc高级而是Cubit和ConsumerWidget配合很轻量。后面会展示用法。2.3 配置Android和iOS的网络权限新手最容易忽略网络权限。Android默认不允许HTTP明文请求你调用一个不加密的本地LLM网关时会直接被拒。需要改AndroidManifest.xml两处uses-permission android:nameandroid.permission.INTERNET/然后在application标签上加上android:usesCleartextTraffictrueiOS那边默认只允许HTTPS。如果你的LLM接口不是HTTPS比如局域网测试要在Info.plist加一段keyNSAppTransportSecurity/key dict keyNSAllowsArbitraryLoads/key true/ /dict这里不是让你无视安全本地开发调试阶段可以这样省事上架前必须换回HTTPS。3. 让Flutter能“跟LLM说话”从请求结构到流式输出3.1 理解LLM API的核心参数调用LLM接口不管用的是OpenAI、Claude还是各种国产模型核心请求结构都一样{ model: gpt-4o-mini, messages: [ {role: system, content: 你是产品经理负责输出需求文档}, {role: user, content: 帮我想一个宠物交友App的核心功能} ], temperature: 0.7 }注意messages里的角色只有三种system、user、assistant。system用来设定人设和边界user是当前用户或上一个智能体喂给你的内容assistant是模型之前的回复在多轮对话里用来保留历史。网上有个关于“Token三个点”的说法——把Prompt拆成Key、Query、Value三类信息。简单理解Key是模型身份认知比如“你是一个资深的产品经理”Query是用户想要得到的指令比如“列出5个核心功能”Value是额外参考素材比如公司背景、之前Agent产出结果。你在设计每个智能体的Prompt时一定要把这三类信息区分开因为system里放Key和Valueuser里放Query效果最好。混在一起会让模型抓不住重点输出飘。3.2 封装一个统一的LLM客户端不要在每个页面都直接写Dio请求。我在项目里建了一个llm_service.dart统一管import package:dio/dio.dart; class LLMService { LLMService({required this.baseUrl, required this.apiKey, this.model gpt-4o-mini}); final String baseUrl; final String apiKey; final String model; static final Dio _dio Dio(BaseOptions( connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 60), headers: { Content-Type: application/json, }, )); FutureString chat({ required ListMapString, String messages, double temperature 0.7, }) async { final body { model: model, messages: messages, temperature: temperature, }; final response await _dio.post( $baseUrl/v1/chat/completions, data: body, options: Options( headers: { Authorization: Bearer $apiKey, }, ), ); if (response.statusCode ! 200) { throw Exception(LLM API error: ${response.statusCode} ${response.data}); } final data response.data; return data[choices][0][message][content] as String; } }这里有几个细节receiveTimeout要设大一点因为LLM生成本身需要时间60秒不算夸张。API Key不要写死在代码里。先用String.fromEnvironment或者放到配置类中后面接环境变量。3.3 流式响应StreamBuilder与EventChannel的关系非流式请求简单但真实用户觉得“等半天一个转圈”体验很差。所以最终项目一定要做流式输出。流式接口一般走SSEServer-Sent Events也就是返回一段段以data:开头的文本。Dio可以直接请求SSE流用法final response await _dio.post( $baseUrl/v1/chat/completions, data: {...body, stream: true}, options: Options(responseType: ResponseType.stream), );然后不断监听流里的数据拼出来一段文字。这个过程在Flutter UI侧用StreamBuilder就能很好展示每个新字符到达时刷新气泡。有人会问流式和EventChannel是什么关系这里要拨开一个概念EventChannel是Flutter和原生端之间的通信通道主要用来从原生侧往Dart侧发事件。如果你需要使用原生SDK请求LLM比如某些模型只提供了Android/iOS原生SDK那么原生收到Token增量后通过EventChannel推给Flutter是标准做法。但我们现在用的是纯Dart请求不涉及原生侧所以完全不需要EventChannel。理解这点很重要别被热搜词带偏。4. 多智能体是怎么在Flutter端跑起来的编排层设计4.1 从单个LLM调用到智能体流程当你有了LLMService单个智能体就是一个特定的函数或类给它一段输入它调用一次LLM返回一段输出。但多智能体不是单纯的多个LLM调用它需要让不同智能体之间共享上下文。我在项目里定义了一个Agent抽象类abstract class Agent { String get name; String get role; String get systemPrompt; FutureString run(String task, String context); }每个具体的Agent比如产品经理Agent实现run方法构造messages数组mixin BaseAgent on Agent { override FutureString run(String task, String context) async { return llm.chat(messages: [ {role: system, content: systemPrompt}, {role: user, content: task}, {role: user, content: 前序信息$context}, ]); } }这里注意一个细节前序所有信息不要一股脑拼进去而是按需给。否则Token长度爆炸而且模型会混乱。4.2 三条核心机制Prompt路由、上下文广播、结果汇聚多智能体编排器要处理的事情无非三件路由Routing根据任务类型决定第一个跑哪个Agent。比如用户输入“帮我策划一个功能”路由就先去产品经理Agent如果输入“分析这个方案的漏洞”路由直接去技术评估Agent。广播Broadcasting)当前Agent产出的结果要作为下一轮Agent的上下文。这个可以在编排器内部维护一个消息列表每跑完一个Agent往里边追加一条。汇聚Aggregation最后一个Agent通常是一个“总结者”它接收前面所有输出合成一份最终报告。这个汇聚逻辑也可以简单成拼字符串后让总结Agent重写。编排器的核心循环大概是这样的伪代码final results []; for (final agent in agents) { results.add(await agent.run(initialTask, context: results.join(\n\n))); updateUI(agent.name, agent.status, results.last); }这里的agents是一个ListAgent顺序就是流程顺序。你可以把它设计成有向无环图但入门阶段跑通这个顺序循环已经是很大的进步了。4.3 会话状态管理用Cubit还是全局Store多智能体是异步的可能同时有几个Agent在跑如果你做并行。这时UI必须能响应状态变化你不能在Navigator.push了下一个页面之后就丢了上一屏的状态。对于状态管理我实际用的是flutter_riverpod里的StateNotifierProvider思路和Cubit几乎一样。核心是维护一个AgentRunStateclass AgentRunState { final bool isRunning; final ListAgentResult agentResults; final String currentAgent; final String errorMessage; }页面通过ref.watch监听这个状态每当一个Agent跑完就更新状态界面自然刷新。这里有一个经验不要用setState在深层嵌套Widget里管理这些数据会非常痛苦。另外当你用Navigator切换到详情页再切回来时如果Agent还在跑状态会因为你用了全局Provider而保留。这也正好解答了热搜词里“flutter navigator切换页面后会丢失状态吗”——答案是如果你用局部State会丢如果用Provider/全局Store不会丢。4.4 异步细节Future.then回调到底是微任务还是宏任务很多教程讲Future.then会放到“微任务队列”但在Flutter里要注意then里的代码确实在当前事件循环的微任务队列里执行优先级比Timer高。这带来一个问题如果你不小心在then里做了重计算会阻塞UI渲染。我在多智能体编排器里尽量统一使用async/await而不是大量.then链。因为编排器的逻辑是顺序的用await会更容易读。当你需要并行Agent时可以用Future.waitfinal futures agentList.map((agent) agent.run(task, context)); final results await Future.wait(futures);Future.wait本身也是微任务调度的但最终结果会合并为一个Future不会泄漏。5. 实战Demo做一个“三角色策划助手”5.1 场景与交互流程设计我做的这个Demo叫“三角色策划助手”。用户在输入框输入一个想法例如“帮我做一个面向大学生的二手书交易App”。点击运行后流程如下产品经理Agent先输出需求清单目标用户、核心功能、优先级。技术Agent读取产品经理的输出评估技术难度并给出技术选型建议。运营Agent读取前两者输出补充推广策略和冷启动玩法。汇总Agent把三份结果整合成一份结构完整的策划报告。界面用三个小卡片展示每个Agent的实时状态等待中、思考中、已完成。每个卡片下还能展开细节看原始输出。主页面底部显示最终报告。5.2 Agent的Prompt模板设计Prompt设计直接决定输出质量。我的模板是这样写的产品经理AgentSystem: 你是一名有10年经验的互联网产品经理。你的输出风格是条理清晰、可执行。 User: 请根据用户想法生成需求文档包括目标用户、核心功能列表、 MVP 范围。用 Markdown 格式输出。技术AgentSystem: 你是一名全栈架构师。你擅长评估技术方案的风险和工作量。 User: 以下是一份产品需求文档请评估技术可行性推荐具体技术栈指出可能的技术难点。不要写代码只做分析。运营AgentSystem: 你是一名擅长冷启动的运营专家。 User: 基于以下需求和技术方案设计一套上线前3个月的推广计划。注意每个Agent的User里都要带上前面Agent的输出否则就是闭门造车。5.3 核心代码实现我来写一个简化但完整的编排器代码供你参考class Orchestrator { Orchestrator({required LLMService llm, required String task}) : _llm llm, _task task; final LLMService _llm; final String _task; final ListAgentResult _results []; StreamAgentEvent run() async* { final agents [ ProductAgent(llm: _llm), TechAgent(llm: _llm), MarketingAgent(llm: _llm), SummaryAgent(llm: _llm), ]; for (final agent in agents) { yield AgentEvent(agentName: agent.name, status: AgentStatus.thinking); final context _results.map((r) ## ${r.agentName}\n${r.output}).join(\n\n); final output await agent.run(_task, context); _results.add(AgentResult(agentName: agent.name, output: output)); yield AgentEvent(agentName: agent.name, status: AgentStatus.done, output: output); } } }这里用StreamAgentEvent来向UI层推送事件比回调函数更干净。UI层直接监听这个Streamorchestrator.run().listen((event) { ref.read(runStateProvider.notifier).addEvent(event); });5.4 UI层怎么把过程展示出来我用了一个简单的方式三个Agent卡片用AnimatedBuilder监听状态正在思考的卡片显示一个进度条组件完成的卡片显示Markdown文本。Markdown解析我用flutter_markdown不用自己写。关键是把AgentRunState设计好例如class AgentResult { final String agentName; final String output; final AgentStatus status; } enum AgentStatus { pending, thinking, done, error }这样UI层就很机械ListView.builder( itemCount: agentNames.length, itemBuilder: (context, index) { final status ref.watch(agentStatusProvider(index)); // 根据 status 显示不同卡片 }, )5.5 跑起来之后的实测表现第一次跑通时你会直观感受到一个现象每个Agent单独输出质量还不错但串联之后经常出现“信息丢失”。比如技术Agent会忽略产品经理给的某个功能约束。这不是代码bug而是上下文被压缩或模型注意力偏移。解决办法有两个在传给每个Agent之前显式用“你需要注意以下关键要求xxx”强调一遍。控制每个Agent的输出长度不要输出整篇几千字只输出结构化要点。结构化信息不容易丢失。我用第二招后效果立刻提升。产品经理Agent输出控制在15个要点以内技术Agent输出控制在10个要点以内汇总Agent再展开。这就是入门级多智能体最重要的调优经验。6. 必踩的坑和对应的解药6.1 LLM请求失败Provider rejected the request schema or tool payload这是一个很典型的报错我在一开始接入函数调用Function Calling时经常遇到。它的意思是你给LLM API传的tools参数格式不符合接口规范或者某个模型根本不支持该工具声明。如果你不需要让模型调用工具直接删掉tools字段就行。如果确实要传去对照接口文档检查两个点一是工具的类型是function二是parameters必须是JSON Schema不能漏掉type和properties。很多服务端报错是在嵌套参数里少了type: string之类的声明。6.2 构建期报错Gradle插件用apply方式及SDK版本不支持在Flutter的Android构建报错里最常撞见的就是You are applying Flutters main Gradle plugin imperatively using the apply script.大意是建议你切换到Gradle的plugins DSL方式而不是用旧版apply plugin。在新版本的Flutter模板里已经是plugins方式但老人老项目可能还有这个问题。解决办法是打开android/settings.gradle确认里面写了plugins { id dev.flutter.flutter-plugin-loader version 1.0.0 }而不是apply from: $flutterRoot/packages/flutter_tools/gradle/app_plugin_loader.gradle。另一个SDK版本不支持的问题多半是你本机的Flutter SDK版本比项目模板预期新太多。最简单的方法删除项目根目录的pubspec.lock和.dart_tool重新flutter pub get能解决大部分玄学问题。6.3 Flutter Web端启动慢与Impeller引擎有热搜词提到“flutter web 引擎启动慢”这确实是不少人的痛点。如果你把多智能体Demo部署到Web端首屏加载会比移动端慢很多。可选优化方案把大的图片、字体资源放到CDN减少主包体积。用flutter build web --web-renderer html降级渲染策略在较老版本中或者在新版本里尝试--wasm。避免在首屏初始化大量状态对象把Agent编排器延迟到用户点击运行按钮时才创建。至于Impeller它是Skia之后的下一代渲染引擎目前在移动端默认开启好处是动画更稳定减少掉帧。但在某些设备或Web端可能表现不如预期。如果你发现UI渲染异常可以在AndroidManifest里加上io.flutter.embedding.android.EnableImpellerfalse临时切回Skia。仅限排查问题使用。6.4 周边小问题PlatformView、TabBar动画与原生嵌入PlatformView主要用在需要嵌入原生Map、WebView等视图的场景。跟LLM多智能体关系不大但如果你要做语音输入、相机扫码等原生能力它和EventChannel是绕不开的知识点。多智能体App往往需要语音输入到时候你会用到这两个机制。TabBar点击取消动画这是UI层的强迫症需求。在TabBar的onTap里如果不想让页面切换自带动画可以先设置controller.index index或者直接在TabController的animateTo前加判断这是纯UI小技巧不影响业务逻辑。安卓原生项目嵌入Flutter页面如果你想在现有安卓App里逐步引入Flutter用flutter_fragment或FlutterActivity的cachedEngine方案。入口注意管理FlutterEngine的生命周期不然容易内存泄漏。最后再分享一个我自己的体会FlutterLLM多智能体的核心不在Flutter也不在LLM而在“编排思维”。你把一次复杂任务拆成几个角色、几步Prompt再设计好上下文传递规则那么用Flutter写逻辑非常顺手因为Dart的异步模型天然适合这种流程编排。我建议你从最简单的两个Agent串联开始不要一上来就做并行、做回溯。跑通一个顺序流程再试试Future.wait并行最后考虑给Agent加工具调用。每一步都要亲手把每个Agent的输入输出打印出来——我在调试阶段就是靠Log流来观察哪个Agent丢信息这个习惯帮我省了至少一半时间。
返回列表