Day 05 / 共 20 天 · 第 1 周收官

一个 Agent 的标准解剖

第 1 周收官。前 4 天的概念(图 / State / 节点 / supervisor),今天全部落到"一个真实 Agent 的目录结构 + 真源码"上。以旗舰 sre-rca 为范本:黑板怎么定义、图怎么装配、专家工厂怎么把 30 行代码背后的一整套模板收进 toolkit、失败怎么被双层兜住。这是 Day 14 逐行精读的地图,也是第 2 周钻进 toolkit 之前的最后一块拼图。

📍 你在 20 天里的位置(第 1 周:建立心智 · 收官)
D01 全景架构 D02 跑起来 D03 LangGraph D04 Supervisor D05 Agent 解剖
💡 用一个类比先兜住今天(延续「盖楼/装修」世界观) 一个 Agent 的目录,就像一户精装房的施工档案袋,每份文件对应一样东西:state.py=这户的"公告板"定义(Day 03 那块黑板);builder.py=施工蓝图(工序怎么连,最该先看);nodes/=各道工序;specialists/=取证师傅;tools/=只读的检测仪器;prompts/=施工作业指导书;server/main.py=两种入户方式;BOM=全楼统一的建材采购清单。全平台每户档案袋格式都一样——看懂一户就看懂所有户。今天我们把这户的档案袋一页页翻开,翻到真代码。
L01

标准目录结构:每个 Agent 长一个样

本框架的一大好处是所有 Agent 目录结构统一——看懂一个就看懂全部。以 apps/sre-rca-agent/sre_rca/ 为例(下面每个文件今天都会读到真代码):

state.py
定义"共享黑板" RcaState(TypedDict)——全图数据在这里流动。L02
builder.py
★ 装配 LangGraph 图:加节点、连边、条件路由、compile。读 agent 从这里入手。L03
nodes/
普通节点:triage / recall / rag / synthesizer / critic / writeback。L04
specialists/
取证专家:trace/metric/deploy/log 四个,各从一个视角分析,可并行。L05-L08
tools/
只读数据源工具:查 trace/指标/发布记录/日志,全用 @safe_tool_result 包。L09
prompts/
版本化的 prompt 文件(*-v1.md),带设计说明的工程文档。L09
server.py / main.py
两种入口:挂到平台 host vs 独立部署。L09
pyproject.toml
依赖声明:一行 ai-trust-toolkit-bom 锁死所有版本。L09

回忆 Day 01 的铁律:L3 业务 Agent 只写 State / Specialists / Prompts / DAG(图) 这四类东西,护栏能力全从 toolkit import。上面这个结构正是这条铁律的体现。真实文件清单印证了这点——nodes/ 下是 triage/recall/rag/synthesizer/critic/writeback,specialists/ 下是 trace_sp/metric_sp/deploy_sp/log_sp + 一个 _factory.py(后面 L08 会揭穿这个"化石")。

L02

state.py — 共享黑板与两种 Reducer

state.py:15 定义 class RcaState(TypedDict, total=False)。回忆 Day 03:这是整张图的"黑板",每个节点读它、返回部分字典合并回去。真代码按"数据流阶段"分了组:

class RcaState(TypedDict, total=False):        # state.py:15
    # ── 输入 ──
    trace_id: str
    service_name: str | None
    user_question: str
    # ── Triage 输出 ──
    alert_type: str
    need_full_dag: bool
    # ── 4 Specialist 输出(字段独立,并行写零冲突)── state.py:29
    trace_finding: dict[str, Any] | None
    metric_finding: dict[str, Any] | None
    deploy_finding: dict[str, Any] | None
    log_finding: dict[str, Any] | None
    # ── Synthesizer / Critic 输出 ──
    conclusion: dict[str, Any] | None
    critique_passed: bool
    critique_layer: int          # 1=代码层失败 / 2=LLM层失败 / 0=通过
    # ── Reducer:累加(防并发竞态)── state.py:48
    retry_count: Annotated[int, add]
