Android工程师的AI开发实战系列 · 第2/4篇
用Android思维理解RAG、Agent和微调,从移动端老兵到AI开发者的跨界之路
第1篇:RAG:给大模型装一个靠谱的「本地数据库」——Android工程师秒懂的检索增强生成
第2篇:Agent智能体:让AI自己调API干活——从Android Service到AI Agent的思维跃迁(本篇)
⏳ 第3篇:微调:让通用大模型变成你的「专属定制ROM」——从AOSP到LoRA的迁移学习
⏳ 第4篇:RAG+Agent+微调组合拳:搭建一个完整的AI驱动Android开发助手
上一篇我们聊了RAG——给大模型接一个"外挂知识库"。当时有读者问我:RAG能让AI"知道"更多东西,但能不能让它"做"更多事情?比如让它自己去查TAPD上的Bug,读一下相关代码,然后帮我写个周报?
说实话,这个问题第一次被问到的时候,我脑子里闪过的画面是:一个Service在后台默默干活,处理完了通过Callback通知你。
然后我意识到——这不就是Agent吗?
Agent到底是个什么东西
先抛开所有花哨的定义。在我看来,Agent就是一个能自己想、自己干的AI系统。普通的ChatGPT对话是"你问一句它答一句",而Agent是"你给它一个目标,它自己拆解步骤、调用工具、处理中间结果,最后给你一个完整的交付物"。
用Android的话来类比:
普通LLM对话
↓
类比 → Activity:用户点一下,响应一下,交互驱动
↓
Agent智能体
↓
类比 → Service + WorkManager:接到任务后自主运行,中间不需要用户干预,完成后回调通知
更准确地说,Agent的演进路径和Android后台任务的演进惊人地相似:
| AI领域 | Android类比 | 特点 |
|---|---|---|
| ChatBot | Activity | 一问一答 |
| Chain(链式调用) | IntentService | 固定流程 |
| Agent | Service+WM | 自主决策 |
| Multi-Agent | 多进程IPC | 协作分工 |
ReAct:Agent的Handler消息循环
Agent最核心的运行范式叫ReAct(Reasoning + Acting)。我第一次看到这个概念的时候,直觉反应是:这不就是Handler的消息循环吗?
Android的Handler循环:取消息 → 判断类型 → 分发处理 → 产生新消息 → 继续循环
Agent的ReAct循环:观察环境 → 推理思考 → 选择行动 → 获取结果 → 继续循环
来看一个具体例子。假设你让Agent"帮我查一下项目里最近一周的高优Bug数量":
Thought: 需要调TAPD接口查Bug
↓
Action: 调用search_bugs工具
↓
Observation: 返回12条Bug
↓
Thought: 需要按优先级过滤
↓
Answer: 高优Bug 5个,分别是...
看出来了吗?Agent不是一次就把事情做完的,它是迭代式的——每一步都基于上一步的结果来决定下一步该干什么。就像Handler不断从MessageQueue里取消息处理一样,Agent不断从环境反馈中获取信息,推理下一步动作。
Function Calling:AI的Intent系统
Agent要"干活",就需要工具。这些工具怎么告诉AI呢?答案是Function Calling——我第一次看到这套机制的时候,整个人都精神了:这TMD不就是Intent系统吗?
Android里你要启动一个Activity或Service,需要声明IntentFilter:
// Android IntentFilter声明
<intent-filter>
<action
android:name=
"com.app.SEARCH_BUG"
/>
<data
android:scheme=
"tapd"
android:host=
"bugs"
/>
</intent-filter>
而在Agent的世界里,你要给AI定义一个工具,长这样:
// Function Calling 工具定义
{
"name": "search_bugs",
"description":
"搜索TAPD上的Bug列表",
"parameters": {
"type": "object",
"properties": {
"project_id": {
"type": "string",
"description":
"项目ID"
},
"priority": {
"type": "string",
"enum": [
"high",
"medium",
"low"
]
}
}
}
}
对比一下:
| 概念 | Android | Agent |
|---|---|---|
| 声明能力 | IntentFilter | Tool Schema |
| 传递参数 | Bundle/Extra | JSON参数 |
| 路由分发 | PackageManager | LLM推理 |
| 返回结果 | onActivityResult | Tool Response |
区别在哪?Android的Intent路由是确定性的——PackageManager查注册表,找到匹配的组件就分发。而Agent的路由是概率性的——LLM根据语义理解来选择该调哪个工具。这既是它强大的地方(灵活),也是它坑爹的地方(不稳定)。
实战:用Kotlin写一个开发助手Agent
光说概念没意思,来写代码。我们搭一个能"查Bug→读代码→写周报"的Agent。作为Android工程师,我选择用Kotlin来实现(毕竟这是我们最熟悉的语言):
定义工具接口
// 工具定义,像不像AIDL?
interface AgentTool {
val name: String
val description: String
val parameters:
JsonSchema
suspend fun execute(
args: JsonObject
): ToolResult
}
// TAPD Bug查询工具
class SearchBugsTool(
private val tapd:
TapdClient
) : AgentTool {
override val name =
"search_bugs"
override val description =
"搜索项目Bug列表"
override suspend fun execute(
args: JsonObject
): ToolResult {
val projectId =
args["project_id"]
.asString()
val bugs =
tapd.searchBugs(
projectId
)
return ToolResult(
bugs.toJson()
)
}
}
Agent核心循环
接下来是最关键的部分——Agent的执行循环。我把它写成了一个类似ViewModel的结构,因为逻辑真的很像:
class DevAssistantAgent(
private val llm:
LlmClient,
private val tools:
List<AgentTool>,
private val maxSteps:
Int = 10
) {
// 对话历史=短期记忆
// 类比ViewModel里的State
private val messages =
mutableListOf<
Message>()
suspend fun run(
task: String
): String {
messages += Message(
role = "user",
content = task
)
repeat(maxSteps) {
// 1.调LLM推理
val resp = llm.chat(
messages,
tools.toSchema()
)
// 2.没有工具调用=完成
if (resp.toolCalls
.isEmpty()) {
return resp.content
}
// 3.执行工具调用
messages += resp.toMsg()
for (call in
resp.toolCalls) {
val tool =
tools.find {
it.name ==
call.name
} ?: continue
val result =
tool.execute(
call.args
)
messages += Message(
role = "tool",
content =
result.data,
toolCallId =
call.id
)
}
}
return "超过最大步数限制"
}
}
关键洞察:注意到
maxSteps这个参数了吗?这就是Agent的"ANR超时保护"。没有它,Agent可能会无限循环下去。这和Android设置5秒ANR超时是同一个设计思路——你不能信任任何执行路径总是能正确终止。
Agent的记忆:ViewModel vs Room
Agent有两种记忆,我发现它们完美对应Android的数据持久化层级:
短期记忆(Working Memory):就是当前对话上下文。每轮对话的消息列表,Agent用它来理解"我之前做了什么"。这就像ViewModel里的StateFlow——进程活着就在,进程死了就没了。
长期记忆(Long-term Memory):跨会话持久化的信息。比如"用户偏好用Kotlin"、"项目用的是MVI架构"。这就是Room数据库——需要主动存,重启后还能读。
// Agent记忆系统
class AgentMemory(
private val db:
MemoryDatabase
) {
// 短期=ViewModel State
private val shortTerm =
mutableListOf<
Message>()
// 长期=Room持久化
suspend fun remember(
key: String,
value: String
) {
db.memoryDao()
.upsert(
MemoryEntity(
key, value
)
)
}
// 检索相关记忆(RAG!)
suspend fun recall(
query: String
): List<Memory> {
return db.memoryDao()
.searchSimilar(
query
)
}
}
看到recall方法了吗?没错,这就是上一篇讲的RAG!Agent的长期记忆检索本质上就是一个RAG系统。第一篇和第二篇在这里串起来了。
多Agent协作:Android多模块通信的翻版
单个Agent能力有限,复杂任务需要多个Agent协作。这像极了Android大型项目的多模块架构——每个模块独立负责一块业务,通过Router或EventBus通信。
我见过的多Agent协作模式,基本能映射到Android的模块通信方案:
| 协作模式 | Android类比 | 适用场景 |
|---|---|---|
| 主从式(Orchestrator) | ARouter统一路由 | 有明确主流程 |
| 对等协商式 | EventBus广播 | 无中心决策 |
| 流水线式 | RxJava操作符链 | 数据逐步加工 |
| 竞争式 | 多Provider优先级 | 择优选方案 |
来看一个"主从式"多Agent的实现,用协程写起来非常自然:
// 多Agent编排器
class OrchestratorAgent(
private val workers:
Map<String,
Agent>
) {
suspend fun dispatch(
task: String
): String {
// 1.规划子任务
val plan =
planSubTasks(task)
// 2.并发分发
val results =
coroutineScope {
plan.map { sub ->
async {
workers[sub.type]
!!.run(
sub.desc
)
}
}.awaitAll()
}
// 3.汇总结果
return synthesize(
results
)
}
}
有没有感觉很像WorkManager的链式任务?beginWith(taskA).then(taskB, taskC).then(finalTask)——先规划,再并发执行,最后汇总。底层思维是完全一样的。
Agent的稳定性:和网络请求容错一模一样
最后聊聊Agent工程化的核心问题——稳定性。这是大部分教程不会讲的,但却是线上跑Agent最头疼的事。
我列几个典型问题和对应的Android容错策略:
幻觉(编造工具调用参数)
Agent可能会编造不存在的参数值,就像用户传了个非法的Intent Extra。解法一样:校验输入。
// 工具执行前校验参数
suspend fun safeExecute(
tool: AgentTool,
args: JsonObject
): ToolResult {
// 校验,像Intent参数检查
val validation =
tool.validate(args)
if (!validation.isValid) {
return ToolResult(
error =
"参数错误:${
validation.msg}"
)
}
return try {
withTimeout(
30_000
) {
tool.execute(args)
}
} catch (e: Exception) {
ToolResult(
error = e.message
)
}
}
无限循环(Agent反复调同一个工具)
这就是Agent版的死循环。除了前面提到的maxSteps限制,还可以加"重复检测":
// 检测Agent是否在"转圈"
fun detectLoop(
history:
List<ToolCall>
): Boolean {
val recent =
history.takeLast(3)
// 连续3次调同一工具+同参数
return recent.size == 3
&& recent.distinct()
.size == 1
}
降级策略
这是我觉得最有Android味的一个设计。我们做网络请求的时候不是经常这样吗——先试主接口,失败了降级到缓存,再不行显示兜底UI。Agent一模一样:
// Agent降级策略
suspend fun runWithFallback(
task: String
): String {
return try {
// L1: 完整Agent推理
agent.run(task)
} catch (e: AgentLoopEx) {
// L2: 简化为单步Chain
simpleChain.run(task)
} catch (e: Exception) {
// L3: 降级为纯LLM回答
llm.chat(task)
}
}
Agent框架选型:Hilt vs Koin式的纠结
最后快速过一下主流Agent框架。我对比的维度和当年选DI框架一样——灵活性 vs 上手成本 vs 社区生态。
| 框架 | 类比 | 特点 |
|---|---|---|
| LangChain | Dagger2 | 生态最全,但重且抽象泄漏多 |
| CrewAI | Hilt | 在LangChain之上的高级封装,上手快 |
| AutoGen | Koin | 轻量灵活,多Agent对话模型优秀 |
| 自研(Kotlin) | 手动DI | 完全可控,但造轮子成本高 |
我的建议:如果你只是想快速体验Agent,用CrewAI三五行代码就能跑起来。如果要做生产级别的事情,自己用Kotlin/Python写核心循环+按需接LangChain的工具包是最稳的——就像现在大家基本都是"Hilt注入+手动管理关键组件"一样。
下期预告
这篇讲了Agent的核心思想——自主推理、工具调用、记忆系统、多Agent协作。如果说RAG让AI"知道得多",那Agent让AI"能干的活多"。
但你有没有发现一个问题:不管是RAG还是Agent,我们都在用通用大模型。如果我想让模型更懂我们的业务术语?更适应我们的代码风格?更准确地理解TAPD工单的格式?
下一篇,我们聊微调——让通用大模型变成你的"专属定制ROM"。就像从AOSP出发做一个定制系统,LoRA微调本质上就是在基础模型上叠一层"薄薄的定制补丁",不改原始权重,但效果立竿见影。
到时候见
Android工程师的AI开发实战系列 · 第2/4篇
用Android思维理解RAG、Agent和微调,从移动端老兵到AI开发者的跨界之路
第1篇:RAG:给大模型装一个靠谱的「本地数据库」——Android工程师秒懂的检索增强生成
第2篇:Agent智能体:让AI自己调API干活——从Android Service到AI Agent的思维跃迁(本篇)
⏳ 第3篇:微调:让通用大模型变成你的「专属定制ROM」——从AOSP到LoRA的迁移学习
⏳ 第4篇:RAG+Agent+微调组合拳:搭建一个完整的AI驱动Android开发助手