Chainlink fix-flaky-tests 技能:LogAnalyzer 子代理配置与 Flaky 测试日志分析协议

原创2026-09-15 10:38:361,612 阅读
文章标签:区块链Web3后端

Chainlink fix-flaky-tests 技能:LogAnalyzer 子代理配置与 Flaky 测试日志分析协议

在 Chainlink 仓库中,tools/test 目录下的测试诊断工具(testrig 封装的 diagnose 模式)会在排查 flaky 测试时产出结构化的日志目录(report.json、iteration-n.log.jsonl、logs/pkg_TestName_iter-n.log 等)。本文聚焦于 log-analyzer-subagent.md 所定义的 LogAnalyzer 子代理协议:如何以严格的初始化参数(系统提示词、只读工具集、零温度)派生一个“无头日志解析器”,让它从 chainlink 节点日志与测试日志中自底向上地诊断测试失败原因,并以固定 JSON 结构返回证据化结论。读完后你可以完整复刻这套子代理配置,理解它在 fix-flaky-tests 技能 调查循环中的触发时机、输入输出契约与配套日志目录约定。

1. 背景:LogAnalyzer 在 fix-flaky-tests 技能中的定位

Chainlink 的 CI 中存在一个针对 flaky、race、timeout 类测试问题的 Agent 技能 SKILL.md,其核心循环是:用 make test ARGS="diagnose ..." 反复运行测试 → 读取结果目录 → 形成假设 → 修复 → 再验证。在该技能的 <sub_agent_protocol> 部分明确规定了三个子代理的触发条件:

  • 当需要阅读 logs/ 目录或 iteration-n.log.jsonl 时,派生 LogAnalyzer,配置依据即 log-analyzer-subagent.md;
  • 当需要检查 CI 失败(GitHub Actions 日志与 artifacts)时,派生 GithubFailureAnalyzer,见 github-failure-analyzer.md;
  • 当需要与 JIRA 交互(领票、评论、流转)时,派生 JiraManager,见 jira-mananger-subagent.md。

LogAnalyzer 是其中唯一面向“本地 diagnose 运行产物”的分析器:它不碰网络、不碰外部 API,只处理 harness 落盘的日志文件。这正是 README.md 中描述的 diagnose 模式的价值——它捕获 go test -json 输出并生成机器可读报告,LogAnalyzer 则负责把“海量日志”压缩为“带证据的失败原因候选”。

2. diagnose 产出的日志目录结构

LogAnalyzer 的输入是 SKILL.md 中 <logs_structure> 所约定的结果目录([resultsDir]),理解这一结构是理解子代理职责的前提:

[resultsDir]/
|-- iteration-n.log.jsonl     # 仅在需要时读取,完整输出
|-- postgres-state-n.md       # 出现 DB 错误/挂起时读取,数据库最终状态
|-- report.json               # 读取摘要;可用 jq .run 提取运行参数
|-- report.csv                # 禁止读取
|-- logs/
|---- pkg_TestName_iter-n.log # 针对具体测试失败时读取

几个值得注意的约定:

  • report.json 是首选入口:它承载运行摘要,编排代理应先读它定位问题,而不是直接翻完整 JSONL;
  • iteration-n.log.jsonl 是“全量输出”,文件大、噪声多,因此 SKILL.md 把它划入“交由 LogAnalyzer 子代理读取”的范畴,避免主代理被大上下文淹没;
  • postgres-state-n.md 针对 Chainlink 特有的依赖——测试共享/重建 Postgres,数据库错误或挂起是 flake 的重要来源,harness 会落盘 DB 最终状态供诊断;
  • report.csv 被显式标记为 DO NOT READ,说明结果目录中存在面向人/机器两种视图,协议必须明确取舍。

3. 三项精确的初始化参数

文档的第一部分以 You MUST configure the sub-agent with these exact initialization parameters 开头,强调子代理必须按以下三个参数精确初始化,不允许自由发挥:

3.1 系统提示词(System Prompt)

原文要求使用如下系统提示词(方括号内为需要注入的动态内容——即“我们正在调查的测试/原因”):

"You are a headless, read-only log parser. Your sole purpose is to read Go test logs from the end up. Each log file contains logs from chainlink nodes, plus test-specific logs. Read the logs and construct possible reasons why the test [input reason we're investigating]. You do not converse. You output raw JSON and nothing else."

