面向移动端的企业级 AI Agent 架构设计:从 100 个 API 到按需注入
引言 一、为什么需要移动端原生 Agent? 典型场景: 二、整体架构(七层解耦) 三、核心模块设计 class DaemonEngine( private val hook: AgentHook, private val scope: CoroutineScope, private var llmApiStrategy: LlmAdapterStrategy ) { private val skillRegistry = mutableMapOf() fun dispatch(event: EngineEvent, activeSkillNames: Set? = null) { if (event is EngineEvent.HardwareInterrupt) { currentExecutionJob?.cancel() hook.onFinalize("🛑 [拦截] 已强行终止思考。") return } // 进入 LLM 推理 → ReAct 循环 } } 3.2 ReAct 循环 3.3 可热切换的 LLM 适配器 3.4 技能工厂(McpProxySkill) uspend fun buildSkills(): List = withContext(Dispatchers.Default) { apiDocs.map { doc -> McpProxySkill( name = "${namespace}/${doc.apiId}", description = doc.description, parametersJsonSchema = gson.fromJson(doc.paramSchemaJson), executeAction = { args, ctx -> /* 真实 HTTP 请求 */ } ) } } 3.5 语义感知的上下文截断器 fun getRecentMemoryWindow(maxSize: Int = 20): MutableList { var safeStartIndex = totalSize - maxSize while (safeStartIndex > 0) { val node = memoryNodes[safeStartIndex] if (node.role == "tool_results") { safeStartIndex--; continue } if (node.role == "assistant" && node.toolCallsJson != null) { safeStartIndex--; continue } break } return memoryNodes.subList(safeStartIndex, totalSize) } 四、实现中常见的 7 类陷阱与解决方案 陷阱 1:工具调用响应类型误判 原因:将字符串类型的字段直接作为布尔条件判断。 解决方案:使用明确的类型检查,例如 if (response.replyText.isNotEmpty())。 陷阱 2:System Prompt 条件逻辑错误 原因:添加 system prompt 的条件写反(有 prompt 时不加,没有时加空内容)。 解决方案:正确判断 !sysPrompt.isNullOrEmpty() 后再添加。 陷阱 3:安全护栏条件反置 原因:前置依赖检查逻辑写反。 解决方案:检查“未执行前置接口”时才拦截,而非“已执行”。 陷阱 4:核心 HTTP 请求被注释 原因:调试期间注释了真实请求代码,上线前未恢复。 解决方案:使用 feature flag 或 mock 开关,避免注释核心逻辑。 陷阱 5:提示词中的占位符未格式化 原因:构建 prompt 时忘记调用 String.format()。 解决方案:使用模板引擎或明确调用格式化方法。 陷阱 6:多套工具调用协议冲突 原因:提示词中描述的格式与实际代码使用的协议不一致。 解决方案:保持提示词与实际解析逻辑完全一致,只使用一种协议。 陷阱 7:SharedFlow 信号因配置丢失 原因:MutableSharedFlow 的 replay 参数为 0,重建后旧信号丢失。 解决方案:设置 replay = 1 以保留最近一个信号。 五、从 100 个 API 中按需注入:意图路由 + 模块依赖 5.1 三种方案对比 方案 做法 优点 缺点 全量注入 100 个接口全塞 实现零成本 注意力稀释、烧钱 关键词匹配 contains 匹配 零成本 多意图处理不了 意图路由+模块注入 匹配模块→展开依赖精准、 低成本 需维护模块关系 推荐采用意图路由 + 业务模块注入。 5.2 业务模块定义与依赖展开 data class BusinessModule( val id: String, // "calendar" val keywords: List, // 触发关键词 val apiIds: List, // 该模块包含的所有接口 val dependsOn: List, // 依赖的其他模块(自动带出) val systemPrompt: String ) 模块依赖示例: 核心路由代码(零三方库): class IntentRouter(private val modules: List) { fun resolve(userInput: String): ResolvedContext { val lower = userInput.lowercase() val matched = modules.filter { it.keywords.any { kw -> lower.contains(kw.lowercase()) } }.toMutableSet() val resolved = mutableSetOf() matched.forEach { resolveDependencies(it, resolved) } return ResolvedContext(resolved, resolved.flatMap { it.apiIds }.distinct()) } private fun resolveDependencies(module: BusinessModule, resolved: MutableSet) { if (module in resolved) return module.dependsOn.forEach { depId -> modules.find { it.id == depId }?.let { resolveDependencies(it, resolved) } } resolved.add(module) } } 5.3 模块热切换策略 用户输入 操作 5.4 系统提示词分层设计(API 缓存优化) 层内容 变化频率 缓存效果 5.5 效果对比 六、完整交互时序图 七、相关讨论与局限 7.2 并行工具执行的条件 7.3 与其他移动端 Agent 方案的对比 八、总结 守护引擎:支持硬件中断,避免无限推理。 该方案已在企业内部实测,不依赖任何第三方 LLM 编排框架,纯 Kotlin 实现。核心原则: 定义业务模块 + 依赖关系 → 用户输入通过关键词匹配路由 → 递归展开依赖 → 只注入相关接口和规则。不加分词库,不加 LLM 分类,不加重型框架。 九、参考资料与配套代码 https://github.com/g-wellsa/DroidAgent.git (请替换为实际 Gist 链接) 相关概念:ReAct 模式、Function Calling、Agent Memory、Android Agent、Api 按需加载