trace/metric/deploy/log_finding
4 个专家各写各的字段。默认是"覆盖"语义,而它们并行执行——因为字段互不相同,并行写入零冲突,覆盖就够用。
retry_count
唯一用 Annotated[int, add] 的字段。Critic 打回重做时可能有并发写入,用累加 Reducer 而非覆盖,避免两次 +1 互相盖掉导致计数丢失。它是判断"重试是否触顶"的依据。
critique_layer
一个巧妙的编码:0=质检通过、1=被代码层拦下、2=被 LLM 层拦下。一个 int 就把"过没过、被哪层拦的"都说清了(Day 07 主角)。
💡 设计取舍①:为什么 4 个 finding 用 4 个独立字段,而不是一个 findings: dict如果 4 个专家都往同一个 findings 字段写,并行时就会互相覆盖——LangGraph 的默认 Reducer 是"覆盖",最后一个写的赢。拆成 4 个独立字段,每个专家只碰自己那格,并行零冲突、无需给字段配特殊 Reducer。这是"用数据结构的设计规避并发问题",比加锁优雅得多。
💡 设计取舍②:为什么只有 retry_count 用累加 Reducer,别的都用覆盖?因为其它字段都是单写者(一个字段只有一个节点会写),覆盖天然安全、也最省心。只有 retry_count 是"可能被并发 +1"的计数器,才值得用 add Reducer。这体现了一个原则:Reducer 是成本,能不用就不用,只在真有并发累加需求处才上。state.py 顶部注释把这个决策写得清清楚楚。
L03

builder.py — 图的装配(最重要)

核心函数 build_rca_graph()builder.py:42)。它就是 Day 03/04 学的那套 StateGraph → add_node → add_edge → compile,只是节点更多。整张图长这样:

START triage分诊 recall召回历史 trace专家 metric专家 deploy专家 log专家 rag查 SOP synth综合 critic质检 writeback沉淀 END

关键代码(来自真实 builder.py,行号已核对):

builder = StateGraph(RcaState)                    # builder.py:73
builder.add_node("triage", triage_node)           # builder.py:76
builder.add_node("recall", build_recall_node(episodic))
builder.add_node("trace_sp", trace_sp_node)       # 4 个专家节点
builder.add_node("metric_sp", metric_sp_node)
builder.add_node("deploy_sp", deploy_sp_node)
builder.add_node("log_sp", log_sp_node)
builder.add_node("critic", build_critic_for_rca())
# ── 线性入口 ── builder.py:88
builder.add_edge(START, "triage")
builder.add_edge("triage", "recall")
# ── 4 SP 并行扇出 + 扇入到 RAG ── builder.py:92
for sp in ("trace_sp", "metric_sp", "deploy_sp", "log_sp"):
    builder.add_edge("recall", sp)                # recall → 每个专家
    builder.add_edge(sp, "rag")                   # 每个专家 → rag
builder.add_edge("rag", "synthesizer")
builder.add_edge("synthesizer", "critic")
# ── Critic 三态条件路由:直接用 toolkit 封好的!── builder.py:100
critic_router = build_critic_router(
    pass_dest="writeback", retry_dest="synthesizer",
    end_dest=END, max_retries=max_critic_retries)
builder.add_conditional_edges("critic", critic_router,
    {"writeback": "writeback", "synthesizer": "synthesizer", END: END})
builder.add_edge("writeback", END)
return builder.compile(checkpointer=checkpointer) # builder.py:118

逐段翻译:(1)StateGraph(RcaState) 建一张以刚才那块黑板为状态的图;(2)10 个节点一个个 add_node;(3)并行的秘密就在那个 for 循环——4 个专家都"从 recall 出发、都汇入 rag",LangGraph 看到这种"同源扇出、同汇扇入"就自动让它们并行,业务作者一个线程都不用管;(4)Critic 后面接的不是手写 if/else,而是从 toolkit import 的 build_critic_router——通过/打回重做/触顶三态路由全封好了(Day 07/08 细讲);(5)compile(checkpointer=...) 收尾,checkpointer 是"工作记忆"(Day 09)。

