← 全部示例
// 场景实例

事故复盘 DAG(并行 fan-out)

同时调查日志、部署和影响范围,再汇总成根因假设与事故复盘文档。

适合负责事故复盘的 SRE 与工程团队

01 使用情境

事故证据分散在不同位置。等一项调查完成才开始下一项会拖慢复盘;过早混合原始证据,又可能抹掉重要差异。

同一事故的三类视角

示例提供日志模式、部署关联和影响范围证据。

02 工作方式

一支分工明确、交接清楚的团队。

每个专职 Agent 只处理它需要的证据;互不依赖的工作同时进行,需要上游结果的工作则按顺序等待。

  1. 分别展开调查

    三个根调查任务同时开始。

    并行执行
    日志调查员

    从日志中提取模式。

    部署调查员

    核对事故与部署的关联。

    影响调查员

    分析影响范围。

  2. 形成根因假设

    等待三路证据全部返回后再推断。

    根因分析员

    调和三项调查,形成有证据支持的假设。

  3. 撰写复盘

    最后把假设与证据整理成可长期保存的产物。

    复盘撰写员

    生成最终文档,并写入临时文件。

03 最终结果

综合事故复盘文档

输出保留证据链,并显示示例运行时长与 token 成本信息。

  1. 01三项独立调查的证据
  2. 02根因假设
  3. 03最终事故复盘文档
  • 独立调查可以立即开始。
  • 综合步骤会等待证据完整。
  • 分析与对外表达由不同角色负责。
范围与限制
这个示例聚焦并行调查与综合;与合同审查示例不同,它刻意不演示强制失败或重试。
04 开发者实现

先运行,再查看具体实现。

上面是面向业务的解释;下面的命令、运行前提、API 和源码继续与真实仓库同步。

实现信息 OpenMultiAgent
场景实例 437 行

运行

在仓库的克隆里运行这个文件:

terminal
npx tsx packages/core/examples/cookbook/incident-postmortem-dag.ts
前置条件
  • ANTHROPIC_API_KEY env var must be set.

OMA 与 provider 无关——这个示例按上面的 key 编写,但你也可以用 OpenAI、Gemini、Groq 等任意 provider 运行。 全部 provider →

展开完整同步源码 · 437 行

完整示例,从固定的 Framework commit 同步。

cookbook/incident-postmortem-dag.ts
/**
* Incident Postmortem DAG with Parallel Fan-Out
*
* Demonstrates DAG task orchestration with three independent root tasks
* fanning out from t=0, then synthesizing into a final postmortem document.
*
* log-pattern-extractor |
* correlate-deploys |--> root-cause-hypothesizer --> postmortem-writer
* analyze-blast-radius |
* departures from that template:
* - No FORCE_FAIL / retry-demonstration machinery -- this example's
* theme is parallel fan-out + multi-source synthesis, not retry.
* - Three parallel root tasks (instead of two), so the parallelism
* check asserts that all three start within a 500ms window.
* - Final postmortem is also written to disk via os.tmpdir().
* - Token cost on claude-sonnet-4-6 is printed alongside token totals.
*
* Run:
* npx tsx packages/core/examples/cookbook/incident-postmortem-dag.ts
*
* Prerequisites:
* ANTHROPIC_API_KEY env var must be set.
*
* Local validation hatch:
* EXAMPLE_PROVIDER=openrouter switches every agent + the orchestrator
* defaults to a free OpenRouter model (openrouter/free)
* via the OpenAI-compatible adapter. The framework reads OPENAI_API_KEY
* and OPENAI_BASE_URL from env.
*/
 
import { OpenMultiAgent } from '../../src/index.js'
import type { AgentConfig, OrchestratorEvent } from '../../src/types.js'
import { readFileSync, writeFileSync } from 'fs'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
import { tmpdir } from 'os'
 
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
 
// ---------------------------------------------------------------------------
// Step 1: Agent configurations (5 agents)
// ---------------------------------------------------------------------------
 
const logPatternExtractorConfig: AgentConfig = {
name: 'log-pattern-extractor',
model: 'claude-sonnet-4-6',
provider: 'anthropic',
systemPrompt: `You are a log analysis specialist. Parse the provided raw backend logs and identify:
- Error clusters (group by error message / endpoint / service)
- The timestamp of the first regression (when error rate clearly departs from baseline)
- Noisy hosts/services/endpoints
Output a JSON object with this shape:
{
"first_regression_at": "ISO 8601 timestamp",
"error_clusters": [
{ "service": string, "endpoint": string, "error": string, "count": number, "first_seen": string, "last_seen": string }
],
"noisy_endpoints": [ { "endpoint": string, "error_count": number } ],
"notes": string
}
Output ONLY valid JSON, no other text.`,
maxTurns: 1,
temperature: 0.1,
}
 
