☰
Flutter+LLM多智能体实战:从任务编排到RAG与原生桥接
2026/9/29 22:11:35 网站建设 项目流程

前一阵我想在手机上做一个自己的AI助手,不是那种一问一答的玩具,而是能把“帮我整理这周笔记并生成摘要”“根据知识库写一份产品简报”这类任务拆开、分给不同角色处理的App。Flutter是现成的跨平台框架,LLM负责理解和生成,剩下最大的问题是怎么让多个模型角色协作跑起来——这就是我入坑“Flutter+LLM多智能体”的起点。

这篇内容不是讲概念,是我从零搭一套可运行方案的过程记录。适合刚开始接触Flutter和LLM、想绕过纯Demo、直接做一个带“多角色协同”能力的应用的开发者。你不需要先读懂什么高深的Agent论文,跟着我把链路拆开,把每一层该干什么、为什么这么干弄清楚,就能在自己的工程里跑起来。

1. 这个组合到底在解决什么问题:从单轮聊天到协同干活

1.1 我对Flutter、LLM、多智能体三者关系的朴素理解

很多人把Flutter和LLM放在一起,第一反应是“做个聊天界面”。这不叫AI应用,这只是给模型套了一个壳。真正的AI应用,至少要满足三件事:能感知用户意图、能调用工具或查询数据、能把复杂任务拆给不同角色协作完成。

我的理解是:Flutter是“手和脸”,负责界面交互和多端运行;LLM是“脑”,负责理解、生成、判断;多智能体是“管理脑的神经系统”,负责把一个复杂问题拆分、派发、汇总。三者组合在一起,才能做一个真正的“助手”,而不是一个“说漂亮话的接口”。

在我之前做的一些简单接里,LLM只是个文本进出管道:用户输入一句话,请求发出去,响应显示在界面上。这种方式做Demo没问题,但一遇到多步骤任务就露馅。比如“帮我对比三篇笔记里的观点,然后写一段摘要”这种请求,单次调用根本做不到,要么靠模型硬编,要么靠后端写一长串代码。而多智能体是把任务拆成三个角色:一个读笔记、一个做对比、一个写摘要,每个角色各干各的,最后汇总。

1.2 客户端接入LLM的三条路线对比

在我动手之前,我先列了一下现有的主流做法,具体对比是这样的:

接入方式优点缺点适合场景
Flutter直接调模型API实现最快,代码量少,适合验证想法对话状态、工具调用逻辑全部堆在客户端,请求容易被拦截Demo、自用工具、轻量助手
后端编排Agent状态管理、多模型协同、安全管控都放在服务端,能力强要维护服务端,成本高,个人项目容易放弃生产级应用、团队项目
客户端编排+云端模型Flutter本地管理会话状态和Agent调度,只把推理请求发给LLMAgent逻辑都在客户端,依赖端上计算和本地存储中小型应用、离线和弱网场景

我最后选了第三条。原因很直接:我不想为了一个自用项目买一台服务器跑Agent框架,也不想把用户对话状态全扔给后端去管。Flutter本地有状态管理、有数据库、有文件读写能力,把一个简单的Agent调度器放在本地是完全可行的。

1.3 为什么选择“客户端编排+云端模型”

选这条路不是因为它最好,而是因为它最符合我的实际约束:没有后端资源、想把App做成跨平台、同时希望后续能无缝接本地部署的模型。

本地编排最大的好处,是你能精确控制Agent之间的消息流转。比如我需要一个“总结Agent”读完文件后把结果传给“撰写Agent”,在客户端可以直接用一个本地队列完成,不需要网络请求。整个推理过程只有模型API是远程的,其他一切都在本地,调试起来非常直观。

当然也有代价。第一,API Key存在客户端,有泄露风险,自用没问题,上架就要考虑托管中转。第二,Flutter的Dart是单线程模型,大量Agent并行调度时要注意异步编排,不然界面会卡。第三,多Agent涉及的上下文管理如果全部堆在客户端,内存和存储压力会比后端大。这些坑后面我会详细说。

2. 多智能体运行骨架:把一个人的思考拆成一支队伍

2.1 单Agent和多Agent的本质区别

我之前一直以为多智能体就是把同一个模型调用好几遍。后来发现不是这个逻辑。单Agent是“一个脑子从头想到尾”,它既要理解用户问题,又要规划步骤,还要写结果。一到复杂任务,要么忘记前文,要么上下文爆炸。