看出来了吗? Day 03/04 的每一个抽象(普通边、并行扇出扇入、条件边循环、checkpointer)在这里全部用上了。业务作者没有重新发明轮子——连"Critic 三态路由"都是 import 的。这就是"薄业务 + 厚 toolkit"。
🧍 第一人称请求之旅:现在你是一次「支付超时」故障请求 你带着 {"alert":"支付超时率突增到8%"} 进门 →(triage)门口分诊师傅用关键词把你归到 latency 类,没花一分钱 →(recall)档案员翻出去年相似旧 case 塞进你口袋 →(4 个专家并行)trace/metric/deploy/log 四位师傅同时给你取证,各自往公告板各自那格写 finding →(rag)查出相关 SOP →(synthesizer)请贵师傅 Sonnet 把所有材料综合成结论 →(critic)验收师傅 Haiku 挑刺:编造证据?证据不够?不过就把你打回 synthesizer 重来(最多 2 次,靠 retry_count 计数)→ 过了就 writeback 存档 → 出门。你全程只是"数据",被这张图推着走完了每道工序。
薄业务 + 厚 toolkit:这户人家自己写的 vs 从楼下拿的 L3 业务自己写(薄) state.py · RcaState 字段 builder.py · 图怎么连 specialists/ · 填配置(各30行) prompts/ · 作业指导书 tools/ · 只读数据源 = State/Specialists/Prompts/DAG L2 toolkit 给的(厚) make_specialist_node 专家工厂 build_critic_router 三态路由 wrap_with_fallback 失败兜底 make_checkpointer 工作记忆 memory / cost / eval … import 即用,不重复造轮子
图注:业务只写左边四类东西,右边所有护栏能力都 import 自 toolkit——这就是"薄业务 + 厚 toolkit"。
L04

nodes/ — 6 个普通节点

每个节点是 async def node(state) -> dict,或返回这种函数的"工厂"(把依赖闭包进去,比如 build_recall_node(episodic))。逐个看角色:

triage规则版分诊,0 成本、不调 LLMtriage.py:17_classify 用关键词字典把问题归到 5 类,need_full_dag = alert_type != "HEALTH_QUERY"。体现"能用代码就别用 LLM"的省钱哲学
recall从"情景记忆"里捞相似历史故障 case(Day 09)。异常吞掉返回空——挂了也不影响主流程
rag检索 SOP/运维手册。用 4 个专家的发现拼查询语句
synthesizer综合节点,调 Sonnet(贵但强):把 4 专家 + 历史 + SOP + 上轮反馈拼成结论。外面包了 toolkit 的"证据不足就跳过 LLM"护栏(Day 08)
critic质检节点,调 Haiku(便宜):直接复用 toolkit 的 build_critic_for_rca()。Day 07 主角
writeback经验沉淀:先用 toolkit 的 should_writeback 守门(只有质检通过且信息充分才写),把这次 case 存回记忆库
省钱三档:triage 用规则(0 元)、critic 用 Haiku(便宜)、synthesizer 用 Sonnet(贵)。把贵模型只用在最难的一步,是全框架的成本纪律。Day 11 会看到 get_llm(tier) 怎么支撑这种分档。
L05

specialists/ — 先看数据结构 SpecialistConfig

4 个专家文件每个都只有约 30 行,因为它们都是"填一份配置 SpecialistConfig → 交给工厂 make_specialist_node"。先把这份配置的字段看清(specialists/factory.py:37):

@dataclass
class SpecialistConfig:                                       # factory.py:37
    state_field: str          # 结果写进 state 的哪个字段(如 'metric_finding')
    system_prompt: str        # system prompt 内容(业务从 prompts/*.md 读)
    fetch_fn: Callable[[dict], Awaitable[dict]]   # 拉证据的 async 函数
    fallback_finding: dict = field(default_factory=lambda: {  # 抓取/执行失败时的降级结果
        "health": "unknown", "confidence": "低",
        "evidence_ids": [], "hint": "证据获取失败"})
    model_tier: ModelTier = "sonnet"
    name: str = "specialist"
    cache_system: bool = False        # 打开 prompt 缓存(Day 07/11)
    # ─── 3 个可选自定义点(不填就用默认实现)───
    user_prompt_builder: UserPromptBuilder | None = None
    response_parser: ResponseParser | None = None
    finding_post_process: FindingPostProcessor | None = None