const deployCorrelatorConfig: AgentConfig = {
name: 'deploy-correlator',
model: 'claude-sonnet-4-6',
provider: 'anthropic',
systemPrompt: `You are a deploy correlation analyst. Given a list of recent deploys and a window of incident symptoms, identify which deploy(s) most plausibly caused the symptoms based on timing and changed paths.
Output a JSON object:
{
"ranked_candidates": [
{
"sha": string,
"deployed_at": string,
"summary": string,
"confidence": "high" | "medium" | "low",
"rationale": string
}
],
"ruled_out": [ { "sha": string, "reason": string } ]
}
Output ONLY valid JSON, no other text.`,
maxTurns: 1,
temperature: 0.1,
}
 
const blastRadiusAnalystConfig: AgentConfig = {
name: 'blast-radius-analyst',
model: 'claude-sonnet-4-6',
provider: 'anthropic',
systemPrompt: `You are a blast radius analyst. From the provided logs, estimate user-facing impact.
Output a JSON object:
{
"affected_endpoints": [ { "endpoint": string, "method": string } ],
"approximate_failed_requests": number,
"approximate_total_requests_in_window": number,
"error_rate_pct": number,
"impact_window": { "start": string, "end": string },
"user_facing_severity": "low" | "medium" | "high" | "critical",
"notes": string
}
Output ONLY valid JSON, no other text.`,
maxTurns: 1,
temperature: 0.1,
}
 
const rootCauseHypothesizerConfig: AgentConfig = {
name: 'root-cause-hypothesizer',
model: 'claude-sonnet-4-6',
provider: 'anthropic',
systemPrompt: `You are a root cause analyst. Using the log patterns and deploy correlation from the previous tasks (available via shared memory), propose 2-3 ranked root cause hypotheses.
For each hypothesis, include:
- A short title
- Confidence (high / medium / low)
- Supporting evidence (cite specific log clusters and deploy SHAs)
- What you'd check next to confirm or refute it
Output in Markdown format, with hypotheses as numbered ## headings ranked from most to least likely.`,
maxTurns: 1,
temperature: 0.2,
}
 
const postmortemWriterConfig: AgentConfig = {
name: 'postmortem-writer',
model: 'claude-sonnet-4-6',
provider: 'anthropic',
systemPrompt: `You are a postmortem author. Synthesize a final incident postmortem in Markdown using the log patterns, deploy correlation, blast radius, and root cause hypotheses from the previous tasks (available via shared memory).
Use these top-level sections, in order:
- # Incident Postmortem
- ## Summary
- ## Timeline
- ## Impact
- ## Contributing Factors
- ## Action Items
Keep it tight and factual. Cite specific timestamps, endpoints, and deploy SHAs where they support a claim. Action items should be concrete and assignable.`,
maxTurns: 1,
temperature: 0.2,
}
 
// ---------------------------------------------------------------------------
// Step 2: Task configurations (5 tasks)
// ---------------------------------------------------------------------------
 
interface TaskConfig {
title: string
description: string
assignee?: string
dependsOn?: readonly string[]
maxRetries?: number
retryDelayMs?: number
retryBackoff?: number
}
 
// Task configs will be created after reading fixture files (see Step 6b)
 
// ---------------------------------------------------------------------------
// Step 3: Progress tracking
// ---------------------------------------------------------------------------
 
interface TaskTiming {
startTime: number
endTime?: number
}
 
const taskTimings = new Map<string, TaskTiming>()
const taskStartTimes = new Map<string, number>()
 
function handleProgress(event: OrchestratorEvent): void {
const ts = new Date().toISOString()
 
switch (event.type) {
case 'task_start': {
const task = event.data as { id: string; title: string } | undefined
if (task) {
const now = Date.now()
taskTimings.set(task.id, { startTime: now })
taskStartTimes.set(task.title, now) // Use title as key for easier verification
console.log(`[${ts}] task_start: ${task.title}`)
}
break
}
 
case 'task_complete': {
const task = event.data as { id: string; title: string } | undefined
if (task) {
const timing = taskTimings.get(task.id)
if (timing) {
timing.endTime = Date.now()
const duration = timing.endTime - timing.startTime
console.log(`[${ts}] task_complete: ${task.title} (${duration}ms)`)
}
}
break
}
 
case 'task_retry':
console.log(`[${ts}] task_retry:`, JSON.stringify(event.data))
break
 
case 'task_skipped':
console.log(`[${ts}] task_skipped: ${event.task}`)
break
 
case 'agent_start':
if (event.agent) {
console.log(`[${ts}] agent_start: ${event.agent}`)
}
break
 
case 'agent_complete':
if (event.agent) {
console.log(`[${ts}] agent_complete: ${event.agent}`)
}
break
 
case 'message':
if (typeof event.data === 'string') {
console.log(`[${ts}] message: ${event.data}`)
}
break
 
case 'error':
console.log(`[${ts}] error:`, JSON.stringify(event.data))
break
 
default:
break
}
}
 
