深入 Android 端侧 AI Agent 的多步推理与任务规划架构
在做端侧 AI Agent 时,我踩过一个典型的坑:用户说「帮我把昨天拍的那张表格照片转成 Excel,然后发到工作群里」,模型通过 Function Calling 顺利调用了图片搜索和文件转换接口,但发给群的那一步,它传了空路径——因为转换还没完成,文件路径根本没返回。
这个问题的本质不是模型不够聪明,而是 Function Calling 只解决了工具调用,没解决任务编排。端侧 Agent 需要一个独立的规划层来管理多步推理中的状态、依赖和异常恢复。
Function Calling 的天花板
标准的 Function Calling 流程是线性的:用户输入 → 模型决策是否调工具 → 调工具 → 返回结果让模型继续生成。单步操作这条流水线跑得很顺畅,但面对多步任务立刻暴露三个短板:
依赖链断裂。工具 A 的输出是工具 B 的输入,但模型可能在 A 的结果回来之前就决定了 B 的参数——它不懂「等一等」。
分支路径缺失。用户意图可能有多种实现方式:传文件可以用邮件、IM、网盘链接。单轮决策没法回退尝试备选方案。
上下文膨胀。每轮调用都把历史工具结果塞进 prompt,5 步之后 token 消耗翻倍,端侧推理延迟直线上升。
在 Mate 60 Pro 上跑一个 7B 量化模型,超过 3 步的工具链调用就能感觉到明显卡顿。端侧的规划层不能只是把云端方案照搬过来,得直面内存和算力约束。
ReAct:让模型边想边做
规划层的第一个可落地方案是 ReAct(Reasoning + Acting)范式。核心思路:每一步都让模型输出一个三元组——思考(Thought)、行动(Action)、观察(Observation),然后在观察结果的基础上继续下一轮思考。
data class AgentStep(
val thought: String, // 模型推理:"需要先找到昨天的照片"
val action: ToolCall?, // 具体操作:调用 searchPhotos()
val observation: String // 工具返回:"找到 3 张表格照片"
)
class ReActLoop(
private val model: LocalLLM,
private val tools: Map<String, AgentTool>
) {
suspend fun run(userInput: String): String {
var context = userInput
repeat(MAX_STEPS) {
val step = model.generateStep(context)
if (step.action == null) return step.thought // 模型认为任务完成
val result = tools[step.action.name]?.execute(step.action.params)
context += "\nObservation: ${result ?: "执行失败"}"
}
return "任务超出步数限制"
}
}
这段循环就是 ReAct 的全部骨架。每轮推理明确记录「在想什么—做了什么—得到了什么」,模型据此判断下一步。
我在项目里用这个结构跑通了 5 个典型场景,成功率从单轮 Function Calling 的 40% 左右提升到 75%。但问题也暴露了:简单的循环无法处理需要并行调用的子任务,而且「边想边做」的串行执行在端侧延迟叠加后体验很差。
分层任务分解:先规划,再执行
实际改进方案是在 ReAct 之前加一层 任务规划(Task Planning)。把模型的一次推理拆成两个阶段:
- 规划阶段:分析用户意图,输出一个有向无环图(DAG)的子任务序列
- 执行阶段:按 DAG 的拓扑顺序调度执行,注入中间结果
data class TaskDAG(
val tasks: List<SubTask>,
val dependencies: Map<Int, List<Int>> // taskId → 依赖的 taskId 列表
)
data class SubTask(
val id: Int,
val description: String, // 面向模型的可读描述
val tool: String, // 目标工具名
val params: Map<String, Any>,
val status: TaskStatus = TaskStatus.PENDING
)
规划阶段的 prompt 强调输出结构化任务,而非直接调用工具:
分析用户请求,拆解为原子子任务。每个子任务包含:
- id: 数字编号
- description: 任务描述
- tool: 所需工具(如需)
- depends_on: 依赖的子任务 id 列表
要求:子任务粒度尽可能小,无依赖关系的子任务可以并行执行。
这样拆分后,模型只需要在规划阶段做一次深度推理,后续执行交给确定性调度器。7B 模型做一次规划推理 vs 每步都推理,延迟从 3 秒 × 4 步降到 3 秒 × 1 次 + 工具执行时间,体感差别很大。
DAG 调度引擎的端侧优化
有了 DAG 不代表万事大吉。实际执行过程中,我遇到了几个只在端侧才会放大的问题。
内存约束下的并行调度。云上可以无脑并行所有无依赖任务,但在端侧同时跑 3 个工具——比如图片分类 + OCR + 文件转码——内存直接爆了。调度器需要感知任务的计算类型:
class ResourceAwareScheduler(private val maxConcurrency: Int = 2) {
fun schedule(tasks: List<SubTask>, deps: Map<Int, List<Int>>) {
val ready = tasks.filter { task ->
deps[task.id]?.all { depId ->
tasks.find { it.id == depId }?.status == TaskStatus.COMPLETED
} ?: true
}
val (heavyTasks, lightTasks) = ready.partition { it.cost == TaskCost.HEAVY }
// 重量级任务最多并行 1 个,轻量级可以并行
heavyTasks.take(1).forEach { execute(it) }
lightTasks.take(maxConcurrency - heavyTasks.size).forEach { execute(it) }
}
}
依赖失败的回退路径。端侧工具更容易失败——文件权限问题、传感器数据异常、模型推理超时。DAG 的依赖关系如果写死,一个叶子节点失败就让整条链断裂。需要给每个任务定义 fallback:
data class SubTask(
// ... 原有字段
val fallback: FallbackStrategy? = null
)
sealed class FallbackStrategy {
data class Retry(val maxAttempts: Int = 2) : FallbackStrategy()
data class AlternativeTool(val toolName: String) : FallbackStrategy()
object Skip : FallbackStrategy() // 非关键任务允许跳过
}
部分执行与增量返回。等所有子任务完成再返回结果,用户看到的是长时间白屏。我的做法是:可展示的中间结果立刻回调 UI,比如「已找到照片」「正在识别表格」这种状态更新,本质上是把 Agent 的思考过程做了透传。
模型选择与推理策略
端侧规划对模型有两个额外要求:指令遵循能力和结构化输出。
指令遵循差的模型在规划阶段容易出现幻觉——编造不存在的工具、搞错参数类型、生成循环依赖。我在 4 款端侧模型上做了对比测试,7B 级别的 Qwen 和 Gemma 系列能稳定输出 JSON 格式的任务列表,但部分精调不足的模型会在 depends_on 里写出不存在的 taskId。
我在工程上加了一层轻量校验:
fun validateDAG(dag: TaskDAG): Result<TaskDAG> {
val taskIds = dag.tasks.map { it.id }.toSet()
// 检查所有依赖引用的任务 id 是否存在
dag.dependencies.values.flatten().forEach { depId ->
if (depId !in taskIds) return Result.failure(IllegalStateException("依赖 $depId 不存在"))
}
// 检查无环
if (hasCycle(dag)) return Result.failure(IllegalStateException("存在循环依赖"))
return Result.success(dag)
}
校验失败的规划结果不要直接丢弃,而是把错误信息注入下一轮规划 prompt——模型往往能自动修正。这个「校验—反馈—重规划」的小闭环比直接报错更符合 Agent 的自主决策理念。
结构化输出方面,端侧模型对 JSON Schema 约束的支持还不成熟。我更倾向于用较弱的约束——在 prompt 中描述格式要求,解析后做上述校验——而不是依赖 model-level 的 constrained decoding。后者的推理速度损失在端侧太明显了。
落地实践中的三条准则
规划层的抽象程度决定了 Agent 的泛化能力和运行稳定性的平衡点。做得太少,还是靠模型即兴发挥;做得太多,业务接入成本陡增。三条实践准则:
工具粒度对齐任务分解粒度。「搜索照片」这种粗粒度工具会让规划层无事可做,而「读取相册」「筛选日期」「降采样」「OCR 识别」这种拆分又过度暴露实现细节。把工具定义为一个可独立完成且有明确输出的操作单元,通常就是对的粒度。
规划缓存是端侧的必备优化。相似的用户 intent——「把昨天的表发给 XX」和「把前天的表发给 YY」——DAG 结构完全一致,只差参数。对规划结果做语义相似度匹配后复用,能省掉一次完整的模型推理。
日志即 observability。端侧 Agent 运行在用户设备上,出了问题没法远程排查。每一步的 thought、action、observation 全部落盘,同时暴露一个最小的性能面板——每步耗时、token 消耗、工具调用成功率。这些数据直接指导 prompt 迭代和模型选型。
规划层不是银弹。对于单步查询类任务,多加一层 DAG 解析反而是负优化。判断是否需要的标准很朴素:你的 Agent 是否经常因为多步依赖而「忘记」上一步的结果?如果是,那就是时候了。