报告生成:怎么让AI输出结构化内容

报告生成:怎么让AI输出结构化内容

性能分析跑完了,数据有了,归因也做了------最后一步,把结果喂给LLM,让它生成一份人能看懂的报告。

听起来简单对吧?实际上这是整个SmartInspector pipeline里最"不可控"的环节。LLM不是函数,你传进去同样的prompt,出来的格式可能完全不同。这篇文章讲我怎么从"让LLM自由发挥"一步步驯服到"稳定输出结构化报告"。

先看问题在哪

最初的设计非常朴素:把perf数据拼成一段文本,扔给LLM说"帮我分析一下",然后直接输出结果。

python 复制代码
# 初版,简单粗暴
messages = [
    SystemMessage(content="你是性能分析专家"),
    HumanMessage(content=f"以下是性能数据:{perf_data}"),
]
response = llm.invoke(messages)

跑起来之后遇到三个大问题:

  1. 格式不稳定:有时候输出Markdown表格,有时候输出纯文本列表,有时候还给你加个"总结"和"展望"
  2. 关键信息丢失:10条归因结果,报告里只写了6条,剩下4条被LLM"精简"掉了
  3. 内容冗余:LLM会把输入数据原封不动复述一遍,导致报告比输入还长

这三个问题不是个别case,而是几乎每次跑都会出现。原因很本质:LLM是语言模型,不是报告生成器。它的训练目标是"生成合理的文本",不是"按格式输出结构化数据"。

第一层驯服:Prompt Engineering

驯服LLM的第一步是写一个足够严格的prompt。SmartInspector的report-generator.txt经过了十几次迭代,核心思路是:

明确告诉LLM"不要做什么"比告诉它"做什么"更有效。

markdown 复制代码
# 输出规则
1. 不要重复输出报告头部(测试概要、性能总览等表格),这些已作为参考数据提供
2. 你只需要生成:
   - 问题列表 --- 按P0/P1/P2分级
   - 附录 --- 采集工具和原始数据位置
3. 如果没有性能问题,输出一句话:"未发现显著性能问题"

关键设计决策:把报告头部和LLM生成分离。头部(测试概要、性能总览表格)是确定性数据,用纯Python生成,不让LLM碰。LLM只负责"问题分析"这一块------因为只有这部分需要语言理解能力。

python 复制代码
# reporter/__init__.py
# 报告头部用纯代码生成,不经过LLM
header_md = _build_report_header(perf_json, trace_path)

# LLM只生成问题分析部分
full_content = generate_report(report_prompt, user_content)

# 最终报告 = 确定性头部 + LLM分析
complete_report = header_md + "\n" + full_content

这个设计的trade-off很明显:牺牲了LLM对整体报告的掌控力,换来了格式的绝对稳定。头部表格永远是对齐的,数据永远是准确的,不会出现LLM"编造"FPS数字的情况。

第二层驯服:强制不遗漏

解决了格式问题,下一个难题是"遗漏"。LLM看到10条归因结果,觉得第3条和第7条"类似",就给合并了,甚至直接跳过。

这在技术写作里是优点(精炼),但在性能报告里是灾难------用户花了时间跑分析,结果你不告诉他某个方法有性能问题?

解决方案是在prompt里加计数约束

bash 复制代码
# 问题生成规则
必须为"源码归因结果"中的每一条记录生成一个独立的问题条目。
不可遗漏、不可合并。

生成问题列表前,先清点"源码归因结果"中的条目总数,
确保每个条目都在问题列表中有对应的问题条目。
输出前逐条检查,不可遗漏。

实测效果:加了这条规则后,归因条目的覆盖率从70%左右提升到95%以上。剩下5%的遗漏基本是LLM确实判断两条记录属于同一根因的合理合并------这种情况下允许合并但要求在"现象"里列出所有方法。

第三层驯服:输入裁剪和优先级排序

前面说的问题都是"LLM输出端"的,但还有一类问题出在"输入端":数据太长,被截断了

SmartInspector一次完整分析的用户输入可能有上万字符(归因结果、性能数据、线程状态分析......),而LLM的context window不是无限的。如果归因数据排在后面,前面塞了一大堆帧时间线数据,归因结果可能被截断------LLM根本看不到。

解决方案是显式控制输入顺序和截断策略

python 复制代码
# reporter/__init__.py
# IMPORTANT: attribution section MUST come first
# 归因数据排在最前面,确保不被截断
user_parts: list[str] = []

# 归因优先级最高
user_parts.extend(format_attribution_section(attribution_result))

# 性能数据次之
if perf_json:
    user_parts.extend(format_perf_sections(perf_json))
    user_parts.append(header_md)

# LLM分析结论放最后(可被截断)
if perf_analysis:
    user_parts.append(f"## 性能分析\n{perf_analysis}")

截断时按段落边界切割,不会从中间截断一条归因记录:

python 复制代码
if estimated_tokens > MAX_REPORT_INPUT_TOKENS:
    sections = user_content.split("\n\n")
    truncated: list[str] = []
    total = 0
    for sec in sections:
        if total + len(sec) > target_chars and truncated:
            break
        truncated.append(sec)
        total += len(sec)
    user_content = "\n\n".join(truncated) + "\n\n[... 数据过长已截断 ...]"

这个设计的核心思想:归因结果是不可压缩的(每条都是一个问题),性能数据是可压缩的(LLM只需要关键指标)。所以归因排前面,数据排后面。

第四层驯服:预计算结论