多智能体是“多个脑子分工”,每个Agent只需要关心自己的那一段职责。比如一个“任务规划Agent”只负责拆解步骤,拆完之后把子任务发给“执行Agent”,执行完再由“汇总Agent”收口。每个Agent的上下文都被限制在自己的职责范围内,既省Token,又不容易乱。

我在实际项目里把Agent分成三类:

  • 规划型Agent:接收用户原始请求,拆解任务清单。
  • 执行型Agent:只处理单一子任务,比如搜知识库、算结果、生成文案。
  • 审核型Agent:对执行结果做检查,不合格就打回重做。

这三类角色由客户端的一个调度器统一管理,消息格式做成统一的JSON结构。

2.2 常用编排模式:路由、编排者-工作者、群聊协作

多智能体不是随便写个循环就叫多智能体,编排模式决定任务质量和容错。我实测下来常用的有三种:

模式工作方式适用场景缺点
路由器模式一个Agent分析任务类型,分发给不同专用Agent任务类型分明、职责边界清晰无法处理复合型任务
编排者-工作者模式编排Agent规划、派发、回收、校验,工作者Agent只执行需要严格流程控制的任务编排Agent容易成为瓶颈
群聊协作模式多个Agent自由发言,通过消息协议互相衔接头脑风暴、创意生成结果不可控,容易跑题

我项目里最常用的是编排者-工作者模式。因为自用工具最怕跑题和失控,编排者统一协调,每个执行Agent收不到用户直接消息,只处理结构化子任务,输出也是结构化的,最终由编排者校验。

2.3 消息协议设计:角色、任务、结果的结构化

多智能体之间通信不能靠自然语言的自由发挥,必须定义一套结构化协议。我参考了主流Agent框架(比如LangGraph、AutoGen)的思路,简化成自己的三段式消息结构:

{ "message_id": "msg_001", "conversation_id": "conv_001", "role": "planner", "task": { "type": "summarize_notes", "params": { "source_ids": ["note_1", "note_2"], "output_length": "short" } }, "context": { "history": ["..."], "knowledge_base_ids": ["kb_a"] }, "result": null }

这里最关键的是role和task.type。Agent订阅感兴趣的task.type,处理完把结果写回result字段,再由调度器决定下一步发给谁。这样每一个Agent都是“无状态工人”,状态统一放在context里,由调度器管理。

我踩过的一个坑是:一开始让每个Agent自己携带全部对话历史,结果几个Agent打完一轮,消息体积翻了好几倍,Token费用直接起飞。后来改成只传当前任务相关的上下文,消息体积降了80%,而且每个Agent的判断反而更准了。

2.4 Token背后的Key/Query/Value逻辑:我如何理解“我是谁、找什么、能提供什么”

做多智能体绕不开Token和上下文,但“Token”不要只理解成计费单位。我习惯用一套类比来设计Agent上下文:Key是“我是谁”,Query是“我在找什么”,Value是“我能提供什么”。

比如一个“知识库检索Agent”,它的Key就是它的身份定位和职责描述,告诉模型“你是检索助手,只能查知识库”;Query是它收到的问题和任务目标;Value是它能调用的检索工具、能访问的文档索引。这个三元组定义清楚了,Agent就不会越权,也不会在无关问题上乱发挥。

这套思路同样适用于RAG检索。我后面会把知识库里的文档切片也按这三个维度做索引,检索时先匹配Query,再结合Key做过滤,最后返回Value。它不一定是传统深度学习里Attention的严格定义,但对工程实践来说,比模糊的“语义相似”好用得多。

3. Flutter侧的Agent运行时:状态、异步与线程细节

3.1 数据流的分层:UI层、Agent协调层、模型接入层

把多智能体搬进Flutter,最怕的是UI和Agent逻辑混在一起。我项目里的分层是这样的:

  • UI层:只负责渲染,监听状态变化,不直接调LLM。
  • Agent协调层:维护Agent列表、消息队列、任务状态机,对外暴露runTask()接口。
  • 模型接入层:封装LLM API调用、流式响应解析、Token统计,内部跟Dio或http打交道。
UI层(Widget/Cubit) -> Agent协调层(消息队列与状态机) -> 模型接入层(LLM API)

这样做的好处是,模型接入层换成任何一家厂商的API,或者从云端API换成本地Ollama,UI层和Agent协调层都不需要改。我前后换过三次模型服务商,只动了模型接入层一个文件。

3.2 Cubit状态管理与Agent状态的映射

Flutter的状态管理我选了flutter_bloc的Cubit,因为它轻量,适合把Agent状态机映射进UI。我的AgentCubit大致长这样:

