Two LangGraph Projects That Taught Me How to Design Multi-Agent Systems

작성자

카테고리:

← 피드로
DEV Community · Debashish Ghosal · 2026-07-10 개발(SW)

i built two langgraph projects and learned that agents are junior engineers with amnesia

I’ve been a DevOps engineer for a while — 15 years in people management roles before that. lately I’ve been trying to figure out where AI fits into DevOps, not just running someone else’s tool, but actually building agents myself.

LangGraph clicked for me because it maps to stuff I already know: DAGs, conditional branches, parallel stages, approval gates. it’s basically a CI pipeline where some nodes happen to call an LLM instead of a shell script.

I built two projects back-to-back with it. different problems, same patterns. here’s what I learned — including what broke.

the analogy that made it click

after 15 years of managing DevOps teams, the supervisor-worker pattern in LangGraph didn’t need a tutorial. it’s just staff work. you don’t review every PR yourself — you route by area. a security incident gets a different review process than a feature PR. you don’t let two engineers argue in a loop forever — you set a decision deadline. you checkpoint long investigations so they survive context switching. you don’t let one engineer’s bad output block the pipeline.

the mental shift that helped me most: agents are junior engineers with amnesia. they’re capable, fast, and will absolutely produce invalid output if you don’t give them a schema and a retry policy. you manage them the same way you manage a new hire — clear scope, structured handoffs, and a review gate on anything irreversible.

project 1: release narrator — supervisor + sub-agents with hybrid interrupts

writing release notes sucks. you read 200 commits, figure out which 15 matter, group them, write something that doesn’t sound like a robot, and decide if it’s a major or minor version. it’s mechanical work with a few judgment calls buried in it.

the agent I built does the mechanical part. a supervisor routes each commit to sub-agents based on what kind of change it is. breaking changes stop for human review one at a time — each one needs different migration advice, so batching them would lose context. security fixes get reviewed in a batch because CVEs are standardized — the reviewer needs to see all advisories together to decide disclosure timing. the draft stage reads past release notes from the same repo to match the writing style.

the key takeaway: router supervisor + hybrid interrupts. not all human review is the same — design for the kind of judgment each step needs.

the conditional edge that almost caused an infinite loop

here’s the routing function for the semver approval gate — the most bug-prone edge in the graph:

def route_semver_approval(state: AgentState) -> str:
    if state.get("semver_cancelled"):
        return "write_output"
    if state.get("semver_approved"):
        return "write_output"
    if state.get("semver_override"):
        return "propose_semver"
    return "write_output"

Enter fullscreen mode Exit fullscreen mode

looks simple. the first version checked semver_override before semver_approved, and interrupt_semver never cleared semver_override on approve/cancel. result: override → propose → interrupt → “approved” → but semver_override still set → loop back → propose → interrupt → forever. the fix was two things: return semver_override=None on approve/cancel, and check semver_approved before semver_override in the router. priority matters in conditional edges.

project 2: CI/CD bottleneck optimizer — cyclic refine loop with one trade-off gate

this one came from real frustration. big repos have CI pipelines that run 30-120 minutes. nobody audits a 500-line workflow YAML by hand to find the waste, but the waste is usually obvious once you look: missing cache, serial steps that could run in parallel, oversized build contexts.

the agent fetches workflow files and run history, profiles each job, classifies bottlenecks, and proposes fixes with time estimates. then it stops and says “here’s the trade-off — caching saves 5min but costs $0.02/run” and lets you decide.

the key takeaway: cyclic refine loop. propose → simulate → refine → show human. one gate, not many. the cycle has a termination condition (convergence on best set of optimizations) — without that, it spins.

where this project fell short (honest gap)

the Bottleneck Optimizer ships real LangGraph orchestration — cycles, interrupts, checkpointing, conditional routing. but it does not use LangChain’s primitives. LLM calls go through the raw openai SDK. YAML generation is a string prompt + PyYAML retry loop, not with_structured_output(). the GitHubClient is a plain Python class called deterministically inside nodes, not LLM-invoked @tool calls.

the lesson I’ll carry forward: orchestration (LangGraph) and primitives (LangChain) are two different skills. having a state machine with cycles and interrupts doesn’t mean you’ve built an agent in the full sense — the LLM also needs to choose tools and produce schema-enforced output. one without the other is half the story.

what went wrong (the real education)

this is the section I wish more LangGraph posts had. the patterns are easy — the bugs are the real education.

the infinite loop on semver override. human overrides semver version → agent re-proposes → interrupts again → human approves → agent re-proposes again → forever. root cause: interrupt_semver never cleared semver_override on approve/cancel. fix: clear the flag, check priority order. lesson: in conditional edges, the order of checks is the logic.

