Skip to content

#341: Deep Agents in LangGraph

With Deep Agents we get a relatively new component from LangChain/LangGraph that allows us to build high-level agents. Let us see how they fit into the big picture and how we can use them in our own applications.

Where do Deep Agents fit into LangGraph/LangChain?

deepagents is a standalone library built on top of LangChain’s core building blocks for agents. It uses the LangGraph runtime for durable execution, streaming, human-in-the-loop, and other features. (from docs.langchain.com)

With Deep Agents we get an “agent harness” and not just a low-level library to build our own solutions. We can get a pre-configured system with the name Deep Agents Code that behaves like Claude Code, or we wire up our tools to build a custom agent where we do not need to build a workflow as we usually do in LangGraph.

Installation

To use this new feature, we need to install the deepagents package:

uv pip install deepagents langchain-openai

Rebuild the multi-agents debate tool with Deep Agents

In Python Friday #338 we used multi-agents to debate the merit of an idea with an optimist and a pessimist before we created a summary and gave advice on how to proceed. We can take this idea and rebuild it with Deep Agents to see how those two approaches differ.

As with multi-agents, we start with a few imports and the configuration of our LLM:

import sys
import warnings
from typing import Any

from langchain_openai import ChatOpenAI

warnings.filterwarnings("ignore", message=".*allowed_objects.*")

from deepagents import create_deep_agent

sys.stdout.reconfigure(encoding="utf-8")

# Same LM Studio initialization as the original script.
llm = ChatOpenAI(
    base_url="http://localhost:1234/v1",
    api_key="lm-studio",
    model="openai/gpt-oss-20b",
    temperature=0.1,
)

For the pessimist and optimist, we need prompts and a structure to hold our subagents:

OPTIMIST_SYSTEM = (
    "You are an extreme optimist in a structured debate. Argue the maximum "
    "upside, growth, and opportunity of the topic. When earlier pessimistic "
    "points are included in the task, rebut them directly. Return EXACTLY "
    "3 short points, one per line, with no numbering and no preamble."
)

PESSIMIST_SYSTEM = (
    "You are an extreme pessimist in a structured debate. Argue the severe "
    "risks, hidden costs, and technical bottlenecks of the topic. When earlier "
    "optimistic points are included in the task, rebut them directly. Return "
    "EXACTLY 3 short points, one per line, with no numbering and no preamble."
)

COORDINATOR_SYSTEM = """You run a four-turn optimist-versus-pessimist debate.

You MUST use the task tool exactly four times and in this order:

1. Call the optimist with the topic and ask for its initial 3 points.
2. Call the pessimist with the topic plus the optimist's exact points, asking
   for 3 risks or rebuttals.
3. Call the optimist again with the complete debate so far, asking for 3
   rebuttals or additional opportunities.
4. Call the pessimist again with the complete debate so far, asking for 3
   rebuttals or additional risks.

Do not skip a turn. Pass previous points into every later task so each speaker
can respond to what is already on the table.

After all four task calls, act as a neutral judge. Return EXACTLY 5 lines,
with no numbering, headings, markdown fences, or preamble:

Lines 1-2: the two strongest opportunities, each beginning with [+]
Lines 3-4: the two most serious risks, each beginning with [-]
Line 5: begin with [>] and state either:
- PROCEED, followed by a one-clause reason, or
- REVISIT, followed by a one-clause reason explaining why the risks should
  be addressed first.

Judge importance rather than simply counting claims. Do not mention agents,
delegation, task calls, or the debate process in the final five lines.
"""


subagents = [
    {
        "name": "optimist",
        "description": (
            "Produces exactly three concise opportunities or optimistic "
            "rebuttals for an idea. Use for turns 1 and 3."
        ),
        "system_prompt": OPTIMIST_SYSTEM,
        "model": llm,
    },
    {
        "name": "pessimist",
        "description": (
            "Produces exactly three concise risks or pessimistic rebuttals "
            "for an idea. Use for turns 2 and 4."
        ),
        "system_prompt": PESSIMIST_SYSTEM,
        "model": llm,
    },
]

We can create the Deep Agent with these lines:

1
2
3
4
5
debate_agent = create_deep_agent(
    model=llm,
    system_prompt=COORDINATOR_SYSTEM,
    subagents=subagents,
)

With a few helper methods we can get a robust output that works even when the LLM may trip and produces output that does not exactly match our requirements:

