DEV Community

Debashish Ghosal
Debashish Ghosal

Posted on

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

I've been in DevOps for a while. I have been in people management positions for 15 years and did very little coding. I am getting back into coding with help of AI. Lately I've been trying to figure out where AI fits into that role — 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.

Project 1: Release Narrator

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. Architecture looks like this:

fetch → router_supervisor → [classify_sub, breaking_sub, security_sub]
                           → interrupt(breaking changes, per-item)
                           → interrupt(security fixes, batch)
                           → retrieve_tone → draft → propose_semver
                           → interrupt(semver approval)
                           → write_output
Enter fullscreen mode Exit fullscreen mode

A supervisor routes each commit to a sub-agent based on what kind of change it is. Breaking changes stop for human review one at a time — each one needs different migration advice. Security fixes get reviewed in a batch because CVEs are standardized. The draft stage reads past release notes from the same repo to match the writing style.

Key takeaway: Router supervisor + hybrid interrupts. Not all human review is the same — design for the kind of judgment each step needs.

Project 2: CI/CD Bottleneck Optimizer

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.

fetch_workflows → profile_runs → classify_bottlenecks
                                    → propose_optimizations
                                    → simulate_savings
                                    → refine_proposal
                                    → interrupt(trade-off matrix)
                                    → generate_PR
Enter fullscreen mode Exit fullscreen mode

Key takeaway: Cyclic refine loop. Propose → simulate → refine → show human. One gate, not many.

Patterns I Keep Reaching For

1. A router at the top

Both projects have a node that looks at input and decides where to send it. Release Narrator routes by commit type. Bottleneck Optimizer routes by bottleneck class. The routing logic is just if/elif — nothing fancy — but it means adding a new sub-agent is one rule, not a graph rewrite.

2. 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 (each one has different context)
  • Security fixes can batch (CVE data is the same shape every time)
  • Trade-off decisions (bottleneck optimizer) need a matrix view

If you get the interrupt model wrong, the human either ignores everything or misses something important.

3. LLMs fail. Plan for it.

Both projects hit the same problem: the LLM returns invalid JSON or empty content. You have three choices:

  • Crash — "API down, try again"
  • Rules fallback — worse output but the pipeline keeps moving
  • Retry with a stricter prompt

I went with retry + rules. First attempt has a normal prompt. If JSON parse fails, retry with "CRITICAL: respond with ONLY valid JSON." If that also fails, rules take over. The output is worse but the pipeline finishes.

4. 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. Doesn't matter if you use with_structured_output() or manual parsing — just make sure every LLM response has a defined schema.

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 200MB. Runs fine on a laptop or in CI. It's just Python making HTTPS calls.

Top comments (0)