让LLM按维度自动切换分析策略:SmartInspector 的 Prompt Skill 系统

让LLM按维度自动切换分析策略:SmartInspector 的 Prompt Skill 系统

项目地址:github.com/mufans/AppS...

问题背景:一个 prompt 装不下所有领域知识

SmartInspector 的分析流水线包含三个 LLM Agent:perf_analyzer(性能分析)、attributor(源码归因)、frame_analyzer(帧级分析)。最初每个 Agent 只有一个固定的 system prompt,写在一对 .txt 文件里。比如 attributor 的 prompt 定义了 SI$ 标签的解析规则、源码搜索策略(Glob→Grep→Read)等。

问题在引入「分析维度」后爆发了。我们新增了 7 个维度:lock_contention(锁竞争)、gc_events(GC)、file_io(文件IO)、sched_latency(调度延迟)、memory_trend(内存趋势)、binder_ipc(Binder IPC)、cpu_throttling(CPU降频)。每个维度都有独立的领域知识------GC 要知道 Concurrent vs Stop-the-World 的区别、锁竞争要懂 futex_wait_queue_me 的含义、文件 IO 要区分 io_wait 和 blocked_function。

如果把这些知识全部塞进一个 prompt,几个现实问题立刻浮现:

  1. Token 浪费:一次 trace 中通常只有 2-3 个维度有异常数据。比如 CPU 降频没发生,加载 cpu-throttling 的领域知识就是纯浪费。
  2. 维护困难:每新增一个维度,就要改所有 prompt 文件。而且领域知识和通用指令混在一起,改一个怕影响另一个。
  3. 无法独立演进:attributor 和 perf_analyzer 都需要同一份 GC 领域知识,但知识是内联在各自的 prompt 里的,改一处忘一处。

解决思路很直接:把领域知识从 prompt 里抽出来,变成独立的 skill 文件;运行时按需加载,只有出现异常数据的维度才注入对应知识。

技能文件的目录设计

技能文件放在 prompts/skills/ 目录下,按用途分两层:

bash 复制代码
prompts/skills/
├── SKILL.md                          # 索引文件
├── shared/                           # 跨维度共享知识
│   ├── si-tag-system.md              # SI$ 标签格式和解析规则
│   └── search-strategy.md            # Java/Kotlin/XML 源码搜索策略
└── dimensions/                       # 按维度组织的领域知识
    ├── lock-contention.md            # 锁竞争
    ├── gc-analysis.md                # GC 事件
    ├── io-analysis.md                # 文件 IO
    ├── cpu-scheduling.md             # CPU 调度
    ├── cpu-throttling.md             # CPU 降频
    ├── memory-analysis.md            # 内存趋势
    ├── binder-ipc.md                 # Binder IPC
    ├── ui-jank.md                    # UI/帧率
    └── startup.md                    # 冷启动

shared/ 放所有 Agent 都需要的公共知识,dimensions/ 放每个维度的专属领域知识。每个 skill 文件是一个自包含的 Markdown 文档,包含四个标准段落:

lock-contention.md 为例:

markdown 复制代码
# 锁竞争分析

## 数据源
- SQL 表: `__intrinsic_thread_state` (blocked_function GLOB '*futex*')
- 需要 `sched_switch` ftrace 事件支持

## 领域知识
- futex (Fast Userspace Mutex): Linux 用户空间互斥锁
- blocked_function 常见值: futex_wait_queue_me, futex_wait, do_futex
- 主线程锁竞争直接导致 ANR 和 jank

## 严重度标准
- P0: 主线程 futex 等待 max > 帧预算
- P1: 主线程 futex 等待 max > 5ms
- P2: 其他线程 futex 等待 max > 10ms

## 优化方向
- 减小锁粒度: 使用细粒度锁替代全局锁
- 避免主线程持锁: 将耗时操作移到子线程

这个结构是刻意设计的。数据源 让 LLM 知道 trace 里的哪些 SQL 表和字段与当前维度相关;领域知识 补充 LLM 可能不了解的专业背景;严重度标准 统一了 P0/P1/P2 的判定阈值;优化方向让 LLM 的建议不只是"这里慢了",而是给出具体的修复方向。

对比 gc-analysis.md,同样四段,但内容完全不同:GC 要知道 Concurrent vs Non-concurrent、Alloc vs Explicit 触发原因、堆大小波动的影响。

