多Agent协作:从采集到报告的流水线设计

多Agent协作:从采集到报告的流水线设计

做CLI工具容易,做4个Agent串联还能稳定跑的CLI工具不容易。

SmartInspector的全量分析流程是:用户说"帮我全面分析一下" → 采集设备trace → LLM分析性能瓶颈 → 定位到项目源码 → 生成结构化报告。四个步骤,四个Agent,数据在它们之间流转。

这篇文章拆解这个流水线是怎么搭的,踩了哪些坑。

Orchestrator:不干活的调度中心

很多多Agent系统的Orchestrator自己干一堆事------解析意图、准备数据、甚至直接调LLM。我的做法是:Orchestrator只做一件事------路由

python 复制代码
@node_error_handler("orchestrator")
def orchestrator_node(state: AgentState) -> dict:
    """Pure LLM classification to decide routing."""
    messages = state.get("messages", [])
    
    # 提取最后一条用户消息
    user_msg = ""
    for m in reversed(messages):
        if isinstance(m, dict):
            if m.get("role") == "user":
                user_msg = m.get("content", "")
                break
        else:
            content = getattr(m, "content", "")
            msg_type = getattr(m, "type", "")
            if content and msg_type == "human":
                user_msg = content
                break

    # LLM分类,max_tokens=5,只返回一个标签
    llm = _get_route_llm()
    response = llm.invoke(orch_input)
    raw = response.content.strip().lower()
    
    # 解析路由标签
    decision = RouteDecision.END
    for v, rd in valid.items():
        if v in raw:
            decision = rd
            break
    
    return {"messages": [], "_route": decision, "skip_wait": skip_wait}

关键设计决策:

  1. Orchestrator不修改任何数据字段 。它只往state里塞一个_route字符串,其他字段原样透传。这保证了下游节点拿到的数据是干净的。
  2. max_tokens=5 ,LLM只需要返回一个标签(如full_analysis),不浪费Token。
  3. fallback机制:LLM分类失败时路由到fallback节点,不会直接报错挂掉。

路由结果存在AgentState["_route"]里,LangGraph的conditional edges根据这个值决定走哪条边:

python 复制代码
def route_from_orchestrator(state: AgentState) -> str:
    decision = state.get("_route", "end")
    mapping = {
        RouteDecision.FULL_ANALYSIS: "collector",
        RouteDecision.ANDROID: "android_expert",
        RouteDecision.EXPLORER: "explorer",
        RouteDecision.END: "fallback",
    }
    return mapping.get(decision, "fallback")

AgentState:数据流转的骨架

多Agent系统最核心的设计是状态。每个Agent都是一个纯函数:输入state → 输出部分state更新。LangGraph负责merge。

python 复制代码
class AgentState(TypedDict):
    messages: Annotated[list, operator.add]    # 消息列表,只追加不覆盖
    perf_summary: str          # JSON: 采集结果
    perf_analysis: str         # Markdown: 分析结果
    attribution_data: str      # JSON: 可归因的slice列表
    attribution_result: str    # JSON: 归因结果(含源码片段)
    _route: str                # 路由决策
    _trace_path: str           # trace文件路径

这里有三个设计要点:

1. messages用operator.add而非覆盖

LangGraph的Annotated[list, operator.add]意味着每个节点返回的messages会被追加到已有列表,而不是替换。这样collector产生的[trace collected]消息不会丢,reporter产生的最终报告也不会覆盖前面的进度信息。

2. 数据字段用字符串不用对象

perf_summaryattribution_result都是JSON字符串,不是Python对象。原因很简单:LangGraph的state serialization要求所有字段可序列化。用字符串虽然每次要json.loads(),但避免了自定义serializer的麻烦。

3. _pass_through保证数据不丢

每个节点只修改自己负责的字段,其他字段必须透传:

python 复制代码
def _pass_through(state: AgentState, *, extra_keys: tuple = ()) -> dict:
    keys = _PASS_THROUGH_KEYS + extra_keys
    return {k: state.get(k, "") for k in keys}