the if/elif routing bug that silently dropped breaking changes. a commit like fix(security)!: patch CVE with breaking API change only routed to security_sub, missing breaking_sub. root cause: the router used if/elif — first match wins. fix: changed to independent if checks so a single commit can route to multiple sub-agents. this is the kind of bug that doesn’t show up in unit tests — you need the graph integration test to catch it. lesson: multi-match routing is a design decision, not a default.

the reanalyze recursion that never terminated. test used an always-reject mock for the trade-off interrupt. the graph looped until Python’s recursion limit killed it. fix: recursion_limit on the graph invocation + GraphRecursionError catch. same lesson as managing engineers: you don’t let two people argue in a loop forever — you set a deadline.

LLM returns invalid JSON or empty content. both projects hit this. fix: three-tier strategy — first attempt normal prompt, if JSON parse fails retry with “CRITICAL: respond with ONLY valid JSON”, if that fails rules-based fallback. the output is worse but the pipeline finishes. in DevOps terms: a degraded pipeline that completes beats a perfect pipeline that’s down.

v-prefix dropped from semver proposal. for angular/angular (v18.0.0), agent proposed 18.1.0 instead of v18.1.0. root cause: _rules_based_semver built major.minor.patch without tracking whether the input had a v prefix. fix: track has_v_prefix through the pipeline. lesson: round-trip your outputs through the input format. a version isn’t just numbers.

patterns I keep reaching for

a router at the top. both projects have a node that looks at input and decides where to send it. the routing logic is just if/elif — nothing fancy — but it means adding a new sub-agent is one rule, not a graph rewrite.

not all interrupts are the same. I learned this the hard way. first version of Release Narrator stopped for everything — every breaking change, every security fix, semver approval. way too many stops. you tune out after the third interrupt. breaking changes need per-item stops. security fixes can batch. trade-off decisions need a matrix view. if you get the interrupt model wrong, the human either ignores everything or misses something important.

LLMs fail. plan for it. retry + rules fallback is the pattern. the key insight: the fallback path must produce usable output, not just “error.” a draft with no LLM tone matching is still useful. an empty error message is not.

structured output is non-negotiable. every LLM call in both projects returns JSON. without that, you can’t route reliably, you can’t extract CVEs, you can’t parse version bumps. it’s the single most important pattern.

when to use networked vs hierarchical agents

my rule of thumb: start linear. add a supervisor only when the input type forces different processing paths. reach for networked only when agents need to negotiate (not just hand off). I haven’t hit a problem that needs networked yet — and I think most “multi-agent” tutorials reach for it too early.

the boring but useful part

both projects run on the same stack: LangGraph for the graph, DeepSeek V4 Flash for the LLM calls, GitHub REST API for data, SQLite + JSON files for storage, Docker for packaging. no GPUs. no vector DB. no message queue. the Docker image is 200-425 MB. runs fine on a laptop or in CI. it’s just Python making HTTPS calls.

the Bottleneck Optimizer ran a field study against 278 real PRs across 8 repos (react, cpython, nodejs, etc.) and found 755 bottlenecks. the Release Narrator ran against 8 repos (nodejs, angular, next.js, vscode, kubernetes, axios, lodash, moment) with 17 output files reviewed and 7 bugs fixed before ship. a field study with real data transforms “I built a thing” into “I validated a thing against reality.”

discussion

I want to hear from people building LangGraph agents in production, not just tutorials:

how are you handling interrupt fatigue? I learned the hard way that stopping for everything means the human stops paying attention. what’s your interrupt model — per-item, batch, matrix? how do you decide which gets which?

has anyone hit the multi-match routing bug? the if/elif vs independent if checks issue seems niche but it silently drops data. did you catch it in testing or in production?

orchestration vs primitives — are you using LangChain’s @tool and with_structured_output() or raw SDK calls? I shipped orchestration without primitives and I’m rebuilding with them now. curious if anyone started with primitives from the beginning and whether it changed the architecture.

networked multi-agent — has anyone actually needed it? I keep seeing tutorials that reach for peer-to-peer agent negotiation on day one. my experience says start linear, add a supervisor only when forced. am I wrong? what problem actually needed networked?

what’s your fallback strategy when the LLM returns garbage? I use retry → stricter prompt → rules fallback. is anyone doing something smarter — judge model, cross-encoder verification, something else?

if you’re building LangGraph agents for real DevOps workflows, drop a comment with your architecture and what broke. especially what broke.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다