// ---------------------------------------------------------------------------
// Step 4: Parallelism verification
// ---------------------------------------------------------------------------
 
function verifyParallelism(): void {
const t1 = taskStartTimes.get('extract-log-patterns')
const t2 = taskStartTimes.get('correlate-deploys')
const t3 = taskStartTimes.get('analyze-blast-radius')
 
if (t1 !== undefined && t2 !== undefined && t3 !== undefined) {
const spread = Math.max(t1, t2, t3) - Math.min(t1, t2, t3)
console.log('\n=== Parallelism Check ===')
console.log(`Task 1 (extract-log-patterns) start: ${t1}`)
console.log(`Task 2 (correlate-deploys) start: ${t2}`)
console.log(`Task 3 (analyze-blast-radius) start: ${t3}`)
console.log(`Spread (max - min): ${spread}ms`)
 
if (spread >= 500) {
console.error(
`ASSERTION FAILED: Tasks 1, 2, 3 start times spread by ${spread}ms (>= 500ms). ` +
`Expected parallel execution from t=0.`,
)
process.exit(1)
}
console.log(`Parallel execution (< 500ms): YES`)
console.log('========================\n')
} else {
console.log('\n=== Parallelism Check: Unable to verify (missing timing data) ===\n')
}
}
 
// ---------------------------------------------------------------------------
// Step 5: Orchestrator and Team configuration
// ---------------------------------------------------------------------------
 
// Local validation hatch -- flip every agent's provider/model to a free
// OpenRouter model when EXAMPLE_PROVIDER=openrouter. The committed
// AgentConfig values above remain anthropic / claude-sonnet-4-6.
const useOpenRouter = process.env.EXAMPLE_PROVIDER === 'openrouter'
const OPENROUTER_MODEL = 'openrouter/free'
 
const agentConfigs: AgentConfig[] = [
logPatternExtractorConfig,
deployCorrelatorConfig,
blastRadiusAnalystConfig,
rootCauseHypothesizerConfig,
postmortemWriterConfig,
]
 
const effectiveAgents: AgentConfig[] = useOpenRouter
? agentConfigs.map((c) => ({ ...c, provider: 'openai', model: OPENROUTER_MODEL }))
: agentConfigs
 
const orchestrator = new OpenMultiAgent({
defaultModel: useOpenRouter ? OPENROUTER_MODEL : 'claude-sonnet-4-6',
defaultProvider: useOpenRouter ? 'openai' : 'anthropic',
onProgress: handleProgress,
})
 
const team = orchestrator.createTeam('incident-postmortem-team', {
name: 'incident-postmortem-team',
agents: effectiveAgents,
sharedMemory: true,
})
 
console.log(
`[mode] provider=${useOpenRouter ? 'openai' : 'anthropic'} ` +
`model=${useOpenRouter ? OPENROUTER_MODEL : 'claude-sonnet-4-6'}`,
)
 
// ---------------------------------------------------------------------------
// Step 6: Read fixture files
// ---------------------------------------------------------------------------
 
const logsText = readFileSync(
join(__dirname, '..', 'fixtures', 'incident-logs.txt'),
'utf-8',
)
 
const deploysText = readFileSync(
join(__dirname, '..', 'fixtures', 'incident-deploys.json'),
'utf-8',
)
 
// ---------------------------------------------------------------------------
// Step 6b: Create task configs with fixture content embedded
// ---------------------------------------------------------------------------
 