这解决了一个实际Bug:早期attributor节点忘了透传perf_analysis,导致reporter拿到的分析结果是空的。加了_pass_through后,每个节点都显式声明"我不改的字段原样传下去",再没出过数据丢失的问题。

四个Agent,四步流水线

Collector:采集

Collector是最重的节点------调adb采集Perfetto trace,解析成结构化JSON,还要和WebSocket通道合并block事件。

python 复制代码
def collector_node(state: AgentState) -> dict:
    # 冷启动模式:先force-stop,采集开始后再launch
    if is_startup and cold_start_target:
        _adb_force_stop(cold_start_target)
    
    # 采集trace
    trace_path = PerfettoCollector.pull_trace_from_device(
        duration_ms=duration_ms,
        target_process=target_process,
    )
    
    # 解析并生成summary
    collector = PerfettoCollector(trace_path)
    summary = collector.summarize()
    
    # 合并WS block事件(SQL数据+App端stack_trace)
    merged = _merge_block_events(sql_events, ws_events)
    summary.block_events = merged
    
    return {
        "perf_summary": summary.to_json(),    # 核心输出
        "_trace_path": trace_path,            # 传给下游
    }

Collector的输出是perf_summary(JSON字符串),这是整个流水线的数据源。后续所有节点都依赖这个字段。

Analyzer:分析

Analyzer拿perf_summary做LLM分析,输出Markdown格式的性能诊断:

python 复制代码
def analyzer_node(state: AgentState) -> dict:
    perf_json = state.get("perf_summary", "")
    analysis = analyze_perf(perf_json)
    
    return {
        "messages": [AIMessage(content=analysis)],
        "perf_analysis": analysis,     # 核心输出
    }

这里有个perf_analyzer_nodeanalyzer_node的区别------前者是独立使用的(用户说"分析一下这份数据"),后者是pipeline中的一环。功能一样,区别在于perf_analyzer_node会尝试从历史消息中找数据,而pipeline版本的analyzer_node只从state取。

Attributor:归因

Attributor做的是从性能热点追溯到项目源码:

python 复制代码
def attributor_node(state: AgentState) -> dict:
    perf_json = state.get("perf_summary", "")
    
    # 第一步:提取可归因的slice(过滤系统类)
    attributable = extract_attributable_slices(perf_json, min_dur_ms=1.0)
    
    # 第二步:在项目源码中搜索方法定义
    results = run_attribution(attributable)
    
    return {
        "attribution_data": json.dumps(attributable),
        "attribution_result": json.dumps(results),  # 核心输出
    }

归因的核心逻辑:Perfetto记录的是SI$tag#ClassName.methodName#duration格式的slice,Attributor从中提取类名和方法名,用glob+grep在项目目录搜索匹配的源文件,返回文件路径和代码片段。

Reporter:报告

Reporter是流水线最后一环,把前面三个节点的输出合并成一份结构化报告:

python 复制代码
def reporter_node(state: AgentState) -> dict:
    perf_json = state.get("perf_summary", "")
    perf_analysis = state.get("perf_analysis", "")
    attribution_result = state.get("attribution_result", "")
    
    # 拼装输入:归因数据放最前面(最重要,不能被截断)
    user_parts = []
    user_parts.extend(format_attribution_section(attribution_result))
    user_parts.extend(format_perf_sections(perf_json))
    user_parts.append(f"## 性能分析\n{perf_analysis}")
    
    # Token超限时按段落截断
    if estimated_tokens > MAX_REPORT_INPUT_TOKENS:
        sections = user_content.split("\n\n")
        truncated = []
        for sec in sections:
            if total + len(sec) > target_chars and truncated:
                break
            truncated.append(sec)
    
    # 流式生成报告
    full_content = generate_report(report_prompt, user_content)
    
    return {
        "messages": [AIMessage(content=complete_report)],
    }

归因数据放最前面这个设计是踩坑后的结论。早期把perf_summary放最前面,结果Token超限截断时,归因数据(最核心的输入)被截掉了,报告质量断崖式下降。调整顺序后,即使截断也只丢一些辅助指标数据。