state_field
强制项。这个专家把结果写进黑板的哪一格(对应 L02 那 4 个独立 finding 字段之一)。
fetch_fn
强制项。一个 async 函数,负责先去拉证据;约定返回 {ok, data, error} 形状(Day 11 的 safe_tool_result 约定)。
fallback_finding
拉数据/推理失败时的降级结果。有默认值(health=unknown / confidence=低),业务可覆盖。
3 个 *_builder / parser / post_process
全部 = None,即可选钩子。不填就走工厂的默认实现;业务有特殊需求才覆盖。
💡 设计取舍①:为什么把 3 个自定义点做成"可选 = None"而不是"必填"或"写死"?这是"约定优于配置"的经典落地。90% 的专家用默认的"dump state + raw 为 JSON 喂给 LLM、再 JSON 解析回来"就够了——这些人一个钩子都不用填,配置极短。只有 10% 有特殊格式需求的专家(比如要自定义 prompt 拼法)才覆盖对应钩子。默认能跑、特殊能改,既保证了绝大多数专家的简洁,又留了逃生口。RCA 的 trace_id/service_name/user_question 三件套、risk-reviewer 的 pr_id/diff_summary 就是靠覆盖 user_prompt_builder 实现的。
SpecialistConfig:4 个强制项 + 3 个可选钩子 强制项(每个专家必填) state_field · 写哪格 system_prompt · 作业指导 fetch_fn · 怎么拉证据 fallback_finding · 失败降级 + model_tier / name 可选钩子(默认 None) user_prompt_builder response_parser finding_post_process 不填 → 用工厂默认实现
图注:填 4 个强制项就能跑;3 个可选钩子留给需要特殊格式的业务覆盖。约定优于配置。
L06

make_specialist_node — 工厂内部的四步走读

配置填好后交给工厂 make_specialist_node(cfg)factory.py:84)。它返回一个 async def specialist(state) -> dict 节点,内部就是一条"取数→喂 LLM→解析→写字段"的流水线,最后再包一层兜底。逐块读它的核心 inner_nodefactory.py:93):

async def inner_node(state: dict) -> dict:
    # ① 拉数据 · factory.py:95
    tool_result = await cfg.fetch_fn(state)
    if not tool_result.get("ok"):                     # 拉失败 → 早返回 fallback
        finding = dict(cfg.fallback_finding)
        finding["error"] = tool_result.get("error", "tool failed")
        return {cfg.state_field: finding}
    raw = tool_result["data"]
    # ② 调 LLM · factory.py:104
    llm = get_llm(cfg.model_tier, cache_system=cfg.cache_system)
    response = await llm.ainvoke([
        {"role": "system", "content": cfg.system_prompt},
        {"role": "user", "content": user_prompt_builder(state, raw)}])
    content = getattr(response, "content", str(response))
    # ③ 解析 + 后处理 · factory.py:114
    finding = response_parser(content)
    finding.setdefault("evidence_ids", [])            # 保证关键字段一定存在
    if cfg.finding_post_process:
        finding = cfg.finding_post_process(finding, raw)
    return {cfg.state_field: finding}

逐步翻译: 先调 fetch_fn 拉证据;拉失败(ok=False)就地返回 fallback,压根不浪费钱去调 LLM(这是第一处边界处理)。 用配置里的档位 get_llm(cfg.model_tier) 调 LLM,把 system prompt + 用户内容喂进去。 解析 LLM 输出成 finding,setdefault("evidence_ids", []) 保证这个关键字段一定存在(哪怕 LLM 没给),然后如果配了后处理钩子就再加工一下,最后写回 state_field 那一格。

