Documents
Home>Documents>AI>Agent>Geny

Integrating ClaudeCLIChatModel with LangGraph

7 min readFeb 10, 2026Feb 22, 2026

ClaudeCLIChatModel and LangGraph Integration

I wanted to connect Claude Control sessions to the LangChain/LangGraph ecosystem. LangGraph's StateGraph lets you define complex agent workflows, and pairing it with a Claude CLI session as the LLM backend makes for a powerful combination.

Implementing ClaudeCLIChatModel

I subclassed LangChain's BaseChatModel to wrap a Claude CLI session as an LLM.

from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import BaseMessage, AIMessage, HumanMessage
from langchain_core.outputs import ChatResult, ChatGeneration
from typing import List, Optional, Any

class ClaudeCLIChatModel(BaseChatModel):
    session_manager: Any
    session_id: str
    model_name: str = "claude-cli"

    class Config:
        arbitrary_types_allowed = True

    @property
    def _llm_type(self) -> str:
        return "claude-cli"

    def _generate(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        **kwargs,
    ) -> ChatResult:
        raise NotImplementedError("Use async version")

    async def _agenerate(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        **kwargs,
    ) -> ChatResult:
        # Convert messages into a single prompt
        prompt = self._format_messages(messages)
        response = await self.session_manager.send_prompt(
            self.session_id, prompt
        )
        message = AIMessage(content=response)
        generation = ChatGeneration(message=message)
        return ChatResult(generations=[generation])

    def _format_messages(self, messages: List[BaseMessage]) -> str:
        parts = []
        for msg in messages:
            if isinstance(msg, HumanMessage):
                parts.append(f"사용자: {msg.content}")
            elif isinstance(msg, AIMessage):
                parts.append(f"어시스턴트: {msg.content}")
            else:
                parts.append(msg.content)
        return "\n".join(parts)

Using LangGraph StateGraph

I used LangGraph's state graph to define a code review workflow.

from langgraph.graph import StateGraph, END
from typing import TypedDict

class ReviewState(TypedDict):
    code: str
    review_comments: list
    is_approved: bool
    revision_count: int

async def analyze_code(state: ReviewState) -> ReviewState:
    llm = ClaudeCLIChatModel(
        session_manager=manager,
        session_id="review-session",
    )
    response = await llm.ainvoke([
        HumanMessage(content=f"다음 코드를 분석하세요:\n{state['code']}")
    ])
    state["review_comments"].append(response.content)
    return state

async def check_approval(state: ReviewState) -> str:
    if state["is_approved"] or state["revision_count"] > 3:
        return "end"
    return "revise"

graph = StateGraph(ReviewState)
graph.add_node("analyze", analyze_code)
graph.add_node("revise", revise_code)
graph.add_conditional_edges("analyze", check_approval, {
    "end": END,
    "revise": "revise",
})
graph.add_edge("revise", "analyze")
graph.set_entry_point("analyze")

workflow = graph.compile()

Results in Practice

With the Claude CLI session running as a LangGraph node, I was able to fully automate the review → revise → re-review loop. Compared to direct API calls, the key advantages are that Claude CLI has direct filesystem access and tool use feels natural.

Troubleshooting

The biggest pain point was reconciling LangChain's sync and async interfaces. You need to implement both _generate and _agenerate, but Claude CLI sessions are inherently async — calling asyncio.run() inside the sync version causes event loop conflicts. I ended up settling on async-only usage.

Tags
LangGraphLangChainClaude CLIAgentIntegration