核心实现:按需加载机制

prompts.py 是整个 skill 系统的核心,提供三个加载函数。

1. load_prompt() --- 加载基础 prompt

最简单的函数,直接读取 prompts/ 下的 .txt 文件:

python 复制代码
def load_prompt(name: str) -> str:
    path = _PROMPTS_DIR / f"{name}.txt"
    return path.read_text(encoding="utf-8")

2. load_skill() --- 加载单个 skill 文件(带缓存)

python 复制代码
_skill_cache: dict[str, str] = {}

def load_skill(name: str, category: str = "dimensions") -> str:
    cache_key = f"{category}/{name}"
    if cache_key in _skill_cache:
        return _skill_cache[cache_key]

    base = _DIMENSIONS_DIR if category == "dimensions" else _SHARED_DIR
    path = base / f"{name}.md"
    if not path.exists():
        return ""

    content = path.read_text(encoding="utf-8")
    _skill_cache[cache_key] = content
    return content

两个要点:第一,缓存机制避免重复磁盘 I/O(同一个 GC 知识可能被三个 Agent 各加载一次);第二,category 参数区分 dimensions/shared/ 子目录。

3. load_skills_for_dimensions() --- 根据数据动态加载

这是最关键的部分。它接收 perf_summary JSON,扫描其中有实际数据的维度,只加载对应的 skill 文件:

python 复制代码
def load_skills_for_dimensions(perf_json: str) -> str:
    data = json.loads(perf_json)

    # 1. 检查维度注册表数据
    dimensions_data = data.get("dimensions", {})
    skill_names: list[str] = []

    if dimensions_data:
        from smartinspector.collector.dimensions import DimensionRegistry
        DimensionRegistry.discover()
        for dim in DimensionRegistry.all():
            dim_data = dimensions_data.get(dim.name)
            if not dim_data:
                continue
            # 跳过错误或空数据
            if isinstance(dim_data, dict) and dim_data.get("error"):
                continue
            if dim_data in ("", None, [], {}):
                continue
            skill_names.append(dim.skill_name)

    # 2. 检查非维度字段
    ft = data.get("frame_timeline") or {}
    if ft.get("jank_frames", 0) > 0 or ft.get("jank_detail"):
        skill_names.append("ui-jank")

    startup = data.get("startup_metrics") or {}
    if startup.get("startups") or startup.get("breakdowns"):
        skill_names.append("startup")

    # 3. 去重并加载
    for skill_name in unique_skills:
        content = load_skill(skill_name, category="dimensions")
        if content:
            parts.append(f"

# Knowledge: {skill_name}

{content}")

    return "".join(parts)

逻辑分三步:先遍历维度注册表中已注册的所有维度,检查 perf_summary.dimensions.<name> 是否有有效数据;再检查一些非维度的特征字段(如 jank 帧、冷启动指标);最后去重加载。

核心判断逻辑是:只有 dimensions_data.get(dim.name) 非空、非错误、非零值时才加载 。这意味着如果一次 trace 没有出现锁竞争(lock_contention 字段为空),lock-contention.md 就不会被注入到 prompt 中------省下了几百 token。

4. load_prompt_with_skills() --- 静态组合

对于需要固定 skill 的场景(比如 attributor 始终需要 SI$ 标签知识和搜索策略),提供了静态组合函数:

python 复制代码
def load_prompt_with_skills(name: str, *skill_names: str) -> str:
    parts = [load_prompt(name)]
    for skill_name in skill_names:
        if skill_name.startswith("shared:"):
            ref = load_skill(skill_name[7:], category="shared")
        else:
            ref = load_skill(skill_name, category="dimensions")
        if ref:
            parts.append(f"

# Knowledge: {skill_name}

{ref}")
    return "
".join(parts)

引用格式用 shared: 前缀区分共享技能和维度技能。例如 "shared:si-tag-system" 会加载 shared/si-tag-system.md

Pipeline 集成:三个 Agent 的用法差异

三个 Agent 对 skill 系统的使用方式各不相同,这是有意为之的------它们的角色和需求不同。

attributor --- 静态 + 动态混合

attributor 的核心任务是源码归因(找到性能热点对应的源码位置)。它有两个层次的知识需求:

静态需求 :每次运行都必须知道 SI$ 标签怎么解析、源码搜索用什么策略。这部分在初始化时通过 load_prompt_with_skills() 一次性加载:

python 复制代码
# attributor.py:110-114
_system_prompt = load_prompt_with_skills(
    "attributor",
    "shared:si-tag-system",    # SI$ 标签解析规则
    "shared:search-strategy",  # Glob→Grep→Read 搜索策略
)

动态需求 :归因时如果能知道当前 trace 有哪些维度异常,LLM 就能更好地判断代码模式。比如发现 GC 维度异常时,attributor 在分析 onBindViewHolder 里的对象分配会更敏锐。这部分通过 _build_system_prompt_with_dimensions() 按需叠加:

python 复制代码
# attributor.py:124-138
def _build_system_prompt_with_dimensions(perf_json: str) -> str:
    _, base_prompt = _get_llm()
    dim_skills = load_skills_for_dimensions(perf_json)
    if dim_skills:
        return base_prompt + dim_skills
    return base_prompt

实际调用时,_search_group() 在构建 system prompt 时判断是否有 perf_json 可用:

python 复制代码
# attributor.py:745
system_prompt = _build_system_prompt_with_dimensions(perf_json) if perf_json else _get_llm()[1]

perf_analyzer --- 纯动态加载

perf_analyzer 是性能分析的主力,它的 system prompt 在运行时根据 perf_summary 数据动态组装:

python 复制代码
# perf_analyzer.py:51-53
dim_skills = load_skills_for_dimensions(perf_json)
system_prompt = _base_prompt + dim_skills if dim_skills else _base_prompt

_base_prompt 是通用分析指令,dim_skills 是当前 trace 有数据的维度对应的领域知识。如果一次 trace 只有 GC 和锁竞争异常,就只加载 gc-analysis.mdlock-contention.md,不会加载 IO、内存、Binder 等无关知识。

这种设计的直接好处是减少了 prompt 的噪声。LLM 在处理包含大量无关领域知识的 prompt 时,容易"注意力分散",给出泛泛而谈的分析。只注入相关知识后,输出的针对性和准确度明显提升。

frame_analyzer --- 依赖已有数据

frame_analyzer 用于分析用户在 Perfetto UI 中选中的时间范围。它不一定有独立的 perf_summary,而是复用之前 /full 命令的分析结果:

python 复制代码
# frame_analyzer.py:115-119
dim_skills = ""
if existing_summary:
    dim_skills = load_skills_for_dimensions(existing_summary)
system_prompt = _base_prompt + dim_skills if dim_skills else _base_prompt

这里有个细节:if existing_summary 的判断。frame_analyzer 可以独立运行(比如直接 /frame ts=X dur=Y 而不先跑 /full),此时没有已有数据,就不加载维度技能。这是合理的降级------没有上下文时用通用 prompt 分析,有上下文时叠加维度知识。

维度类与技能文件的对齐

维度注册表中的每个 AnalysisDimension 子类都有一个 skill_name 属性,它建立了维度类和技能文件之间的映射关系:

python 复制代码
# base.py:44-47
class AnalysisDimension(ABC):
    @property
    def skill_name(self) -> str:
        """对应的 dimension skill 文件名(不含 .md)。"""
        return self.name  # 默认等于维度名

大多数维度的 skill_name 就是 name 本身。但有例外------file_io 维度的 skill_nameio-analysis

python 复制代码
# file_io.py:22-24
@property
def skill_name(self) -> str:
    return "io-analysis"

这个映射关系是有意设计的:维度在代码中的标识符用 file_io(符合 Python 的 snake_case 命名规范),而技能文件用 io-analysis(更贴近领域术语)。load_skills_for_dimensions() 通过 dim.skill_name 桥接两端,代码侧和数据侧可以独立命名。

这种松耦合设计在扩展时特别方便。新增维度的标准流程是:

  1. collector/dimensions/ 下创建维度类,设置 skill_name
  2. prompts/skills/dimensions/ 下创建对应的 .md 文件
  3. 维度注册表的 discover() 自动发现新类
  4. load_skills_for_dimensions() 自动加载新 skill

不需要改任何已有的代码或 prompt 文件。这是整个系统最重要的特性------零成本扩展。

一次完整的运行流

以一个包含 GC 和锁竞争异常的 trace 为例,看数据在 pipeline 中的流转:

  1. collector 阶段PerfettoCollector.summarize() 遍历所有注册维度,调用 collect()。GC 维度返回了 5 个 GC 事件(max_pause=23.5ms),锁竞争维度返回了 main 线程 futex 等待 12 次。IO 维度返回空数据。最终 perf_summary JSON 的 dimensions 字段包含 gc_eventslock_contention,不含 file_io

  2. perf_analyzer 阶段load_skills_for_dimensions(perf_json) 扫描 JSON,发现 gc_events 有数据且无 error,加载 gc-analysis.md;发现 lock_contention 有数据,加载 lock-contention.md;发现 file_io 为空,跳过。最终 system prompt = 通用分析指令 + GC 知识 + 锁竞争知识。

  3. attributor 阶段_build_system_prompt_with_dimensions(perf_json) 执行同样的扫描,将 GC 和锁竞争知识叠加到归因 prompt 上。LLM 在搜索源码时,会结合 GC 知识识别出 onBindViewHolder 中的大对象分配模式。

  4. frame_analyzer 阶段(如果用户选中了某帧做深度分析):复用步骤 1 的 perf_summary,加载同样的维度技能。

整个过程中,io-analysis.mdcpu-scheduling.mdmemory-analysis.md 等 6 个 skill 文件从未被读取,省下了约 2000 token。

设计收益

回顾这套 skill 系统解决了什么问题:

知识持久化 :领域知识不再散落在 prompt 的某个角落,而是独立的 .md 文件,有版本控制,可以独立 review 和修改。修改 GC 的严重度标准不需要动任何 Python 代码或通用 prompt。

领域专业化 :每个 skill 文件遵循统一的结构(数据源→领域知识→严重度→优化方向),但又完全自包含。GC 专家可以只关注 gc-analysis.md,不需要理解整个 prompt 系统。

维护分离:通用 prompt(分析框架、输出格式)和领域知识(GC 类型、futex 含义)彻底解耦。修改通用 prompt 不会意外影响 GC 分析的准确性,反之亦然。

零成本扩展:新增维度只需两步------创建维度类和创建 skill 文件。已有的 pipeline、prompt、agent 无需任何改动。

按需加载:只注入有数据的维度知识,减少了 prompt 长度和 token 消耗,同时降低了无关知识对 LLM 分析的干扰。

当然这不是唯一解。也可以用 RAG 或 function calling 让 LLM 主动查询知识库,但那增加了架构复杂度和延迟。对于 9 个维度、每个 20-30 行 Markdown 的规模,静态文件 + 按需拼接是最简单可靠的方案------没有向量数据库、没有嵌入计算、没有检索准确率问题,读文件就行。


项目地址:github.com/mufans/AppS...

相关推荐
2501_9160074710 小时前
iOS应用性能优化全面指南:从内存管理到工具使用
android·ios·性能优化·小程序·uni-app·iphone·webview
代码小书生10 小时前
Windows系统优化设置,电脑系统工具箱!支持远程桌面控制、性能优化调节、功能选项增强设置、驱动安装更新、系统更新管理、安全配置与系统维护!
windows·性能优化·系统优化·电脑系统·电脑技巧·windows10·电脑优化
光泽雨11 小时前
ADO.NET 进阶知识与实战坑位深度解析
性能优化·架构·.net
wbs_scy11 小时前
MySQL 索引特性与性能优化全解
性能优化
霞姐聊IT13 小时前
缓存技术:从CPU Cache到AI KV Cache (一)
缓存·性能优化
朝阳58114 小时前
树莓派跑了个 M3U8 下载服务,内存从 600MB 降到 2MB
性能优化·rust
梵得儿SHI14 小时前
SpringCloud 进阶拓展:性能优化指南(缓存三大问题 + 分库分表入门)
spring cloud·缓存·微服务·性能优化·高并发·分库分表·数据库优化
山峰哥14 小时前
索引策略与SQL优化:从Explain对比到生产调优的完整方法论
android·java·数据库·sql·性能优化·深度优先
全球通史15 小时前
Jetson Nano 双摄像头芯片检测视觉系统:小尺度难定位问题解决,从零开始实现教程说明
嵌入式硬件·算法·ubuntu·性能优化