Memory

LangChain

Build and debug Python LangChain chains, agents, tools, retrievers, streaming, and LangGraph state.

What it does

Build or review Python LangChain code across LCEL chains, prompts, agents, tools, retrievers, structured output, and LangGraph. Diagnose symptoms such as missing prompt variables, buffered streams, looping agents, broken tool-call history, empty traces, and migration errors. Apply concrete fixes for persistence, retries, limits, testing, and production safeguards.

When to use it

  • Debugging LCEL prompt and input errors
  • Fixing buffered streams and empty async events
  • Bounding agent loops, retries, and token cost
  • Migrating imports and Pydantic versions

The skill document

User preferences and memory live in ~/Clawic/data/langchain/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/langchain/ or ~/clawic/langchain/), move it to ~/Clawic/data/langchain/.

When To Use

  • Writing or reviewing LCEL chains, agents, tools, prompt templates, retrievers, or LangGraph graphs
  • Debugging a running app: empty streams, silent parser failures, agents that loop, history that vanishes, traces that never appear
  • Choosing the abstraction: plain model call vs chain vs prebuilt agent vs hand-built graph
  • Hardening for production: retries, rate limits, timeouts, caching, concurrency, cost ceilings, secret hygiene in traces
  • Upgrading across versions: moved imports, langchain-classic, deprecated memory classes, pydantic v1→v2
  • Not for framework-agnostic agent architecture (agents), retrieval strategy (rag), chunk-size tuning (rag-chunking), or provider API reference — this covers how all of those land in LangChain code

Quick Reference