逐句拆解其设计意图:

  • "headless, read-only log parser":限定角色为无交互的解析器,杜绝子代理与用户对话、发散执行任务;
  • "read Go test logs from the end up":从文件尾部往前读。这与 fixing-flaky-tests.md 中“先翻日志尾部找堆栈”的人工排查习惯一致——Go 测试失败信息(panic、fatal error、FAIL 行)通常出现在日志末尾;
  • "Each log file contains logs from chainlink nodes, plus test-specific logs":这是 Chainlink 仓库特有的先验知识。Chainlink 节点本身是区块链 oracle,测试会拉起真实的 chainlink 节点进程(见 core/README.md),因此日志中混杂了节点运行日志与测试框架日志,子代理需要区分二者;
  • "construct possible reasons why the test [input reason we're investigating]":调查目标由调用方注入,保证每次派生的分析都聚焦于同一个待验证假设;
  • "You do not converse. You output raw JSON and nothing else.":禁止寒暄与解释性文字,输出必须可被上游程序化消费。

3.2 允许的工具(Allowed Tools)

File read/grep tools ONLY. Revoke all execution, write, and web search capabilities.

子代理只保留文件读取与 grep 搜索两类能力,并显式吊销执行(shell)、写入和联网搜索权限。这一约束保证了:

  1. 分析过程绝不产生副作用——不会误改日志、误触发命令;
  2. 子代理的“视野”被钳制在 diagnose 结果目录内,无法通过联网检索编造证据,所有结论必须锚定在具体日志行上。

3.3 温度(Temperature)

Temperature: 0.0

零温度意味着确定性输出:给定同样的日志与同样的调查目标,子代理应给出同样结构、同样风格的诊断,不引入随机性,便于在自动化循环中复现与比对。

4. 输出契约:固定 JSON 结构

文档第二部分定义了子代理必须输出的 JSON 结构,且明确要求:不得包裹在 markdown 代码块中、不得附加任何解释文字,输出纯原始 JSON:

{
  "logs_read": ["log_path_1.log", "log_path_2.log"],
  "failure_diagnosis": [
    {
      "possible_reason": "explanation",
      "evidence": "specific logs/log lines"
    }
  ]
}

字段语义:

字段 含义 约束
logs_read 子代理实际读取过的日志文件路径列表 供编排代理审计“证据来源”,确认分析没有越过允许的文件范围
failure_diagnosis 失败原因候选列表(数组而非单值) 允许多个假设并存,由后续调查循环逐个验证
failure_diagnosis[].possible_reason 一个可能原因的说明 对应 SKILL.md 中“先输出假设(Output hypothesis first)”的流程
failure_diagnosis[].evidence 支撑该原因的具体日志行 强制“结论必须挂证据”,防止 LLM 凭空归因

这个契约有两个工程要点值得强调:

  • 多候选而非单一结论:与 fixing-flaky-tests.md 的方法论呼应——“如何复现往往就是它为什么 flake 的最好线索”,在证据不足时宁可给出多个带证据的假设,也不强行收敛;
  • logs_read 作为可审计痕迹:编排代理拿到 JSON 后,可以核对子代理确实只读了 logs/、iteration-n.log.jsonl 中约定的文件,这与该技能的 <possible_execution_issues> 一类约束共同构成“受控分析”的闭环。

5. 与兄弟子代理的对比:同一协议族的三种形态

把 LogAnalyzer 与同目录下的另外两个子代理放在一起看,可以更清楚地看出这套协议族的设计原则:

子代理 场景 允许的工具 输出 JSON 的标志性字段
LogAnalyzer 阅读本地 logs/ 或 iteration-n.log.jsonl 仅文件读取/grep logs_read、failure_diagnosis
GithubFailureAnalyzer 检查 CI 失败,拉取日志与 artifacts Bash(gh, grep, find, wc, cat, sed)、gh、Write(*) urls_read、artifact_errors、artifact_locations、failure_diagnosis
JiraManager 通过 Atlassian MCP 读/写 JIRA 工单 仅指定的 mcp__atlassian__* 工具、LSP、只读 Bash/Read 操作回执 {operation, jira_key, success, details, slim_record}

