Implementing a Multi-Agent State Machine for Automated Code Review using LangGraph
The trick to a stable code review agent is treating the process as a directed graph where the state is the single source of truth. I've set up a workflow where the State object tracks the current code version, a list of identified issues, and a "review cycle count" to prevent infinite loops.
Here is the core logic of how I structured the graph:
from typing import Annotated, TypedDict, List
from langgraph.graph import StateGraph, END
class ReviewState(TypedDict):
code: str
critiques: List[str]
iteration: int
is_approved: bool
def reviewer_agent(state: ReviewState):
# Prompt focuses on identifying bugs and style violations
# Returns a list of critiques
response = llm.invoke(f"Review this code: {state['code']}")
return {"critiques": response.content, "iteration": state['iteration'] + 1}
def fixer_agent(state: ReviewState):
# Prompt focuses on fixing the specific critiques provided
response = llm.invoke(f"Fix these issues: {state['critiques']} in code: {state['code']}")
return {"code": response.content, "critiques": []}
def check_approval(state: ReviewState):
if not state['critiques'] or state['iteration'] > 3:
return "approve"
return "fix"
workflow = StateGraph(ReviewState)
workflow.add_node("reviewer", reviewer_agent)
workflow.add_node("fixer", fixer_agent)
workflow.set_entry_point("reviewer")
workflow.add_conditional_edges("reviewer", check_approval, {"approve": END, "fix": "fixer"})
workflow.add_edge("fixer", "reviewer")
app = workflow.compile()One major "gotcha" I hit was the LLM's tendency to say "The code looks great!" while still leaving a minor syntax error. To fight this, I shifted from a general prompt to a Role-Specific System Prompt for the reviewer. Instead of asking "Is this correct?", I tell it: "You are a pedantic senior engineer. Your goal is to find at least one reason to reject this PR. If you cannot find any, only then mark it as approved."
To make this actually useful in a production CI/CD pipeline, I integrated this with Cursor's .cursorrules file so that when I manually trigger a review, the AI knows the exact schema the LangGraph agent expects.
Productivity gains I've noticed:
Reduced Manual Review Time: I no longer spend 10 minutes catching missing null checks; the agent catches them in 15 seconds.
Consistency: Unlike a human reviewer who might be tired on Friday afternoon, the agent applies the same strict linting rules every time.
State Persistence: Using LangGraph's checkpointer, I can pause the review, manually intervene in the code, and then resume the graph from the last state without re-running the entire chain.
If you're using Claude 3.5 Sonnet as the backbone, the reasoning capabilities are significantly better at the "Fixer" stage. It understands the context of the critique without needing the entire codebase injected into every single turn, which keeps the token cost down.
All Replies (0)
No replies yet — be the first!
