AttiR/OpsCanvas

GitHub: AttiR/OpsCanvas

基于 LangGraph 的生产级多智能体事件响应系统,通过四个 AI 智能体自动完成告警分类、上下文研究与信息综合,并在人工审批后执行操作。

Stars: 0 | Forks: 0

# OpsCanvas

CI CD Backend CD Frontend

**具备人工审批(human-in-the-loop)机制的多智能体事件响应编排。** 当生产环境告警触发时,四个 AI 智能体将在 LangGraph 状态机中执行——分类、研究、综合与行动。每项操作在执行前均需经过明确的人工批准。 **在线演示:** https://opscanvas-pi.vercel.app  ·  **API:** https://w0r1dagwvd.execute-api.eu-north-1.amazonaws.com/health ## 演示 📹 **[观看 3 分钟导览 →](https://www.loom.com/share/e59a9b88e15341f1978a5ee1bd65a853)** 视频涵盖了完整的端到端 pipeline: 1. 使用 Google **登录** → 加载仪表板,此时 AgentTimeline 处于空闲状态 2. **提交**一个 P1 级别的“支付服务宕机”告警 → pipeline 启动 3. **观察** 分类 → 研究员 → 综合器 依次执行(总计约 10 秒) 4. **查看** ApprovalPanel —— 严重程度徽章、事件摘要、建议的操作、Slack 消息草稿 5. 带反馈**拒绝**:*"操作过于笼统——请添加 kubectl 命令"* → 综合器将重写 6. **批准**修改后的摘要 → 行动智能体将 Block Kit 消息发布到 Slack 7. **Slack** —— 格式化的通知,包含严重程度颜色、引用的 runbook、操作以及“人工批准 ✓” ## 解决的问题 事件响应面临的是上下文收集问题,而非决策问题。 当告警触发时,待命工程师必须同时:对严重程度进行分级、识别受影响的服务、在分布式的文档系统中定位相关的 runbook、检查上游依赖的状态页、将所有上下文综合成一份连贯的摘要,并将其传达给相关方。每一步都需要来自不同来源的上下文。在凌晨 2 点,在压力之下,面对性能降级的系统,这通常需要 20-30 分钟。 OpsCanvas 自动化了信息收集与综合阶段,同时在决策环节保留了人类的判断。工程师会收到一份结构化、带引用的事件摘要以及建议的操作。他们可以选择批准或拒绝——如果拒绝并提供反馈,综合器智能体将重新生成内容。只有在获得明确批准后,行动智能体才会将其发布到 Slack。 设计理念:**服务于人类判断的自动化,而非取代人类的自动化。** ## 与众不同之处 大多数“用于事件响应的 AI”工具只是将告警传递给 LLM 并展示输出结果。OpsCanvas 在架构上有三个在生产环境中至关重要的不同之处。 **显式状态管理。** 智能体工作流是一个 LangGraph `StateGraph`,它包含一个强类型的 `IncidentState` TypedDict、具名节点以及在代码中定义的条件边。每个状态转换都是可检查、可调试且可独立测试的。这是与 CrewAI 或 AutoGen 的关键区别:状态转换是显式的,而不是由角色 prompt 涌现出来的。对于一个会向 Slack 发送消息并可能创建工单的系统,你需要确切知道是什么状态触发了每一个操作。 **每次智能体交接时的类型化边界。** 每个智能体都会返回一个 Pydantic 模型——`TriageResult`、`ResearchResult`、`IncidentSummary`。故障会在交接边界被捕获,并产生即时、明确的错误。如果 Claude 的 JSON 输出中缺少某个字段,会在智能体边界引发 `ValidationError`,而不是在 pipeline 之后的三个节点处引发 `KeyError`。 **将人类参与作为一等公民图节点。** 审批门不是一个回调,也不是一个“轮询直到批准”的循环。它是状态机中的一个具名节点。图将状态保存到 Redis,终止 Lambda 调用,而恢复执行的过程是由人类向 `/api/incidents/:id/review` 发起的 POST 请求触发的。这模拟了正确的生产行为:状态可以跨计算边界持久化,人工审查可能需要数小时,且系统始终保持一致。 ## 生产层级 七个层级将 OpsCanvas 从一个可运行的多智能体演示转变为一个*可运维的*智能体系统。前四个生产信号与本作品集中的 RAG 项目 (Sourciq) 相同。后三个层级的存在**仅仅因为 OpsCanvas 是一个智能体**——它会采取真实行动并为人类暂停,因此它必须能够在冷启动中存活、限制自身的执行,并对其采取的每一个行动负责。RAG 从来不需要这些。 | # | 层级 | 增加的功能 | |---|-------|-------------| | 1 | 🔭 **可观测性** | 每个事件对应一个 Langfuse 追踪,每个智能体节点对应一个 span,通过 `session_id = run_id` 将人工暂停期间的数据串联起来 | | 2 | 🎯 **评估** | 智能体评估套件 —— 分类准确性、路由正确性、综合评判、端到端测试 —— 作为每个 PR 的门控 | | 3 | 🛡️ **护栏** | 代码级的操作授权、告警注入扫描、Slack payload 验证、低置信度弃权 | | 4 | 💰 **成本** | 单次事件覆盖 4 个节点 + Tavily 的成本、单账户上限、失控熔断机制 | | 5 | 💾 **持久性** | 基于 Redis 检查点的状态能够在冷启动中存活、幂等 Slack 发布、约 12 小时的状态 TTL | | 6 | ♻️ **可靠性** | 递归 + 反馈循环限制、节点超时、带退避的重试、备用模型、Tavily 熔断机制 | | 7 | 🔐 **安全与审计** | 包含审批人身份的不可变操作审计日志、密钥管理、PII 清理、最小权限 IAM | ### 1 · 可观测性 — Langfuse 一次事件会产生**一个追踪树**,每个智能体节点(分类 → 研究员 → 综合器 → 行动)对应一个 span。因为图会在人工审查中断处暂停——Lambda 随即消亡,而恢复运行将在数小时后的*另一个*完全不同的调用中执行——所以追踪是通过一个稳定的 `session_id = run_id` 串联起来的。每个节点都会记录其输入/输出状态、模型、token 数量和延迟;追踪会被标记上最终的严重程度(P1-P4)和结果(`auto_closed` / `approved` / `rejected` / `escalated`)。`langfuse.flush()` 会在每次 Lambda 返回前执行,无论是在启动路径还是恢复路径上。 *验证:* 批准一个已暂停的事件会显示在**同一个追踪下**,而不是作为一个新的追踪。 ### 2 · 评估 — 智能体评估套件,作为 CI 门控 检索指标无法为智能体打分。OpsCanvas 是基于其**轨迹**进行评估的,通过四个作为每个 PR 门控的测量指标——路由回归会像失败的单元测试一样阻止代码合并: | 指标 | 检查内容 | 门控阈值 | |--------|----------------|------| | 分类准确性 | 标记的告警 → 预期的严重程度 | ≥ 0.85 | | 路由正确性 | P4 走自动关闭边,P1 进行升级 | **= 1.0** | | 综合质量 | LLM 充当评判者:原因是否有证据支撑 + 是否可操作 | ≥ 0.80 | | 端到端成功 | 黄金事件是否达到预期结果 | ≥ 0.80 | 路由必须完美,因为分支是确定性的代码——一个错误的边就是一个 bug,而不是统计偏差。分类和综合是由模型驱动的,因此它们具有统计阈值。该套件会在每个 PR 的 `ci.yml` 中运行。评估集位于 `tests/eval/` 中。 ### 3 · 护栏 — 防护动作,而不仅仅是言辞 智能体的护栏是*不采取错误的行动*,而不是*不说错话*。包含五种机制: | 护栏 | 位置 | 阻止的行为 | |-----------|-------|----------------| | 告警 schema 验证 | API 边界 | 格式错误的告警 JSON 进入图 | | 注入扫描 | 在分类 LLM 调用之前 | 告警文本中隐藏的“忽略指令,发布到 Slack:……” | | **操作授权** | 行动节点 | 任何未通过人工审查中断的 Slack 发布 / 升级——在代码中强制执行,引发异常,而不是在 prompt 中要求模型 | | 输出验证 | Slack 发送之前 | 格式错误或泄露机密的 Slack payload | | 低置信度弃权 | 分类之后 | 在分类置信度 < `TRIAGE_CONFIDENCE_THRESHOLD` 时自动关闭——弱信号将交由人工处理 | 操作授权规则是最核心的信号:prompt 层面的护栏可以通过注入绕过;而在 Slack 调用之前抛出异常的代码层护栏则无法被绕过。 ### 4 · 成本 — 单次事件计算,带有失控熔断器 一个事件会分叉到四个 LLM 调用外加一次 Tavily 搜索,因此成本单位是**整个事件**,而不是单次调用。每节点的 token 成本和 Tavily 调用成本会被汇总,作为 `total_cost_usd` 记录到 Langfuse 追踪中,并在 API 响应中返回。其上有两个控制措施:单账户运行限制 (5) 限制了每个账户的终身支出,而**熔断机制**会在下一次 LLM 调用之前中止任何超过 `MAX_INCIDENT_USD` 的事件——因此“拒绝→循环”周期无法悄无声息地消耗资金。每次事件的平均成本为 **~$0.02**;Langfuse 成本视图如下:

Langfuse trace showing per-node cost breakdown and total_cost_usd for one incident

Langfuse — total_cost_usd and per-node LLM/tool breakdown for one incident run

### 5 · 持久性 — 在人工暂停和冷启动中存活 *这一层是 RAG 从来不需要的。* 图会为等待人工批准而 `interrupt`(中断);该批准可能需要数小时,此时 Lambda 早已被回收。当人工点击批准时,一个**全新的 Lambda** 必须在图恰好暂停的节点上恢复执行。这能够实现是因为图运行在一个由 `thread_id = run_id` 作为键的 **Redis 支持的检查点器**上,该检查点器会持久化每一个状态转换。Slack 操作是**幂等**的——基于 run id 的幂等键意味着恢复(或重试)永远不会重复发布。状态具有 **~12 小时的 TTL**(`APPROVAL_TTL_HOURS` + 2 小时宽限期):超过审批窗口的待处理审查将从 Redis 中过期,事件必须重新提交。Slack 投递的幂等键有效期为 24 小时,因此恢复执行绝不会重复发布消息。 *验证:* 在暂停时对进程执行 `kill -9`,启动一个全新的进程,然后恢复——图将从行动节点继续执行,并且仅发布一次消息。 ### 6 · 可靠性 — 限制循环,在工具不稳定时存活 如果没有限制,`拒绝 → 综合器` 的边将永远循环,并且每个节点都会调用不稳定的外部服务。可靠性使得故障变得有界且可恢复: - **有限的执行** — 图上有 `GRAPH_RECURSION_LIMIT`,反馈循环上有 `MAX_RETRIES = 3`;经过三次拒绝后,事件会强制升级给人工处理,而不是陷入循环。 - **超时控制** — 每个 LLM 和工具调用都带有 `NODE_TIMEOUT_SECONDS` 的截止时间,因此挂起的 Tavily 调用不会冻结整个事件。 - **带退避的重试** — `tenacity` 用于处理瞬态故障(Tavily 429、Slack 5xx、模型过载)。 - **备用模型** — 如果主模型不可用,在宣告失败之前会在 `FALLBACK_MODEL` 上重试一次。 - **Tavily 熔断机制** — 如果网络搜索宕机,研究员会降级为仅基于告警的综合,并标记为低置信度,而不是让整个事件失败。 - **死信队列** — 耗尽所有重试次数的事件将被推送到 DLQ,并触发运维人员 Slack 告警;它永远不会被静默丢弃。 ### 7 · 安全与审计 — 每一个操作都有迹可循 OpsCanvas 会向共享的 Slack 频道发送消息并升级事件,因此“谁批准关闭 INC-4471?”必须有一个不可更改的明确答案。每一个操作——包括自动关闭的 P4(`approver: "system"`)——都会写入一条**仅追加的审计记录**:操作、脱敏的 payload、审批人、严重程度、时间戳。审批人是经过身份验证的 Google Auth `sub`,也会被记录在 Langfuse 追踪中。在任何数据到达日志之前,PII 和疑似机密的值都会被清理,机密信息仅存放在 GitHub Secrets / Lambda 环境变量中,并且 Lambda 运行在一个最小权限的 IAM 角色下,该角色只能访问 Redis、Slack、Tavily 和模型 endpoint。 ## 智能体架构 ``` Incoming alert (CloudWatch / PagerDuty / manual) │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ LangGraph StateGraph (IncidentState) │ │ │ │ ┌─────────────┐ │ │ │ TRIAGE │ Claude → TriageResult (Pydantic) │ │ │ AGENT │ severity: P1 | P2 | P3 | P4 │ │ └──────┬──────┘ affected_services: list[str] │ │ │ │ │ ── P4 ──► END (auto-close, no human needed) │ │ │ P1 / P2 / P3 (low-confidence → human review) │ │ ▼ │ │ ┌─────────────┐ │ │ │ RESEARCHER │ Tool 1: Sourciq /api/query (Agent 2) │ │ │ AGENT │ Tool 2: Tavily web search │ │ └──────┬──────┘ → ResearchResult (Pydantic) │ │ ▼ │ │ ┌─────────────┐ │ │ │ SYNTHESISER │ Merge triage + research context │ │ │ AGENT │ Draft: summary, actions, Slack message │ │ └──────┬──────┘ → IncidentSummary (Pydantic) │ │ ▼ │ │ ┌─────────────┐ ◄── React UI: Approve / Reject + feedback │ │ │ HUMAN │ Graph checkpoints to Redis, terminates │ │ │ REVIEW │ Resumes on POST /incidents/:id/review │ │ └──────┬──────┘ (idempotent — resume never double-posts) │ │ │ approve → action · reject → synthesiser + retry │ │ ▼ │ │ ┌─────────────┐ │ │ │ ACTION │ Slack Block Kit post to #incidents │ │ │ AGENT │ Severity-coded colour · audited · cited │ │ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ every node → Langfuse span · cost-metered · timeout-bounded ``` ## Sourciq 成 — 将 Agent 2 作为知识工具 OpsCanvas (Agent 3) 在研究员智能体内部将 Sourciq (Agent 2) 作为外部 HTTP 工具进行调用。 当研究员智能体运行时,它会根据告警 payload 构建一个具备上下文感知能力的问题,并调用 Sourciq 的 `/api/query` endpoint: ``` # 来自 app/integrations/sourciq.py response = httpx.post( f"{SOURCIQ_API_URL}/api/query", json={"question": "What is the recovery procedure for {alarm_name}?", "namespace": "kubernetes-docs"}, ) ``` Sourciq 会从已索引的工程文档中提取内容,返回带有置信度评分且包含引用的回答。研究员会将其整合到 `ResearchResult.runbooks_found` 中,综合器则会利用它来撰写引用特定 runbook 流程的事件摘要。该调用由第 6 层的熔断机制包裹——如果 Sourciq 不可用,研究员将降级为仅基于告警的综合,而不是导致整个事件流程失败。 **这是作品集连贯性的关键所在。** Agent 2 是知识基础设施。Agent 3 是使用该基础设施的运维智能层。每个系统都可以独立部署和独立测试。OpsCanvas 可以在不改变其智能体逻辑的情况下,将 Sourciq 替换为任何 RAG API。Sourciq 也可以在不知道 OpsCanvas 存在的情况下,为任意数量的下游工具提供服务。 **这为 Sourciq 解决的实际问题:** Sourciq 只能在工程师主动想到提问时回答问题。OpsCanvas 使 Sourciq 变得具有主动性——当告警触发时,研究员智能体会自动查询受影响服务的 runbook,并在事件摘要中呈现答案。工程师能够在正确的时间获得正确的文档,而无需事先知道这些文档的存在。 ## 状态 schema ``` class IncidentState(TypedDict): # Input (immutable after creation) alert_id: str alert_source: Literal["cloudwatch", "pagerduty", "manual"] alert_payload: dict[str, Any] # Triage output severity: Literal["P1", "P2", "P3", "P4"] | None triage_confidence: float # < threshold → route to human (abstain) affected_services: list[str] triage_reasoning: str # Research output runbooks_found: list[dict] # from Sourciq: cited runbook answers web_results: list[dict] # from Tavily: status pages, known issues degraded: bool # Tavily/Sourciq unavailable → low-confidence synth # Synthesis output incident_summary: str proposed_actions: list[str] draft_slack_msg: str # Human review human_decision: Literal["approved", "rejected"] | None human_feedback: str | None # rejection reason for Synthesiser retry approver_sub: str | None # Google Auth subject id → audit log # Action output slack_posted: bool # Metadata run_id: str # Redis checkpoint key + Langfuse session_id retry_count: int # max 3 — prevents infinite rejection loops total_cost_usd: float # summed across nodes + Tavily ``` ## 技术栈 | 层级 | 技术 | 理由 | |-------|-----------|-----------| | 智能体编排 | LangGraph 0.2 | 显式类型化状态。Human-in-the-loop 中断。代码中的条件边。确定性图——输入相同,路径即相同。 | | LLM | Claude Sonnet (+ Haiku 备用) | 用于全部 4 个智能体。结构化 JSON 输出。在引用和格式约束上具有可靠的指令遵循能力。在主模型不可用时提供备用模型。 | | 可观测性 | Langfuse | 每个事件对应一个追踪,每个节点对应一个 span,通过 `session_id` 跨越人工暂停进行串联。每个节点的成本与延迟。 | | 网络搜索 | Tavily API | 供 LLM 消费的结构化结果。专为智能体工作流构建。免费额度涵盖演示使用。带有熔断机制——可实现优雅降级。 | | 知识库 | Sourciq (Agent 2) | 实时 HTTP 调用至 Agent 2 的 RAG API。松耦合——可独立部署。 | | 状态持久化 | Upstash Redis | LangGraph 检查点器——图状态可在 Lambda 冷启动中存活。人工审查可能需要数小时。Serverless 架构,提供免费额度。 | | 可靠性 | tenacity | 针对瞬态工具/模型故障进行带指数退避的重试。 | | 通知 | Slack Block Kit | 严重程度颜色编码的标头、结构化操作、带有运行 ID 和审批归属的页脚。幂等发送。 | | 后端 | FastAPI + Python 3.12 | 异步原生。与 Lambda 使用相同的 runtime。 | | 计算 | AWS Lambda (eu-north-1) | 与 Sourciq 同区域。可缩容至零。在作品集规模下成本约为 ~$0。 | | 前端 | React 18 + TypeScript strict | 可辨识联合 `AppStatus` 状态机。使用 `usePollIncident` hook 获取实时状态。 | | 前端托管 | Vercel | 自动配置 HTTPS、CDN、预览部署。 | | 身份验证 | Google OAuth | 在每次请求时于服务端验证 id_token。凭证仅保留在内存中——绝不存入 localStorage。审批人身份会被记录在审计日志中。 | ## 工程决策 ### 选择 LangGraph 而非 CrewAI CrewAI 将状态抽象到智能体的记忆中。如果不解析自然语言,你无法检查每个智能体究竟决定了什么。LangGraph 为你提供了每个节点处的强类型状态对象。对于一个会向 Slack 发布消息并创建工单的系统,你需要确切知道是什么状态导致了每一个操作。这种要求在生产环境的事件响应中是不可妥协的。 ### 在代码中实现操作授权,而非在 prompt 中 “没有人工批准不得执行外部操作”这一规则在行动节点中强制执行,如果不存在批准 token,该节点将引发 `GuardrailError`——这不是系统 prompt 中要求模型遵守的一句空话。Prompt 层面的护栏可以通过告警 payload 中的注入绕过;而在 Slack 调用前抛出异常的代码层护栏则无法被绕过。 ### Slack 操作的幂等键 由于图在一个全新的 Lambda 上恢复执行,且可靠性机制增加了重试,同一个操作可能会被尝试不止一次。Slack 发送时会检查基于 run id 的幂等键,如果消息已经发送,则执行空操作,因此恢复或重试永远不会向事件频道重复发布消息。 ### 主模型不可用时的备用模型 如果主模型过载,该节点会在宣告事件失败之前,先在备用模型上重试一次。模型提供商的短暂故障应该只导致质量下降,而不是拖垮整个事件 pipeline。 ### Synthesiser 的 MAX_RETRIES = 3 人类可以无限次拒绝事件摘要。如果没有重试限制,感到困惑或怀有敌意的用户可能会阻碍事件的解决。三次重试是正确的操作限制——这足以实质性地改善摘要,又不足以造成无限的阻塞情况。在第三次之后,事件将被强制升级。 ### 状态 TTL ≈ 12 小时 待处理的事件将在 `APPROVAL_TTL_HOURS`(默认 **10h**)后过期,运行/检查点键会保留 **+2h** 的宽限期(在 Redis 中总计约 **12h**)。如果人类未能及时审查,状态将被丢弃,事件必须重新提交。过时的 P1 上下文不应该被执行。Slack 幂等键 (`opscanvas:sent:{run_id}`) 使用单独的 **24h** TTL,因此即使是延迟的恢复执行也无法重复发布。 ### Sourciq 作为外部服务,而非作为导入模块 研究员智能体通过 HTTP 调用 Sourciq,而不是将其作为 Python 模块导入。这是正确的生产模式。各个服务之间是松耦合且可独立部署的。Sourciq 的 Lambda 可以在不更改 OpsCanvas 代码的情况下进行更新、扩容或替换。 ### 在 Lambda 上使用后台线程执行 `POST /api/incidents` 会在后台线程中启动图,并立即返回带有 `run_id` 的 `202 Accepted` 响应。客户端会轮询 `GET /api/incidents/:id`。这避免了在整个 pipeline 上触发 Lambda 的 30 秒超时限制(分类 + 研究 + 综合需要 8-15 秒)。在生产规模下,请使用 SQS + 独立的 Lambda 替换后台线程来执行每次图运行。 ### P4 自动关闭的条件边 P4(信息性)告警将完全跳过研究员、综合器、人工审查和行动节点。分类后的条件边会直接将 P4 路由至 `END`。这可以防止告警疲劳——并非每个警报都需要工程师的关注。自动关闭的操作仍会被写入审计日志。 ## 安全与审计 | 机制 | 防范内容 | |-----------|----------------| | 不可变的操作审计日志(操作 · payload · 审批人 · 时间戳) | 无人负责的操作——每一个 Slack 发布、升级和自动关闭都可追溯 | | 每次操作上的审批人身份 (Google Auth `sub`) | 匿名审批 | | 日志记录前的 PII / 机密清理 | 机密或 PII 泄露到 Langfuse / 审计存储中 | | 服务端验证 Google id_token | 伪造的身份验证 | | 凭证仅保留在内存中(绝不存入 localStorage) | XSS token 窃取 | | `APP_ENV=prod` 检查移除了开发绕过 | 开发绕过机制流入生产环境 | | 速率限制:每个 IP 10 请求/分钟 | API 滥用和爬取 | | TrustedHostMiddleware | DNS 重绑定攻击 | | 全局异常处理程序 | 错误响应中暴露堆栈跟踪 | | CORS 显式来源列表(无通配符) | 跨域请求伪造 | | 最小权限 Lambda IAM 角色 | 超出 Redis / Slack / Tavily / 模型 endpoint 范围的横向移动 | | ~12 小时的状态 TTL (`APPROVAL_TTL_HOURS` + 2h) | 陈旧的事件状态累积 | | MAX_RETRIES = 3 | 无限的拒绝循环 | | 所有机密信息均通过 GitHub Secrets 提供 | 通过 git 历史记录暴露凭证 | ## API 参考 ### POST /api/incidents 需要:`Authorization: Bearer ` ``` { "alert_source": "cloudwatch", "alert_payload": { "AlarmName": "CRITICAL-PaymentService-ErrorRate", "AlarmDescription": "Error rate exceeded 25% for 10 minutes", "NewStateValue": "ALARM", "Region": "eu-north-1" } } ``` 响应 `202 Accepted`: ``` { "run_id": "a3f8c2d1-4b5e-6789-abcd-ef0123456789", "status": "running", "message": "Incident run started. Poll GET /incidents/{run_id} for status." } ``` ### GET /api/incidents/:run_id 返回当前状态。每 3 秒轮询一次,直到 `status` 变为 `awaiting_review` 或 `completed`。 状态值:`running` · `awaiting_review` · `completed` · `failed` ### POST /api/incidents/:run_id/review ``` { "decision": "approved" } ``` ``` { "decision": "rejected", "feedback": "Actions too generic — add kubectl commands" } ``` 当 `decision` 为 `rejected` 时,`feedback` 是必填项。否则将返回 `422`。 ### GET /health ``` { "status": "healthy", "version": "1.0.0", "agent": "opscanvas" } ``` ## 在本地运行 **前置条件:** Python 3.12、Node 22,以及 Anthropic、Tavily、Upstash Redis、Slack webhook、Google OAuth client ID、Langfuse 公开/密钥的 API 密钥。Sourciq 必须正在运行(或使用已部署的 Sourciq API)。 ``` git clone https://github.com/AttiR/OpsCanvas cd OpsCanvas # 后端 python3.12 -m venv venv && source venv/bin/activate pip install -r backend/requirements.txt cp .env.example .env # fill in all API keys make run # starts API on port 8001 # 前端(单独的 terminal) cd frontend && npm install cp .env.local.example .env.local # fill in VITE_ vars npm run dev # starts at http://localhost:5173 ``` ``` make test # 58 unit tests — zero API/Redis/LLM calls make eval # agent eval suite — triage · routing · synthesis · e2e (CI-gated) make lint # ruff check + format make graph # visualise LangGraph state machine → graph.png make verify # confirm all imports resolve ``` ## 成本 | 服务 | 成本 | |---------|------| | AWS Lambda + API Gateway (eu-north-1) | $0(免费额度——每月 100 万次请求) | | Upstash Redis | $0(免费额度——每天 1 万条命令) | | Tavily | $0(免费额度——每月 1 千次搜索) | | Langfuse | $0(免费额度) | | Vercel | $0(免费额度) | | Slack webhooks | $0 | | **基础设施总计** | **$0/月** | 每次事件运行的 Anthropic API 成本:**~$0.02**(4 个智能体 × 每个约 500 个 token,按 Claude Sonnet 定价计算),在 Langfuse 中按事件进行追踪,并由成本熔断器 (`MAX_INCIDENT_USD`) 限制单次运行额度。有关 Langfuse 追踪截图,请参见[第 4 层 · 成本](#4--cost--per-incident-with-a-runaway-breaker)。 ## 项目结构 ``` OpsCanvas/ ├── backend/ │ ├── app/ │ │ ├── agents/ │ │ │ ├── triage.py Severity classification → TriageResult │ │ │ ├── researcher.py Sourciq + Tavily research → ResearchResult │ │ │ ├── synthesiser.py Incident summary → IncidentSummary │ │ │ ├── action.py Slack Block Kit post (authorized · idempotent · audited) │ │ │ └── models.py Pydantic output models for all agents │ │ ├── graph/ │ │ │ ├── state.py IncidentState TypedDict │ │ │ └── builder.py StateGraph assembly + conditional edges │ │ ├── api/ │ │ │ └── incidents.py POST/GET /incidents, POST /incidents/:id/review │ │ ├── core/ │ │ │ ├── redis_store.py checkpointer + save/load/delete/list │ │ │ ├── observability.py Langfuse trace/span setup, flush │ │ │ ├── guardrails.py schema + injection + action-auth + abstain │ │ │ ├── cost.py per-incident CostMeter + circuit breaker │ │ │ ├── reliability.py timeouts, retries, fallback, circuit breaker │ │ │ └── audit.py append-only action audit log │ │ └── integrations/ │ │ └── sourciq.py HTTP client for Sourciq (Agent 2) │ ├── lambda_handler.py Mangum ASGI adapter │ ├── main.py FastAPI app — CORS, rate limiting, health │ ├── requirements.txt │ └── template.yaml AWS SAM — Lambda + API Gateway + CloudWatch ├── frontend/ │ └── src/ │ ├── components/ │ │ ├── AgentTimeline.tsx 5-node LangGraph state visualisation │ │ └── ApprovalPanel.tsx Human review — approve/reject/feedback │ ├── pages/ │ │ ├── LoginPage.tsx Split panel: proof points + Google auth │ │ └── DashboardPage.tsx Alert input → polling → review → resolved │ ├── context/ │ │ └── AuthContext.tsx Google id_token in memory only │ ├── hooks/ │ │ └── usePollIncident.ts Polls GET /incidents/:id every 3s │ ├── api/ │ │ └── client.ts Typed fetch wrapper, ApiError class │ └── types/ │ └── api.ts TypeScript types + EXAMPLE_ALERTS ├── tests/ │ ├── test_graph_agents.py State machine, Triage, Researcher (20 tests) │ ├── test_api_pipeline.py Synthesiser, Redis, FastAPI contracts (25 tests) │ ├── test_action_agent.py Slack Block Kit, webhook mocked (13 tests) │ └── eval/ │ ├── triage_set.jsonl labelled alert → expected severity │ ├── golden_incidents.jsonl alert → expected outcome │ ├── test_triage_accuracy.py │ ├── test_routing.py P4 auto-close · P1 escalate (must = 1.0) │ ├── test_synthesis_judge.py │ └── test_e2e_success.py ├── .github/workflows/ │ ├── ci.yml pytest + ruff + tsc + Trivy + agent-eval on every PR │ ├── cd-backend.yml SAM deploy to Lambda on merge to main │ └── cd-frontend.yml Vercel deploy on merge to main ├── infra/ │ └── secrets-reference.sh All secrets documented ├── Makefile └── .env.example ``` *由 Atti Rehman 构建  ·  [LinkedIn](https://www.linkedin.com/in/attirehman/)  ·  [GitHub](https://github.com/AttiR)*
标签:AWS Lambda, LangGraph, 多智能体, 搜索引擎查询, 运维