错误处理:一个挂了不影响全局

多Agent系统最怕的是某个节点异常后整个pipeline崩掉。我用了一个装饰器统一处理:

python 复制代码
def node_error_handler(node_name: str):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(state: AgentState) -> dict:
            try:
                return func(state)
            except Exception as e:
                print(f"  [{node_name}] ERROR: {e}", flush=True)
                return {
                    "messages": [AIMessage(content=f"[{node_name}] Error: {e}")],
                    **_pass_through(state),
                }
        return wrapper
    return decorator

核心思路:任何节点报错都返回一个安全的state更新------把错误信息塞进messages,其他字段透传。这样:

  • Collector挂了 → Analyzer拿到空的perf_summary → 跳过分析 → 用户看到错误提示
  • Analyzer挂了 → Attributor仍然能基于perf_summary做归因(降级但不中断)
  • Attributor挂了 → Reporter用perf_analysis生成报告(少源码归因,但报告不缺)

降级而不中断,这是流水线系统的核心原则。

Graph构建:把节点串起来

最后看LangGraph的图构建:

python 复制代码
def create_graph():
    builder = StateGraph(AgentState)
    
    # 注册所有节点
    builder.add_node("orchestrator", orchestrator_node)
    builder.add_node("collector", collector_node)
    builder.add_node("analyzer", analyzer_node)
    builder.add_node("attributor", attributor_node)
    builder.add_node("reporter", reporter_node)
    
    # 入口
    builder.add_edge(START, "orchestrator")
    
    # Orchestrator条件路由
    builder.add_conditional_edges("orchestrator", route_from_orchestrator, ...)
    
    # 全量分析流水线
    builder.add_edge("collector", "analyzer")
    builder.add_conditional_edges("analyzer", _route_from_analyzer, ...)
    builder.add_edge("attributor", "reporter")
    builder.add_edge("reporter", END)
    
    return builder.compile(checkpointer=MemorySaver())

注意analyzer后面有条件分支:TRACE模式直接END(只要分析),STARTUP模式走冷启动分析,FULL_ANALYSIS模式走attributor→reporter。同一个analyzer节点服务三种场景,靠_route字段区分后续路径。

总结:几个关键决策

  1. Orchestrator只路由不处理------职责单一,好维护,Token消耗极低(5个Token)
  2. State用字符串不用对象------序列化零风险,代价只是多一点json.loads
  3. _pass_through强制透传------防止节点遗漏数据,这是个Bug驱动的抽象
  4. 错误处理统一装饰器------降级不中断,用户总能拿到部分结果
  5. 归因数据放报告输入最前面------Token截断时保核心丢边缘

这套流水线跑了快两个月,从最开始的collector→analyzer两步,逐步加到现在的4步。LangGraph的StateGraph模型让扩展很自然------加个节点,加条边,改个路由函数,完事。


SmartInspector项目地址:GitHub 系列上一篇:意图路由:用5个Token决定该找谁

相关推荐
Gauss松鼠会2 小时前
效率起飞!GaussDB 管理平台(TPOPS)升级指南
服务器·数据库·性能优化·gaussdb·经验总结
ironinfo3 小时前
.net 高并发服务性能瓶颈排查处理
性能优化·.net·grpc
小羊子说21 小时前
Android ANR 原理浅析
android·性能优化·车载系统
HackTorjan1 天前
深度解析雪花算法及其高性能优化策略
人工智能·深度学习·算法·性能优化·dreamweaver
小短腿的代码世界1 天前
Qt 2D 绘制实战与性能优化深度解析
开发语言·qt·性能优化
H Journey1 天前
C++ 性能瓶颈分析与优化
c++·性能优化·gprof·perf·valgrind·瓶颈分析
帅次1 天前
Android 性能优化专题面试稿
android·面试·性能优化
Gauss松鼠会1 天前
【GaussDB】GaussDB逻辑操作符入门指南
数据库·性能优化·gaussdb·经验总结·逻辑操作符