三者共同的骨架是:精确的系统提示词 + 白名单工具 + Temperature 0.0 + 裸 JSON 输出。差异只在于任务域(本地日志 / GitHub Actions / JIRA)与对应的最小权限集合。值得注意的是 github-failure-analyzer.md 还规定了编排侧的后处理义务——若 artifact_errors 非空,必须把每个错误上报给用户并在其回复前暂停调查循环;LogAnalyzer 虽然没写这条,但其 logs_read 字段承担了类似的“让编排代理可复核”的职责。

6. 日志的生产端:tools/test 诊断 harness

LogAnalyzer 消费的文件不是凭空出现的,它们由 tools/test 下的 harness 生产。README.md 说明了使用方式:

# 从仓库根目录执行
make test ARGS="diagnose --iterations 50 -- --failfast ./path/to/package"
  • make test 会把 harness 构建为 tools/test/.bin/test(gitignored)并透传参数;
  • diagnose 模式负责:每轮迭代的 Postgres 准备、go test -json 捕获、以及结果目录(report.json/iteration-n.log.jsonl/logs/)的落盘;
  • 为什么需要包装一层而不是直接 go test:README 解释了 go test 无法声明“一次性的全局前置步骤”(例如创建 Postgres DB),Chainlink 的测试又普遍依赖共享/重建的 Postgres,因此需要一个轻量 wrapper。

从源码看,harness 是一个“薄的 testrig 消费者”:main.go 中 main() 仅调用 testrig.Run,并通过 dbProvider 为每个 diagnose worker 提供一个准备就绪的 Postgres(--database-url 可复用外部库,否则为每个 worker 提供带快照的临时容器以支持快速 Reset)。report.json 与 logs/ 目录正是这一运行在失败时的诊断产物——也就是 LogAnalyzer 的输入集。SKILL.md 中还规定了诊断迭代次数与置信度的对应关系(如 30 次迭代约 10% 漏检率、300 次约 1%),供编排代理在“升级验证强度”时选择。

7. 实操串联:从一次 diagnose 失败到 LogAnalyzer 诊断

把 SKILL.md 的调查循环(<loop>)与本文档组合起来,一次典型流程是:

  1. 规划运行:按 <diagnose-iterations> 表选择 profile(Quick/Standard/Deep),执行 make test ARGS="diagnose --iterations 30 -- --race --run '^TestX$' ./core/somepkg"(预计超过 2 分钟时后台执行,禁止轮询,等待 report.json 就绪);
  2. 读摘要:主代理先读 report.json(可用 jq .run 提取运行参数)确认失败信号,必要时读 postgres-state-n.md 排除 DB 挂起;
  3. 派生 LogAnalyzer:当需要深入 logs/pkg_TestName_iter-n.log 或全量 iteration-n.log.jsonl 时,按 log-analyzer-subagent.md 的三项参数(系统提示词 + 只读工具 + 温度 0)派生子代理,并在提示词中注入“正在调查的测试与假设”;
  4. 消费 JSON:主代理拿到 logs_read 与 failure_diagnosis 后,按 SKILL.md 的要求“先输出假设、展示 diff、不做抽象化修复”,对最可能的原因实施修复;
  5. 记录与复验:把本次假设/实验/结果追加到 diagnose-attempted-fixes-[test/package]-[flake/broken/timeout/slow].jsonl(SKILL.md <loop> 第 9 步定义的 JSON 格式),代码变更后至少以 Standard profile 重跑 diagnose,确认 flake 消失,并跑 golangci-lint 保证没有引入新的 lint 问题。

8. 小结:一份“最小权限 + 强契约”的子代理范例

log-analyzer-subagent.md 虽然只有十余行,但它示范了让 LLM 子代理进入自动化工程循环所需的四要素:固定角色提示词(无头、只读、从尾读到首)、最小工具白名单(仅文件读/grep,吊销执行、写入与联网)、确定性采样参数(temperature 0.0)、可解析且带证据的输出契约(logs_read + failure_diagnosis)。它与 github-failure-analyzer.md、jira-mananger-subagent.md 共同构成 Chainlink fix-flaky-tests 技能的受控子代理族,配合 tools/test harness 的结构化诊断产物,把“翻日志找 flake 根因”这件高上下文开销的工作,收敛成一次可审计、可复现的 JSON 交换。对在其他 Go 仓库搭建同类测试诊断流水线的读者,这份协议可以直接作为模板:按你的日志目录结构调整 <logs_structure> 约定,按你的日志形态改写系统提示词中的领域先验,保留“只读 + 零温度 + 证据化 JSON”的骨架即可。

登录后查看全文
chainlink