AI Agent三层架构:Skill-Tool-Knowledge解耦实践
发布时间:2026/9/19 18:52:24来源:尧图网络
1. 项目概述当“Skill”变成万能垃圾桶AI工程就走偏了最近在三个不同规模的AI产品团队做技术复盘时反复听到一句让我头皮发紧的话“这个逻辑先塞进Skill里跑着后面再拆。”——不是临时方案而是日常开发节奏。更可怕的是有人把Android原生API调用、本地知识库检索、Office文档解析、甚至VMware虚拟机状态检查全打包进一个叫“DebugSkill”的模块里美其名曰“统一技能调度”。这根本不是工程实践是技术债务的雪球式滚存。我们团队去年花四个月重构整个Agent层核心动因就一句话Skill不该是代码垃圾桶而应是意图-能力映射的最小可信单元。所谓AI Debug Engineer不是写更多Skill的人而是能精准判断“这个需求该由Skill承载还是该由Tool暴露或是该沉淀为Hindsight Knowledge”的人。它不依赖Android Studio汉化程度也不靠Office Tool Plus这类工具堆砌而是一套可验证、可审计、可演进的决策框架。适合正在搭建AI Agent平台的后端工程师、负责AI功能落地的Android客户端开发者、以及想摆脱“写完就忘”困境的AI产品经理。如果你正被“Skill越写越多、问题越修越乱、上线后不敢动”的循环困住这篇就是为你写的实战笔记。2. 内容整体设计与思路拆解从“塞一切”到“分三层”的认知跃迁2.1 为什么“往Skill里塞一切”是典型反模式我见过最夸张的案例一个Android端AI助手的Skill模块体积达83MB包含7个SDK含腾讯企业微信文件Provider适配、百度搜索Box路径解析、QQ分享文件路径提取、4类Office文档解析逻辑Word/Excel/PPT/OneNote、3套本地加密解密流程对应不同厂商设备外加一套VMware Cleanup Tool的JNI封装。表面看功能齐全实则埋下三颗雷耦合性雷修改一个Office解析逻辑需重新编译整个Skill包触发全量Android App热更新失败率从0.7%飙升至12%可观测性雷当用户反馈“点不开奥创中心的tool下载文件”日志里只显示Skill.execute() failed根本无法定位是Provider URI解析失败、还是SD卡权限异常、或是ARM64架构下JNI符号未导出演进性雷当需要接入新能力如Deveco Device Tool的设备诊断发现现有Skill框架连动态加载so库的接口都没预留只能推倒重来。这本质是混淆了三个本应隔离的抽象层Skill意图执行单元、Tool原子能力接口、Knowledge上下文记忆。就像你不会把电饭锅、微波炉、烤箱的功能硬塞进一个叫“厨房电器”的黑盒里再让厨师对着黑盒喊“加热米饭”AI系统也必须分层解耦。2.2 我们采用的三层架构Skill-Tool-Knowledge铁三角我们最终落地的架构不是凭空设计而是基于对27个真实故障Case的归因分析提炼而成。核心原则就一条每个抽象层只解决一类问题且层间通信必须有契约约束。Skill层意图驱动仅负责接收用户指令如“查昨天会议纪要”、解析意图识别出需调用“文档检索”“时间过滤”、编排Tool调用顺序、处理最终结果呈现。它不碰任何具体实现体积控制在50KB以内纯Kotlin/Java逻辑无资源、无so库。Tool层能力暴露每个Tool是独立可测试的原子能力单元。例如FileProviderResolverTool只做一件事将content://com.tencent.wework.fileprovider/...这类URI安全转为File对象并返回权限校验结果。它不关心上层是Skill还是Agent框架调用也不处理业务逻辑。所有Tool通过统一注册中心暴露支持按Android SDK版本、ABI架构、存储路径策略动态启用/禁用。Knowledge层上下文沉淀专用于存储Hindsight Knowledge回溯式知识即运行时产生的、可复用的上下文信息。比如用户首次授权访问/storage/emulated/0/android/data/com.mi.health/files/log/后系统自动生成一条结构化记录{ scope: mi_health_logs, grant_time: 2024-06-15T14:22:00Z, path_pattern: /log/*.txt }。后续Skill调用相关Tool时自动注入此Knowledge避免重复申请权限。提示三层不是物理隔离而是逻辑契约。Skill通过ToolRegistry.get(file_resolver)获取Tool实例Tool通过KnowledgeStore.query(mi_health_logs)读取Knowledge。这种松耦合让每个层可独立演进——上周我们替换了OfficeParserTool的底层引擎从Apache POI切换到Docx4j零改动Skill层代码。2.3 为什么放弃“大一统Skill”而选择分层四个硬指标对比我们用真实数据对比了旧架构单Skill和新架构三层分离在关键指标上的差异。测试环境为Android 12设备集群覆盖华为、小米、OPPO主流机型压力场景为连续1000次混合指令含文件操作、文档解析、设备状态查询指标单Skill架构三层架构改进原理说明平均启动耗时3.2s ± 0.8s0.9s ± 0.3sSkill层剥离所有so库和SDK初始化冷启动仅加载轻量意图解析器热更新成功率88.3%99.7%Tool层变更只需推送单个aar包无需重编译整个AppKnowledge更新走独立同步通道故障定位耗时22.4分钟/Case3.7分钟/Case日志中明确标记[Tool:file_resolver] ERROR: permission_denied而非模糊的[Skill:debug] execute failed新增能力交付周期5.2工作日1.3工作日接入新Tool如Deveco Device Tool只需实现DeviceDiagnosticsTool接口并注册无需修改Skill编排逻辑这个对比不是理论推演而是我们踩坑后用A/B测试验证的结果。当你看到热更新成功率从88%跳到99.7%就知道分层不是增加复杂度而是降低系统熵值。3. 核心细节解析与实操要点如何定义Skill、Tool、Knowledge的边界3.1 Skill的黄金法则三不原则与两个必做动作Skill的本质是意图路由器不是代码仓库。我们给所有Skill开发者立下铁律不实现绝不直接调用ContentResolver.query()、不写FileInputStream、不调用PackageManager.getPackageInfo()。所有这些都必须委托给Tool。不存储禁止在Skill内部维护任何状态缓存如最近打开的文件列表。状态必须存入Knowledge Store由统一生命周期管理。不决策不判断“该用哪个Provider”、“该走哪条解析路径”。这些路由规则由Tool Registry根据设备特征Android版本、厂商、已安装App动态计算。取而代之每个Skill必须完成两个动作意图标准化将用户口语化指令转为结构化Schema。例如用户说“把小冉Android自动注入关掉”Skill解析为{ intent: disable_feature, target: android_injection, context: { app_package: com.xiaoran.injector, device_model: Xiaomi 13 } }这个Schema是Skill与Tool间的唯一契约Tool不关心用户怎么说只认这个JSON。Tool调用编排根据Schema决定调用哪些Tool及顺序。仍以上例Skill会按序执行FeatureControllerTool.disable(packagecom.xiaoran.injector)PermissionCheckerTool.verify(grant_typeruntime, scopeINJECT_EVENTS)KnowledgeStore.update(keyxiaoran_injection_status, valuedisabled)注意编排逻辑必须可逆。我们强制要求每个Skill提供undo()方法当用户说“撤回刚才操作”系统能精确还原到前一状态。这倒逼开发者思考每步操作的副作用边界。3.2 Tool的设计规范原子性、契约性、可插拔性Tool不是函数而是有严格契约的组件。我们定义Tool必须满足原子性一个Tool只解决一个明确问题。FileProviderResolverTool绝不同时处理腾讯、百度、QQ三家的Provider路径——那是三个独立ToolWeworkFileProviderTool、BaiduSearchBoxTool、QQShareFileTool。它们共享同一基类但各自实现resolveUri()方法互不影响。契约性所有Tool必须实现标准接口interface ToolT : ToolInput, R : ToolResult { fun name(): String // 工具唯一标识如 wework_file_resolver fun version(): String // 语义化版本如 1.2.0 fun inputSchema(): JsonSchema // 输入参数JSON Schema fun execute(input: T): ResultR, ToolError fun capabilities(): SetCapability // 声明支持的能力如 Capability.ANDROID_12_PLUS }这个接口强制Tool自我描述能力边界。当Skill请求ToolRegistry.get(wework_file_resolver)时Registry会检查当前设备是否满足Capability.ANDROID_12_PLUS不满足则抛出ToolNotAvailableException而非静默失败。可插拔性Tool必须支持热插拔。我们用Android的ServiceLoader机制实现// 在Tool的META-INF/services/com.example.ai.tool.Tool文件中写入 com.example.ai.tool.WeworkFileProviderTool新增Tool只需发布aar包并声明Service无需修改主App代码。上周接入SpdServiceTool展锐芯片诊断工具时客户端团队只花了15分钟——拉取aar、添加依赖、重启App即可在Skill中调用。3.3 Knowledge的沉淀逻辑Hindsight Knowledge不是日志而是可编程上下文Hindsight Knowledge常被误解为“运行日志备份”这是致命误区。真正的Hindsight Knowledge必须满足结构化不是{timestamp:2024-06-15,message:permission granted}而是{ id: perm_grant_7a3f9b, type: android_permission_grant, scope: mi_health_logs, granted_at: 2024-06-15T14:22:00Z, expires_at: 2024-06-22T14:22:00Z, device_fingerprint: xiaomi_13_12.0.12345, valid_until: 2024-06-22T14:22:00Z }valid_until字段让Knowledge具备时效性避免过期权限被误用。可索引Knowledge Store支持多维查询。Skill可这样获取val latestGrant knowledgeStore.queryPermissionGrant( filter { it.scope mi_health_logs it.valid_until now() }, sort granted_at DESC, limit 1 )这比遍历日志高效百倍。可演化Knowledge Schema支持版本迁移。当Android 14引入新权限模型我们发布PermissionGrantV2旧V1数据自动转换Migration(from 1.0, to 2.0) fun migratePermissionGrant(old: PermissionGrantV1): PermissionGrantV2 { return PermissionGrantV2( scope old.scope, granted_at old.granted_at, // 新增字段 permission_group health_data ) }实操心得我们曾因忽略valid_until导致用户升级Android 13后旧版Knowledge中的MANAGE_EXTERNAL_STORAGE权限被持续误用。现在所有Knowledge创建必填有效期且默认值不超过7天——这是用3次线上事故换来的教训。4. 实操过程与核心环节实现从零搭建三层架构的完整流水线4.1 环境准备Android Studio配置与依赖管理别被“Android Studio怎么设置中文”这类问题带偏重点。我们的架构对IDE无特殊要求但必须确保构建环境支持模块化。以下是经过验证的配置清单Android Studio版本Flamingo | 2022.2.1 Patch 2或更高关键在于Gradle Plugin 8.1因需使用featureSplit特性。模块划分:app主App模块仅含Activity/Fragment和入口逻辑:core:skillSkill基础框架意图解析、编排引擎:core:toolTool抽象层和Registry实现:core:knowledgeKnowledge Store核心:tool:wework腾讯企业微信Provider解析Tool:tool:baidu百度搜索Box路径解析Tool:tool:officeOffice文档解析Tool独立于Office Tool Plus关键依赖build.gradle// core:tool 模块 implementation com.squareup.moshi:moshi-kotlin:1.14.0 // JSON Schema序列化 implementation androidx.datastore:datastore-preferences:1.1.0 // Knowledge持久化 api androidx.lifecycle:lifecycle-viewmodel:2.6.2 // Tool生命周期管理 // tool:wework 模块 implementation project(:core:tool) // 强制依赖抽象层 implementation androidx.core:core:1.10.1 // ContentResolver兼容 // 注意不引入任何腾讯SDK只用Android原生API提示禁用所有“一键汉化”插件。我们坚持英文IDE界面因为所有Tool接口、Schema字段、错误码都用英文定义避免中英混杂导致的命名混乱。汉化需求由产品团队在UI层统一处理。4.2 Skill层实现意图解析器与编排引擎以“关闭小冉Android自动注入”为例展示Skill完整实现// :core:skill/src/main/kotlin/DisableFeatureSkill.kt class DisableFeatureSkill : SkillDisableFeatureInput, DisableFeatureResult() { override fun parseIntent(rawInput: String): ResultDisableFeatureInput, ParseError { // 使用预训练的小型NLU模型非LLM响应快、体积小 return try { val nluResult nluModel.parse(rawInput) // 输出结构化意图 val input DisableFeatureInput( target nluResult.target, context mapOf( app_package to nluResult.packageName, device_model to deviceInfo.model ) ) Result.success(input) } catch (e: Exception) { Result.failure(ParseError(NLU parse failed: ${e.message})) } } override suspend fun execute(input: DisableFeatureInput): ResultDisableFeatureResult, SkillError { return try { // 步骤1调用FeatureControllerTool val disableResult toolRegistry.getFeatureControllerTool() .execute(FeatureControllerInput( action disable, packageName input.context[app_package]!! )) // 步骤2验证权限状态 val permCheck toolRegistry.getPermissionCheckerTool() .execute(PermissionCheckerInput( grantType runtime, scope INJECT_EVENTS )) // 步骤3更新Knowledge knowledgeStore.upsert(xiaoran_injection_status, InjectionStatus(disabledAt System.currentTimeMillis())) Result.success(DisableFeatureResult( success true, message 已关闭小冉Android自动注入 )) } catch (e: ToolNotAvailableException) { Result.failure(SkillError(所需工具不可用: ${e.toolName})) } catch (e: Exception) { Result.failure(SkillError(执行失败: ${e.message})) } } } // 输入Schema强制校验 data class DisableFeatureInput( val target: String, val context: MapString, String ) // 结果Schema data class DisableFeatureResult( val success: Boolean, val message: String )关键点parseIntent()用轻量NLU模型我们选TinyBERT仅2MB避免调用远程LLM增加延迟execute()中所有Tool调用都带try-catch捕获ToolNotAvailableException并转化为Skill级错误保证上层用户体验一致knowledgeStore.upsert()使用乐观锁防止并发写入冲突。4.3 Tool层实现以WeworkFileProviderTool为例// :tool:wework/src/main/kotlin/WeworkFileProviderTool.kt class WeworkFileProviderTool : ToolWeworkFileInput, WeworkFileResult { override fun name(): String wework_file_resolver override fun version(): String 1.3.0 override fun inputSchema(): JsonSchema JsonSchema.fromJson( { type: object, properties: { uri: {type: string, format: uri} }, required: [uri] } .trimIndent()) override fun capabilities(): SetCapability setOf(Capability.ANDROID_11_PLUS) override suspend fun execute(input: WeworkFileInput): ResultWeworkFileResult, ToolError { return try { // 1. 验证URI格式 val uri Uri.parse(input.uri) if (!uri.authority.equals(com.tencent.wework.fileprovider, ignoreCase true)) { return Result.failure(ToolError(Invalid authority: ${uri.authority})) } // 2. 获取ContentResolver val resolver context.contentResolver // 3. 安全解析不直接openInputStream先query val cursor resolver.query( uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), null, null, null ) ?: return Result.failure(ToolError(Query returned null)) // 4. 提取文件信息 val displayName cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) val size cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) // 5. 构建安全File对象不暴露真实路径 val file File.createTempFile(wework_, .tmp, context.cacheDir) val inputStream resolver.openInputStream(uri) ?: return Result.failure(ToolError(Cannot open stream)) inputStream.use { stream - file.outputStream().use { out - stream.copyTo(out) } } Result.success(WeworkFileResult( fileName displayName, fileSize size, tempFilePath file.absolutePath )) } catch (e: SecurityException) { Result.failure(ToolError(Permission denied: ${e.message})) } catch (e: FileNotFoundException) { Result.failure(ToolError(File not found: ${e.message})) } catch (e: Exception) { Result.failure(ToolError(Resolve failed: ${e.message})) } } } data class WeworkFileInput(val uri: String) data class WeworkFileResult( val fileName: String, val fileSize: Long, val tempFilePath: String )关键设计inputSchema()用JSON Schema强制校验输入避免非法URI传入capabilities()声明仅支持Android 11旧设备调用时Registry自动返回ToolNotAvailableException所有IO操作都在try-catch中且区分SecurityException权限问题和FileNotFoundException文件不存在便于Skill层针对性处理返回tempFilePath而非原始路径杜绝路径遍历风险。4.4 Knowledge层实现Hindsight Knowledge的存储与同步// :core:knowledge/src/main/kotlin/KnowledgeStore.kt class KnowledgeStore private constructor(private val datastore: DataStorePreferences) { companion object { private var INSTANCE: KnowledgeStore? null fun getInstance(datastore: DataStorePreferences): KnowledgeStore { if (INSTANCE null) { INSTANCE KnowledgeStore(datastore) } return INSTANCE!! } } // 通用upsert方法支持任意类型 suspend fun T : Any upsert(key: String, value: T, ttlSeconds: Long 604800L) { val json Moshi.Builder().build().adapter(T::class.java).toJson(value) val expiry System.currentTimeMillis() ttlSeconds * 1000 datastore.edit { preferences - preferences[stringPreferencesKey($key.json)] json preferences[longPreferencesKey($key.expiry)] expiry } } // 类型安全查询 suspend fun T : Any query(key: String, type: KClassT): ResultT, KnowledgeError { return try { val json datastore.data .map { it[stringPreferencesKey($key.json)] } .firstOrNull() ?: return Result.failure(KnowledgeError(Key not found: $key)) val expiry datastore.data .map { it[longPreferencesKey($key.expiry)] } .firstOrNull() ?: return Result.failure(KnowledgeError(Expiry not found for $key)) if (expiry System.currentTimeMillis()) { // 自动清理过期Knowledge datastore.edit { prefs - prefs.remove(stringPreferencesKey($key.json)) prefs.remove(longPreferencesKey($key.expiry)) } return Result.failure(KnowledgeError(Knowledge expired: $key)) } val adapter Moshi.Builder().build().adapter(type.java) val value adapter.fromJson(json) ?: return Result.failure(KnowledgeError(Parse failed for $key)) Result.success(value) } catch (e: Exception) { Result.failure(KnowledgeError(Query failed: ${e.message})) } } } // 使用示例 val store KnowledgeStore.getInstance(datastore) store.upsert(xiaoran_injection_status, InjectionStatus(true), ttlSeconds 86400) // 24小时 val status store.query(xiaoran_injection_status, InjectionStatus::class)关键保障upsert()自动计算expiry避免手动管理时间戳出错query()中检查expiry过期自动清理防止Knowledge库膨胀使用DataStorePreferences而非SharedPreferences保证线程安全和异步操作。5. 常见问题与排查技巧实录那些没写在文档里的坑5.1 典型问题速查表问题现象根本原因解决方案预防措施Skill执行报ToolNotAvailableException但Tool模块已集成Tool未在META-INF/services/中正确声明或name()返回值与Registry查找键不匹配检查META-INF/services/com.example.ai.tool.Tool文件内容是否为完整类名用adb shell am broadcast -a com.example.ai.tool.DEBUG_LIST查看Registry已加载Tool列表CI流水线加入Tool注册检查脚本扫描aar包内META-INF/services/文件Android 10设备上WeworkFileProviderTool始终返回Permission deniedAndroid 10 Scoped Storage限制ContentResolver.query()需READ_EXTERNAL_STORAGE权限但Tool未声明在Tool模块AndroidManifest.xml中添加uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE/并在execute()前调用ContextCompat.checkSelfPermission()校验所有Tool的capabilities()必须包含Capability.SCOPED_STORAGE_REQUIREDRegistry在调用前自动检查权限Knowledge查询返回null但确认数据已写入DataStore未正确初始化或datastore实例在不同Scope中不一致确保KnowledgeStore.getInstance()传入的DataStore与App全局DataStore为同一实例检查Application.onCreate()中初始化逻辑在KnowledgeStore构造函数中添加requireNotNull(datastore)断言CI阶段运行DataStore一致性测试Office文档解析Tool在华为设备上崩溃报UnsatisfiedLinkErrorTool依赖的so库未适配arm64-v8a架构华为设备强制使用该ABI在tool:office模块build.gradle中指定ndk.abiFilters [arm64-v8a, armeabi-v7a]用readelf -A liboffice.so验证so库ABICI流水线增加ABI兼容性检查对每个so库运行file命令验证架构5.2 踩过的坑那些文档不会告诉你的细节坑1content://URI的厂商定制陷阱百度搜索Box的content://com.baidu.searchbox.fileprovider/baiddpath/...在部分OPPO设备上baiddpath会被系统重写为baidupath。我们最初以为是URI拼写错误折腾两天才发现是厂商ROM魔改。解决方案在BaiduSearchBoxTool中增加动态路径修复逻辑private fun fixBaiduPath(uri: Uri): Uri { val path uri.path ?: return uri return if (path.contains(baiddpath) Build.MANUFACTURER.equals(oppo, ignoreCase true)) { uri.buildUpon().path(path.replace(baiddpath, baidupath)).build() } else uri }教训永远不要假设content://URI是标准的。每个厂商、每个App版本都可能魔改Tool必须内置容错。坑2KnowledgeStore的并发写入冲突当用户快速连续执行“开启注入→关闭注入”upsert()可能因DataStore.edit()的乐观锁失败而丢弃第二次写入。我们最初用retryOnConflict但发现重试次数过多导致ANR。最终方案改用AtomicReference内存缓存后台持久化private val memoryCache AtomicReferenceMapString, PairString, Long(emptyMap()) suspend fun upsert(key: String, value: T, ttlSeconds: Long) { val json moshi.adapter(T::class.java).toJson(value) val expiry System.currentTimeMillis() ttlSeconds * 1000 // 先更新内存缓存 memoryCache.updateAndGet { old - old (key to Pair(json, expiry)) } // 后台异步持久化 viewModelScope.launch { datastore.edit { prefs - prefs[stringPreferencesKey($key.json)] json prefs[longPreferencesKey($key.expiry)] expiry } } }教训DataStore的edit()不是万能的高频写入场景必须分层缓存。坑3ToolRegistry的类加载器隔离问题当tool:wework模块通过ServiceLoader加载时在某些Android 12设备上ClassLoader找不到Tool接口。原因是模块间类加载器不一致。解决方案强制使用App ClassLoader// 在ToolRegistry初始化时 private val appClassLoader this::class.java.classLoader fun loadTools(): ListTool*, * { return ServiceLoader.load(Tool::class.java, appClassLoader) .toList() }教训Android的ClassLoader机制比想象中复杂跨模块服务发现必须显式指定ClassLoader。5.3 生产环境监控让三层架构“看得见、管得住”没有监控的架构等于裸奔。我们在生产环境部署了三层监控Skill层监控统计每个Skill的parseIntent()成功率、execute()平均耗时、undo()调用频次。告警阈值成功率95%或耗时2s持续5分钟。Tool层监控记录每个Tool的调用次数、成功/失败率、各错误类型分布如SecurityException占比突增提示权限策略变更。告警失败率10%且SecurityException占比80%。Knowledge层监控跟踪Knowledge总条目数、平均TTL、过期清理率。告警条目数10万或过期率5%说明TTL设得太长。所有监控数据通过WorkManager定时上报不阻塞主线程。我们发现一个关键规律当WeworkFileProviderTool的SecurityException失败率突然升高90%概率是用户刚升级了企业微信新版本其Provider权限模型变更。此时自动触发Tool版本升级提醒。6. 后续演进从AI Debug Engineer到AI System Architect这套三层架构上线半年后我们团队角色发生了质变。不再有人自称“AI Skill工程师”大家自然形成了新头衔AI Debug Engineer。这不是职称而是能力共识——它意味着你能在需求评审会上一眼判断“这个需求该走Tool还是Knowledge”当线上故障发生3分钟内定位是Skill编排逻辑错误、Tool能力缺失还是Knowledge过期设计新功能时本能地先画三层交互图再写代码。最近我们正推动下一步将Tool层能力开放给第三方。例如SpdServiceTool展锐芯片诊断已作为独立aar发布手机厂商可直接集成无需修改自身AI框架。这印证了当初的选择——Skill不是终点而是连接Tool与Knowledge的智能枢纽真正的价值永远在可复用、可组合、可演进的原子能力里。我在实际落地中最大的体会是别再用“塞一切”的懒惰思维对待AI工程。每一个Skill的精简都是对系统熵值的削减每一次Tool的拆分都是对技术债的偿还每一处Knowledge的沉淀都是对用户意图的理解深化。当你的Android Studio项目不再因一个Skill变更而全量重编当你的Office文档解析不再受制于某个Tool的Bug当你能自信地说“这个需求我们有现成Tool三天内上线”你就真正踏入了AI工程的深水区。
网站建设高端定制企业官网