class AgentCubit extends Cubit<AgentState> { AgentCubit(this._coordinator) : super(AgentState.idle()); final AgentCoordinator _coordinator; Future<void> startTask(TaskRequest request) async { emit(AgentState.running(taskId: request.taskId)); await for (final progress in _coordinator.runTask(request)) { emit(AgentState.progress( taskId: request.taskId, step: progress.step, detail: progress.description, )); } emit(AgentState.completed(taskId: request.taskId)); } }

我特意不用Bloc的Event机制,因为Agent的进度是天然流式的:规划、执行、校验、汇总,每一步都是一个中间态。Cubit配合Stream处理这种场景非常顺。

状态设计上我只有五种:idle、running、progress、completed、failed。多了没必要,少了用户反馈不够。进度文案从Agent协调层上报,UI层只是展示。

3.3 Future的then回调真的在微任务队列吗:异步顺序实测

热搜词里有人在问“Flutter的Future.then回调是放入微任务队列吗”,这个问题我在Agent调度器里真实踩过。Dart事件循环里有两套队列:微任务队列和事件队列。Future.then注册的回调,在Future完成时会被调度进微任务队列,微任务队列处理完之前,事件队列里的任务不会开始。

这句结论不是背出来的,是我跑实验验证的。我给Agent调度器连续派发三个子任务,分别用await和.then处理,打印输出顺序:

Future<void> testQueueOrder() async { Future(() => print('task1')).then((_) => print('then1')); Future(() => print('task2')).then((_) => print('then2')); await Future(() => print('task3')); print('after await'); }

实测输出是:task1、then1、task2、then2、task3、after await。这说明.then回调确实按微任务规则执行,但多个Future之间的调度顺序还得看它们本身的完成时机。

在Agent协调层里,我没有依赖then的调度来做任务顺序控制,而是用了显式的队列:一个任务完成后,由协调器手动发下一个。因为Agent任务有依赖关系,比如“规划Agent”必须先于“执行Agent”,不能靠微任务顺序去赌,那样迟早出竞态。

3.4 流式输出的处理:Stream加增量渲染

LLM生成是流式的,等全部生成完再显示,用户会以为卡死了。我在模型接入层用Stream输出增量,Flutter端用StreamBuilder逐段渲染。

模型接入层的最小实现看起来是这样:

Stream<String> chatStream(List<ChatMessage> messages) async* { final response = await _dio.post( '/chat/completions', data: {'messages': messages.map((m) => m.toJson()).toList()}, options: Options( responseType: ResponseType.stream, ), ); final stream = response.data as ResponseBody; await for (final chunk in stream.stream.transform(utf8.decoder)) { final content = _parseDelta(chunk); if (content != null) yield content; } }

这里有个细节:流式响应的解析不能写死单条JSON。SSE格式是每个事件一块数据,中间有换行分隔,我的_parseDelta实现会先做缓冲区拼接,把不完整的JSON行缓存起来,等下一块来了再拼成完整JSON解析。这个处理不对的话,流式输出经常会在中间位置断掉或者抛出FormatException。

4. 让Agent动手:工具调用与Function Calling的落地细节

4.1 模型是嘴,工具是手:函数调用的原理

我之前一直有个疑问:LLM只会输出文本,凭什么能“调用工具”?后来搞懂了,模型不是自己执行代码,而是“说出”想调用哪个函数、传什么参数,由客户端去执行,再把结果回传给它。这就是Function Calling,也叫工具调用。

整个流程其实是一个循环:

  1. 客户端把“有哪些工具可用、每个工具的参数是什么”拼进请求。
  2. 模型判断需要调用工具时,返回一个结构化的调用指令。
  3. 客户端解析指令,执行对应函数。
  4. 把函数执行结果作为新消息发给模型。
  5. 模型基于结果继续回答或决定是否再调用。

在这个循环里,工具是否好用的核心不在模型,而在“工具描述写得好不好”。描述不清晰,模型就会瞎编参数。

4.2 定义工具的JSON Schema:参数校验是重点

我给每个工具定义一份JSON Schema,模型看到的就是这个结构。以一个“查询知识库”工具为例:

{ "name": "search_knowledge_base", "description": "在本地知识库中检索与问题最相关的文档片段,返回片段内容和来源ID", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "检索目标,必须是与用户问题直接相关的关键词或语句" }, "top_k": { "type": "integer", "description": "返回的片段数量,默认3,最大5", "minimum": 1, "maximum": 5 } }, "required": ["query"] } }