还有一个发现:LLM在做一些判断时经常犯错,比如判断"这个切片是被IO阻塞还是代码执行慢"。

但这个判断其实是确定性的------Perfetto的线程状态数据已经告诉你了Running占多少、Sleeping占多少。不需要LLM来猜。

所以SmartInspector引入了预计算结论(deterministic hints)

python 复制代码
# formatter.py
from smartinspector.agents.deterministic import compute_hints

hints = compute_hints(perf_json)
if hints:
    user_parts.append(f"## 预计算结论\n{hints}")

compute_hints跑10个确定性分析模块,输出中文文本格式的结论:

python 复制代码
def compute_hints(perf_json: str) -> str:
    sections = [
        _detect_empty_scenario(data),      # 空场景检测
        _classify_severity(data),          # 严重度分级
        _compute_call_chain_distribution(data),  # 调用链分布
        _rank_rv_hotspots(data),           # RV热点排名
        _correlate_jank_frames(data),      # 卡顿帧关联
        _identify_cpu_hotspots(data),      # CPU热点
        _analyze_thread_state(data),       # 线程状态
        _analyze_io_slices(data),          # IO分析
        _analyze_compose_slices(data),     # Compose重组
        _analyze_memory(data),             # 内存分析
    ]
    return "\n\n".join(s for s in sections if s)

这些结论直接告诉LLM:"这个切片主导状态是Sleeping,根因是IO阻塞",LLM只需要据此生成自然语言的描述和建议。把"判断"交给确定性代码,把"表达"交给LLM

流式输出:一行一行打出来

最后一个细节:报告是流式输出的。

python 复制代码
# generator.py
full_content = ""
try:
    for chunk in llm.stream(messages):
        token = chunk.content
        if token:
            print(token, end="", flush=True)
            full_content += token
except Exception as e:
    # Stream failed --- retry with invoke
    logger.warning("Stream interrupted (%s), retrying...", e)
    response = llm.invoke(messages)
    full_content = response.content

为什么报告要一行一行打出来?不是炫技,是因为一次完整分析要跑2-3分钟。如果最后生成报告这一步还要等30秒什么都不显示,用户体验会非常焦虑。流式输出让用户看到"正在生成",心理上好受很多。

另外加了一个fallback机制:如果stream中断(网络抖动、API超时),自动切换到非流式的invoke重试。这个设计在生产环境里救过好几次。

JSON报告:给机器看的结构化输出

除了Markdown给人看的报告,SmartInspector还支持JSON格式的结构化报告,给CI/CD流水线用:

python 复制代码
# json_formatter.py
def format_json_report(perf_json, perf_analysis, attributable, trace_path, target):
    report = {
        "version": "1.0",
        "timestamp": datetime.datetime.now().isoformat(),
        "target": {"package": target},
        "summary": _extract_summary(perf_data),
        "issues": _extract_issues(perf_data, attributable),
        "metrics": _extract_metrics(perf_data),
    }
    return report

这里有个有意思的设计:issues的严重度分级是用纯代码算的,不走LLM:

python 复制代码
def _classify_issue_severity(dur_ms: float) -> str:
    if dur_ms > 16.67:   # 超过一帧时间
        return "P0"
    if dur_ms >= 4.0:    # 超过帧预算的25%
        return "P1"
    return "P2"

这个阈值不是拍脑袋定的------是基于设备的刷新率动态计算的。60Hz设备阈值是16.67ms,120Hz设备自动调整为8.33ms。这就是_detect_frame_budget_ms干的事,从Perfetto数据里检测实际刷新率,然后推导帧预算。

总结:确定性优先,LLM兜底

回过头看,整个报告生成的架构思路是:

  1. 能确定的部分用确定性代码:报告头部、严重度分级、阈值计算、线程状态判断
  2. 必须用LLM的部分严格约束:prompt里明确格式、计数、优先级
  3. 数据流向上保证优先级:归因数据排最前(不可截断),辅助数据排最后(可截断)
  4. 格式上双轨输出:Markdown给人 + JSON给机器

最终的效果:SmartInspector的报告生成跑了几百次,格式一致性保持在95%以上。剩下5%的"不稳定"基本是LLM在措辞上的差异------这反而是优点,每次报告读起来不完全一样,不像模板。

但如果你问我最大的教训是什么?是这句:别指望LLM做它不擅长的事。格式化、计数、数学计算、精确引用------这些确定性代码1行就能搞定的事,别丢给LLM,它真的做不好。

相关推荐
光影少年4 小时前
Webpack打包性能优化方面的经验
前端·webpack·性能优化
一起搞IT吧18 小时前
Android性能系列专题理论之十:systrace/perfetto相关指标知识点细节含义总结
android·嵌入式硬件·智能手机·性能优化
techdashen1 天前
从 51% CPU 占用到 SIMD 加速:Cloudflare 防火墙引擎的性能优化实录
性能优化
草履虫君1 天前
VMware 虚拟机网络性能优化指南:从 11 秒到 4 秒的完整调优实践
服务器·网络·经验分享·性能优化
kyriewen1 天前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
一起搞IT吧1 天前
Android性能系列专题理论之十一:block IO问题分析思路
android·嵌入式硬件·智能手机·性能优化
懋学的前端攻城狮1 天前
iOS 列表性能优化实战:从 45fps 到 60fps 的蜕变
ios·性能优化·ui kit
ellis19701 天前
Unity UI性能优化一之插件【Unity UI Optimization Tool】
unity·性能优化
mit6.8241 天前
CUDA Mode - Lecture 8
性能优化