// FOUNDATIONS · MODULE 02/12
LangChain v1 essentials for quants
The Runnable mental model, LCEL, structured output with Pydantic, and low-level tool calling.
Module 1 established the rule that governs the entire course: the LLM reasons, the math decides, the human vetoes. This module turns that rule into code. The primitives presented here —uniformly initialized models, composable LCEL chains, Pydantic-validated structured outputs, and low-level tool calling— are the minimum vocabulary on which Module 3 will build full agents. The idea worth retaining from now on is simple: in LangChain v1 every component, from a prompt to an agent, implements the same Runnable interface, so an agent is nothing more than one additional Runnable that can be invoked, batched, and streamed like any other.
2.1 The v1 package architecture and the Runnable mental model
2.1.1 v1.0 (Oct 22, 2025): reduced namespace, langchain-classic as a bridge, semver commitment
LangChain 1.0 was announced on October 22, 2025 as the framework's first major version, with the explicit commitment of introducing no breaking changes until 2.0 (MDPI). The langchain==1.0.0 artifact had been published on PyPI a few days earlier, on October 17, 2025 (questdb.com). The release is designated LTS: it remains in ACTIVE status until 2.0 ships and then enters maintenance for at least one year, while the 0.3 series stays in maintenance until December 2026 (Github). The official cadence is minors every one to two months and weekly patches (Github).
As of this course's reference date, the versions verified against the PyPI JSON API are: langchain 1.3.14 (Jul 16, 2026), langchain-core 1.5.2 (Jul 28, 2026), langchain-openai 1.4.1 (Jul 23, 2026), langchain-anthropic 1.5.3 (Jul 28, 2026), and langchain-classic 1.0.8 (Jun 10, 2026) (questdb.com). All v1 packages require Python 3.10 or higher (Github).
The structural change in v1 is the reduction of the langchain namespace to essential building blocks: langchain.agents (with create_agent), langchain.messages, langchain.tools, langchain.chat_models (with init_chat_model), and langchain.embeddings, mostly re-exports of langchain-core (Github). Legacy code —LLMChain, classic retrievers, the indexing API, the hub, and community re-exports— moved to the langchain-classic package, which is installed separately and maintains backward compatibility (MDPI). Import migration is mechanical: from langchain.chains import LLMChain becomes from langchain_classic.chains import LLMChain (Github). In this course langchain-classic is used only as a historical reference; no new example requires it.
Table 2.1 — LangChain v1 ecosystem packages: role, verified version, and typical use in the quant stack
| Package | Role in v1 | Verified version (PyPI, Jul 2026) | Typical use in the quant stack |
|---|---|---|---|
langchain | Essential namespace: agents, models, tools, embeddings | 1.3.14 | Entry point: init_chat_model, create_agent (M3) |
langchain-core | Base abstractions: Runnable, messages, prompts, parsers, rate limiters | 1.5.2 | LCEL, with_structured_output, bind_tools, callbacks |
langchain-openai / langchain-anthropic | Per-provider partner packages | 1.4.1 / 1.5.3 | Access to GPT and Claude with interface parity |
langchain-classic | Compatibility bridge for legacy code | 1.0.8 | Migration only; forbidden in new course code |
langchain-text-splitters | Document chunking | 1.x series | Preparing filings and transcripts for RAG (M4) |
The table above summarizes the anatomy of the ecosystem, but three practical observations matter more than the list itself. First: the separation between langchain and langchain-core is not cosmetic — the abstractions this module uses daily (Runnable, ChatPromptTemplate, RunnableLambda, InMemoryRateLimiter) live in langchain-core, while langchain provides the convenience facades. An institutional requirements.txt must pin both with strict pins, because the weekly patch cadence means two environments installed one week apart are not identical. Second: partner packages version independently of the core, which makes it possible to update one provider's integration without touching the rest of the pipeline — a valuable property when a provider changes its API in the middle of a model validation cycle. Third: the existence of langchain-classic as a separate package turns deprecation into an explicit installation decision; if a legacy research pipeline still imports LLMChain, the dependency is visible in the lockfile and auditable, rather than hidden inside a monolithic namespace. For a risk manager, that dependency traceability is part of the model inventory required by model risk management practices inherited from SR 11-7, whose principles persist even though the framework has been rescinded.
2.1.2 init_chat_model: unified interface, runtime configuration, built-in resilience
init_chat_model is the entry point to models in v1: a factory that returns a BaseChatModel with the same interface across more than eighty providers, with "provider:model" syntax and kwargs passed inline (alphanova.tech). The following snippet initializes a primary model with retries and an explicit rate limiter — two controls that are not optional in a mass research environment:
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
# Thread-safe limiter: maximum 2 requests/second, burst of 10
rate_limiter = InMemoryRateLimiter(
requests_per_second=2.0, # provider call budget
check_every_n_seconds=0.1, # check every 100 ms
max_bucket_size=10, # maximum burst size
)
model = init_chat_model(
"openai:gpt-5.5",
temperature=0, # financial classification: determinism
timeout=30,
max_tokens=1_000,
max_retries=6, # default; retries 429/5xx/network with backoff
rate_limiter=rate_limiter,
)
respuesta = model.invoke("Resume en una frase el riesgo principal de este titular: "
"ACME Corp recorta su guía de ingresos un 12%.")
print(respuesta.text) # in v1, .text is a property, not a methodChat models automatically retry with exponential backoff up to six times on network errors, rate limits (HTTP 429), and server errors (5xx); client errors such as 401 or 404 are not retried (alphanova.tech). The .text property and the AIMessage return type are part of v1's official breaking changes relative to the 0.3 series (Github).
The second pattern is the runtime-configurable model, directly relevant for cost routing: a lightweight model for mass headline screening and a frontier model for thesis writing, interchangeable without recompiling the chain (alphanova.tech):
from langchain.chat_models import init_chat_model
# No `model`: remains configurable at runtime via config
router_model = init_chat_model(temperature=0, configurable_fields=("model", "model_provider"))
# Mass screening: cheap model
router_model.invoke(
"Clasifica el sentimiento de este titular: ACME eleva dividendo un 8%.",
config={"configurable": {"model": "gpt-5.4-mini"}},
)
# Investment thesis: frontier model, same call, same code
router_model.invoke(
"Redacta la tesis de riesgo para ACME a 12 meses.",
config={"configurable": {"model": "claude-sonnet-4-6"}},
)When a chain contains several models, config_prefix prevents key collisions in the configurable dict (alphanova.tech). Finally, langchain-core 1.0 introduced standard content blocks: the .content_blocks property of messages exposes text, reasoning, citations, tool calls (including server-side ones), and multimodal data (image, audio, video, PDF) with uniform typing and full backward compatibility (MDPI). For the quant this means that a candlestick chart attached as an image, an earnings call transcript, and the model's reasoning are consumed through the same interface regardless of provider (PyQuant News).
2.2 LCEL: composing financial pipelines
2.2.1 The | operator: prompt | model | parser applied to a news-analysis pipeline
LCEL (LangChain Expression Language) is the declarative composition style of langchain-core: components are chained with the | operator, which builds a RunnableSequence where each link's output is the next one's input (Source). The Runnable interface is the framework's universal contract: any component —prompt, model, parser, custom function, or compiled graph— can be "invoked, batched, streamed, transformed and composed" (Source). A literal dict inside a sequence is automatically coerced into a RunnableParallel, which runs the branches concurrently over the same input and returns a dict of results (Source). The composed chain inherits synchronous, asynchronous, batch, and streaming execution for free (Source).
The following pipeline analyzes a corporate news item and produces two views in parallel: a sentiment classification and an extraction of mentioned metrics:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
model = init_chat_model("openai:gpt-5.4-mini", temperature=0)
prompt_sentimiento = ChatPromptTemplate.from_messages([
("system", "Eres un analista sell-side. Clasifica el sentimiento del titular "
"como Positive, Negative o Neutral. Responde solo con la etiqueta."),
("user", "Titular: {titular}"),
])
prompt_metricas = ChatPromptTemplate.from_messages([
("system", "Extrae en una línea cada cifra financiera mencionada en el titular, "
"con su unidad y escala. Si no hay ninguna, responde NO_DISPONIBLE."),
("user", "Titular: {titular}"),
])
# Parallel branch: same input, two concurrent analyses
pipeline_noticias = (
RunnablePassthrough()
| {
"sentimiento": prompt_sentimiento | model | StrOutputParser(),
"metricas": prompt_metricas | model | StrOutputParser(),
}
)
resultado = pipeline_noticias.invoke(
{"titular": "ACME Corp reporta ingresos de 1.234,56 millones USD (+9% interanual) "
"y eleva su guía de margen EBIT para 2027."}
)
# {'sentimiento': 'Positive', 'metricas': '1.234,56 millones USD (ingresos); +9% interanual'}The four composition primitives cover the remaining structural needs. RunnablePassthrough forwards the input unchanged and, with .assign(), adds computed keys to the dict flowing through the chain (NewYorkCityServers). RunnableLambda wraps any Python callable as a Runnable — the standard way to embed deterministic preprocessing, such as normalizing tickers or truncating text (Source). RunnableParallel is the concurrent fan-out already seen (Source). RunnableBranch implements conditional routing as a list of condition→runnable pairs plus a default (Dividend Capture Pro), a useful pattern for routing the analysis by news type (earnings, M&A, guidance) toward specialized prompts. The following diagram summarizes the pipeline above:
2.2.2 invoke/ainvoke/batch/stream: streaming for dashboards, batch for mass screening
The Runnable interface defines a complete family of execution methods; choosing the right one is an engineering decision with direct impact on latency and cost (Source):
Table 2.2 — Execution methods of the Runnable interface: semantics, async variant, and use in quantitative trading
| Method | Semantics | Async variant | Use in quantitative trading |
|---|---|---|---|
invoke(input, config) | One input → one output | ainvoke | Single signal: classifying the headline that just arrived |
batch(inputs, config) | List of inputs → list of outputs; parallel with a thread pool by default | abatch | Mass screening: sentiment of 500 pre-open headlines |
batch_as_completed | Like batch, returns results as they complete | abatch_as_completed | Pipelines with heterogeneous per-provider latencies |
stream(input) | Generator of chunks as they are produced | astream | Research dashboards: live tokens from a report |
astream_events | Stream of intermediate chain events | native async | Step-by-step traceability in debugging and audit |
map() | Applies the runnable to each element of a list | — | Declarative fan-out over a portfolio |
Reading this table in an institutional context involves three points. First, batch is not a cosmetic for loop: by default it runs invoke() in parallel on a thread pool executor, and accepts max_concurrency via config, so the throughput of a ticker screening is bounded by the provider's rate limit and not by per-call latency (Source). In formal terms, if each call takes seconds, sequential cost grows as , while the batch with concurrency scales as : for headlines and , the improvement is close to an order of magnitude. Second, the methods with the a prefix are asynchronous and, by default, run their synchronous counterpart in the asyncio thread pool; chat model integrations override them with native async, which matters when the pipeline coexists with a market data event loop (Source). Third, streaming is not a demo luxury: in v1 the updates, messages, and custom modes make it possible to emit tokens, per-step progress, and arbitrary data to a research dashboard, and since version 1.3 the documentation recommends typed event streaming as the preferred API (Github). If a component does not implement streaming, the interface degrades gracefully to invoke (Source).
The following snippet shows both patterns over the same headline classifier:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = init_chat_model("openai:gpt-5.4-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "Clasifica el sentimiento del titular: Positive, Negative o Neutral."),
("user", "{titular}"),
])
clasificador = prompt | model | StrOutputParser()
# 1) Mass pre-open screening: parallel batch over 200 tickers
titulares = [{"titular": f"Titular de mercado nº {i} sobre el ticker T{i:03d}"}
for i in range(200)]
etiquetas = clasificador.batch(
titulares,
config={"max_concurrency": 8, "tags": ["screening", "pre-market"],
"metadata": {"desk": "equity-research", "prompt_version": "v3"}},
)
# 2) Research dashboard: token-by-token streaming of a long report
for chunk in clasificador.stream({"titular": "ACME eleva guidance y anuncia buyback."}):
print(chunk, end="", flush=True)The config argument with tags and metadata is not decorative: those tags travel with each execution and feed traceability in LangSmith, the foundation of the observability that Module 10 develops as a defense against the agentic AI governance gap (Source).
max_concurrency=8), July 2026. The shape of the curves is structural; absolute values depend on the provider and the model.The figure above quantifies the point with synthetic illustrative data: with a mean latency of 1.15 s per call, classifying 200 headlines in sequence would cost about 230 s, versus about 31 s with batch and max_concurrency=8. The exact numbers depend on the provider and the model, but the shape of the curves —linear versus stepped— is structural, not empirical.
2.2.3 Robustness: with_retry with backoff, with_fallbacks across providers, callbacks
Every Runnable exposes .with_retry(...), which returns a new runnable that retries on exceptions with a configurable policy, and .with_fallbacks([...]), which chains backup alternatives on failure (Source). Because chat models already internally retry 429/5xx/network errors up to six times, with_retry is reserved for application-logic failures —for example, a parse that fails validation— while with_fallbacks resolves the major contingency: a complete provider outage (alphanova.tech). The canonical institutional pattern is primary OpenAI → backup Anthropic:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "Clasifica el sentimiento del titular: Positive, Negative o Neutral."),
("user", "{titular}"),
])
primario = init_chat_model("openai:gpt-5.5", temperature=0, timeout=30)
respaldo = init_chat_model("anthropic:claude-sonnet-4-6", temperature=0, timeout=30)
cadena_resiliente = (
prompt
| primario.with_fallbacks([respaldo]) # failover on outage
| StrOutputParser().with_retry(stop_after_attempt=3) # application logic
)
# Per-call callbacks for ad hoc observability
from langchain_core.tracers import ConsoleCallbackHandler
etiqueta = cadena_resiliente.invoke(
{"titular": "ACME suspende su dividendo trimestral citando preservación de liquidez."},
config={"callbacks": [ConsoleCallbackHandler()],
"tags": ["sentiment", "fallback-enabled"]},
)Three production details complete the picture. Fallbacks are also migrating to infrastructure: LangSmith offers an LLM Gateway (in private beta as of Jul 2026) where the backup order is defined once and applied by HTTP error code (429, 500, 502, 503, 504), without touching each pipeline's code (Quant Memo). Development callbacks (ConsoleCallbackHandler, set_debug(True)) are for local debugging; in production the path is centralized tracing (Source). And switching between providers is only safe if the output contract is stable — a weighty reason for the structured output of the next section: a StrOutputParser receiving free text from two different models offers no parity guarantee whatsoever.
2.3 Structured output and low-level tool calling
2.3.1 with_structured_output with Pydantic: the first financial schema, include_raw for audit
with_structured_output(schema) wraps a chat model so that its outputs match a Pydantic, TypedDict, or JSON schema, and returns validated instances instead of text (wallstreetmojo.com). Internally it chooses the strategy according to provider capability: if the provider supports tool calling or native JSON schema, the schema travels as a tool definition or response_format; if not, it falls back to the prompt+parser pattern (Quant Memo). One design detail with consequences: the class name, the docstring, and the field descriptions are effectively injected into the model's prompt, so designing the Pydantic schema is designing prompting (Quant Memo).
This is the point where the Module 1 rule becomes code. The institutional sentiment classifier does not return a loose word: it returns a typed contract with a bounded score and textual evidence that an auditor can verify:
from typing import Literal, Optional
from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model
class SentimientoNoticia(BaseModel):
"""Sentiment classification of a financial headline with auditable evidence."""
etiqueta: Literal["positive", "negative", "neutral"] = Field(
description="Sentimiento del titular para el precio de la acción a 1-5 días")
score: float = Field(
ge=-1.0, le=1.0,
description="Intensidad del sentimiento normalizada en [-1, 1]; "
"0.0 solo si la etiqueta es neutral")
evidencia: str = Field(
description="Frase VERBATIM del titular que justifica la clasificación")
tickers_mencionados: list[str] = Field(
description="Tickers explícitos en el titular; lista vacía si no hay ninguno")
confianza: Optional[Literal["baja", "media", "alta"]] = Field(
default=None,
description="Auto-evaluación de confianza; null si el titular es ambiguo")
model = init_chat_model("openai:gpt-5.5", temperature=0)
# include_raw=True: dict output with 'raw', 'parsed', and 'parsing_error'
extractor = model.with_structured_output(SentimientoNoticia, include_raw=True)
salida = extractor.invoke(
"Clasifica este titular: ACME Corp desploma un 18% tras revelarse "
"una investigación regulatoria sobre sus cuentas de 2025."
)
assert salida["parsing_error"] is None
analisis: SentimientoNoticia = salida["parsed"]
mensaje_crudo = salida["raw"] # full AIMessage: complete traceability
print(analisis.etiqueta, analisis.score, analisis.evidencia)With include_raw=True the output is always a dict with keys raw (the original AIMessage, with usage_metadata and response_metadata), parsed (the validated instance), and parsing_error (the captured exception, without raising it) (wallstreetmojo.com). For a desk subject to audit requirements, persisting raw alongside parsed is the difference between being able and not being able to reconstruct exactly what the model said six months later. The schema can also be forced into strict mode on OpenAI with method="json_schema", strict=True (manishgoelstocks.com), with the documented restriction that in strict mode fields cannot carry validation metadata or default values on some providers (wallstreetmojo.com). There is also a known open bug (issue #38223, June 2026): with the Responses API, streaming, and nested Pydantic schemas, the streaming path builds an invalid JSON schema where the non-streaming path works — a reminder that structured-output edge cases are still active (ryanoconnellfinance.com).
2.3.2 bind_tools and the manual tool-calling loop: the LLM as a router of deterministic tools
bind_tools is the low-level BaseChatModel method for attaching tools —functions, Pydantic classes, dicts, or BaseTool— and returns a Runnable whose output is an AIMessage with the tool_calls list (arXiv.org). The contract is the second half of the course rule: the LLM proposes, the code disposes. The model never executes the tool; it only emits dicts {'name', 'args', 'id', 'type': 'tool_call'} that the application validates and executes (alphanova.tech). In a trading system this is not a technical nuance: it is the boundary between the non-deterministic component (reasoning) and the deterministic, auditable components (computation, data, execution):
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool
@tool
def get_market_data(ticker: str, field: str) -> dict:
"""Returns a point-in-time market data point (price, volume, beta)
for a ticker. Deterministic source: never invented by the LLM."""
datos = {"ACME": {"precio": 184.32, "beta": 1.21, "volumen_medio": 8_400_000}}
return {"ticker": ticker, "field": field, "valor": datos.get(ticker, {}).get(field)}
@tool
def calculator(expression: str) -> float:
"""Only authorized path for arithmetic. The LLM does not compute in text."""
import ast, operator as op
OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
ast.Div: op.truediv, ast.USub: op.neg}
def ev(n):
if isinstance(n, ast.Constant): return n.value
if isinstance(n, ast.BinOp): return OPS[type(n.op)](ev(n.left), ev(n.right))
if isinstance(n, ast.UnaryOp): return OPS[type(n.op)](ev(n.operand))
raise ValueError("expresión no permitida")
return ev(ast.parse(expression, mode="eval").body)
tools = {t.name: t for t in [get_market_data, calculator]}
model = init_chat_model("openai:gpt-5.5", temperature=0).bind_tools(list(tools.values()))
# Manual tool-calling loop: the LLM routes, the code executes
historial = [HumanMessage(
"Con el precio y la beta de ACME, calcula su coste de equity aproximado "
"si rf = 4% y la prima de mercado es 5% (CAPM: rf + beta * prima).")]
respuesta = model.invoke(historial)
historial.append(respuesta)
while respuesta.tool_calls: # is the model proposing tools?
for call in respuesta.tool_calls:
resultado = tools[call["name"]].invoke(call["args"]) # the CODE executes
historial.append(ToolMessage(content=str(resultado), tool_call_id=call["id"]))
respuesta = model.invoke(historial) # new round with results
historial.append(respuesta)
print(respuesta.text) # final answer: 4% + 1.21 * 5% = 10.05% (computed by the tool)Three observations close the section. First, tool_choice="any" forces the model to call a tool — a pattern the documentation itself uses for guardrailed structured classification (Github). Second, in streaming, tool calls arrive as tool_call_chunk with partial JSON that must be aggregated before executing anything; executing half-parsed arguments is an operational security failure (Github). Third, this thirty-line manual loop is, in essence, what create_agent encapsulates with state, persistence, and middleware; Module 3 will formalize it, but it is worth arriving knowing that an agent does not add magic but orchestration over this same contract — and that the resulting agent is still a composable Runnable.
2.4 When NOT to use LangChain
2.4.1 Documented criticisms, comparison with direct calls, and the dominant hybrid pattern
The canonical criticism is the Octomind post (June 2024), which after twelve months in production abandoned LangChain because its high-level abstractions made the code more expensive to understand and maintain; it reached 480 points on Hacker News and drew a direct response from LangChain's CEO (lilys.ai). Its iconic example: translating a text is a direct API call, but in classic LangChain it required four abstractions (prompt template, chat model, output parser, chain) (arXiv.org). Anthropic, in its December 2024 engineering guide, formulates the positive version of the same advice: the most successful implementations are built with "simple, composable patterns" instead of complex frameworks, starting with direct calls and adding layers only when they demonstrably improve results — although the guide notes that frameworks are useful for getting started quickly (arXiv.org). Later criticisms add hidden costs from implicit LLM calls and stack traces five or more abstraction layers deep (arXiv.org).
The 2026 consensus is nuanced and practical: for a single call or a trivial linear pipeline, the provider's direct SDK plus Pydantic is more readable and free of versioning risk; LangChain is justified when there is multi-model orchestration, one-line provider swapping, stateful agents, or a need for built-in observability (arXiv.org). The framework's own trajectory —from monolithic namespace to v1's slimmed-down package, with the legacy exiled to langchain-classic— is a direct response to those criticisms, acknowledged in the official announcement (MDPI). In document retrieval, the dominant pattern combines frameworks: LlamaIndex as a retrieval layer wrapped as a tool inside LangChain orchestration (arXiv.org). And adoption is real: the top five frameworks concentrate more than 93% of segment downloads, with LangChain at around 233M monthly downloads (QASkills.sh), and 57.3% of organizations surveyed in the State of Agent Engineering 2026 report agents in production — 67% at companies with more than 10,000 employees — with independent Datadog telemetry showing agent framework adoption nearly doubling year over year (AlphaCreek).
The decision guide for the quant is summarized as follows: direct SDK for the trivial; short LCEL for repeatable analysis pipelines over many assets; structured output and langchain-core tool calling as the stable contract between providers; agents (Module 3) only where a genuine tool-decision loop exists. Over-engineering — putting an agent where a batch is enough — is the most expensive and most common architecture mistake.
Exercises
-
Version verification. Write a script that queries the PyPI JSON API (
https://pypi.org/pypi/langchain/json) and checks that the installed versions oflangchain,langchain-core, and one partner package match the pins inrequirements.txt. It must fail with a non-zero exit code if there is version drift. -
Configurable model. Using
init_chat_modelandconfigurable_fields, build a headline classifier that routes to a lightweight or frontier model according to the value ofconfig["configurable"]. Measure the latency of ten invocations in each configuration withtime.perf_counterand tabulate the relative cost assuming prices of USD 0.40 and USD 5.00 per million input tokens (illustrative). -
Parallel LCEL pipeline. Extend the news pipeline from 2.2.1 with a third branch that extracts the mentioned tickers and a downstream
RunnableLambdathat normalizes the output dict to lowercase and adds anas_offield with the cutoff date. Run the result with.batchover a list of 20 synthetic headlines withmax_concurrency=4. -
Robustness. Wrap the pipeline from exercise 3 with
with_fallbackstoward a second provider andwith_retry(stop_after_attempt=3)on the parser. Simulate the primary's failure with aRunnableLambdathat raisesConnectionErrorand verify that the chain fails over to the backup without exception. -
Auditable classifier. Implement the
SentimientoNoticiaschema from 2.3.1 withinclude_raw=True, add a two-step verification in code —theevidenciastring must appear literally in the input headline— and persist each record to a JSONL withparsed,raw.usage_metadata, and a hash of the prompt. Classify 30 headlines and report the fraction that passes verification. -
Tool router. Implement the manual
bind_toolsloop from 2.3.2 with three tools (get_market_data,calculator, a simulatedxbrl_lookup) and a five-round limit. Demonstrate with a case that the model chains two tool calls (lookup → computation) and that no arithmetic appears in the model's free text.
Module 2 of 15 — LangChain for Quantitative Trading. Data and versions verified as of July 2026; prices and market figures subject to change. LangChain 1.3.14 / langchain-core 1.5.2.
// check
Check your understanding
Self-assessment with instant feedback. No score is stored.
Show answer
B. Section 2.1.2 states it explicitly: chat models retry with backoff on network errors, rate limits (429), and server errors (5xx); client errors such as 401 or 404 are not retried. A 401 indicates invalid credentials: retrying would burn budget without fixing anything, and failover requires explicit with_fallbacks.
Show answer
B. A literal dict inside an LCEL sequence is coerced into a RunnableParallel: concurrent fan-out over the same input that returns a dict of results (Section 2.2.1). Dependencies between steps are expressed with sequential composition or RunnablePassthrough.assign(), never inside the parallel dict.
Show answer
B. batch parallelizes with a thread pool, but throughput is bounded by the provider's call budget: at 2 req/s, 500 calls require at least 250 s. The module formula T_batch ≈ ⌈N/c⌉·t assumes headroom in that budget; useful concurrency is c* ≈ req/s × latency ≈ 2.4.
Show answer
B. Chat models already internally retry transient failures (429/5xx/network), so with_retry is reserved for application logic and with_fallbacks resolves the major contingency: the provider outage. And failover is only safe with a stable output contract between both, a weighty reason for structured output.
Show answer
B. With include_raw=True the output is a dict with raw (the full AIMessage), parsed (the validated instance), and parsing_error (2.3.1). Persisting raw alongside parsed is the difference between being able and not being able to reconstruct the model's exact response six months later before an auditor.
Show answer
B. Section 2.3.1 flags it as a design detail with consequences: the class name, the docstring, and the field descriptions travel into the prompt the model receives. Writing "Frase VERBATIM del titular" in the description of evidencia is an operative instruction, not a comment.
Show answer
C. Constrained decoding guarantees form, not truth: a well-typed field can contain a made-up value (the "confidently wrong" failure). The mitigation is schema design —legitimate null, mandatory verbatim evidence— plus two-step verification in code (2.3.1). JSON mode, moreover, has worse documented compliance.
Show answer
B. If the inputs can be enumerated before running, the pattern is batch/map: throughput governed by max_concurrency and the limiter. Over-engineering —putting an agent where a batch is enough— is, in the module's words, the most expensive and most common architecture mistake; agents (Module 3) are reserved for genuine tool-decision loops.