为什么写这篇文章
这段时间我用 Flutter 做了个习惯追踪 App,一开始只是想练手,后来发现把 AI 真正用好、用深 这件事比我想象中有趣得多------不是简单地调一个 /chat/completions 就完了,而是要在 prompt 工程、用户体验、成本控制、个性化、容错兜底之间找到平衡。
这篇文章会把 HabitGo 的 AI 系统从头到尾拆解一遍,包含:
- API 封装层怎么做
- Prompt 工程如何面向场景设计
- 用户反馈驱动的个性化 prompt 进化
- 自研 Markdown 渲染器的实现
- 多场景 AI 融合(问候语 / 每日一课 / 成就邮件 / 数据分析报告)
- 免费版 AI 用量限制的设计
AI 系统全景
先看一张 AI 系统的全景架构图:
scss
┌─────────────────────────────────────────────────────────────┐
│ UI Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ AI 对话 │ │ 首页卡片 │ │ 统计报告 │ │ 成就邮件 │ │
│ │ ChatPage │ │ 问候/挑战│ │ 周/月/年 │ │ Mailbox │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼──────────────┼──────────────┼──────────────┼─────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ DeepSeekService (Static) │ │
│ │ chat() │ analyzeHabits() │ dailyEncouragement() │ │
│ │ generateAchievementMail() │ generateSuggestions() │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────┴─────────────────────────────────┐ │
│ │ AiPersonalization (个性化) │ │
│ │ addFeedback() │ buildPersonalizedPrompt() │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ DailyGreeting│ │ DailyLesson │ │ DailyChallenge │ │
│ │ Service │ │ Service │ │ Service │ │
│ └──────┬───────┘ └──────┬───────┘ └───────┬──────────┘ │
└─────────┼────────────────┼─────────────────┼────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Cache Layer (SharedPreferences) │
│ 每日一份,日期 → 内容映射,跨 App 生命周期持久化 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Monetization │
│ FreeTierLimits │ AiUsageTracker │ aiChatRemainingProvider │
└─────────────────────────────────────────────────────────────┘
AI 不是作为一个独立模块存在,而是像血液一样渗透到产品的各个毛细血管。下面逐一拆解。
一、API 封装层:不只是包个 HTTP 请求
1.1 静态类 + 多语言自适应 System Prompt
dart
class DeepSeekService {
static const String _apiKey = 'sk-xxx';
static const String _baseUrl = 'https://api.deepseek.com/v1';
static const String _model = 'deepseek-chat';
/// 当前用户语言 --- 在 main.dart 中启动时注入
static String currentLanguage = '中文';
static String get _localizedSystemPrompt {
// 根据语言返回不同语言的 system prompt
// 覆盖:中文 / English / 日本語 / 한국어 / Français / Español / Deutsch
if (lang == '中文' || lang == 'zh') {
return '你是 HabitGo 的习惯养成 AI 教练。'
'你温暖、鼓励、务实。建议简洁具体...回复使用简体中文。';
}
if (lang == 'English' || lang == 'en') {
return 'You are HabitGo\'s habit-building AI coach...';
}
// ... 其他 5 种语言
}
}
为什么用静态方法而不是实例化?
AI 调用在 App 中是稀疏且低频的(每天顶多几十次),不需要连接池、不需要长生命周期。静态方法足够,同时避免了注入 boilerplate。唯一需要跨模块共享的状态是 currentLanguage,一个静态字符串即可。
1.2 通用 chat() 方法 --- 一次封装,七处复用
dart
static Future<String> chat({
required String userMessage,
String systemPrompt = '',
List<Map<String, String>>? extraHistory,
}) async {
final sys = systemPrompt.isEmpty
? _localizedSystemPrompt // 默认教练 prompt
: systemPrompt; // 调用方自定义(如成就邮件、报告生成)
final messages = <Map<String, String>>[
{'role': 'system', 'content': sys},
if (extraHistory != null) ...extraHistory,
{'role': 'user', 'content': userMessage},
];
final response = await http.post(
Uri.parse('$_baseUrl/chat/completions'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_apiKey',
},
body: jsonEncode({
'model': _model,
'messages': messages,
'temperature': 0.7,
'max_tokens': 800,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['choices']?[0]?['message']?['content']
?? '抱歉,我暂时无法回复。';
}
return 'API 请求失败 (${response.statusCode})';
}
设计要点:
| 参数 | 设计意图 |
|---|---|
systemPrompt 为空 → 默认教练 prompt |
对话场景最常用,减少调用方心智负担 |
systemPrompt 非空 → 覆盖默认 |
成就邮件、报告生成等专用场景 |
extraHistory |
多轮对话上下文注入,不污染 userMessage |
temperature: 0.7 |
在创意性和一致性之间取平衡,既不死板也不跑偏 |
max_tokens: 800 |
控制回复长度和成本,移动端用户不需要看论文 |
二、Prompt 工程:每个场景都是一次「编程」
调用 API 只是骨架,prompt 才是灵魂。HabitGo 为每个 AI 使用场景精心设计了专属 prompt。下面逐个拆解设计思路。
2.1 习惯数据分析 --- 结构化输入 + 格式约束
dart
static Future<String> analyzeHabits({
required List<Habit> habits,
required int totalCheckins,
required int activeDays,
required int bestStreak,
required int todayCheckins,
}) async {
final prompt = '''
你是一个习惯养成教练。请分析以下用户的习惯数据,给出 3-5 条简洁的个性化建议(每条不超过 60 字):
📊 用户数据:
- 习惯列表:
${habits.map((h) => '- ${h.icon ?? '🎯'} ${h.name} (${h.isActive ? "进行中" : "已暂停"})').join('\n')}
- 总打卡次数:$totalCheckins
- 活跃天数:$activeDays
- 最佳连击:$bestStreak
- 今日已打卡:$todayCheckins
回复格式:每条建议前加 emoji 编号(1️⃣ 2️⃣ 3️⃣),语气温暖鼓励。
语言:$currentLanguage
''';
return chat(userMessage: prompt);
}
设计思路:
- 把原始数据直接注入 prompt,让 AI「看到」用户的全部上下文。这不是幻觉风险,而是必要的领域知识注入。
- 明确约束:3-5 条、每条 ≤60 字、emoji 编号 --- 防止 AI 啰嗦或跑偏。
- 语气定调:「温暖鼓励」四个字决定了整段回复的情感基调。
2.2 每日问候语 --- 每天一次,不阻塞 UI
dart
class DailyGreetingService {
static Future<String> getGreeting() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString('greeting_date');
if (saved == _today()) {
return prefs.getString('greeting_text')!; // 缓存命中
}
// ⚡ 异步生成,不阻塞 UI:先返回旧值或默认值
_generateAndCache(today);
return prefs.getString('greeting_text')
?? '每一天都是新的开始,坚持就是胜利 ✨';
}
static Future<void> _generateAndCache(String today) async {
final text = await DeepSeekService.chat(
userMessage: '''
请生成一条今日问候语:
1. 一句温暖的问候(根据时间:早上/下午/晚上)
2. 一句名人名言、古诗词或哲理短句
3. 一句针对习惯养成的鼓励
格式:共 2-3 行,简洁有品位,风格每天不同。总字数不超过 120 字。
''',
systemPrompt: '你是 HabitGo 的每日问候助手。风格温暖、励志、有文化底蕴。'
);
// 缓存到 SharedPreferences
await prefs.setString('greeting_date', today);
await prefs.setString('greeting_text', text);
}
}
设计亮点:
- 异步生成,先返回兜底值:首页打开后立即返回缓存或默认问候语,后台异步请求 AI。用户感知不到任何加载过程。
- 日期作为缓存键 :
2026-07-17格式的日期字符串作为 key,每天同一份,跨页面切换复用。 - 风格差异化指令:「风格每天不同」让 DeepSeek 的随机性(temperature 0.7)转化为产品需要的多样性。
2.3 每日一课 --- 日期驱动的主题轮换
dart
class DailyLessonService {
static const List<String> topics = [
'宇宙天文', '地理奇观', '历史冷知识', '生物趣闻',
'心理学效应', '经济学原理', '哲学思考', '文学典故',
'艺术常识', '科技前沿', '民俗文化', '语言奥秘',
];
static Future<void> _generateAndCache(String today) async {
// 基于日期决定主题:一年 365 天,循环覆盖 12 个主题
final dayOfYear = DateTime.now()
.difference(DateTime(2024, 1, 1)).inDays.abs();
final topic = topics[dayOfYear % topics.length];
final prompt = '''
请讲一个关于「$topic」的有趣冷知识:
1. 真实可靠(基于科学/历史事实)
2. 3-5 句话,约 80-120 字
3. 开头用一个 emoji 表示主题
4. 结尾加一句"今日启发"
语言:${DeepSeekService.currentLanguage}
''';
// 调用 + 缓存 ...
}
}
核心设计 :dayOfYear % 12 让 365 天均匀覆盖 12 个领域,既保证了多样性,又确保了确定性(同一天不重复生成)。
2.4 成就邮件 --- 最长、最精细的 prompt
dart
static Future<String> generateAchievementMail({
required String userName,
required String achievementName,
required int currentStreak,
required int totalCheckins,
}) async {
final prompt = '''
给 $userName 写一封达成「$achievementName」成就的鼓励信:
- 当前连击:$currentStreak 天
- 累计打卡:$totalCheckins 次
要求:
- 3-4 段,200-300 字
- 称呼亲切、语气温暖
- 引用一句名人名言/古诗词
- 展望下一个目标
- 结尾鼓励继续
语言:$currentLanguage
''';
return chat(userMessage: prompt, systemPrompt: '你是 HabitGo 的习惯养成教练,会给用户写温暖的鼓励信。');
}
这是整个系统中最长、约束最多的 prompt。为什么不直接用对话接口?因为邮件是一个独立的「文体」------它需要结构感、仪式感,而这些需要通过 prompt 显式传达。
2.5 AI 周报/月报/年报 --- 严格的格式约束
统计页面的「AI 报告」功能:
dart
final fullPrompt = '''
你是一个数据分析师。请根据以下用户习惯数据写一份简洁的周报/月报/年报。
📊 数据摘要:
- 习惯:$habitList
- 累计打卡:$total 次
- 活跃天数:$activeDays
- 历史最佳连击:$bestStreak
报告格式要求(严格遵守):
1. 第一行:📅 习惯周报/月报/年报
2. 分 3 个板块:✅ 亮点 / ⚠️ 不足 / 💡 建议
3. 每个板块 2-3 句,每条用 • 开头(不是 - 也不是数字)
4. **绝对不要使用 markdown 格式**
5. 纯文本,100-200 字
''';
为什么不直接用 analyzeHabits? 数据分析是对话式的(「你最近坚持得不错,建议...」),而报告是结构化的(板块分明、格式固定)。两套 prompt 服务于两种不同的用户心智。
2.6 Follow-up 建议 --- Prompt 链式调用
对话结束时,自动生成 3 个 follow-up 问题:
dart
static Future<List<String>> generateSuggestions({
required String userMessage,
required String aiReply,
}) async {
final prompt = '''
基于以下对话,生成 3 个用户可能想问的后续问题(每个不超过 15 字):
用户:$userMessage
AI:$aiReply
输出格式(每行一个):
1. 第一个问题
2. 第二个问题
3. 第三个问题
语言:$currentLanguage
''';
final reply = await chat(userMessage: prompt, systemPrompt: '...');
// 解析:按行分割 → 去掉数字编号 → 最多取 3 条
final lines = reply
.split('\n')
.where((l) => l.trim().isNotEmpty)
.map((l) => l.replaceFirst(RegExp(r'^\d+[\.\)、]'), '').trim())
.where((l) => l.isNotEmpty)
.take(3)
.toList();
return lines.isEmpty ? ['继续说说?', '有什么建议?', '怎么改进?'] : lines;
}
这是 Prompt Chain 的实践:用第一次对话的输出作为第二次 prompt 的输入,不增加 UI 的等待时间(两次调用是串行的,但第二次很快)。
三、个性化进化:用用户的反馈训练 AI
这是我认为整个 AI 系统中最有意思的设计。
3.1 核心思路
大型语言模型支持 Few-shot Learning:在 system prompt 中放入几个「用户问 → AI 答」的示例,模型的回复风格会向示例靠拢。
HabitGo 把这个能力产品化为:「用户觉得某次回答有用 → 点 『让 AI 记住』→ 对话样本被持久化 → 后续所有 AI 调用自动注入这些样本」。
3.2 实现细节
dart
class AiPersonalization {
static const int maxPerDay = 3;
static const int maxTotal = 30;
/// 添加一条反馈
static Future<bool> addFeedback({
required String userMessage,
required String aiReply,
}) async {
if (!await canFeedbackToday()) return false;
final list = prefs.getStringList('ai_feedback_list') ?? [];
list.add('$userMessage|||$aiReply');
// 超限自动清理最旧的
if (list.length > maxTotal) {
list.removeRange(0, list.length - maxTotal);
}
await prefs.setStringList('ai_feedback_list', list);
return true;
}
/// 构建追加到 system prompt 的个性化片段
static Future<String> getAddendum() async {
final list = prefs.getStringList('ai_feedback_list') ?? [];
if (list.isEmpty) return '';
final buf = StringBuffer();
buf.writeln('\n\n[用户已确认对你有用的历史回答示例 --- 你应当延续这类风格与方向]');
for (final line in list) {
final parts = line.split('|||');
buf.writeln('---');
buf.writeln('用户问:${parts[0]}');
buf.writeln('你答:${parts[1]}');
}
return buf.toString();
}
/// 最终拼装:基础 prompt + 个性化 addendum
static Future<String> buildPersonalizedPrompt() async {
final base = '你是 HabitGo 的习惯养成 AI 教练。';
final addendum = await getAddendum();
return '$base$addendum';
}
}
3.3 限制机制的必要性
| 限制 | 值 | 原因 |
|---|---|---|
| 每日上限 | 3 次 | 防止用户在一天内塞入大量样本,导致 prompt 过长、费用爆炸 |
| 总上限 | 30 条 | DeepSeek 的 context window 有限,且太多样本会稀释每条的影响力 |
| FIFO 淘汰 | 超出删除最早 | 保证样本的时效性 |
3.4 在对话中的注入时机
dart
// ai_chat_page.dart 中的 _send() 方法
if (isAnalysis) {
reply = await DeepSeekService.analyzeHabits(/* ... */);
} else {
// ⬇ 关键:对话场景使用个性化 prompt
final sysPrompt = await AiPersonalization.buildPersonalizedPrompt();
reply = await DeepSeekService.chat(
userMessage: actualText,
systemPrompt: sysPrompt, // 基础 + addendum
);
}
注意:数据分析场景不走个性化 prompt,因为分析建议需要客观,不宜被用户偏好带偏。这是一个有意识的区分。
四、自研 Markdown 渲染器:因为不需要引入重型依赖
AI 返回的内容天然带 Markdown。现有方案 flutter_markdown 很好,但它引入了一整套 HTML→Markdown→AST 的解析链,对只渲染加粗/斜体/标题/列表/行内代码的场景来说过于冗余。
4.1 设计原则:极简、够用、零依赖
markdown
支持的语法:
✅ **加粗** ✅ *斜体* ✅ `行内代码`
✅ # ## ### 标题 ✅ - 无序列表 ✅ 1. 有序列表
✅ 空行 → 段落分隔 ❌ 链接/图片 ❌ 代码块
❌ 表格 ❌ 嵌套列表
不支持的语法就不解析(AI 也很少输出表格和嵌套列表),直接当普通文本渲染。
4.2 实现:两遍扫描
第一遍(行级解析):逐行正则匹配,生成对应的 Widget。
dart
List<Widget> _parse(String input, TextStyle style, BuildContext ctx) {
final widgets = <Widget>[];
final lines = input.split('\n');
int i = 0;
while (i < lines.length) {
final line = lines[i];
// 标题:正则 ^#{1,3}\s+(.+)$
final hMatch = RegExp(r'^(#{1,3})\s+(.+)$').firstMatch(line);
if (hMatch != null) {
final level = hMatch.group(1)!.length;
widgets.add(_inline(hMatch.group(2)!,
style.copyWith(fontSize: style.fontSize! + (3 - level) * 1.5,
fontWeight: FontWeight.w800)));
i++; continue;
}
// 列表项:合并连续的列表行 → Row(bullet + content)
final ulMatch = RegExp(r'^[\-\*]\s+(.+)$').firstMatch(line);
final olMatch = RegExp(r'^\d+[\.\)]\s+(.+)$').firstMatch(line);
if (ulMatch != null || olMatch != null) {
// 向后扫描,合并连续列表
int j = i; int idx = 1;
while (j < lines.length) {
// 用 Row(SizedBox(22) + Expanded(_inline(content))) 构建列表项
j++;
}
i = j;
widgets.addAll(items);
continue;
}
// 空行 → SizedBox(height: 6)
// 普通段落 → _inline(line)
// ...
}
return widgets;
}
第二遍(行内解析) :单行内扫描 ** / * / `````,生成 TextSpan。
dart
List<InlineSpan> _toSpans(String text, TextStyle style, BuildContext ctx) {
final spans = <InlineSpan>[];
int i = 0;
while (i < text.length) {
final c = text[i];
// 加粗:找到闭合的 **
if (c == '*' && i + 1 < text.length && text[i + 1] == '*') {
final end = text.indexOf('**', i + 2);
if (end != -1) {
spans.add(TextSpan(text: text.substring(i + 2, end),
style: style.copyWith(fontWeight: FontWeight.w800)));
i = end + 2; continue;
}
}
// 行内代码:用 WidgetSpan 包裹带背景色的 Container
if (c == '`') {
final end = text.indexOf('`', i + 1);
if (end != -1) {
spans.add(WidgetSpan(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: codeBg ?? scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: Text(text.substring(i + 1, end),
style: style.copyWith(fontFamily: 'monospace',
color: codeFg ?? scheme.primary)),
),
));
i = end + 1; continue;
}
}
// 普通字符:扫描到下一个 * 或 ` 再分段
int next = i + 1;
while (next < text.length && text[next] != '*' && text[next] != '`') next++;
spans.add(TextSpan(text: text.substring(i, next), style: style));
i = next;
}
return spans;
}
为什么行内代码用 WidgetSpan 而不是 TextSpan? 因为代码需要背景色 + 圆角,纯 TextSpan 做不到。WidgetSpan 可以内嵌任意 Widget,代价是多了几个 Widget 节点,但对消息列表来说性能影响可以忽略。
五、用量控制:免费版的 AI 经济学
5.1 设计思路
DeepSeek 虽然便宜,但每 100 万 token 也要几分钱。App 免费版如果不做限制,成本会线性增长。
策略:
| 限制项 | 免费版 | Premium |
|---|---|---|
| AI 对话 | 10 次/天 | 无限 |
| AI 数据分析 | 5 次/天 | 无限 |
| 每日问候/挑战/一课 | 不限制 | 不限制 |
前两项是高频操作(用户主动触发),后三项是每天固定一次(系统自动触发,频率可控)。
5.2 技术实现
dart
class AiUsageTracker {
static Future<bool> tryConsumeChat() async =>
_tryConsume('ai_chat_usage', 'ai_chat_date', FreeTierLimits.aiChatPerDay);
static Future<bool> _tryConsume(
String key, String dateKey, int maxPerDay) async {
final prefs = await SharedPreferences.getInstance();
final today = _todayKey();
final savedDate = prefs.getString(dateKey);
// 新的一天 → 重置计数
if (savedDate != today) {
await prefs.setString(dateKey, today);
await prefs.setInt(key, 0);
}
final used = prefs.getInt(key) ?? 0;
if (used >= maxPerDay) return false; // 超限
await prefs.setInt(key, used + 1);
return true; // 允许调用
}
}
在 Riverpod 层的反应式绑定:
dart
final aiChatRemainingProvider = FutureProvider<int>((ref) async {
ref.watch(isPremiumProvider); // 订阅状态变化时自动刷新
if (ref.read(isPremiumProvider)) return -1; // -1 表示无限
return AiUsageTracker.remainingChat();
});
-1 的语义设计很巧妙:UI 层只需要判断 value == -1 就知道是会员,不需要额外传 isPremium。
六、缓存策略:每天一份,永不过期(不请求)
每日问候、每日一课、每日挑战共用同一套缓存模式:
scss
┌─────────────────────────────┐
│ getXxx() │
│ │ │
│ ├── 缓存命中 (日期==今天) │ → 立即返回
│ │ │
│ ├── 缓存未命中 │
│ │ ├── 立即返回旧值/默认值 │ → UI 无等待
│ │ └── 后台 async 生成 │ → 成功后更新缓存
│ │ │
│ └── 网络失败 │ → 返回 fallback 默认值
└─────────────────────────────┘
核心代码模式:
dart
static Future<String> getXxx() async {
final saved = prefs.getString(dateKey);
if (saved == today) return prefs.getString(contentKey)!; // 命中
_generateAndCache(today); // 🔥 不 await!后台异步执行
return prefs.getString(contentKey) ?? fallbackDefault; // 立即返回旧值
}
这个不 await 是精髓。如果 await,首页打开后要等 2-3 秒才有内容,用户体验极差。改为 fire-and-forget 后,首页秒开,2-3 秒后内容静默刷新。
七、AI 融入产品的七个触点
最后梳理一下 AI 和产品功能的一一对应关系:
| # | 触点 | 触发时机 | AI 做的事 | 代码位置 |
|---|---|---|---|---|
| 1 | 首页问候 | 每次打开首页 | 生成名人名言 + 习惯鼓励 | daily_greeting_service.dart |
| 2 | 每日挑战 | 同上 | 推荐一个 5-30 分钟的小任务 | daily_challenge_service.dart |
| 3 | 每日一课 | 同上 | 讲一个冷知识(12 领域轮换) | daily_lesson_service.dart |
| 4 | AI 教练对话 | 用户主动进入 AI Tab | 自由对话 + 习惯数据分析 | ai_chat_page.dart |
| 5 | AI 数据分析 | 对话中含「分析」关键词 | 读取习惯数据 + 给 3-5 条建议 | analyzeHabits() |
| 6 | 成就邮件 | 达成里程碑(3/7/14/30/100天) | 写一封鼓励信 + 名人名言 | generateAchievementMail() |
| 7 | AI 周/月/年报 | 统计页点击报告按钮 | 亮点/不足/建议三板斧 | _AiReportPanel._generate() |
再加上 AI 个性化进化 (反馈 → system prompt addendum)和 AI 成就名称生成 (心愿银行每次启动随机 6 个名称),一共 9 个 AI 触点,覆盖了 App 的首页、对话、统计、邮箱、心愿银行五个主要页面。
八、可以继续深挖的方向
8.1 Streaming 响应
目前所有 AI 调用都是一次性返回 ,用户需要等 2-5 秒才能看到完整内容。改成 SSE Streaming 后可以逐字显示 ,体验会提升一大截。DeepSeek API 支持 stream: true,技术上是可行的,只是 UI 层需要改造为流式接收 + 增量更新。
8.2 本地模型 / 端侧推理
把日问候/每日一课这种非实时、可预生成的内容做到端侧,进一步缩减 API 成本。Google 的 MediaPipe / ML Kit 正在往这个方向走。
8.3 A/B 测试 Prompt
目前所有 prompt 是基于个人经验手写的。如果能做 A/B 测试(两套 prompt → 对比用户行为数据),就可以数据驱动地优化 prompt,而不是靠感觉。
8.4 RAG(检索增强生成)
将用户的全部历史打卡数据做 embedding,查询时通过向量相似度检索相关上下文注入 prompt。这样 AI 能回答「我三个月前那个习惯后来怎么样了」之类的问题。
写在最后
做一个「AI 驱动的 App」真的不是接个 API 就完了。这篇文章涵盖了 HabitGo 从 prompt 设计、多场景集成、个性化进化、自研渲染器、用量控制到缓存策略的全流程,希望能给同样在探索 AI + 移动端的同学一些启发。
如果你也在做类似的事情,或者对某个技术细节有疑问,欢迎在评论区交流 👇