const taskConfigs: TaskConfig[] = [
{
title: 'extract-log-patterns',
description: `Parse the following backend logs and extract structured error patterns.\n\n=== LOGS ===\n${logsText}\n=== END LOGS ===\n\nOutput only valid JSON.`,
assignee: 'log-pattern-extractor',
},
{
title: 'correlate-deploys',
description: `Given the recent deploys below, identify which deploy(s) most likely caused the incident symptoms based on timing and changed paths. Symptoms: a sharp regression starting around 14:12 UTC on 2026-04-22, manifesting as 5xx errors. Use the timing alone for now; later tasks will cross-check against logs.\n\n=== DEPLOYS ===\n${deploysText}\n=== END DEPLOYS ===\n\nOutput only valid JSON.`,
assignee: 'deploy-correlator',
},
{
title: 'analyze-blast-radius',
description: `Estimate user-facing impact (affected endpoints, approximate failed request count, error rate, severity) from the following logs.\n\n=== LOGS ===\n${logsText}\n=== END LOGS ===\n\nOutput only valid JSON.`,
assignee: 'blast-radius-analyst',
},
{
title: 'hypothesize-cause',
description: 'Propose 2-3 ranked root cause hypotheses for the incident, using the log patterns and deploy correlation from the previous tasks. Cite specific log clusters and deploy SHAs.',
assignee: 'root-cause-hypothesizer',
dependsOn: ['extract-log-patterns', 'correlate-deploys'],
maxRetries: 2,
retryDelayMs: 500,
retryBackoff: 2,
},
{
title: 'write-postmortem',
description: 'Write the final incident postmortem in Markdown, synthesizing the log patterns, deploy correlation, blast radius, and root cause hypotheses from the previous tasks. Use sections: Summary, Timeline, Impact, Contributing Factors, Action Items.',
assignee: 'postmortem-writer',
dependsOn: ['extract-log-patterns', 'correlate-deploys', 'analyze-blast-radius', 'hypothesize-cause'],
maxRetries: 2,
retryDelayMs: 500,
retryBackoff: 2,
},
]
 
console.log('Incident Postmortem DAG with Parallel Fan-Out')
console.log('='.repeat(60))
console.log(`\nLogs: ${logsText.split('\n').length} lines`)
console.log(`Deploys: ${(JSON.parse(deploysText) as unknown[]).length} entries\n`)
 
// ---------------------------------------------------------------------------
// Step 7: Run the DAG
// ---------------------------------------------------------------------------
 
async function main(): Promise<void> {
try {
console.log('[Orchestration] Starting DAG execution...\n')
 
const result = await orchestrator.runTasks(team, taskConfigs)
 
// Verify parallelism
verifyParallelism()
 
console.log('\n' + '='.repeat(60))
console.log('FINAL RESULT')
console.log('='.repeat(60))
console.log(`Success: ${result.success}`)
console.log(`Total tasks: ${result.tasks?.length ?? taskConfigs.length}`)
console.log(`Total token usage: ${result.totalTokenUsage.input_tokens} input, ${result.totalTokenUsage.output_tokens} output`)
 
// Cost estimate on claude-sonnet-4-6 -- even when running on OpenRouter,
// this represents what the same token volume would cost on Anthropic.
const inputCost = (result.totalTokenUsage.input_tokens / 1_000_000) * 3
const outputCost = (result.totalTokenUsage.output_tokens / 1_000_000) * 15
console.log(
`Estimated cost on claude-sonnet-4-6: ~$${(inputCost + outputCost).toFixed(4)} USD ` +
`(input @ $3/M, output @ $15/M)`,
)
 
// Output each agent's result
console.log('\n' + '='.repeat(60))
console.log('AGENT RESULTS')
console.log('='.repeat(60))
for (const [agentName, agentResult] of result.agentResults) {
console.log(`\n--- ${agentName} ---`)
console.log(agentResult.success ? agentResult.output : `[FAILED] ${agentResult.output}`)
}
 
if (result.success) {
console.log('\n--- Final Postmortem ---')
const writerResult = result.agentResults.get('postmortem-writer')
const writerOutput = writerResult?.output ?? ''
if (writerOutput) {
console.log(writerOutput)
const outPath = join(tmpdir(), 'incident-postmortem.md')
writeFileSync(outPath, writerOutput, 'utf-8')
console.log(`\nPostmortem written to: ${outPath}`)
}
} else {
console.log('\nWorkflow failed.')
for (const task of result.tasks ?? []) {
if (task.status === 'failed') {
console.log(` - ${task.title}: failed`)
}
}
}
 
console.log('\n' + '='.repeat(60))
console.log('Done.')
} catch (error) {
console.error('Fatal error:', error)
process.exit(1)
}
}
 
main()
在 GitHub 查看 / 编辑
// 企业服务

要把它用到生产环境?

open-multi-agent 采用 MIT 许可、可自行免费运行。当你需要在期限内交付、集成,或获得支持时,元定义科技(YuanASI)提供商业交付与支持。

// 直接联系

把 Open Multi-Agent 用进真实业务

联系框架作者本人,帮你梳理 AI 落地目标、让 AI 真正与业务结合

可提供的工程服务
S-01

AI Agent 定制开发

业务梳理、Agent 设计、Prompt 评估、生产部署、私有化与持续支持。

S-02

多智能体系统集成

多 Agent 架构编排、RAG、CRM / ERP / API 对接、性能与稳定性调优。

S-03

企业 AI 咨询

AI 场景评估、技术选型、POC、ROI 估算与落地路线规划。