参数schema里最容易忽略的是“描述”。同一个参数,写“要搜索的内容”和写“必须是与用户问题直接相关的关键词或语句”,模型执行出来的效果差别很大。因为模型不是按参数名理解语义,主要是读描述。

还有一个常见错误:参数类型定义模糊。比如top_k我一开始没写minimum和maximum,模型偶尔传一个20,后面检索模块直接超时。加上边界约束之后,这类问题基本消失。

4.3 Flutter端工具注册表的实现

在Flutter里我实现了一个简单的工具注册表,本质是Map加一个执行包装器:

typedef ToolHandler = Future<dynamic> Function(Map<String, dynamic> args); class ToolRegistry { final Map<String, ToolHandler> _handlers = {}; void register(String name, ToolHandler handler) { _handlers[name] = handler; } Future<dynamic> execute(String name, Map<String, dynamic> args) async { final handler = _handlers[name]; if (handler == null) { throw Exception('未知工具: $name'); } return handler(args); } List<Map<String, dynamic>> toSchemaList() { return _handlers.keys.map((name) => _schemas[name]!).toList(); } }

调度器拿到模型返回的tool_calls之后,别直接执行。我的习惯是先做一次参数白名单校验:只保留schema里声明的字段,过滤掉模型瞎传的字段。这一步能防止脏参数污染工具函数。

4.4 工具调用失败时最常见的错误:schema和tool payload被拒绝

我在接不同模型服务时,经常遇到这样的报错:llm request failed: provider rejected the request schema or tool payload。

这个报错看起来像是网络问题,实际上90%都是工具定义格式不符合供应商要求。我遇到过的原因有这几个:

  • 用了供应商不支持的parameters格式,有些厂商只接受strict模式或特定字段名。
  • 工具描述里带了空字符串或超长文本。
  • tool_calls返回结果回传时没有按厂商要求的角色字段格式写。

排查思路很简单:先注释掉所有工具定义,只发普通对话,确认能通;然后逐个加工具,二元查找是哪个定义的问题。不要一次性把20个工具全怼上去,那排查成本太高。

5. 给模型装上记忆库:RAG与知识库集成

5.1 为什么纯对话模型回答不了私域问题

LLM训练数据截止到某个时间点,它不知道你的笔记内容、你的项目文档、你的私有资料。硬问它就胡编。解决方案不是换更大的模型,而是给它一个“外挂记忆库”,也就是RAG:先把文档切分成片段、做向量化存储,用户提问时先检索最相关的片段,再把这些片段拼进提示词,最后让模型基于检索结果作答。

这套东西的工程链路过一遍之后,你会发现它没有想象中神秘,但细节决定效果好坏。

5.2 检索增强RAG的最小可运行链路

我落地RAG时,链路简化成四步:

  1. 文档切片:把长文档按固定长度(比如500字)切块,并保留段落标题作为上下文前缀。
  2. 向量化:用嵌入模型把每个切片转成向量。
  3. 检索:用户提问时,把问题向量化,做余弦相似度检索,取Top-K。
  4. 拼接与生成:把命中的切片内容和来源ID拼进提示词,让LLM回答。

切片这一步最影响质量。我一开始按字符硬切,结果把一个段落从中间截断,检索出来的片段语义不完整,模型回答经常前言不搭后语。后来改成“按段落切,段落太长再按句子断”,效果明显改善。

向量化我用的是现成的嵌入API,成本不高。自用小工具完全够用。

5.3 Wiki知识库、本体与GraphRAG:从检索到推理

单纯向量检索有个天花板:它是“按语义找相似片段”,但回答不了需要跨文档推理的问题。比如“过去三个月哪些笔记提到过性能优化方案”,这种问题分散在多个文档里,向量检索能各自命中,但无法把关系串起来。

这就是为什么现在大家讨论GraphRAG和本体。简单说,GraphRAG在向量检索之上,把文档里的实体和关系抽出来,组成一张知识图谱,查询时不只找片段,还沿着实体关系找关联。本体的作用更偏“规范”:它定义了这个领域里有哪些概念、概念之间是什么关系,让检索器知道“优化方案”和“性能问题”是相关的概念。

我在项目里没有一上来就上GraphRAG,因为成本高、实现重。我的路径是:先用纯RAG跑通,然后给知识库加了一层很轻的“标签本体”,每个切片手动或半自动打上“主题”、“来源”、“时间”等标签。查询时先按标签过滤,再走向量检索。实测效果已经比纯向量好不少,也够我用。等数据量大了再考虑GraphRAG。

