如何在Flutter应用中实现Python版GPT-3聊天交互功能
GPT-3聊天模块实现方案
临近项目截止日期优先选Dart直连接入Flutter的方案,无需额外部署后端,改完前端代码即可跑通全流程,耗时最短。如果后续需要做密钥隔离、多端复用,再考虑Python搭建API的方案。
方案1:Dart直连GPT-3(推荐,最快落地)
前置准备
- 拿到OpenAI平台的API_KEY
- 在Flutter项目的
pubspec.yaml中添加网络请求依赖:
dependencies: flutter: sdk: flutter http: ^1.1.0
- 给Android项目加网络权限:在
android/app/src/main/AndroidManifest.xml的<manifest>标签下加一行:
<uses-permission android:name="android.permission.INTERNET" />
代码修改
直接替换原有homepage的代码即可,核心逻辑:
- 维护
generative_conversation字符串存储完整会话上下文,初始值预置AI开场语 - 用户点击发送后,先把用户消息追加到会话串、添加到消息列表
- 调用GPT-3接口拿到回复后,把AI消息追加到会话串、添加到消息列表,自动滚动到底部
- 加了简单的加载状态,请求过程中禁用发送按钮避免重复提交
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'messageModel.dart'; class homepage extends StatefulWidget { const homepage({Key? key}) : super(key: key); @override State<homepage> createState() => _homepageState(); } class _homepageState extends State<homepage> { final ScrollController _scrollController = ScrollController(); final TextEditingController myController = TextEditingController(); List<ChatMessage> messages = []; // 替换成你自己的OpenAI API Key final String openAiApiKey = "YOUR_API_KEY_HERE"; // 会话上下文字符串,初始预置Carl Rogers的开场 String generativeConversation = "T:Hello, nice to see you today.\nP:"; bool isLoading = false; @override void initState() { super.initState(); // 初始化时添加开场消息 messages.add(const ChatMessage( messageContent: "Hello, nice to see you today.", messageType: "receiver" )); } // 调用GPT-3接口拿回复 Future<String> getGptResponse() async { final response = await http.post( Uri.parse("https://api.openai.com/v1/completions"), headers: { "Content-Type": "application/json", "Authorization": "Bearer $openAiApiKey" }, body: jsonEncode({ "model": "text-davinci-002", "prompt": generativeConversation, "max_tokens": 100, "temperature": 0.8, "stop": "P:" }) ); if (response.statusCode == 200) { final resData = jsonDecode(response.body); return resData["choices"][0]["text"].toString().trim(); } else { throw Exception("接口请求失败: ${response.body}"); } } // 发送消息逻辑 void handleSend() async { final inputText = myController.text.trim(); if (inputText.isEmpty || isLoading) return; setState(() { // 添加用户消息 messages.add(ChatMessage( messageContent: inputText, messageType: "sender" )); isLoading = true; }); myController.clear(); // 先滚动到底部 Future.delayed(const Duration(milliseconds: 100), () { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut ); }); try { // 把用户消息追加到会话串 generativeConversation += "$inputText\nT:"; // 拿AI回复 final aiReply = await getGptResponse(); // 把AI回复追加到会话串,准备下一次请求 generativeConversation += "$aiReply\nP:"; setState(() { messages.add(ChatMessage( messageContent: aiReply, messageType: "receiver" )); isLoading = false; }); // 再次滚动到底部 Future.delayed(const Duration(milliseconds: 100), () { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut ); }); } catch (e) { setState(() { isLoading = false; messages.add(const ChatMessage( messageContent: "抱歉,当前响应出错,请稍后重试", messageType: "receiver" )); }); } } @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( appBar: AppBar( toolbarHeight: 100, elevation: 0, automaticallyImplyLeading: false, backgroundColor: const Color.fromARGB(255, 62, 238, 176), flexibleSpace: SafeArea( child: Container( padding: const EdgeInsets.only(right: 16), child: Row( children: <Widget>[ const SizedBox(width: 10), const CircleAvatar( backgroundImage: NetworkImage( "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtutzqepv7vSde5mQLzT00fWEbynYBq70VaQ&usqp=CAU"), maxRadius: 40, ), const SizedBox(width: 15), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: const <Widget>[ Text( "Carl Rogers", style: TextStyle( fontSize: 32, fontWeight: FontWeight.w600), ), SizedBox(height: 6), ], ), ), ], ), ), )), body: Stack( children: <Widget>[ SizedBox( height: MediaQuery.of(context).size.height - 160, child: SingleChildScrollView( child: ListView.builder( controller: _scrollController, itemCount: messages.length, shrinkWrap: true, padding: const EdgeInsets.only(top: 10, bottom: 10), itemBuilder: (context, index) { return Container( padding: const EdgeInsets.only( left: 14, right: 14, top: 10, bottom: 10), child: Align( alignment: (messages[index].messageType == "receiver" ? Alignment.topLeft : Alignment.topRight), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: (messages[index].messageType == "receiver" ? Colors.grey.shade200 : const Color.fromARGB(255, 93, 232, 183)), ), padding: const EdgeInsets.all(16), child: Text( messages[index].messageContent, style: const TextStyle(fontSize: 15), ), ), ), ); }, ), ), ), Align( alignment: Alignment.bottomLeft, child: Container( padding: const EdgeInsets.only(left: 10, bottom: 10, top: 10), height: 60, width: double.infinity, color: Colors.white, child: Row( children: <Widget>[ const SizedBox(width: 15), Expanded( child: TextField( controller: myController, enabled: !isLoading, decoration: const InputDecoration( hintText: "Write message...", hintStyle: TextStyle(color: Colors.black54), border: InputBorder.none), ), ), const SizedBox(width: 15), FloatingActionButton( onPressed: isLoading ? null : handleSend, child: isLoading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white) ) : const Icon( Icons.send, color: Colors.white, size: 18, ), backgroundColor: const Color.fromARGB(255, 62, 238, 176), elevation: 0, ), ], ), ), ), ], ), ), ); } }
注:原Python代码中对
generative_conversation做固定长度切片的逻辑存在bug,会丢失历史对话上下文,上面的代码修正为直接追加会话内容,保证上下文完整。如果是本地演示交作业,硬编码API_KEY可以正常跑,如果要上线正式版本,建议把API_KEY放到后端,避免泄露。
方案2:Python搭建API供前端调用
如果需要隔离API密钥,用Flask搭接口最快,步骤如下:
- 安装依赖:
pip install flask openai flask-cors
- 编写后端服务代码
app.py:
from flask import Flask, request, jsonify from flask_cors import CORS import openai app = Flask(__name__) CORS(app) # 解决跨域问题 openai.api_key = "YOUR_API_KEY_HERE" @app.route("/chat", methods=["POST"]) def chat(): # 前端传完整的会话字符串即可,后端无需存会话状态 prompt = request.json.get("prompt", "") response = openai.Completion.create( model="text-davinci-002", prompt=prompt, max_tokens=100, temperature=0.8, stop="P:" ) reply = response["choices"][0]["text"].strip() return jsonify({"reply": reply}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)
- 启动服务后,把方案1里的
getGptResponse方法的请求地址改成你部署的服务地址(本地测试就是http://你电脑的局域网IP:5000/chat),去掉Authorization请求头,请求体只传prompt字段即可。
这个方案需要额外占用时间部署服务、调试网络连通性,时间紧张的话不推荐。
内容的提问来源于stack exchange,提问作者John Jacob
相关产品推荐
相关产品推荐