再看第 ④ 步——工厂返回前把 inner_node 包进兜底闸门factory.py:125):

    return wrap_with_fallback(                         # factory.py:125
        inner_node,
        FallbackConfig(
            state_field=cfg.state_field,
            fallback_finding=dict(cfg.fallback_finding, fallback=True)))
make_specialist_node 内部流水线 ① fetch_fn 拉证据 ② get_llm 喂 prompt ③ parse+后处理 → finding 写 state_field 回黑板 ① ok=False → 直接写 fallback_finding(不调 LLM 省钱) ④ 整条流水线被 wrap_with_fallback 包住:任何异常都不外抛
图注:①②③是正常流水线,① 拉失败就地降级,④ 外层再包一道 fallback 兜住一切异常。
⚠️ 易错点:初学者常把"LLM 输出的 JSON"当成一定合法。response_parser 默认实现(factory.py:157)是三级容错:先 json.loads、再剥 markdown 代码块、都失败就返回 {"raw_text": content[:500], "_parse_warning": "LLM 输出非 JSON"}——返回一个带警告的 finding,而不是抛异常让图崩。跟 Day 04 dispatch 的解析套路一模一样。
L07

wrap_with_fallback — 双层兜底的第一道闸门

上一讲第 ④ 步那个 wrap_with_fallback 是 toolkit failsafe 的第一道闸门(Day 08 主角之一),代码很短(node_fallback.py:33):

def wrap_with_fallback(node_fn, config: FallbackConfig):     # node_fallback.py:33
    async def wrapped(state: dict) -> dict:
        try:
            return await node_fn(state)                       # 正常就原样返回
        except Exception as e:                                # node_fallback.py:55
            logger.log(config.log_level, "Node failed for state_field=%s: %s",
                       config.state_field, e, exc_info=True)
            return {config.state_field: dict(config.fallback_finding, error=str(e))}
    wrapped.__name__ = getattr(node_fn, "__name__", "wrapped_node")
    wrapped.__wrapped__ = node_fn
    return wrapped

大白话:把任意 async 节点函数包成"永远不抛异常"的版本。正常时原样返回;一旦内部抛任何异常,它 catch 住、打日志、返回一个带 errorfallback_finding。这样这个专家节点再怎么炸,也只是往黑板写一格"降级结果",绝不会让整张图崩掉

💡 设计取舍②:既然 inner_node 里 ① 已经处理了"拉数据失败",为什么外面还要再包一层 fallback?因为这是两种不同的失败。① 处理的是"预期内的失败"——fetch_fn 明确返回 ok=False(比如接口 404);而 wrap_with_fallback 兜的是"预期外的失败"——LLM 调用抛异常、解析器崩了、后处理钩子里有 bug、甚至你没想到的空指针。内层处理已知问题、外层兜住未知问题,这就是"双层兜底"。注意外层的 fallback 会额外标 fallback=True(factory.py:129),下游一看就知道"这是兜底出来的,别太当真"。
📝 举个例子:两种失败分别怎么走 场景 A(预期内):metric 专家的 get_metrics 接口超时 → 返回 {ok:False, error:"timeout"} → ① 就地返回 {"metric_finding":{"health":"unknown","confidence":"低","error":"timeout"}}没调 LLM,省钱

场景 B(预期外):LLM 返回一个诡异结构,后处理钩子里 KeyError 崩了 → 外层 wrap_with_fallback catch 住 → 返回 {"metric_finding":{"health":"unknown", "fallback":True, "error":"KeyError: ..."}},图继续跑。业务作者一行 try/except 都不用写。
L08

metric_sp.py — 一个真实专家有多"薄" + DEPRECATED 化石

前面读了配置和工厂,现在看真实的一个专家文件——specialists/metric_sp.py去掉 import 和读 prompt,实质就一个配置(这是完整文件,没有省略):

from ai_trust_toolkit.specialists import SpecialistConfig, make_specialist_node
from ..tools import get_metrics