5.4 嵌入模型与向量库的取舍

嵌入模型的选择,我建议直接看Open LLM Leaderboard这类公开榜单作为初筛,再看跑分不能只看总分,要看检索相关任务的单项分。自用场景不求最强,但求稳定。

向量库方面,Flutter端项目我推荐两种选择:

方案特点适合场景
本地JSON加内存向量计算无额外依赖,支持余弦相似度手写实现知识库小于几千条,自用工具
SQLite加向量插件数据落盘,支持增删改查,查询快知识库体量增长,需要持久化

我一开始用纯内存,结果App重启之后知识库全丢,还得重新加载。后来改成了SQLite方案,把向量以二进制存库,查询时取出来算相似度,几百个片段的量级,效率完全没问题。

6. 桥接原生世界:EventChannel、PlatformView与平台适配

6.1 为什么Flutter应用还需要原生能力

我的AI助手做了几个月后,发现自己绕不开原生能力:有的模型SDK只有原生版本,没有Dart SDK;有的系统级API比如扫描本地文件、读取系统剪贴板,用Flutter插件做不灵活;还有一些硬件能力必须走原生通道。Flutter的优势是UI和逻辑跨平台,但和系统底层打交道的活,还是得靠原生代码补。

6.2 EventChannel做LLM请求的跨端通道

我用的原生AI SDK只提供了安卓和iOS版本,Dart这边直接用HTTP调不了。我的做法是用MethodChannel发起推理请求,用EventChannel接收流式文本返回结果。

原生侧发流式事件的核心代码逻辑如下:

// Kotlin侧:通过EventChannel推送增量文本 private var eventSink: EventChannel.EventSink? = null override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { eventSink = events } fun pushToken(content: String) { eventSink?.success(mapOf("type" to "token", "content" to content)) } fun pushDone() { eventSink?.success(mapOf("type" to "done")) }

Flutter侧接收端我是这样组织的:

// Dart侧:原生流式事件直接接到Agent协调层的Stream EventChannel('agent_inference').receiveBroadcastStream().listen((event) { final map = event as Map<dynamic, dynamic>; if (map['type'] == 'token') { _inferenceController.add(map['content']); } else if (map['type'] == 'done') { _inferenceController.close(); } });

这里有个非常容易踩的坑:EventChannel必须在原生页面创建之后监听,过早监听会丢失事件。我一开始把监听放在了App启动时,结果原生侧消息发出来时Flutter监听还没注册,丢了一堆token。后来改成在进入Agent页面时才创建EventChannel并注册监听。

6.3 PlatformView嵌入原生地图、网页与扫描组件

需要嵌入原生视图的场景,Flutter也有方案,就是用PlatformView。比如我的助手需要展示一篇原生渲染的Markdown网页预览,直接用Flutter的WebView插件在部分机型上有兼容问题,我是用PlatformView桥接的。安卓端用PlatformViewLink或者AndroidView,iOS端用UiKitView,代码不算复杂,但要注意生命周期同步:原生视图的创建和销毁必须跟Flutter Widget的挂载和卸载对齐,否则页面跳转后原生资源不释放,内存会持续上涨。

6.4 安卓原生项目嵌入Flutter页面的反向集成

我现在这个项目不是纯Flutter工程,而是安卓原生App里嵌入Flutter页面,也就是热搜词里说的“安卓原生项目嵌入Flutter页面”。反向集成的坑比纯Flutter多一个:Flutter引擎的创建时机。

我的做法是在Application初始化时,预先创建好FlutterEngine并缓存起来,等需要跳转Flutter页面时,直接用缓存的engine展示,加载速度和体验会好很多。如果每次跳转都新建一个引擎,冷启动感特别明显,而且内存压力很大。

// Java侧:预创建FlutterEngine FlutterEngine flutterEngine = new FlutterEngine(this); flutterEngine.getDartExecutor().executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ); FlutterEngineCache.getInstance().put("agent_engine", flutterEngine);

页面跳转时:

FlutterActivity.withCachedEngine("agent_engine").build(context);

用缓存引擎有个注意点:多个Flutter页面共用一个引擎时,路由状态是共享的,需要自己在Dart侧处理页面栈,不然从页面A回退时可能直接退到原生界面。

6.5 Impeller渲染引擎与Web引擎启动慢的观察