def message_text(message: Any) -> str:
    """Extract plain text from a LangChain message."""
    content = getattr(message, "content", message)

    if isinstance(content, str):
        return content.strip()

    if isinstance(content, list):
        parts: list[str] = []
        for item in content:
            if isinstance(item, str):
                parts.append(item)
            elif isinstance(item, dict) and item.get("type") == "text":
                parts.append(str(item.get("text", "")))
        return "\n".join(part for part in parts if part).strip()

    return str(content).strip()


def is_valid_summary(text: str) -> bool:
    """Check the same five-line conclusion contract as the old script."""
    lines = [line.strip() for line in text.splitlines() if line.strip()]
    return (
        len(lines) == 5
        and all(line.startswith("[+]") for line in lines[:2])
        and all(line.startswith("[-]") for line in lines[2:4])
        and lines[4].startswith("[>]")
        and ("PROCEED" in lines[4] or "REVISIT" in lines[4])
    )


def repair_summary(text: str, topic: str) -> str:
    """Use the same local LLM to enforce the output format if needed."""
    response = llm.invoke(
        [
            (
                "system",
                "Reformat the supplied conclusion into EXACTLY 5 non-empty "
                "lines, with no numbering or preamble. Lines 1-2 must begin "
                "with [+] and contain the two strongest opportunities. Lines "
                "3-4 must begin with [-] and contain the two most serious "
                "risks. Line 5 must begin with [>] and contain either PROCEED "
                "or REVISIT plus a one-clause reason. Preserve the meaning and "
                "do not add unsupported claims.",
            ),
            (
                "user",
                f"Topic: {topic}\n\nConclusion to reformat:\n{text}",
            ),
        ]
    )
    return message_text(response)


def run_debate(topic: str) -> str:
    result = debate_agent.invoke(
        {
            "messages": [
                {
                    "role": "user",
                    "content": (
                        f"Debate this topic and produce the required final "
                        f"five-line conclusion:\n\n{topic}"
                    ),
                }
            ]
        },
        {"recursion_limit": 40},
    )

    summary = message_text(result["messages"][-1])
    if not is_valid_summary(summary):
        summary = repair_summary(summary, topic)

    return summary

As the final step, we glue everything together in a main() function:

def main() -> None:
    topic = sys.argv[1] if len(sys.argv) > 1 else (
        "Migrating a legacy monolith to a fully decentralized "
        "AI-agent architecture."
    )

    png_bytes = debate_agent.get_graph(xray=1).draw_mermaid_png()
    with open("multi_agent_debate.png", "wb") as file:
        file.write(png_bytes)

    print(f"--- DEBATE --- {topic}")
    summary = run_debate(topic)

    print("\n\n--- SUMMARY ---")
    print(summary)
    print("--- /SUMMARY ---")


if __name__ == "__main__":
    main()

We can use our debate_agent and grab the LangGraph graph and visualise it as we did in all other LangGraph applications:

We see the model in the center, a before_agent and an after_model hook to influence the processing and the tools node that does the tool calls.

This gives us a generic workflow that is the blueprint for all Deep Agents.

Run the agent

If we run the agent, we see the main difference to our multi-agent approach: We do not get an intermediate output and need to wait until we have a summary:

--- DEBATE --- 
Migrating a legacy monolith to a fully decentralized AI-agent architecture.


--- SUMMARY ---
[+] Unlock micro‑service scalability by letting each agent run in its own 
    container or serverless function, instantly handling spikes without 
    monolith bottlenecks.
[+] Enable continuous learning: agents can pull the latest models from a 
    shared registry and update locally, so new AI capabilities roll out to 
    all users with zero downtime.
[-] Security becomes a nightmare: each agent exposes an interface; patching 
    vulnerabilities across dozens of containers multiplies attack surface 
    and audit complexity.
[-] Operational overhead skyrockets: orchestrating model updates, monitoring 
    inter‑agent health, and ensuring reliable message passing demands tooling 
    that most teams lack, turning the promised agility into costly maintenance.
[>] PROCEED, because the scalability and market opportunities outweigh the 
    identified risks when mitigated with robust data protection and compliance 
    planning.

We give up control over the different steps but gain flexibility by not needing to define any workflow on our own. We can build complex agents by focusing on the tools and subagents and do not need to think how to connect the nodes.

Next

With this basic introduction to Deep Agents, we get a first glimpse on this new approach. We can focus on the tasks we want to do and not bother with the workflow. Next week we add another example where we use Frontmatter and the file system to tag blog posts.