跳转至

Flutter 文本对话

本文档介绍在 Flutter 中实现聊天功能,支持流式输出、多轮上下文和系统提示词。


功能预览

文本对话页面

左侧导航选择 💬 文本对话 → 输入消息 → AI 流式回复,呈现打字机效果


目录


基础概念

文本对话 API 的核心请求体包含三个部分:

{
  'model': 'gpt-4o-mini',     // 模型名称
  'messages': [...],           // 对话历史,user/assistant交替
  'stream': true/false,        // 是否启用流式输出
}

常用模型:

  • gpt-4o-mini / gpt-4o - OpenAI 系列
  • deepseek-chat / deepseek-v3 - DeepSeek 系列
  • claude-sonnet-4-6 - Anthropic 系列

简单单轮对话

Future<String> simpleChat(String message) async {
  final response = await http.post(
    Uri.parse('${dotenv.env['APINEXUS_BASE_URL']!}/chat/completions'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ${dotenv.env['APINEXUS_API_KEY']!}',
    },
    body: json.encode({
      'model': 'gpt-4o-mini',
      'messages': [
        {'role': 'user', 'content': message}
      ],
    }),
  );
  final data = json.decode(response.body);
  return data['choices'][0]['message']['content'];
}

流式输出 SSE

流式输出可以获得打字机效果,体验更好:

import 'package:flutter/foundation.dart';

Stream<String> streamChat(String message) async* {
  final request = http.Request(
    'POST',
    Uri.parse('${dotenv.env['APINEXUS_BASE_URL']!}/chat/completions'),
  );
  request.headers['Content-Type'] = 'application/json';
  request.headers['Authorization'] =
      'Bearer ${dotenv.env['APINEXUS_API_KEY']!}';
  request.body = json.encode({
    'model': 'gpt-4o-mini',
    'stream': true,
    'messages': [
      {'role': 'user', 'content': message}
    ],
  });

  final response = await request.send();
  String fullText = '';

  await for (final chunk in response.stream.transform(utf8.decoder)) {
    for (final line in chunk.split('\n')) {
      if (line.startsWith('data: ')) {
        final data = line.substring(6);
        if (data == '[DONE]') continue;
        try {
          final jsonData = json.decode(data);
          final delta =
              jsonData['choices'][0]['delta']['content'] ?? '';
          if (delta != null && delta.isNotEmpty) {
            fullText += delta;
            yield fullText;
          }
        } catch (_) {}
      }
    }
  }
}

在 UI 中使用:

StreamBuilder<String>(
  stream: streamChat('用一句话介绍 Flutter'),
  builder: (context, snapshot) {
    return Text(snapshot.data ?? '正在输入...');
  },
)

多轮对话上下文

将历史消息累积到 messages 数组中:

class ChatSession {
  final List<Map<String, String>> messages = [];

  void addUser(String content) {
    messages.add({'role': 'user', 'content': content});
  }

  void addAssistant(String content) {
    messages.add({'role': 'assistant', 'content': content});
  }

  Future<String> send() async {
    final response = await http.post(
      Uri.parse('${dotenv.env['APINEXUS_BASE_URL']!}/chat/completions'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ${dotenv.env['APINEXUS_API_KEY']!}',
      },
      body: json.encode({
        'model': 'gpt-4o-mini',
        'messages': messages,
      }),
    );
    final data = json.decode(response.body);
    final reply = data['choices'][0]['message']['content'];
    addAssistant(reply);
    return reply;
  }
}

系统提示词

通过设置 system role 的消息,可以控制 AI 的行为:

messages = [
  {
    'role': 'system',
    'content': '你是一位专业的 Flutter 开发者顾问。'
        '回答应简洁、包含代码示例,并标注关键注意事项。'
  },
  {'role': 'user', 'content': '如何实现滚动列表懒加载?'}
]

常用参数

参数 类型 说明 默认值
model string 模型名称 必填
temperature number 随机性 (0-2) 1.0
max_tokens number 最大输出 Token 数 可变
stream boolean 是否流式输出 false
top_p number 核采样阈值 1.0

完整示例

一个完整的聊天应用的核心组件:

class ChatPage extends StatefulWidget {
  const ChatPage({super.key});

  @override
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  final List<Map<String, String>> _messages = [];
  final _controller = TextEditingController();
  bool _isStreaming = false;
  String _currentReply = '';

  Future<void> _send() async {
    if (_controller.text.isEmpty || _isStreaming) return;
    setState(() {
      _messages.add({'role': 'user', 'content': _controller.text});
      _isStreaming = true;
      _currentReply = '';
    });
    _controller.clear();

    final history = List<Map<String, String>>.from(_messages);
    await for (final text in streamChatToAI(history)) {
      setState(() => _currentReply = text);
    }
    setState(() {
      _messages.add({'role': 'assistant', 'content': _currentReply});
      _isStreaming = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AI 对话')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: _messages.length + (_isStreaming ? 1 : 0),
              itemBuilder: (_, i) {
                if (i < _messages.length) {
                  final msg = _messages[i];
                  return ListTile(
                    title: Text(msg['content']!),
                    subtitle: Text(msg['role']!),
                  );
                }
                return ListTile(title: Text(_currentReply));
              },
            ),
          ),
          TextField(controller: _controller),
          ElevatedButton(onPressed: _send, child: const Text('发送')),
        ],
      ),
    );
  }
}

继续学习