资讯动态

基于Flutter 3.32.8与硅基流动API的AI对话应用实战

发布时间:2026/10/8 8:49:29 来源:尧图企业网站定制
做这个 flutter 3.32.8 版本的 ai 对话聊天基础项目起因其实很简单我想在自己的 App 里塞一个能正经聊天的 AI 入口又不想自己维护一套推理服务、也不想承担 GPU 成本。硅基流动 API 恰好提供了按 token 计费的大模型在线接口把对接成本直接压缩到一个 HTTP 请求的规模。这个项目的目标就是把整条链路跑通——从环境搭建、密钥申请、请求报文格式到 Flutter 端如何做流式接收、状态管理和界面渲染最终在手机上能和模型正常对话。这里的“基础”两个字不代表粗糙反而意味着把最缠人的协议细节和 SDK 适配都提前趟平了。如果你刚接触 Flutter或者第一次接大模型 API这篇把每一步怎么落地的思路和代码都摆出来直接照着抄就行。表面上看这个项目只是一个“HTTP 请求 列表展示”但真正动手会发现环环相扣版本不一致导致构建失败、密钥泄露风险、流式响应不处理会卡界面、上下文越长 token 越贵。下面我就从头把整套实现过程拆开讲。1. 项目概述与技术选型思考1.1 需求拆解与功能边界在动手之前我先把“聊天基础版”的功能边界划清楚。一个最小可用的 AI 对话应用需要满足这几件事用户输入一句话点击发送后能立即看到消息进入列表请求大模型接口把返回的内容追加到聊天记录中支持多轮上下文模型能记得用户前面说的话请求期间有加载状态不能让用户干等着以为应用死了消息列表可以滚动视觉上区分用户消息和 AI 回复。在基础版里我不做用户系统、不做会话历史持久化、不做多模型切换、不做语音输入。这些功能后面扩展不迟但第一版如果功能堆得太满调试时反而分不清问题是出在网络协议上还是界面上。先跑通链路再加花样这是我做这类项目一贯的顺序。1.2 为什么选 Flutter 3.32.8 搭配硅基流动 APIFlutter 的版本选择上我直接锁定了 3.32.8。这个版本已经默认启用 Impeller 渲染引擎在 Android 真机上滚动列表和刷新消息时明显比旧版的 Skia 渲染更稳定不容易出现掉帧和毛刺。很多人在网上搜“flutter impeller”相关的性能问题其实版本到位以后大部分已经解决。3.32.8 的另一个优势是 Gradle 插件版本和 Android 构建工具链的匹配度更好新建项目后直接能跑起来的概率高很多不会一上来就被各种构建报错劝退。模型 API 服务商的选择硅基流动 API 是我目前综合下来比较省心的一家。它提供的接口协议和 OpenAI 的 chat/completions 格式兼容这意味着 Flutter 端不需要引入特殊的 SDK直接用标准 HTTP 请求就能完成通信。平台上有 DeepSeek、Qwen、GLM 等多个开源模型的在线推理版本按 token 计费还有面向新用户的免费体验额度。对个人开发者和独立项目来说这比自己在服务器上部署一个 70B 大模型要现实得多——后者光是显存成本和推理延迟就能拖垮项目进度。1.3 整体架构与数据流设计这个项目的架构虽然简单但我还是分成了四层避免后续扩展时牵一发动全身。UI 层聊天页面和消息气泡组件负责渲染数据和接收用户输入状态管理层基于 Provider 维护消息列表、请求状态和错误信息服务层封装硅基流动 API 的调用逻辑包括请求头、参数序列化、流式解析配置层集中管理 API Key、模型名、系统提示词等常量。数据流是单向的用户在输入框提交文字后状态层把用户消息加入列表紧接着调用服务层发起请求服务层收到流式片段后回调状态层状态层更新消息内容并调用 notifyListenersUI 层监听变更后重新构建对应的消息气泡。通信方式不搞花活规规矩矩的请求-回调模式这对一个基础项目来说够用也最容易排查问题。2. 开发环境准备与项目初始化2.1 Flutter 3.32.8 环境搭建细节如果你用 Windows 做开发先把 Flutter 3.32.8 的稳定版压缩包下载下来解压到一个不带空格的路径比如D:\flutter。然后把D:\flutter\bin添加进系统 PATH。macOS 上我建议直接用 fvm 管理 Flutter 版本一条fvm install 3.32.8就能隔离多个版本避免不同项目互相干扰。安装完成后终端里跑一下flutter doctor它会检查 Android SDK、Java、Xcode 这些配套工具。第一次跑的时候大概率会提示 Android licenses 未接受执行flutter doctor --android-licenses一路回车或者输入 y 即可。这一步如果在 Windows 上做还需要确认你的 Android Studio 里已经装好了 SDK Platform 和 Build Tools。我在这个环节踩过最常见的坑是机器上原来装了旧版本 Flutter环境变量 PATH 里指向的是旧路径。后面明明输入flutter --version显示的是 3.32.8但新建项目时依然沿用旧版本的缓存导致模板异常。建议切版本后顺手执行一次flutter clean和flutter pub cache repair代价不高却能避免大量诡异报错。2.2 创建项目与依赖管理环境顺利通过后创建项目就很简单了flutter create ai_chat_demo --org com.example --platforms android,ios--platforms参数可以按需保留我只保留 Android 和 iOS避免生成一堆用不到的桌面或 Web 目录。依赖方面我没有引入重量级框架只加了几个基础包dependencies: flutter: sdk: flutter http: ^1.2.0 provider: ^6.1.0 shared_preferences: ^2.2.0网络请求用http而不是用dio主要是因为基础版只有一个 POST 请求不需要拦截器、Cookie 管理、请求队列这些功能http的 API 简单直接学习成本低。状态管理用provider它是官方推荐方案里最轻量的一种核心概念就两个把共享状态放进ChangeNotifierUI 层用context.watch监听变化。对比 riverpod 和 blocprovider 的模板代码更少对新手友好得多。shared_preferences是为了后面存配置用的比如把用户选择的模型名、历史记录数量保留在本地。第一版即使还没用到先把依赖装好也不会造成负担。2.3 目录规划与配置隔离项目创建后我把lib目录整理成下面这个结构lib/ ├── main.dart ├── config/ │ └── api_config.dart ├── models/ │ └── chat_message.dart ├── services/ │ └── chat_api_service.dart ├── providers/ │ └── chat_provider.dart ├── screens/ │ └── chat_screen.dart └── widgets/ └── message_bubble.dart这个结构的好处是哪怕项目后面膨胀到几百个文件核心的 API 调用逻辑还是在services里不会和 UI 搅在一起。关于 API Key 的配置我特别强调一下绝对不要把密钥硬编码在代码里然后推到 Git 仓库。基础项目可以放到config/api_config.dart里但记得把该文件加入.gitignore。如果只是本地调试也可以使用--dart-define方式在启动时注入密钥flutter run --dart-defineAPI_KEYsk-xxxx代码里用String.fromEnvironment(API_KEY)读取。这样做的好处是密钥不会进代码仓库方便多人协作时各自管理自己的密钥。3. 硅基流动 API 接入详解3.1 申请密钥与模型选择使用硅基流动 API 的第一步是注册账号然后在控制台创建一个 API Key。整个过程没有太多门槛生成后的密钥是一段类似sk-开头的字符串妥善保存。这个密钥等同于账户的操作口令一旦泄露别人就能用它调用你的接口并消耗 token 费用所以平时要像对待密码一样对待它。平台上的模型列表是会动态变化的我创建项目时常用这几个整理成表格方便对比模型标识上下文长度适用场景说明deepseek-ai/DeepSeek-V364K综合对话、代码生成推理能力强性价比高Qwen/Qwen2.5-72B-Instruct128K中文对话、内容创作中英文均衡中文更自然THUDM/glm-4-9b-chat8K轻量对话、分类提取部署门槛低响应快deepseek-ai/DeepSeek-R164K复杂推理、数学题思维链内容较多耗时更长我在基础版里默认使用deepseek-ai/DeepSeek-V3。它综合能力够强token 单价也压得比较低作为默认模型不会让新手用户产生“怎么聊几句就没钱了”的错觉。模型的选择会影响请求返回的格式和思维链内容比如 R1 会把推理过程也放进返回体如果代码没有过滤掉这些字段界面上就会出现大段诡异的“思考过程”。所以模型一旦定下来客户端解析逻辑就要和它匹配。3.2 请求报文结构与参数说明硅基流动 API 的端点地址是标准的 chat/completions 路径完整的请求地址是https://api.siliconflow.cn/v1/chat/completions。请求头需要携带两个关键字段Authorization: Bearer sk-xxxx Content-Type: application/json请求体最核心的是 messages 数组每个元素包含 role 和 content。role 有三个取值system 用来设定系统级指令user 表示用户输入assistant 表示模型的历史回复。基础版我会在消息列表最前面塞一条 system 指令比如“你是我的 AI 助手回答时简洁口语化”这样模型不会把每次对话当成一个孤立的问题。一个完整的请求体长这样{ model: deepseek-ai/DeepSeek-V3, messages: [ { role: system, content: 你是我的 AI 助手回答简洁、口语化。 }, { role: user, content: 帮我写一个 Flutter 聊天页面 } ], max_tokens: 1024, temperature: 0.7, stream: false }参数方面max_tokens决定模型最多输出多少 token注意它不是字符数一个汉字大约对应 1 到 2 个 token。temperature控制随机性取值范围一般是 0 到 2数值越高回答越发散数值越低回答越保守。基础对话我用 0.7既能保持自然感又不会跑偏。如果以后做客服机器人建议把 temperature 降到 0.3 左右回答会更稳定。3.3 流式返回协议解析stream参数是这个项目的关键之一。设置成true后API 不会一次性吐完整段回复而是通过 Server-Sent Events 的方式逐段推送数据。每段数据以data:开头最后以data: [DONE]收尾例如data: {choices:[{delta:{role:assistant},index:0}]} data: {choices:[{delta:{content:你},index:0}]} data: {choices:[{delta:{content:好},index:0}]} data: {choices:[{delta:{},finish_reason:stop}]} data: [DONE]为什么一定要用流式因为大模型生成一段完整回答可能要几十秒尤其是一些推理型模型思考时间更久。如果非流式请求用户只能看到转圈图标干等而且请求时间一长网关卡在中间很容易超时。流式方案让每个 token 生成后立刻送到客户端用户的感知是“对方正在打字”体验完全不一样。解析流式返回时直接按行读取遇到data:前缀就提取后面的 JSON。正常分发逻辑只要关心choices[0].delta.content字段即可为空就跳过直到[DONE]或finish_reason出现再收尾。4. Flutter 端 AI 对话核心实现4.1 网络层封装与错误处理我把网络请求单独拆成一个ChatApiService类这样界面和状态管理都不需要关心 HTTP 细节后面想换成别的模型提供商只需要改这一个文件。用 Dart 的HttpClient做流式请求一个最小实现如下class ChatApiService { ChatApiService({required this.apiKey}); final String apiKey; static const _endpoint https://api.siliconflow.cn/v1/chat/completions; Futurevoid chat({ required ListMapString, String messages, required void Function(String delta) onDelta, required void Function(String fullText) onDone, required void Function(String error) onError, }) async { final body jsonEncode({ model: deepseek-ai/DeepSeek-V3, messages: messages, stream: true, max_tokens: 2048, temperature: 0.7, }); final client HttpClient(); try { final req await client.postUrl(Uri.parse(_endpoint)); req.headers.set(HttpHeaders.contentTypeHeader, application/json); req.headers.set(HttpHeaders.authorizationHeader, Bearer $apiKey); req.add(utf8.encode(body)); final res await req.close(); if (res.statusCode ! 200) { final err await res.transform(utf8.decoder).join(); onError(HTTP ${res.statusCode}: $err); return; } final stream utf8.decoder.bind(res).transform(const LineSplitter()); final buffer StringBuffer(); await for (final line in stream) { if (!line.startsWith(data:)) continue; final data line.substring(5).trim(); if (data [DONE]) break; final json jsonDecode(data); final delta json[choices][0][delta][content]; if (delta ! null delta is String) { buffer.write(delta); onDelta(delta); } } onDone(buffer.toString()); } catch (e) { onError(e.toString()); } finally { client.close(); } } }错误处理我习惯分成两层网络层捕获异常并转成错误字符串状态层再根据错误内容决定要不要展示重试按钮。默认的HttpClient没有超时设置我建议在client.connectionTimeout上显式配置连接超时和读取超时否则弱网环境会挂很久才报错。4.2 对话状态管理Provider 实现消息列表需要被输入框、列表、加载状态多处共享直接用 StatefulWidget 的 setState 会越写越乱。我用 Provider 包一个ChatProvider核心代码如下class ChatMessage { ChatMessage({required this.role, required this.content}); final String role; String content; } enum ChatStatus { idle, loading, done, error } class ChatProvider extends ChangeNotifier { ChatProvider(this._api); final ChatApiService _api; final ListChatMessage _messages []; ListChatMessage get messages _messages; ChatStatus status ChatStatus.idle; String errorMessage ; Futurevoid send(String text) async { _messages.add(ChatMessage(role: user, content: text)); final assistant ChatMessage(role: assistant, content: ); _messages.add(assistant); status ChatStatus.loading; notifyListeners(); final history _messages .where((m) m.role ! system) .map((m) {role: m.role, content: m.content}) .toList(); await _api.chat( messages: history, onDelta: (delta) { assistant.content delta; status ChatStatus.loading; notifyListeners(); }, onDone: (_) { status ChatStatus.done; notifyListeners(); }, onError: (e) { status ChatStatus.error; errorMessage e; notifyListeners(); }, ); } }send方法里的逻辑顺序很关键先把用户消息和空白的 assistant 消息都加到列表再发请求。这样 UI 层不需要额外维护“当前正在回复哪条消息”列表里的最后一条就是模型正在生成的回复。每收到一个 delta 就更新 assistant.content然后 notifyListeners 通知刷新界面上的文字就会一个字一个字跳出来。在main.dart里注入 Providervoid main() { final apiKey String.fromEnvironment(API_KEY); runApp( ChangeNotifierProvider( create: (_) ChatProvider(ChatApiService(apiKey: apiKey)), child: const MyApp(), ), ); }4.3 聊天界面搭建与流式渲染聊天界面的骨架是“上方列表 底部输入区”。列表我用ListView.builder消息项按角色渲染不同颜色和排列方向class ChatScreen extends StatefulWidget { const ChatScreen({super.key}); override StateChatScreen createState() _ChatScreenState(); } class _ChatScreenState extends StateChatScreen { final ScrollController _scrollController ScrollController(); final TextEditingController _inputController TextEditingController(); override Widget build(BuildContext context) { final chat context.watchChatProvider(); final visibleMessages chat.messages .where((m) m.role ! system) .toList(); return Scaffold( appBar: AppBar(title: const Text(AI 助手)), body: Column( children: [ Expanded( child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(12), itemCount: visibleMessages.length, itemBuilder: (context, index) { final msg visibleMessages[index]; return MessageBubble(message: msg); }, ), ), _InputArea( controller: _inputController, onSend: (text) { if (text.trim().isEmpty) return; chat.send(text.trim()); _inputController.clear(); WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); }); }, ), ], ), ); } }流式渲染没有额外做动画它的红利来自notifyListeners触发重建。每次重建整个列表会带来滚动抖动所以我给每条消息加了一个稳定的 key值直接用消息内容拼接角色生成。这样即使列表刷新底层状态也不会乱跳。MessageBubble组件就是普通 Flex 布局用户消息靠右、蓝底白字AI 消息靠左、灰底黑字。当 AI 消息内容为空且状态是 loading 时显示一个小的进度圆环表示模型正在生成回复。5. 常见问题与排查技巧实录5.1 状态码与异常消息速查接入硅基流动 API 时最常见的几类问题我整理成一张表状态码/现象可能原因处理方式401密钥错误或已失效检查 Authorization 头和 API Key 是否正确400请求体格式不对核对 model 标识、messages 是否数组、字段类型429触发限流或账户余额不足降低并发请求频率检查控制台余额500/503服务端异常等待几秒后重试或更换模型连接超时网络环境问题或请求体过大增加超时时间确保走流式接口有次我的项目突然返回 400排查半天才发现是历史消息里混进了一条role: system但 content 为空的消息接口校验直接拒绝。从那以后我在组装请求体时都会先用where过滤掉空 content 的消息。还有一种特别容易误导人的报错提示某个模型路由缺少 API Key。这种情况一般不是你 Flutter 代码的问题而是中间的网关或转发层没有正确透传密钥。我建议排查链路时先拿 curl 直接请求接口能复现就说明问题在 API 层不复现就是客户端的问题能省掉大量无用功。5.2 流式解析中的坑与避雷流式解析看起来不复杂实际踩坑不少。第一不要直接jsonDecode(response.body)。如果没开流式一次性返回的 body 可以直接解析但开了流式后整段文本是一行行累积的直接解析整个response.body根本拿不到流式数据。正确做法是逐行读取再逐行解析。第二BOM 问题。某些代理工具会在响应流最前面插入 UTF-8 BOM导致第一行data:前面多一个不可见字符jsonDecode直接报错。我在解析前会先判断一下开头有没有\ufeff有就剥掉。第三脏行问题。流式协议里除了data:行还可能出现空行和注释行。代码里判断line.startsWith(data:)是必须的否则会把空行和事件的注释也送给 jsonDecode。5.3 构建与运行阶段的典型报错很多人在网上搜“flutter 新建项目后跑不起来”我一并说几个实际遇到的构建期问题。最典型的是这个报错You are applying Flutters main Gradle plugin imperatively using the apply script。这通常出现在升级 Flutter 版本后项目里旧版 Gradle 配置和新版插件的兼容性出了问题。解决方法是打开android/settings.gradle把传统的 apply script 方式改成 plugins DSL 方式plugins { id com.android.application version 8.3.0 apply false id com.android.library version 8.3.0 apply false id org.jetbrains.kotlin.android version 1.9.22 apply false }改完以后同步 Gradle再重新构建就好。另一个高频问题是 Android release 包安装后无法联网这是因为 release 模式下 AndroidManifest 缺少网络权限。检查android/app/src/main/AndroidManifest.xml顶部确保有这行uses-permission android:nameandroid.permission.INTERNET/5.4 基础版常用的体验优化基础版跑通之后有几个低成本优化能明显提升体验。第一是控制历史消息长度。多轮对话时messages 数组每轮都会变长而 token 是按总量收费的上下文越长单次请求越贵、响应越慢。我在发送前加了一个截断逻辑只保留最近 20 条消息超出部分直接丢弃。还有一种更聪明的做法是写一个摘要 prompt让模型把早期对话浓缩成几句话再放入上下文不过基础版用截断就够了。第二是请求超时配置。流式请求虽然整体时间可能很长但长时间没有任何数据到达多半是连接已经死了。把连接超时设成 10 秒读取超时设成 60 秒能有效防止界面一直转圈。第三是 API Key 的管理。这个我再强调一次不要提交到 Git不要打包进 release APK。最稳妥的做法是让后端做一个中转代理客户端把请求发给自己的服务器由服务器持有 API Key 再调用硅基流动 API。这样即使客户端被逆向攻击者拿不到你的密钥。基础版如果嫌重至少要用--dart-define注入而不是明文写在源码里。6. 后续扩展方向与个人体会6.1 功能扩展的几个方向基础版完整跑通后我再按优先级列出几个可参考的扩展方向。改成多模型切换成本最低因为服务层已经把请求协议统一了只要在配置层加一个模型名的下拉选择再让ChatProvider初始化时接收模型参数即可。结合shared_preferences用户选中的模型就会被记住。停止生成按钮也值得做。流式请求是持续通道用户可以随时中断。实现方式是在ChatApiService里保留一个HttpClient引用停止时调用client.close()上游连接会立刻断开。配合界面上一个“停止”按钮体验会完整很多。会话历史持久化用sqflite或hive都能做把消息列表按时间存本地App 重启后还能恢复上下文。如果项目后面要做知识库问答那就需要研究 embedding 接口和向量检索这是另一个更大的话题了。我还在热搜里看到不少人问“多 ai 协作”实际上你只要把ChatApiService的接口抽象成统一契约然后实现多个供应商的 Service上层代码就能在不改动 UI 的前提下切换或者并行调用多个模型。这也是为什么我在基础版坚持把网络层单独封装给未来的扩展留好了余地。6.2 我个人在实际操作中的几点体会这个项目真正常规文档里不会写的东西我觉得有三条。第一AI 对话项目的调试思路要转变。普通接口返回的数据是稳定的参数错了立刻能看出来但对话类接口的返回取决于模型同一个请求这次和下次可能都不一样。所以调试时先把stream: false跑一遍确认基本链路没问题再切回流式这样能少踩很多坑。第二界面上的“打字机”效果其实是免费的。只要你用流式接口并且每次 delta 都触发重建自然就会呈现出文字逐个蹦出来的效果不需要额外做定时器动画。我第一次实现时还专门写了一个 Timer 轮询后来发现把简单的事情做复杂了。第三版本管理一定要认真。这个项目如果是拿来学习的可以把每个阶段的完整代码打一个 tag比如v0.1-basic、v0.2-stream如果是上线的API Key 保护和服务端代理迟早要做。反正提前把这部分设计好后面就不会为几行临时代码熬夜。做到这里一个基于 Flutter 3.32.8 和硅基流动 API 的 AI 对话基础应用已经完整落地。往后无论是换模型、加功能还是重构 UI你手头这份结构都会成为扎实的起点。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