_PROMPT = (Path(__file__).resolve().parents[2] / "prompts"
           / "metric-specialist-v1.md").read_text(encoding="utf-8")

metric_sp_node = make_specialist_node(                    # metric_sp.py:13
    SpecialistConfig(
        name="metric_sp",
        state_field="metric_finding",                     # 写黑板哪一格
        system_prompt=_PROMPT,                            # 从 prompts/ 读作业指导书
        fetch_fn=lambda state: get_metrics(               # 怎么拉证据
            service=state.get("service_name") or "unknown", window_sec=60),
        fallback_finding={"health": "unknown", "confidence": "低",
                          "evidence_ids": [], "hint": "指标获取失败"},
        model_tier="sonnet",
    ))

就这些。没有 try/except、没有 LLM 调用循环、没有 JSON 解析、没有并发处理——因为这些全在 L06/L07 那个工厂里。业务作者只回答 5 个问题:叫什么名、写哪格、用哪份 prompt、怎么拉数据、失败给什么降级值。这就是"专家为什么只有 30 行"的真相。其它三个专家(trace/deploy/log)结构一模一样,只是换 state_field / fetch_fn / prompt / tier。

📝 举个例子:metric 专家的一次工作(用真实字段值) 黑板上有 service_name="pay-svc"fetch_fnget_metrics(service="pay-svc", window_sec=60) 抓 60 秒指标(本地是 mock)→ 拼进 prompt 喂 Sonnet → 解析 → 回写 {"metric_finding":{"health":"degraded", "confidence":"中", "evidence_ids":["ev-M2"]}} 到黑板。若抓数据报错 → 自动降级返回 {"health":"unknown","confidence":"低","hint":"指标获取失败"},图不崩。

目录里的"活化石":specialists/_factory.py

你在 specialists/ 里会看到一个 _factory.py,打开一看头顶写着大大的 DEPRECATED(_factory.py:1):

"""DEPRECATED —— 已上移到 ai_trust_toolkit.specialists(toolkit v0.2.0)。
本文件保留仅为兼容旧 import 路径,未来 v0.3 删除。新代码请用:
    from ai_trust_toolkit.specialists import SpecialistConfig, make_specialist_node
"""
from ai_trust_toolkit.specialists import (  # noqa: F401  re-export
    SpecialistConfig, default_response_parser,
    default_user_prompt_builder, make_specialist_node)
这块化石记录了一段真实历史:专家工厂最初写在 sre-rca 业务里(就在这个 _factory.py)。等第二个 agent(risk-reviewer)也需要几乎一样的工厂时,它才被"上移"进 toolkit,旧文件改成 17 行的 re-export 空壳保持向后兼容。这就是 Day 06 要讲的"渐进抽象"——第二次重复出现才抽,且不删旧路径而是留空壳渐进迁移。看到 from ._factory import ...from ai_trust_toolkit.specialists import ... 指向同一批符号,你就见证了这次演进。
🍼 一句话复述专家之所以每个才 30 行,是因为"取数→喂 LLM→解析→写字段→失败降级→双层兜底"这套模板整个搬进了 toolkit 工厂,业务只填 5 个空;而那个 DEPRECATED 的 _factory.py 就是"模板从业务上移进 toolkit"的化石。
L09

tools / prompts / server·main / BOM

tools/ — 只读数据源

tools/observability.py 定义 get_trace_by_id / get_metrics / get_deployment_history / search_logs 四个工具。两个要点:每个都用 @safe_tool_result(fallback=...) 装饰——工具永不抛异常,失败返回约定的 {ok,data,error} 兜底(正好衔接 L06 工厂 ① 步读的那个 ok 字段,Day 11 细讲);还有 USE_REAL 开关:默认走 mock,生产接 SkyWalking/Prometheus。