Flutter 3.44之后,开发者讨论最多的渲染层变化就是Impeller。我升级后观察到的现象是:iOS上的滚动和动画帧率更稳定了,Shimmer、打字机这类特效也没问题。但Android上部分自定义着色器需要在Impeller下重新适配,如果你项目里有复杂的自定义Shader,升级前最好做一个图形预览的回归测试。

另外Flutter Web的引擎启动慢问题,我也处理过。原因多半是首次加载要拉取依赖的JS和CanvasKit。我做过两个优化:一是开启--web-renderer=html(低版本)或按目标平台选择合适渲染器,大幅减少启动体积;二是把资源改成延迟加载,首页只加载核心入口,AI助手模块真正打开时才加载相关的Dart包。

7. 打包发布与版本刺头:我遇到的坑和排查思路

7.1 Gradle插件命令式apply报错

热搜词里有句报错很直观:“you are applying flutter's main gradle plugin imperatively using the apply script”。这个我升级Flutter之后也撞上了。

新旧版本的变化是:新版Flutter要求使用声明式插件管理系统,在settings.gradle里声明:

plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.1.0" apply false }

而不是在app/build.gradle里用apply plugin:命令式应用。这个问题的排查思路很简单:按报错提示,把Flutter插件从build.gradle迁移到settings.gradle统一声明,同步一下。如果项目里引用了多个Flutter插件,注意它们的版本别对不上,一般报错信息会指明是哪个插件依赖冲突。

7.2 Xcode版本过新导致Flutter包报版本低的问题

我的Mac升级到新版Xcode之后,跑Flutter项目经常提示“当前的Flutter SDK不被完全支持”之类的警告,有的Pod安装还会失败。这个问题的本质是:Flutter SDK有个支持上限,Xcode版本超出后被判定为不兼容。

排查思路是先升级Flutter SDK,升级到支持该Xcode大版本的版本;升级后如果还有问题,清理Pod缓存和构建目录,重新pod install。我不建议一上来就改Xcode版本,除非你的项目依赖锁死了Flutter版本。这个问题在Flutter 3.44之后改善了不少,但新Xcode预览版发布初期,还是建议先观望再升级。

7.3 组件通信丢失状态与TabBar点击动画异常

做AI应用时,我经常遇到页面切换后Agent状态丢失的问题。Flutter的Navigator默认会销毁非当前路由,比如你从Agent页面跳到详情页,再返回来,Agent页面被重建,状态没了。我试过几种方案:

  • 用IndexedStack保住页面状态,适合Tab切换场景。
  • 用StatefulShellRoute管理带有状态的Shell分支。
  • 用全局状态管理(Cubit/Bloc)把Agent状态提升到App层,页面重建后重新订阅。

我最后选了第三种,Agent状态全部提升到AgentCubit,页面只是状态的投影。这样无论页面怎么跳,Agent执行进度不会丢,返回页面时界面自动恢复。

TabBar点击动画的问题则是另一个方向:想要取消点击动画,通常是在TabBar的animationDuration设为Duration.zero,或者在Indicator设置里关掉动画。如果用的是自定义TabBar,检查是否有隐式的AnimatedContainer在做动画。

7.4 模型选型参考:公开榜单与本地部署思路

多智能体项目里,模型选型会直接影响工具调用能力和多轮协同效果。我参考了Open LLM Leaderboard这类公开榜单做初筛,但我的判断标准是:

  • 工具调用能力:看模型在Function Calling评测集上的表现,这不是榜单总分能体现的。
  • 上下文长度:多Agent协同需要长上下文,至少8K起。
  • 本地部署可行性:如果不想全部走云端API,检查模型量化版本对内存的需求。7B量化版在16G内存的机器上可以跑,13B级别以上建议上24G以上。

实际测试时,我会准备20个工具调用测试用例,覆盖“正确调用”“不该调用时别乱调用”“参数边界”三类情况。跑一轮就知道模型能不能用,不用看太多榜单。

结尾:一点个人体会

这么一条链路跑下来,我的最大体会是:Flutter、LLM、多智能体单拎出来都有大量文档,难的是把它们缝在一起。这里缝线的地方不在UI,而在状态管理、消息协议、异步调度和原生桥接。你如果也打算做类似项目,我建议从最简单的“一个Agent+一个工具+RAG归档”开始跑,先把链路走通,再加第二个Agent。我一开始贪多,一口气设计了六个角色,结果调试时一半的时间都在查是哪个Agent传了脏数据。如果你也想做一个自己的AI助手,先把架构想清楚,然后把每一步走稳,这个方向能玩的深度绝对超乎你的想象。

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

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

立即咨询