x
配置mobile_bert.tflite文件
https://ai.google.dev/edge/litert/libraries/task_library/bert_nl_classifier?hl=zh-cn
1 | app/ |
同步项目
- 点击右上角出现的
Sync Now按钮(点击小象) - 等待Gradle下载依赖完成
Manifest 合并失败问题
当前错误信息只说了“multiple errors”,但没有具体内容。我们需要让 Gradle 输出更详细的日志。
在 Android Studio 的 Terminal 窗口中执行:
1 | ./gradlew processDebugMainManifest --info |
Android Resource Directory(res/ 资源目录)
Sample Data Directory(sampledata/ 示例数据目录)仅用于开发预览的「占位数据目录」,完全不参与应用发布。
然而都不是这两个,是package
Add .gitignore
- 根据你用的编程语言/框架选择。
- 比如 Python 项目 → 选
Python模板 - Java →
Java - Node.js →
Node
- 比如 Python 项目 → 选
- 如果不确定或还没有代码,可以先不选,之后随时可以添加。
.gitignore的作用是避免把临时文件、依赖文件夹等提交到仓库。
仓库创建完后:
- 进入仓库页面 → 点击 Settings
- 左侧菜单选 Collaborators → Add people
- 输入对方的 GitHub 用户名或邮箱
- 对方接受邀请后,就能和你一起
push/pull了
1 | E:\java_practice\HealthAgent>git init |
然后git add .
git commit -m “初始提交”
git remote add origin url
git pull origin main --allow-unrelated-histories
推送
git push -u origin main
关于 .idea/ 和 gradle/ 等目录
你这次 add 包含了 IDE 配置文件(.idea/)和 Gradle 包装器(gradle/、gradlew)。这些文件可以提交,方便协作者直接导入项目,但有时会引发冲突。如果希望以后忽略它们,可以编辑 .gitignore 文件,加入:
text
1 | .idea/ |
不过第一次推送没关系,先让项目跑起来,后续再优化。
1 |
|
因为远程仓库已经有内容(比如创建时生成的 README.md),而本地没有这些内容,所以 GitHub 拒绝直接推送。按以下步骤解决:
1 | git pull origin main --allow-unrelated-histories |
1 | E:\java_practice\HealthAgent>git pull origin main --allow-unrelated-histories |
遇到了 .gitignore 文件的冲突。这是因为在本地创建了 .gitignore,而远程仓库(GitHub 初始化时)也生成了一个 .gitignore(可能内容不同)。Git 不知道保留哪一个,所以让你手动合并。
左边 <<<<<<< HEAD 到 ======= 之间是你的本地规则(偏向 Android Studio 项目),右边 ======= 到 >>>>>>> 0c05c4c... 是远程仓库的规则(通用 Java 规则,比如忽略 *.class、*.log 等)。
我趣java写的安卓项目竟然不算java吗
1 | 解决方案:合并两边的规则 |
然后继续执行 Git 操作
在终端执行:
bash
1 | git add .gitignore |
1 | remote: warning: File app/src/main/assets/mobile_bert.tflite is 95.80 MB; |
-
这个 .tflite 模型文件 95.8 MB,虽然推送成功了(GitHub 单文件限制是 100 MB),但:
- 以后每次
git clone或git pull都要下载这个 100MB 的文件,会拖慢协作者的速度。 - 如果未来文件超过 100 MB,推送会被直接拒绝。
使用 Git LFS(推荐)
Git Large File Storage 可以管理大文件,避免仓库膨胀。- 安装 Git LFS:
git lfs install - 追踪大文件类型:
git lfs track "*.tflite" - 提交
.gitattributes:git add .gitattributes && git commit -m "track tflite with LFS" - 重新推送(需 GitHub 账户启用 LFS,免费用户有 1GB 存储/月)
- 以后每次
1 | E:\java_practice\HealthAgent>git commit -m "最小闭环跑通版本" |
修改的文件没有被暂存(staged),因此 git commit 没有内容可以提交。
1 | git commit -a -m "最小闭环跑通版本" |
让手机先完成“输入一句话 → 识别意图 → 执行动作 → 返回结果”这一条完整链路。
输入一句血压文本 → 识别为记录血压 → 解析并写入 SQLite → 返回结果
1 | UserIntent 翻译 |
英语不好这一块,纯纯零基础这一块,人工agent这一块
问:
1 | DialogState.java能不能用lombok |
答:
1 | 更建议你对 DialogState 这种很小的状态类先不用,或者只少量用。原因不是“不能用”,而是这类文件字段少、逻辑简单,手写可读性已经很高;比赛项目又强调稳定和可讲解,少一层注解处理链,排错会更直接。这是工程取舍,不是技术限制。 |
补基础,那我就不用了
1 | C++:this->currentIntent = currentIntent。 |
DatabaseHelper.java。先只做血压记录表,够当前阶段使用。
1 | Class 'DatabaseHelper' must either be declared abstract or implement abstract method 'onCreate(SQLiteDatabase)' in 'SQLiteOpenHelper' |
DatabaseHelper 类继承了 SQLiteOpenHelper,但没有实现其中的抽象方法 onCreate(SQLiteDatabase db)。SQLiteOpenHelper 要求子类必须实现该方法(以及通常也建议实现 onUpgrade)。
1 | Android平台:继承 SQLiteOpenHelper |
PRIMARY KEY主键(唯一标识一条记录)
AUTOINCREMENT自动增长(不用手动赋值,自动 + 1)
COL_SYSTOLIC + " INTEGER NOT NULL, "必须是整数
不能为空(必须填写)
1 | INDEX, TABLE, TEMP, TEMPORARY, TRIGGER, UNIQUE, VIEW or VIRTUAL expected, got 'TABLEbp_records' |
TABLE 和表名 bp_records 之间缺少空格,导致解析器把 TABLEbp_records 当成了一个未知的关键字(或者标识符)。解析器本来期望在 CREATE 后面出现 INDEX、TABLE、TEMP、TRIGGER、UNIQUE、VIEW 或 VIRTUAL 这些关键字,结果却收到了 TABLEbp_records,所以报错。
解决方法:
在 TABLE 和表名之间加上空格:
1 | 缺少空格(导致你遇到的错误) |
Cursor 是 Android 数据库查询结果的游标,可以理解为一个指向查询结果集的指针/迭代器。
当执行 SELECT 查询后,数据库会返回一个 结果集(类似一张临时表格)。
Cursor 就是用来遍历、读取这个结果集中每一行数据的一个接口。
rawQuery(sql, null) 直接执行给定的 SQL 语句(第二个参数用于绑定占位符,这里没有占位符所以传 null)。
返回的 Cursor 对象指向查询结果。如果表中有至少一条记录,Cursor 将包含一条数据(最新那条);如果表为空,Cursor 的 getCount() 为 0。
HealthTool.java。为了马上能跑,把血压解析先直接写在这个类里了,不额外依赖 BloodPressureParser。后面再拆出去即可。
1 | Field 'databaseHelper' might not have been initialized |
final 修饰的成员变量必须被显式初始化,并且只能赋值一次。
初始化的时机可以是:
- 声明时直接赋值(例如
private final DatabaseHelper databaseHelper = new DatabaseHelper();) - 在构造器中赋值(这是你的代码采用的方式)
- 在实例初始化块中赋值
在 Java 中,如果只是声明 private final DatabaseHelper databaseHelper; 但没有构造器赋值,编译器会直接报错:
“Variable ‘databaseHelper’ might not have been initialized”
Java 的 final 字段不要求在声明时赋值,可以在构造器中赋值一次。
构造器接收一个参数并赋给 final 字段,保证了该字段在对象创建后永远不会改变,并且值由外部传入(依赖注入的一种形式)。
这种写法在 Android 的 DAO、Repository 或工具类中很常见,用于持有 DatabaseHelper 这样的依赖。
Pattern.compile:编译一个正则表达式模式,用于后续匹配。
正则表达式:"(\\d{2,3})\\s*/\\s*(\\d{2,3})"
(\\d{2,3}):第一个捕获组,匹配 2 或 3 位数字(收缩压,通常范围 90-180 左右)。\\s*:匹配 0 个或多个空白字符(空格、制表符等)。/:匹配字面斜杠。\\s*:再次匹配 0 个或多个空白字符。(\\d{2,3}):第二个捕获组,匹配 2 或 3 位数字(舒张压)
matcher1.group(1):返回第一个捕获组匹配的字符串(收缩压数字部分,如 "120")。
matcher1.group(2):返回第二个捕获组匹配的字符串(舒张压数字部分,如 "80")。
Integer.parseInt:将数字字符串转换为 int 类型。
复习final和static
final —— “不可变”约束
final 可以修饰变量、方法、类,分别表示:
- final 变量:值只能赋值一次(基本类型值不变;引用类型不能指向新对象)。
- final 方法:子类不能重写(override)。
- final 类:不能被继承。
java
1 | // final 变量 |
static —— “属于类,不属于实例”
static 修饰的成员(变量、方法、内部类、代码块)属于类级别,所有实例共享同一个 static 变量,可以直接通过类名访问,无需创建对象。
java
1 | class Counter { |
3. static final —— 类常量
二者经常组合使用,表示一个不可变的类级常量。编译期基本类型的常量会被直接内联。
java
1 | public static final double PI = 3.1415926535; |
AgentOrchestrator.java。规则识别意图、处理简单多轮状态、分发到 HealthTool。
Context 是 Android 中的一个核心类(位于 android.content.Context)
常见的 Context 子类
| 子类 | 说明 |
|---|---|
Application |
整个应用的全局 Context,单例,生命周期与应用相同 |
Activity |
继承自 ContextThemeWrapper,包含主题信息,用于界面交互 |
Service |
服务组件,没有界面,但仍持有 Context |
ContextWrapper |
装饰器模式的基类,用于代理 Context 操作 |
在 Activity/Service 中,this 就是 Context。
工具类或非组件类需要 Context 时,通过构造函数传入(通常传 ApplicationContext 避免内存泄漏)。
注意区分 Activity Context(有主题,用于 UI)和 Application Context(无主题,生命周期长)。
为了能马上联调,有MainActivity 的最小调用示例
1 |
思考顺序(精华)
用户在输入框里输入“我今天血压120/80”,系统先识别这是“记录血压”,再判断当前是不是在多轮对话中,如果不是,就直接解析文本,提取血压值,写进数据库,最后回一句“已记录本次血压”
第一步
先定系统边界,想清楚哪个类负责什么,那些是核心任务,那些只是支撑,哪些东西以后可以便,哪些东西现在可以先写死,在这个项目里,AgentOrchestrator(智能体编排器)是总入口,负责“接单和调度”,DatabaseHelper负责本地存储;DialogState负责记住当前对话是不是还没结束;UserIntent只是一个意图标签
1 | 意图标签(Intent Label)是在对话系统、自然语言理解(NLU)或语音助手中,用来标识用户输入背后真实目的或需求的一个标记/类别名。 |
不应该参杂业务逻辑,所以首先要在脑子里形成一张职责分工图
第二步
定主流程,也就是最小闭环:识别血压记录并库存
要完成这件事,首先需要一个意图类型来表示“记录血压”,一个地方储存当前对话状态,一个地方执行业务,一个地方村书记,还需要一个入口把他们串起来
这个对话需要多轮补全
1 | 多轮补全(Multi-turn Completion)是指对话系统在多轮交互过程中,逐步收集用户未一次性提供的必要信息,最终完成用户意图的执行。 |
所以需要DialogState
总入口分发任务时靠什么判断分支
所以需要UserIntent
首先写UserIntent,先把系统眼下支持的认为列出来,开始写代码的第一步就是把系统的业务分类语言定下来
然后写DialogState
一旦系统要支持多轮对话,就要有一个容器保存上下文
先想状态机会遇到哪些最常见情况,再决定字段,而不是看到别人有 DialogState 就机械照抄。比如用户说“提醒我吃药”,系统缺时间,于是 waitingForMoreInfo=true,missingSlot="remind_time",pendingAction="set_reminder"。程序员会先在脑子里模拟两三轮对话,确认这些字段够不够,然后再落代码。也就是说,DialogState 是从对话案例里反推出来的。
然后写DatabaseHelper
提醒我吃药记录的时间存在哪里呢?数据库里首先要有一张timeTable,先确定要保存什么字段,在像插入接口长什么样,再决定查询是不是现在就要做,只写当前马上要用到的那一两个方法
然后写HealthTool
它是第一个真正承载业务逻辑的类,这时可以从“结构设计“转向”业务细节“了。
比如它要不要自己做血压解析,还是交给专门的 BloodPressureParser
解析失败时回复什么
识别出 300/20 这种明显不合理的值时怎么处理
数据库插入失败时怎么兜底
1 | HealthTool` 的写作顺序往往是先写主方法 `handleBloodPressureRecord()`,再补它依赖的小方法,比如 `parseBloodPressure()` 和 `isReasonableBloodPressure() |
先让主链路通,再把细节塞进去
不是先写一堆工具函数再想怎么拼,而是先把主函数骨架搭出来,例如“解析→校验→存库→返回结果”,然后每一步缺什么就补什么
最后写AgentOrchestrator(智能体编排器)
因为它依赖前面几层:它要用到 UserIntent 做分类,要用到 DialogState 记上下文,要用到 HealthTool 处理业务,甚至间接依赖 DatabaseHelper
等底层零件都差不多了,再来写这个“总装配车间”
真正写 AgentOrchestrator 时,思考顺序通常是先写入口方法 handleUserInput(),然后先处理空输入,再处理是否处于多轮状态,再处理新请求分类,再按意图分发,最后给 UNKNOWN 做兜底。
先想完整用户链路,再拆职责;
先定业务类型,再定状态容器;
先做底层存储,再做单项业务;
最后再写统一调度。
而我往往是先写 MainActivity,然后把所有 if-else 都堆进去,写到一半发现要保存状态,于是再补个 DialogState,写到后面发现分类逻辑很乱,于是再补个 UserIntent
第一层是业务顺序,也就是用户用了什么,系统回了什么。
第二层是调用顺序,也就是 AgentOrchestrator -> HealthTool -> DatabaseHelper。
第三层是依赖顺序,也就是谁必须先定义,谁可以后定义
1 | Expected no arguments but found 1 |
调用了一个不接受参数的方法(或构造器),却传递了一个参数。
如有错误,多多指教