SituationPlay
Input to ChatPromptTemplate is missing variablesName mismatch or a literal { in the template — double it to {{debug.md
A key I need later disappears mid-chain, or I am composing branches, fallbacks, or a configurable modelRunnablePassthrough.assign keeps what RunnablePassthrough() replaces; RunnableBranch, .with_fallbacks, configurable_fieldslcel.md
Building a prompt with few-shot examples, history, or partialsTemplates, placeholders, and example selectors → prompts.md
Chain returns nothing useful and you cannot see whychain.get_graph().print_ascii() for shape, then astream_events(version="v2") for the actual step I/O → debug.md
Streaming arrives as one blob at the endA non-transform step buffers the stream; find the least streamable link → streaming.md
astream_events empty inside a custom functionAsync config not propagated (Python ≤3.10 threads config by hand) → streaming.md
Agent never stops / GraphRecursionErrorrecursion_limit reached (Core Rule 5); bound the loop and handle the failure branch → agent-loop.md
Model ignores a tool, or calls the wrong oneThe tool description is the whole prompt the model sees for it → tools.md
Provider 400: assistant tool_calls not followed by tool messagesHistory trimming split an AIMessage from its ToolMessage → chat-history.md
History resets on restartInMemorySaver dies with the process; move to Sqlite/Postgres checkpointer → chat-history.md
with_structured_output returns None or raisesUse include_raw=True and read parsing_error before blaming the model → structured-output.md
Retrieval returns plausible-but-wrong chunksCheck k, splitter settings, and whether the score is distance or similarity → retrieval.md
Re-ingesting documents duplicates the indexDeterministic ids or the record-manager indexing API, not a second add_documentsretrieval.md
ImportError after upgradingMoved to a partner package or langchain-classicmigration.md
429s, cost spikes, or a hung requestRate limiter, timeouts, caching, and concurrency caps → production.md
Traces empty or tokens unaccountedTracing env vars, usage_metadata, request-time vs constructor callbacks → observability.md
Tests hit the real API, or are flakyDeterministic fake models and cached fixtures → testing.md
Untrusted documents reach the model, a tool can spend money or delete, or users share an indexInjection defense, approval gates, tenant isolation, trace privacy → security.md
Need branching, cycles, human approval, or shared stateThe prebuilt agent loop is the wrong shape — build a graph → langgraph.md
Provider-specific parameter or multimodal inputinit_chat_model, standard content blocks, per-provider differences → models.md
Anything elseReproduce with the smallest runnable that fails (prompt | model alone), then add one link at a time until it breaks

Depth on demand: lcel.md composition · prompts.md templates and few-shot · streaming.md tokens and events · models.md providers and parameters · chat-history.md sessions and persistence · retrieval.md loaders, splitters, retrievers · tools.md tool definition and errors · agent-loop.md loop control and middleware · langgraph.md custom graphs · structured-output.md schemas and parsers · debug.md symptom→cause · observability.md tracing and token accounting · security.md injection, blast radius, isolation · production.md limits, caching, deployment · testing.md deterministic tests · migration.md version boundaries.

Core Rules

  1. Every Runnable takes exactly one argument; a bare dict is a RunnableParallel and a bare function is a RunnableLambda. {"context": retriever, "question": RunnablePassthrough()} | prompt | model works because of that coercion. The corollary bites: a coerced function receives the WHOLE input, so write lambda x: x["question"], never lambda question: ....
  2. Prompt variables are literal and case-sensitive; every real brace must be doubled. {Question} and {question} are different variables, and one JSON example pasted into a template turns {"a": 1} into a variable named "a": 1. Escape as {{"a": 1}}, or keep examples out of the template and pass them as a variable.
  3. A chain streams end-to-end only if every link is a transform. One step that returns a value instead of yielding buffers everything upstream: first-token latency becomes total generation time. StrOutputParser and JsonOutputParser stream; Pydantic parsing and any ordinary RunnableLambda do not (streaming.md).
  4. Count your retry layers — they multiply. Client retries × chain retries: ChatOpenAI(max_retries=2) is 3 attempts, wrapped in .with_retry(stop_after_attempt=3) is 3 outer attempts, so 9 billed provider requests for one invoke. Pick one layer per failure class: client for transport/429, .with_retry for parse and validation failures.
  5. Bound the agent loop with a number you chose. LangGraph's recursion_limit (default 25) counts node steps, and a tool-calling agent spends ~2 steps per tool call — 25 ≈ 12 tool calls before GraphRecursionError. Set it per invocation (config={"recursion_limit": 40}) and write the branch that handles hitting it.
  6. Persistence is a checkpointer keyed by thread_id, not an object you attach to a chain. The old memory classes (ConversationBufferMemory, deprecated in 0.3.1) held state in a Python object that died with the process. InMemorySaver dies with it too; only a Sqlite/Postgres checkpointer survives a restart (chat-history.md).
  7. Trim on message boundaries, before you overflow. trim_messages(max_tokens=N, strategy="last", token_counter=model, include_system=True, start_on="human") — a window starting on a ToolMessage, or one that keeps an AIMessage's tool_calls without their ToolMessages, makes the provider reject the request outright.
  8. Invoke-time config is inherited; constructor config is not. Callbacks, tags, and metadata passed as .invoke(x, config={...}) reach every child runnable; callbacks set on a component's constructor apply to that component alone. In async code on Python ≤3.10 (python_runtime: 3.10), config must be threaded into every custom function by hand — async def f(x, config): await inner.ainvoke(x, config) — or child runs vanish from traces and astream_events yields nothing. Check this before shipping any async path on 3.10.
  9. Pin langchain, langchain-core, and every partner package together. They all resolve one langchain-core; a mismatch surfaces as ImportError or a pydantic ValidationError deep inside a call that looks like your bug. One lockfile, one upgrade commit, and read migration.md before bumping a major.

Error Messages

The message names a layer, rarely the cause. Decode before editing code.

MessageWhat it actually meansFirst move
KeyError: 'x' / missing variables {'x'}Template expects a variable the chain never passes, or an unescaped { created a phantom oneprompt.input_variables vs chain.input_schema.model_json_schema()
Expected mapping type as input to ChatPromptTemplateA bare string reached a chain whose first step is a prompt with named variablesWrap: chain.invoke({"question": q})
OutputParserExceptionThe model returned prose, a fenced block, or a near-miss schemainclude_raw=True and read the raw message (structured-output.md)
ValidationError (pydantic)Schema mismatch, or a v1 BaseModel passed where v2 is expectedCheck which pydantic the model class was defined with (migration.md)
GraphRecursionErrorLoop hit recursion_limit (Core Rule 5)Read the last 3 steps in the trace: the agent is usually re-calling one failing tool
An assistant message with 'tool_calls' must be followed by tool messagesHistory trimming or manual editing broke tool-call pairingchat-history.md trimming rules
ImportError: cannot import name … from 'langchain'The symbol moved to a partner package or langchain-classicmigration.md import table
NotImplementedError on .stream()A component has no streaming implementation; the chain falls back to one chunkstreaming.md
de-serialization relies on loading a pickle fileFAISS refuses to load an index without allow_dangerous_deserialization=TrueOnly pass it for files you produced (security.md)
RateLimitError / HTTP 429Provider throughput limit, usually from unbounded .batch()InMemoryRateLimiter + max_concurrency (production.md)
Empty trace, no errorTracing env vars unset, or async config not propagatedobservability.md

Choosing The Abstraction

NeedBuildWhy
One call with a schemamodel.with_structured_output(Schema)No chain needed; a chain around a single call only adds indirection
Fixed sequence known in advanceLCEL chain (prompt | model | parser)Free streaming, batching, async, and per-step traces
Model decides which tools to call, until donecreate_agent(model, tools)The loop, tool execution, and message bookkeeping are the part you should not rewrite
Branching, cycles, human approval, parallel fan-out over shared stateA LangGraph graphThe prebuilt loop has one shape; a graph has the shape you draw (langgraph.md)
Anything elseStart with the chainPromote to an agent only when the tool sequence genuinely cannot be known ahead of time

Cost Grows Quadratically In Agent Steps

Every agent step resends the whole transcript. If each step adds T tokens, total input across N steps is T × N(N+1)/2, not T × N.

Worked: T = 1,500 tokens per step (tool result + reasoning), N = 10 tool calls → 1,500 × 10 × 11 / 2 = 82,500 input tokens for one task, against the 15,000 a linear estimate predicts. Consequences: cap tool output size at the tool (tools.md), summarize or trim mid-run (chat-history.md), and set recursion_limit as a cost ceiling, not just a safety net.

Output Gates

Before shipping a chain, agent, or graph:

  • Prompt template variables verified against what the chain passes (chain.input_schema), and every literal brace doubled?
  • Exercised with .stream(), not only .invoke() — first-token latency actually observed?
  • Limits counted and bounded — retry layers (Core Rule 4), a client timeout, and a recursion_limit or iteration cap plus a wall clock on every agent path — each with the branch that handles hitting it written?
  • Structured output has an explicit failure path (include_raw=True or a caught parser exception)?
  • History on a checkpointer that survives a restart (or ephemerality documented on purpose), keyed by a thread_id derived from real session identity — never a constant, never shared across users?
  • Nothing secret in tags, metadata, or traced payloads; PII redaction decided per trace_pii?

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/langchain/config.yaml. Never interview the user — record a preference the moment it is stated.

VariableTypeDefaultEffect
lc_version_line0.3 | 1.x1.xSelects import paths and APIs everywhere: 1.x uses langchain.agents.create_agent and assumes legacy chains live in langchain-classic; 0.3 keeps langgraph.prebuilt.create_react_agent and the pre-split imports (migration.md)
chat_provideropenai | anthropic | google | azure | bedrock | ollamaopenaiProvider used in every emitted example and in init_chat_model strings; drives which parameter quirks and rate-limit advice apply (models.md)
vector_storechroma | faiss | pgvector | qdrant | pineconechromaRetrieval examples, persistence advice, and score interpretation (distance vs similarity) in retrieval.md
python_runtime3.10 | 3.11+3.11+At 3.10, async examples thread RunnableConfig manually through custom functions (Core Rule 8) and callback-propagation warnings are emitted
async_stylesync | asyncsyncWhether emitted code uses invoke/stream or ainvoke/astream, and which concurrency controls are shown
tracingnone | langsmith | otelnoneWhether scaffolds wire tracing env vars and which observability path observability.md recommends
trace_piiredact | allowredactWhether prompts and tool payloads may reach a tracing backend unmasked; gates the tracing Output Gate
structured_output_methodtool_calling | json_schema | parsertool_callingDefault strategy in structured-output.md when a schema is requested

Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:

  • Tooling: package manager and lock strategy, LangGraph vs plain LCEL, LangSmith vs OpenTelemetry vs plain logging, notebook vs module layout — affects every scaffold and observability.md
  • Conventions: where prompts live (code, YAML, or a prompt registry), run and tag naming, chain/module file layout, typed state classes vs dicts — affects lcel.md and langgraph.md examples
  • Platform: deployment target (script, FastAPI service, worker queue, serverless, LangGraph Platform) and whether cold starts and event-loop constraints apply — affects production.md
  • Safety posture: human approval before destructive tools, tool allowlists, whether allow_dangerous_deserialization and arbitrary-code tools are permitted at all — affects tools.md and agent-loop.md
  • Cost posture: model tiering (cheap draft, expensive final), caching aggressiveness, per-request token ceilings, acceptable latency-for-cost trades — affects production.md
  • Retrieval conventions: chunk sizing habits, metadata fields carried on every document, whether citations are mandatory in answers — affects retrieval.md
  • Cadence: when the eval set runs (every prompt or model change vs nightly vs pre-release), when recorded fixtures get re-recorded (every model upgrade vs only on failure), how often langchain packages are bumped (scheduled upgrade PR vs on demand) — affects testing.md CI Shape and migration.md
  • Output format: full files vs diffs, inline type hints, how much explanation accompanies emitted code

Traps

TrapWhy it failsDo instead
for x in items: chain.invoke(x)Serial round-trips; latency is the sum, and provider concurrency sits idlechain.batch(items, config={"max_concurrency": 5}), or abatch under async
One thread_id for every userCheckpointer state is keyed only by that id — user B reads user A's conversationDerive it from the authenticated session id (chat-history.md)
handle_parsing_errors=True as the fixHides a schema bug and pays for an extra model call every time it firesRead the raw output once, fix the schema or the prompt, keep the handler as a floor
Chunk overlap set near or above chunk sizeOverlap is subtracted from forward progress; the splitter loops or explodes the chunk countOverlap ≤ 20% of chunk size (retrieval.md)
Comparing scores across vector storesSome return distance (lower is better), others similarity (higher is better) — thresholds silently invertCheck the store's semantics before setting any threshold (retrieval.md)
Building the retriever inside the request handlerRe-loads or re-embeds the index on every call; p99 is dominated by setupBuild once at startup, inject the retriever
verbose=True as the debugging strategyPrints a partial, unstructured view and nothing about async childrenastream_events(version="v2") or a trace (debug.md)
except Exception as e: log(str(e)) around a provider callDiscards the provider's response body, which contains the actual reasonLog e.response.text / e.body where the client exposes it
Pasting the whole document into the prompt when retrieval "doesn't work"Trades a fixable retrieval bug for a permanent cost and recall problemFix k, the splitter, or the query first (retrieval.md)
Wrapping every step in RunnableLambdaEach opaque lambda kills streaming and flattens the trace into one boxCompose runnables; use lambdas for genuinely custom logic, as generators when they sit in a streaming path
FAISS.load_local(..., allow_dangerous_deserialization=True) on a downloaded indexIt is a pickle — loading executes arbitrary codeOnly load indexes you built; otherwise re-embed from source

Where Experts Disagree

  • LCEL vs plain Python. LCEL buys streaming, batching, async, retries, and per-step traces for free; plain functions are easier to step through in a debugger. Boundary: token streaming, parallel branches, or step-level observability → LCEL; a linear script that calls the model twice → plain Python with the model client.
  • Framework vs provider SDK. The framework earns its place with tool loops, retrieval integrations, and provider portability. A single prompt-in/JSON-out endpoint does not need it — that is where "LangChain is bloated" arguments are actually right.
  • Prebuilt agent vs hand-built graph. Start prebuilt: the loop, tool bookkeeping, and streaming are solved. Move to a graph when you need control flow BETWEEN tool calls (approval gates, validation nodes, fan-out) — not because the graph looks more serious.
  • Long context vs retrieval. Stuffing a large corpus works and is simple until cost per request or corpus churn dominates. Boundary: recompute the per-request token bill at real traffic; retrieval wins when the corpus changes faster than you can re-send it.

More Clawic skills, get them at https://clawic.com/skills/langchain (install if the user confirms):

  • agents — agent architecture, framework selection, and memory design before any of it is LangChain code
  • rag — retrieval pipeline design, framework-agnostic
  • rag-chunking — chunk strategy and size tuning
  • prompting — what to write inside the templates
  • structured-output — schema constraint and constrained decoding across providers

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/langchain.

Questions people ask

How does it diagnose missing variables in a ChatPromptTemplate?
It compares the prompt’s input variables with the chain input schema, checks case-sensitive name mismatches, and looks for literal braces that should be escaped as double braces.
Can it help when streaming returns one final blob or no events?
Yes. It identifies non-transform steps that buffer a chain and checks whether async config is propagated into custom functions, especially on Python 3.10 and earlier.
How does it address agents that loop or repeatedly call a failing tool?
It uses traces to inspect recent steps, sets a chosen LangGraph recursion limit per invocation, and requires a failure branch for when that bound is reached.

Related skills

Design, debug, evaluate, and harden framework-agnostic agents with explicit limits, tools, and escalation rules.

79 installs4 stars

Build, debug, and harden FastAPI services from request models through production deployment.

120 installs7 stars

Write, debug, and review Python code across runtime, packaging, concurrency, testing, typing, and performance.

156 installs6 stars

Compress documents, transcripts, threads, data, and code changes while preserving claims, caveats, and actions.

239 installs4 stars

Turn prompts and uploaded data into responsive dashboards, data explorers, visualizations, and interactive web apps.

155 installs5 stars