prompts/*.md — 版本化的工程文档

真实目录里有 7 个 *-v1.mdtriage-v1 / trace/metric/deploy/log-specialist-v1 / synthesizer-v1 / critic-v1。它们不是干巴巴的 prompt,而是带教学说明的工程文档(正文 + 用户模板 + 设计说明 + Token 估算 + 变更记录)。

-v1 版本号是为了 A/B 测试和回滚——想试新 prompt 就加 -v2,评测对比后再切换,老版本随时能退回。这也是把 prompt 当"代码"严肃管理的体现。

server.py / main.py — 两个入口

server.py · build_router()

产出一个 APIRouter,能被平台 gov-agents-server 多 agent 同进程挂载。核心是(server.py:66):graph = build_rca_graph() 后,业务只写 _invoke / _stream,统一的鉴权/envelope/落库/指标全由 build_v1_router 自动装(Day 12)。

main.py · 独立 FastAPI

单独部署这个 agent 时的入口:一个 FastAPI + include_router + 一行接齐可观测性。

还有个宝藏端点 /agents/rca/graphserver.py:142):把每个节点的 role/model/purpose 以 JSON 返回——相当于这个 Agent 的"自我说明书",Day 14 会拿它当权威参照。

pyproject.toml — 依赖走 BOM,一行锁死

dependencies = [
    "ai-trust-toolkit-bom[core,supervisor,monitor]==0.6.0",
]
BOM = Bill of Materials(物料清单),借用自 Maven。它本身没有代码,只声明"一组互相兼容的库的精确版本"。agent 不直接 pin toolkit/langgraph/fastapi 各自版本,而是引一个 BOM——全平台升级只改 BOM 一处,所有 agent 自动对齐。[core,supervisor,monitor] 是"extras",按需勾选能力组。CI 里有专门闸门(Day 18)禁止 agent 绕过 BOM 直接 pin 子模块版本。
L10

今日小结 + 动手

🧠 第 1 周收官自测

  • 一个 Agent 的标准目录有哪些文件,各管什么?(state/builder/nodes/specialists/tools/prompts/server/main)
  • 读一个陌生 agent 先看哪个文件?(builder.py 的 StateGraph→add_edge→compile 段)
  • RcaState 里为什么 4 个 finding 独立、只有 retry_count 用累加 Reducer?(并行零冲突 vs 并发累加防覆盖)
  • 专家为什么每个才 30 行?(填 SpecialistConfig 4 个强制项,工厂封了取数/LLM/解析/写字段/双层兜底)
  • make_specialist_node 里的"双层兜底"是哪两层?(inner_node 内 ok=False 就地降级 + 外层 wrap_with_fallback 兜住任意异常)
  • _factory.py 的 DEPRECATED 说明了什么?(渐进抽象:第二次重复才上移,留空壳兼容)

✋ 动手:把结构落到真实文件

# 1. 看旗舰 agent 的完整结构
find apps/sre-rca-agent/sre_rca -type f -name "*.py" | sort
ls apps/sre-rca-agent/prompts/          # 数一下真的是 7 个 *-v1.md

# 2. 精读黑板的两种 Reducer
sed -n '15,60p' apps/sre-rca-agent/sre_rca/state.py

# 3. 精读图的装配(今天的核心)
sed -n '73,118p' apps/sre-rca-agent/sre_rca/builder.py

# 4. 读专家工厂的数据结构 + 四步流水线 + 双层兜底
sed -n '37,131p' packages/ai-trust-toolkit/src/ai_trust_toolkit/specialists/factory.py
sed -n '33,67p' packages/ai-trust-toolkit/src/ai_trust_toolkit/failsafe/node_fallback.py

# 5. 看一个专家有多"薄" + 那块 DEPRECATED 化石
cat apps/sre-rca-agent/sre_rca/specialists/metric_sp.py
cat apps/sre-rca-agent/sre_rca/specialists/_factory.py
第 2 周预告 · Day 06 起:地图和骨架都清楚了,接下来一整周钻进框架的灵魂——ai-trust-toolkit。Day 06 先总览它的 10 个模块和"渐进抽象"方法论(今天那块化石就是引子),然后逐个拆 Critic / 闸门 / 记忆 / 评测 / 成本。
← Day 04 Supervisor Day 06 · toolkit 总览 & 渐进抽象 →