“记一笔账”这件事,看似简单,背后其实是一堆反人性的操作。打开记账App,找到分类、输金额、选账户、写备注,一套下来十几秒没了,坚持不了几天。所以当我把目光投向本地大模型和Flutter的时候,想法很简单:能不能直接输入一句“中午和黄经理吃饭,AA付了88”,让它自动结构化成一笔账单?这个项目我前前后后倒腾了三周,踩了不少坑,今天把完整思路和实现过程整理出来,给对“Flutter + 本地大模型”组合感兴趣的朋友做个参考。项目本身不算复杂,但把一个通用大模型在端侧落地成真正的工具,里头的水很深,值得好好聊。
这个项目的核心就三个关键词:Flutter(跨端UI)、本地大模型(隐私与离线)、自然语言记账(核心功能)。适合的人群也很明确:会一点Flutter但没碰过大模型工程化的客户端开发,或者想体验把AI塞进本地应用、又不想被云API绑架的个人开发者。接下来我从需求拆解开始,把每一步的思考、选型、实现和踩坑都摊开来说。
1. 需求拆解与方案选型:为什么“本地模型 + Flutter”是记账神器
1.1 从一句话记账到结构化账单:核心需求拆解
先聊最根本的问题:自然语言记账到底要解决什么?
常规记账App让人坚持不下去的痛点在于录入成本太高。那自然语言记账的体验应该是:用户像跟人说话一样,把一笔开销或收入口述出来,剩下的由程序自动处理。比如我随手输一句“昨天打车回家花了23块5”,理想情况下系统要提取出:
- 方向:支出(expense)
- 金额:23.5元
- 分类:交通
- 时间:昨天(实际日期由程序换算)
- 备注:打车回家
这五个字段拆开看,前四个都有比较大的坑。“昨天”是一种相对时间表达,不能让模型输出字符串,得让它输出具体的日期;“23块5”是中文口语数字,不是“23.5”这种标准数字;“打车”到底算“交通”还是“出租”取决于你预设的分类体系;更麻烦的是“和黄经理吃饭,AA付了88”这种语序,金额和场景的关系需要理解才能正确处理。
如果沿用传统的关键词匹配加正则,就得维护一张庞大的关键词表,遇到“周三晚上团建聚餐人均70”这种句子直接崩溃,因为“人均”“团建”这种表达和直接金额没有显式对应关系。这就是我要引入大模型来处理的原因——自然语言解析本质上是一个语义理解问题,不是规则匹配问题。
1.2 本地大模型 vs 云端大模型:数据隐私与成本的权衡
既然要引入大模型,第一个绕不开的选择是:用云端大模型API,还是在本地跑模型?
记账数据有天然的私密属性。虽然不是什么国家机密,但没人希望自己每天几点吃了什么、在哪儿消费了什么被上传到别人服务器上。即便是在安全合规的API服务上,处理这种高度个人化的数据,心里多少有些膈应。而且云端API是按token计费的,记账是个高频操作——如果坚持记账,一天三次、一年上千次请求,累计费用并不划算。
本地大模型刚好把这两块补上:
- 隐私不出设备:数据在本地完成解析,不会外传
- 推理零成本:模型跑在自己的机器或手机上,没有按次计费
- 离线可用:没网也能记,特别适合地铁、飞机这类场景
代价也很实际:模型体积和推理速度。一个几B参数的量化模型需要几GB磁盘空间,在手机上推理一条短句也可能耗时几秒;在PC上部署则顺畅得多。所以这个项目我做了一个折中方案:模型用Ollama部署在PC上,Flutter应用通过局域网HTTP调用它;同时在代码层面上为以后在移动端直接接入端侧推理预留了接口。
为了说清楚这个权衡,我做过一个对比:
| 维度 | 云端大模型API | 本地大模型(PC部署) | 端侧模型(手机直跑) |
|---|---|---|---|
| 隐私安全 | 数据出设备 | 数据不出局域网 | 数据不出设备 |
| 单次耗时 | 快,秒回 | 中等,受电脑性能影响 | 较慢,受手机算力影响 |
| 运行成本 | 按token付费 | 电费可忽略 | 电费可忽略 |
| 网络依赖 | 必须联网 | 局域网即可 | 完全离线 |
| 开发难度 | 极低 | 低 | 较高 |
1.3 模型选型与硬件门槛
模型选型是整个项目中最容易犯迷糊的地方。一开始我也犯过“越大越好”的错误,拉了个14B的模型,结果笔记本风扇起飞,解析一句话要等半分钟,体验稀碎。
后来我把选型思路调整为:能解决问题的最小模型。
对记账这种任务,模型的输出是极其简短的一段JSON,不涉及长文本生成或者复杂推理,其实是个很轻量的任务。实测下来几款主流小模型表现都不错:
| 模型 | 参数量 | 量化后体积 | 一句话解析耗时(CPU推理) | 中文理解 |
|---|---|---|---|---|
| Qwen2.5-1.5B | 1.5B | 约1.2GB | 2-4秒 | 优秀 |
| Qwen2.5-3B | 3B | 约2.2GB | 4-7秒 | 更优 |
| Llama 3.2-1B | 1B | 约0.9GB | 1-3秒 | 够用 |
| Phi-3-mini | 3.8B | 约2.4GB | 5-8秒 | 良好 |
这里要补充一句,如果你没有老显卡、主要靠CPU推理,优先选1.5B-3B这个档位的模型,既能保证语义理解,也不会让用户等得崩溃。如果在手机端直接跑,建议选1B-2B的量化模型,配合llama.cpp或ExecuTorch这类端侧推理框架。我这次PC端演示使用的是Qwen2.5-3B,准确率比1.5B高不少,耗时还在可接受范围内。
2. 环境准备:Flutter工具链与本地模型部署
2.1 Flutter SDK 与 FVM 多版本管理
写项目之前先搭环境。Flutter的安装网上教程汗牛充栋,这里说一下我踩到的两个关键点。
第一,强烈建议用FVM管理Flutter SDK版本。热词里有“fvm安装多版本flutter”,这是一个过来人才会明白的痛点。Flutter的升级节奏很快,你的老项目可能还在用3.16,新项目已经要3.24了。以前我是直接下载SDK压缩包到自定义目录,每次切换就改PATH,麻烦且容易出幺蛾子。用FVM之后舒服太多:
# 安装FVM(通过Homebrew或pub) brew install fvm # 安装指定版本的Flutter fvm install 3.24.0 # 项目根目录指定版本 fvm use 3.24.0 # 查看当前Flutter版本 fvm flutter --versionFVM会为每个项目记录一份.fvmrc配置文件,团队协作时大家各自执行fvm install就自动切换到项目指定的版本,彻底告别“在我电脑上能跑”的魔咒。
第二,第一次执行flutter doctor的时候,耐心把Android SDK、Xcode、Chrome这些依赖项都补齐。通常首次跑 Flutter 项目,真正的拦路虎不是Flutter本身,而是Android的Gradle网络问题。国内环境下载Gradle依赖经常失败,一般需要在android/build.gradle里改成阿里云镜像,或者在gradle-wrapper.properties里改distributionUrl。我建议先跑一个空项目,把环境链路走通,再开始写业务代码。
Flutter本身是跨平台框架,我这个项目最终目标是手机端能用,所以开发阶段主要在Chrome和Android模拟器上调试。这里多提一句热词里的“flutter现在主流开发用什么编译器”,现在主流还是 Android Studio 或 VS Code。我日常用 VS Code,装 Flutter 和 Dart 插件就够了,启动快、内存占用小,调试和热重载用起来非常顺手。
2.2 本地大模型部署:Ollama 一行命令拉起推理服务
本地大模型的部署方案选择上,我用的是Ollama。没有选 llama.cpp 直接编译,也没有上 Dify 这类重工具链,原因只有两个字:省心。
Ollama 把模型管理、量化、推理服务打包得非常友好,基本是“装完就能用”的体验。安装也很简单,官网下载对应平台的安装包,或者用一行命令:
# macOS 或 Linux curl -fsSL https://ollama.com/install.sh | sh # 启动服务(Ollama 默认常驻后台) ollama serve # 拉取模型并运行 ollama pull qwen2.5:3b模型拉下来之后,Ollama 会默认监听http://localhost:11434,通过 REST API 对外提供推理服务。验证一下服务是否正常:
curl http://localhost:11434/v1/models能看到模型列表,就说明服务已经跑起来了。Ollama 还兼容 OpenAI 的API格式,意味着后续要切到任何 OpenAI 兼容的云端服务,代码几乎不用改。
这里再顺带澄清一个容易混淆的点:如果要在局域网里让手机上的Flutter App访问这台电脑的Ollama,光ollama serve默认只监听本机回环地址,需要让它监听局域网地址。可以设置环境变量:
# 监听所有网卡地址 OLLAMA_HOST=0.0.0.0:11434 ollama serve此时手机和电脑连同一个Wi-Fi,App里请求http://<电脑局域网IP>:11434就可以访问。这一点很多人第一次搞会踩坑,以为模型部署好了App却怎么都连不上,其实问题就出在监听地址上。
2.3 用 Dify、LangChain 还是直接调 HTTP?
我注意到热词里有“dify使用ollama设置本地大模型”。Dify 是一个非常优秀的 LLM 应用开发平台,可以做工作流编排、RAG、Agent等,但我的场景只是“一个输入文本到一个JSON输出”的轻量转换,引入 Dify 这种重量级平台反而增加了部署复杂度。
我的判断是:
- 如果目标是快速构建原型,直接调 Ollama 的 HTTP API 就够了
- 如果要给应用加上多轮对话、知识库、复杂的Agent行为,再考虑 Dify
- LangChain 同理,它的核心是组合各种工具链做复杂编排,对我的场景是多余依赖
架构上追求简单,意味着后期排查问题更轻松。我们是要做一个记账工具,不是做一个大模型平台。所以我的实际选择是:Flutter App 直接通过 HTTP 和 Ollama 通信,中间不加任何中转层。
3. 核心架构设计:Flutter 如何与本地大模型协作
3.1 整体架构与数据流
开始写代码之前,先把整个应用的数据流理清楚:
用户输入一句话 ↓ Flutter 界面获取文本 ↓ Dart HTTP 客户端发送请求到 Ollama(携带 Prompt 和 JSON Format 约束) ↓ Ollama 完成本地推理,返回结构化 JSON 文本 ↓ Flutter 解析 JSON ↓ Dart 层做兜底规则校验(金额非零、分类存在性、时间归一化) ↓ 写入 SQLite 本地数据库 ↓ 刷新首页账单列表这个链路是单向的、清晰的,没有复杂的双向绑定,也没有消息队列。整个项目的核心就两个难点:一是让模型稳定吐出结构合格的JSON,二是把模型输出安全准确地转换并写入本地库。
3.2 Flutter 多线程与异步:别让推理卡住UI
Flutter 的UI是单线程模型,但网络请求和JSON解析这种耗时操作不能放在主Isolate里跑,否则界面会直接卡死。尤其JSON解析在Dart里虽然快,处理一个模型输出的几百字节不是问题,但HTTP请求的时间可能长达两三秒,这块必须异步化。
我的处理策略是:
- HTTP请求交由
http包的异步方法执行,它内部自己处理I/O,不会阻塞UI。 - 解析模型输出用
compute或Isolate.run,把结构化JSON解析和规则校验放在后台Isolate执行,避免在主Isolate做大量字符串运算。 - 状态变更通过 ValueNotifier / Stream 通知界面刷新,不在setState里做任何耗时逻辑。
顺带提一个 Flutter 多线程的进阶玩法。如果你以后想把模型直接跑到手机端里,比如用ExecuTorch或llama.cpp的Flutter插件,推理过程本身就是纯CPU计算密集型任务,必须放到独立Isolate,不然哪怕只推理一秒钟,UI也会掉帧卡顿。Flutter 的Isolate.run和compute在这种场景就特别好用,代码几乎不用大改:
final result = await Isolate.run(() { return parseAndValidate(rawText, categories); });3.3 数据存储与本地化:SQLite 与账单表设计
数据存储我用了sqflite插件,它适配 Android、iOS、macOS、Windows 和 Linux,桌面端可以直接用同样的代码。本地数据最大的好处是隐私可控,且导出方便,不依赖任何云服务。
表结构设计上,我保持精简:
CREATE TABLE records ( id INTEGER PRIMARY KEY AUTOINCREMENT, direction TEXT NOT NULL, -- expense / income amount_cents INTEGER NOT NULL, -- 金额以“分”为单位存储,避免浮点误差 category TEXT NOT NULL, -- 餐饮 / 交通 / 购物 / 工资 ... note TEXT, -- 原始备注或模型生成的摘要 record_time INTEGER NOT NULL, -- 记账时间,Unix时间戳 created_at INTEGER NOT NULL );金额为什么不直接存浮点数?记账涉及钱,浮点误差是绝对不能接受的。0.1 + 0.2在二进制浮点里不等于0.3,一旦涉及统计报表就会出大问题。所以我的做法是:模型输出“23.5”,Dart 解析后乘以100,得到2350(分),所有统计计算都在整数域进行,到了展示层再除以100转回元。这是记账类应用最基本的一条原则。
分类字段我预设了一套精简的一级分类,比如餐饮、交通、购物、居住、娱乐、医疗、人情、工资、理财等。模型的作用是识别语义,Dart侧再做一个“分类归一化”映射,把模型可能输出的近义分类(比如“烧烤”“外卖”“聚餐”)映射回预设体系里。
4. 自然语言解析核心实现:从口语到结构化 JSON
4.1 结构化输出的关键:JSON 模式与约束
这一步是整篇文章的重头戏,也是我花了最多时间调优的地方。
大模型默认是内容生成器,它会把“今天中午和黄经理吃饭,AA付了88”这种输入,很自然地回复一长串解释:“好的,根据您提供的信息,这是一笔支出……”。如果每个请求都返回一堆废话,后续就没法处理。
好在 Ollama 提供了一个非常实用的参数叫format。把format设置成"json"之后,模型会被强制输出一个合法的JSON对象,不会再产生额外解释文字。这个约束对结构化任务来说等于把路铺好了,基本消灭了解析不到数据的尴尬情况。
请求的大致样子:
{ "model": "qwen2.5:3b", "messages": [ {"role": "system", "content": "你是记账助手,从口语中提取结构化账单。"}, {"role": "user", "content": "今天中午和黄经理吃饭,AA付了88"} ], "format": "json", "stream": false, "temperature": 0.1 }stream设为 false 是让服务端一次性返回完整结果;temperature设为0.1是让输出尽量稳定、确定,避免返回的结果每次都不一样的“幻觉式抖动”。
4.2 提示词工程:如何让模型稳定输出
光有format: json还不够,如果不在系统提示词里明确要求字段结构,模型会随便给你一个它以为合理的JSON,字段名千奇百怪。所以我用了一个比较完整的 System Prompt,把输出Schema、字段含义、枚举值和几个示例都写进去:
你是一个记账助手。用户会输入一段描述收支的自然语言。 要求: 1. 只输出JSON对象,不要任何多余文字。 2. JSON字段说明: direction: 取值为 "expense" 或 "income" amount: 数字,表示人民币金额(元),精确到小数点后两位 category: 从以下分类中选择一个:餐饮, 交通, 购物, 居住, 娱乐, 医疗, 人情, 工资, 理财, 其他 note: 字符串,一句话总结这笔收支内容 time: 字符串,这笔收支发生的日期,格式为 YYYY-MM-DD,相对今天计算 3. 示例: 用户输入:今天中午和黄经理吃饭,AA付了88 输出:{"direction": "expense", "amount": 88.00, "category": "餐饮", "note": "和黄经理吃饭AA", "time": "2024-12-08"} 用户输入:收到6月工资15000 输出:{"direction": "income", "amount": 15000.00, "category": "工资", "note": "6月工资", "time": "2024-12-08"} 当前日期:2024-12-08注意我在最后显式传入了“当前日期”。为什么不直接用相对时间让模型输出“今天”?因为JSON输出要求标准化日期,模型并不知道“今天”对应的具体是哪一天。把当前日期作为上下文给进去,它才能把“昨天”“上周三”这类表达换算成正确的YYYY-MM-DD。
这段Prompt在实测中效果提升非常明显。一开始没有 few-shot 示例时,模型偶尔会把“收到”误判成支出,或者把时间省略;加入两个示例后输出质量稳定了很多。
4.3 Dart 端解析与兜底逻辑
即使有 format 约束,模型偶尔也会抽风,比如字段名拼错、金额写成字符串、方向值写错。所以我的 Dart 侧解析逻辑不能直接jsonDecode了事,而是要加一层“宽容解析 + 兜底校验”。
具体的处理顺序:
- 先尝试直接解码:
jsonDecode拿到Map。 - 字段名兼容:如果
direction不存在,尝试type、expense_type等;如果amount缺失,尝试money等。 - 类型修复:
amount可能是字符串"88",要转成num;direction如果是"支出",要映射成"expense"。 - 规则校验:金额必须大于0,分类必须存在于预设列表,时间必须是合法日期。
- 兜底处理:如果校验失败,退化为“人工确认”模式——把模型返回的原始文本展示给用户,让用户手动改字段。
第5步极其重要。做一个依赖模型解析的工具,你永远要对模型的不确定性保留一条退路。如果直接信任模型的输出写入数据库,某个数据异常可能让后续所有统计全部错位。我的经验是把它当作“半自动标注工具”——绝大多数时候模型是对的,偶尔失败时人工一秒钟就改好,体验依然远好于全手填。
下面是我封装的一个解析核心片段:
class BillParser { static BillRecord parseModelOutput(String modelRawText, List<String> categories) { try { final Map<String, dynamic> raw = jsonDecode(modelRawText) as Map<String, dynamic>; final String direction = normalizeDirection(raw['direction']); final double amount = double.tryParse(raw['amount'].toString()) ?? 0; final String category = normalizeCategory(raw['category'], categories); final String note = (raw['note'] ?? '').toString(); final String time = normalizeTime(raw['time']); if (amount <= 0) throw const FormatException('金额不合法'); if (direction != 'expense' && direction != 'income') throw const FormatException('方向不合法'); return BillRecord( direction: direction, amountCents: (amount * 100).round(), category: category, note: note, recordTime: DateTime.parse(time), ); } catch (e) { throw const FormatException('模型输出无法解析,需要人工确认'); } } }5. 实操环节:从空白项目到能跑的记账应用
5.1 创建 Flutter 项目与依赖管理
环境就绪之后,创建项目本身很快:
fvm flutter create natural_language_bookkeeping cd natural_language_bookkeeping然后把依赖写进pubspec.yaml:
dependencies: flutter: sdk: flutter http: ^1.2.1 sqflite: ^2.3.3 path: ^1.9.0 provider: ^6.1.2 intl: ^0.19.0http用于发请求给 Ollamasqflite负责本地数据库provider做状态管理intl做日期格式化
5.2 编写模型请求服务层
Flutter 侧请求 Ollama 的核心逻辑,无非是封装一个ApiClient:
class LlmClient { final String baseUrl; final String model; LlmClient({required this.baseUrl, required this.model}); Future<Map<String, dynamic>> chat(String userInput, {required String systemPrompt, required String today}) async { final messages = [ {'role': 'system', 'content': systemPrompt}, {'role': 'user', 'content': userInput}, ]; final resp = await http.post( Uri.parse('$baseUrl/v1/chat/completions'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'model': model, 'messages': messages, 'format': 'json', 'stream': false, 'temperature': 0.1, }), ); if (resp.statusCode == 200) { final data = jsonDecode(resp.body) as Map<String, dynamic>; final content = data['choices'][0]['message']['content'] as String; return jsonDecode(content) as Map<String, dynamic>; } throw Exception('HTTP ${resp.statusCode}: ${resp.body}'); } }这里注意我用的路径是/v1/chat/completions,这是 Ollama 兼容 OpenAI 接口的路径,好处是以后换云服务商只需要改baseUrl,请求体和响应结构的处理代码完全复用。
5.3 界面与状态管理
界面我做了三个模块:输入区、历史账单列表、解析确认弹窗。
输入区最核心的交互是“先解析,后确认”。用户输入一句话,点“智能记账”按钮,App 先发请求给本地模型,拿到候选JSON后弹出一个确认卡片,显示“方向:支出 / 金额:88 / 分类:餐饮 / 时间:今天 / 备注:和黄经理吃饭AA”,用户确认后写入数据库。如果解析失败,就弹出一个带输入框的修正卡片,让用户手动修改。
为什么一定要加“确认”这一步?因为模型永远有出错概率,对于记账这种数据敏感的场景,写入即终态,错了再改成本很高。确认步骤把错误拦截在写入之前,是最既保证效率又保证准确率的方式。
状态管理用的 Provider,按模块拆了几个 ChangeNotifier:
SettingsViewModel:管理服务地址、模型名称等配置RecordListViewModel:账单列表的加载、增删ParseViewModel:管理解析中、解析成功、解析失败的状态
5.4 端到端实测
项目跑通后,我拿一批真实口语做了测试,结果如下:
| 输入语句 | 方向 | 金额 | 分类 | 时间识别 |
|---|---|---|---|---|
| 今天中午和黄经理吃饭,AA付了88 | expense | 88.00 | 餐饮 | 准确 |
| 打车去机场,花了45块3 | expense | 45.30 | 交通 | 准确 |
| 收到6月工资15000 | income | 15000.00 | 工资 | 准确 |
| 上周三超市买牛奶鸡蛋,花了我59.9 | expense | 59.90 | 购物 | 准确 |
| 地铁充值50 | expense | 50.00 | 交通 | 准确 |
这个准确率已经达到可以日常使用的水平。偶尔失败的情况集中在“表情夸张、带反讽”的语句上,比如“今天可真是破财,手机屏摔碎换屏花了800”,这种带情绪的句子模型偶尔会把分类判断成“娱乐”。遇到这种情况我的兜底逻辑就直接弹人工确认页,手动选一下分类就完事,不会对使用造成太大阻碍。
6. 常见问题与避坑实录
6.1 连接不上 Ollama:IP、端口与防火墙问题
这个问题出现的频率最高。App 端报错通常是Connection refused或者SocketException。
排查顺序我给整理成了速查表:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 电脑本机 curl 通,手机连不上 | Ollama 只监听了127.0.0.1 | 设置OLLAMA_HOST=0.0.0.0:11434并重启 |
| 手机和电脑能 ping 通但请求超时 | 防火墙拦截了11434端口 | 放行 TCP 11434 端口 |
| App 里能请求但返回404 | 请求路径错了 | 确认是/v1/chat/completions |
| 请求成功但响应特别慢 | 模型太大,CPU推理吃力 | 换更小模型或开启量化层 |
特别提醒一点,如果手机和电脑不在同一个网段,哪怕连同一个Wi-Fi也可能被AP隔离,这时候只能改网络配置或者用USB端口转发调试,这个坑也值得提前知道。
6.2 模型吐出的 JSON 不稳定怎么办
即便开了format: json,偶尔也有小概率出现字段缺失、金额带单位(比如“88元”)、分类超出范围的情况。我的处理方式是多层保险:
- Prompt 里写死 Schema 和枚举,并给两个 few-shot 例子。
temperature调到0.1,尽量让输出模式稳定。- Dart 侧做字段名兼容映射,对“支出/收入”这类中文词做方向归一化。
- 兜底人工确认,一切都失败就退化为手动表单。
其中最有效的是第1条。Prompt 里的输出示例直接影响模型模仿的格式,好的示例比你在提示词里写十句“不要输出多余内容”都管用。这个技巧在AI圈叫few-shot prompting,一种性价比极高的调优手段。
6.3 Flutter 编译与运行时的性能坑
热词里有“flutter impeller”和“flutter 多线程”,这块我也踩到过。Impeller 是 Flutter 新渲染引擎,用iOS测试的时候,如果遇到文字模糊或某些着色器导致掉帧,可以在Info.plist里关掉它应急,但新版本 Flutter 默认开启已经没太大问题。
更重要的性能坑在多线程上。我最初是直接在await http.post返回后,立刻在主Isolate里执行jsonDecode和规则校验。当模型输出有几百个字段或者做了十几条批量解析时,页面明显掉帧。后来把所有非UI逻辑挪进Isolate.run,体验立刻顺滑了很多。如果后续要接入端侧推理,这一步更是不可避免的。
另外热词里提到“flutter 低功耗蓝牙 ios 有问题嘛”,这跟本主题无关,但提醒我一点:Flutter 端做硬件相关功能,iOS 的系统限制往往比 Android 严格,调试时尽量先跑 Android 再移植 iOS,能省很多时间。
6.4 数据准确率还能怎么提升
如果你发现模型解析准确率还不够,优先做两件事:
第一,扩充 few-shot 示例。把识别错误的语句和对应的正确JSON写进 Prompt,作为新的示例,让模型在推理时参考。这招对提升准确率非常直接。比如我一开始发现“AA制”经常识别成“人情”,添加示例后基本不再出错。
第二,做一次基于本地数据的微调(LoRA)。如果你有一份个人历史账单,可以把“口语描述 → 结构化JSON”的pair整理成数据集,用LoRA微调一个小模型。这个方向我还在摸索期,但本地微调已经是当前很有价值的赛道,值得投入。
最后的一点心得
整个项目从零到跑通,最深的体会是:大模型应用开发的难点不在“调通API”,而在于对不完美输出的系统容错设计。Prompt没有绝对的对错,只有效果好坏;模型输出的JSON不是数据库里可以直接信任的数据,必须经过一层“人机协作”流程把不确定性消化掉。Flutter 在这个项目里扮演的是壳和交互层,它的跨平台能力、Isolate并发模型和丰富的插件生态,让我可以用一套代码同时打通桌面端和移动端,省掉了太多重复劳动。如果你也想试试“本地大模型 + Flutter”这个组合,建议从今天说的这句“今天中午和黄经理吃饭,AA付了88”开始,做一个自己的自然语言记账工具。