Vol. 03 · Application Engineering
The LLM Framework Ecosystem
Key abstractions, architectural patterns, pitfalls and selection criteria for the main frameworks of the LLM ecosystem. Each one is disassembled atomically.
LangChain — Orchestration Foundation
01orchestrationLangChain - key abstractions
LangChain is an abstraction layer on top of the LLM API. His philosophy: everything is a component, components are connected through pipes into chains (LCEL). The main value is not in the “magic”, but in uniform interfaces for 100+ providers and tools.
textKey primitives:
PromptTemplate → prompt template with variables
ChatModel → wrapper for LLM API (OpenAI, Anthropic, ...)
OutputParser → output parsing: str/JSON/Pydantic
Retriever → search abstraction (vector, BM25, hybrid)
Tool → called tool with description for LLM
Memory → conversation context storage
Chain → an outdated way to connect components
LCEL pipe → prompt | model | parser ← modern wayWhen to buy LangChain
- rapid prototype RAG
- need integration with 50+ providers
- the team already knows LangChain
- complex stateful agent → LangGraph
- production without overhead → pure SDK
02orchestrationLCEL — LangChain Expression Language
A modern way to assemble chains is through the | operator. Provides streaming, batching, tracing and async out of the box for any chain. Replaces the old Chain classes.
typescript# Simple RAG chain
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt_template
| chat_model
| StrOutputParser()
)
# Everything works automatically:
chain.invoke(query) # synchronously
chain.stream(query) # token streaming
chain.batch([q1, q2, q3]) # in parallel
await chain.ainvoke(query) #async03pitfallsLangChain - pitfalls
LangChain is often criticized for its abstractions that hide what's going on under the hood - and this is what causes most problems in production.
text✗ Magic prompts
Default prompts inside Chain classes
→ unpredictable, impossible to control
✗ Hidden LLM challenges
RetrievalQA does extra. calls unnoticed
→ unexpected costs, hidden latency
✗ Versioning
Frequent breaking changes between minor versions
→ pin version, test updates carefully
✓ Solution: LCEL + explicit prompts + tracing everythingLangGraph — Stateful Agent Graphs
04graph / stateLangGraph - philosophy and abstractions
LangGraph is a way to describe agents as state machines with explicit state. Not chains, but a graph: nodes are actions/LLM calls, edges are transitions between states. This gives something that LangChain does not: loops, branches, human-in-the-loop, persistent state.
textAgent graph: ReAct with human-in-the-loop
[START]
↓
[agent] ← LLM decides: call the tool or give a response
↙ ↘
[tools] [END]
↓
[agent] ← tool result → next solution
Key idea: cycles are possible - the agent can spin
as much as needed until it solves the problem or hits the limitWhen to take LangGraph instead of LangChain
- agent with loops and branching
- need human-in-the-loop (pause, confirmation)
- persistent state between sessions
- multi-agent with clear orchestration
05graph / stateState + Reducer is the heart of LangGraph
Each node reads and writes to a common State. Reducer determines how the state is updated when there is a conflict (append vs replace). This makes the graph reproducible and debuggable.
sqlfrom typing import Annotated
from langgraph.graph import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
# add_messages = reducer: append, not replace
retrieved_docs: list[str]
iteration_count: int
final_answer: str | None
# each node receives state and returns patch:
def agent_node(state: AgentState) -> dict:
response = llm.invoke(state["messages"])
return {"messages": [response]}06graph / stateConditional Edges - flow control
Edges can be conditional: a function looks at the state and decides which node to go to next. This allows you to build complex logic without imperative code inside nodes.
pythondef should_continue(state) -> str:
last_msg = state["messages"][-1]
if last_msg.tool_calls:
return "tools" # → tools node
elif state["iteration_count"] > 5:
return "force_end" # → force stop
else:
return "end" # → final response
graph.add_conditional_edges("agent", should_continue,
{"tools": "tools", "end": END, "force_end": END})07graph / stateCheckpointing - persistence and HITL
LangGraph can save state after each step in the database (Postgres, SQLite, Redis). This gives: continuation after a failure, human-in-the-loop (pause → wait for confirmation → continue), time travel (rollback to the previous state).
pythoncheckpointer = PostgresSaver(conn)
graph = graph.compile(
checkpointer=checkpointer,
interrupt_before=["dangerous_action"] ← pause here
)
# 1. Run before interrupt
graph.invoke(input, config={"thread_id": "t1"})
# 2. Show the person a pending action
state = graph.get_state(config)
# 3. The person approves → continue
graph.invoke(None, config) # resumeLlamaIndex — RAG & Data Layer
08rag frameworkLlamaIndex - philosophy and abstractions
LlamaIndex is focused on one thing: connecting LLM with your data. While LangChain is a general-purpose orchestration tool, LlamaIndex is a specialized data layer. The main primitive is Index and QueryEngine.
sqlLlamaIndex abstractions:
Document → raw document + metadata
Node → document chunk (atomic index unit)
Index → search structure (VectorStore, Summary, KG...)
Retriever → strategy for retrieving from Index
NodePostprocessor → reranker, filters, metadata enrichment
QueryEngine → retriever + LLM + response synthesis
ChatEngine → QueryEngine + dialogue history
Pipeline → IngestionPipeline for indexingWhen LlamaIndex beats LangChain
- complex RAG logic (parent-doc, multi-hop)
- many data sources (PDF, Notion, SQL, ...)
- need advanced indexes (KG, Summary)
- orchestration is more difficult → LangGraph is better
09rag frameworkTypes of indexes - not just vectors
LlamaIndex offers several types of indexes for different tasks. The choice of index is an architectural decision that affects the quality of answers more than the choice of model.
textVectorStoreIndex standard RAG for embeddings
SummaryIndex summarization of the entire corpus sequentially
KeywordTableIndex keyword extraction → keyword search
KnowledgeGraph entity graph → search by relationships
DocumentSummary summary per doc → document selection → details
TreeIndex hierarchical tree of summations
Multi-Index: different indexes for different types of questions10rag frameworkIngestionPipeline - correct indexing
Document processing pipeline before indexing: transformations, splitters, metadata, deduplication. With caching - re-indexing skips already processed documents.
textpipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=512, overlap=64),
TitleExtractor(), # LLM metadata
QuestionsAnsweredExtractor(), # hypothetical questions
OpenAIEmbedding(),
],
vector_store=chroma_store,
cache=IngestionCache(), # do not reindex old
)Multi-Agent Frameworks — CrewAI · AutoGen · Agno
11multi-agentCrewAI - roles, tasks, teams
CrewAI models multi-agent systems as a team with roles. Each agent is a role + a set of tools + a goal. Tasks are assigned to agents and executed sequentially or in parallel. Very readable declarative API - good for business processes where roles are obvious.
textCrewAI abstractions:
Agent = role + backstory + goal + tools + llm
Task = description + expected_output + agent
Crew = [agents] + [tasks] + process + memory
Process types:
sequential agent 1 → agent 2 → agent 3
hierarchical manager LLM → delegates tasks to agents
↑ better quality control
Crew example for WB seller analytics:
researcher → collect data on competitors
analyst → build BCG matrix
writer → write a report in PDFWhen CrewAI
- business processes with clear roles
- quick start of a multi-agent prototype
- complex graph → LangGraph more precise
- hidden prompts inside the framework
12multi-agentAutoGen — conversational multi-agent
AutoGen (Microsoft) is based on the idea of interlocutor agents: agents exchange messages with each other, like in a chat. GroupChat allows multiple agents to discuss a task until consensus.
textConversableAgent base agent with LLM + human_input_mode
AssistantAgent LLM agent without human input
UserProxyAgent executes code, optionally requests a person
GroupChat multiple agents + next selection manager
GroupChatManager LLM decides who speaks next
Pattern: Coder + Reviewer + Executor
Coder writes → Reviewer criticizes → Executor launches13multi-agentAgno (ex-Phidata) - lightweight agents
Agno is a minimalistic framework for agents: minimum abstractions, maximum control. Agent = LLM + tools + storage. No magic. Easy to read and easy to debug. Built-in support for memory and knowledge bases.
textagent = Agent(
model=Claude(id="claude-sonnet-4"),
tools=[DuckDuckGoSearch(), PythonTool()],
memory=AgentMemory(db=SqliteMemoryDb()),
knowledge=PDFKnowledgeBase(path="docs/"),
instructions=["Answer in Russian", "Use numbers"],
show_tool_calls=True,
markdown=True,
)
agent.print_response("WB 2025 Market Analysis")Applicable
- transparent simple agents
- quick start without learning the framework
14comparisonComparison of multi-agent frameworks
The choice between frameworks is determined not by “which is more powerful”, but by “which abstractions coincide with your task.”
Eval Frameworks — RAGAS · ARES · DeepEval · Braintrust
15evalsRAGAS - standard for RAG eval
RAGAS is a specialized eval framework for RAG systems. Uniqueness: reference-free metrics - you don’t need a dataset with GT answers for most metrics. Assessment via LLM judge with atomic approvals.
textWhat RAGAS evaluates and how:
Faithfulness
answer → atomic statements → NLI vs context
= proportion of statements supported by context
Answer Relevance
answer → LLM generates N questions → embed → cos(questions, query)
= to what extent the answer falls within the topic of the question
Context Precision
Each chunk → LLM : “Is LLM useful for an answer?” → precision@k
= share of useful chunks among retrieved
Context Recall [requires GT]
GT → atomic statements → NLI vs context
= proportion of GT facts present in the contextApplicable
- RAG assessment without GT dataset
- diagnostics: retrieval vs generation problem
- expensive for LLM-judge on large datasets
16evalsDeepEval - pytest for LLM
DeepEval integrates into pytest and allows you to write eval tests as unit tests. Large library of ready-made metrics + support for custom ones. Fits well into CI/CD.
python@pytest.mark.parametrize("case", test_cases)
def test_rag_quality(case):
actual_output = rag_pipeline(case.input)
assert_test(
test_case=LLMTestCase(
input=case.input,
actual_output=actual_output,
retrieval_context=case.context
),
metrics=[
FaithfulnessMetric(threshold=0.8),
AnswerRelevancyMetric(threshold=0.7),
HallucinationMetric(threshold=0.1),
]
)17evalsBraintrust - eval as a product
Braintrust is a platform for eval with UI: launching experiments, comparing prompt versions, history of runs, drill-down on cases. Like Weights & Biases, but for LLM pipelines. The SDK works without a platform.
typescriptEval(
name="RAG pipeline v2 vs v3",
data=dataset, # list {input, expected}
task=rag_pipeline, # function → output
scores=[ # metrics
Faithfulness,
AnswerRelevancy,
NumTokens, # cost tracking
Latency
],
experiment_name="v3-with-rerank"
)18evalsARES - autonomous eval system
ARES (Stanford) is a system that trains small LM classifiers for evaluation instead of expensive LLM judges. You need a small number of human tags for training - further quickly and cheaply at any volume.
text# One-time use:
human_labels = 150 labeled examples
synthetic_data = LLM.generate_training_data(docs)
classifier = finetune(small_lm, human_labels + synthetic)
# Next - cheap and fast:
scores = classifier.score(10_000_examples)
# vs LLM-judge: 100x cheaper, comparable qualityApplicable
- large volumes of assessment
- eval cost reduction
Observability — Langfuse · Phoenix Arize · Helicone
19observabilityLangfuse — open-source LLM observability
Langfuse is the most complete open-source tool: traces, eval, datasets, prompt management, cost tracking - in one place. Self-hosted via Docker. Integration via SDK or OpenTelemetry.
sqlWhat Langfuse can do:
Traces & Spans pipeline visualization, waterfall latency
Scores eval-assessments attached to trace/span
Datasets golden datasets + experiments (A/B eval)
Prompt Management versioning prompts + fetch at runtime
Cost Tracking tokens and $ by model, user, feature
Users & Sessions grouping traces by user
Evaluators LLM-as-judge directly in the interface
Integration: 2 lines of code
from langfuse.openai import openai # drop-in replacementStrengths
- self-hosted, GDPR-compliant
- prompt management + tracing in one
- integration with LangChain/LlamaIndex out of the box
- UI is slower than Arize at high volumes
20observabilityPhoenix Arize - ML observability for LLM
Arize Phoenix - from ML observability to LLM. Strengths: visualization of embeddings, drift detection at the vector space level, UMAP query projections. The best choice if you need to analyze retrieval quality visually.
sqlEmbedding visualization
UMAP/TSNE projections: see query clusters
→ find anomalies and OOD queries visually
Retrieval analysis
scatter plot: query embeddings vs doc embeddings
→ see where retrieval misses
Drift detection
comparison of distributions between periods
→ automatically finds data drift
OpenInference
open tracing standard LLM (like OTEL)21observabilityHelicone — gateway + observability
Helicone works as a proxy gateway between your code and the LLM API. Zero-instrumentation: you change one URL, you get all the metrics. Built-in caching, rate limiting, cost alerts.
textclient = OpenAI(
base_url="https://oai.helicone.ai/v1",
# ← instead of api.openai.com
default_headers={
"Helicone-Auth": f"Bearer {HELICONE_KEY}",
"Helicone-Cache-Enabled": "true", # cache
"Helicone-User-Id": user_id, # per-user
}
)Applicable
- no time for instrumentation
- need cache + rate limit immediately
- less flexibility than Langfuse
22observabilityOpenTelemetry for LLM - tracing standard
OpenInference (Arize) and OpenLLMetry (Traceloop) extend OpenTelemetry to be LLM-specific. The span attributes are standardized: input/output, model, tokens. Allows you to send traces to any OTEL-compatible backend.
textgen_ai.system = "anthropic"
gen_ai.request.model = "cloud-sonnet-4"
gen_ai.usage.input_tokens = 1240
gen_ai.usage.output_tokens = 312
gen_ai.prompt = [messages]
gen_ai.completion = [response]
# → goes to Langfuse / Jaeger / Grafana TempoDSPy — Prompt Optimization as Code
23prompt optimizationDSPy - philosophy: program, don't rush
DSPy reverses the approach: instead of writing prompts manually, you describe the task signature (inputs → outputs) and quality metrics, and the optimizer itself finds the best prompts, few-shot examples and call chains. This is a compiler for LLM programs.
typescriptTraditional approach:
the prompt was written manually → works on GPT-4 → the model was changed → the prompt is broken
DSPy approach:
signature = "question → answer" # what we do
metric = lambda pred, gt: f1(pred, gt) # how to measure
optimizer.compile(program, metric, trainset)
# DSPy generates prompts itself + few-shot examples
# re-optimizes when changing models in minutes
Key primitives:
Signature InputField + OutputField = declarative specification
Module dspy.Predict / dspy.ChainOfThought / dspy.ReAct
Optimizer BootstrapFewShot/MIPRO/BayesianSignatureOptimizer
Metric function (prediction, ground_truth) → floatWhen DSPy beats manual prompting
- the base model changes frequently
- there is a dataset with a metric - the prompt can be optimized
- multi-hop tasks with multiple LLM calls
- no dataset → DSPy will not help
- high entry threshold
24prompt optimizationDSPy Modules - building blocks
DSPy modules are typed LLM call patterns. Each module is independently optimized and can be combined into programs.
textdspy.Predict
direct call by signature, without CoT
dspy.ChainOfThought
adds a reasoning field → response
# prompt with CoT is generated automatically
dspy.ReAct
agent with tools based on the ReAct pattern
dspy.Retrieve
search in the connected knowledge base
dspy.MultiChainComparison
several reasoning paths → the best25prompt optimizationTextGrad & OPRO - DSPy alternatives
TextGrad (Stanford) - "gradient descent" for text: LLM calculates the "gradient" (critique) and updates the prompt. OPRO (Google) - LLM as an optimizer: generates prompt candidates, evaluates, improves iteratively.
textloss = metric(output, gt) # "how bad"
gradient = LLM( # "why is it bad"
f"Output: {output}
Loss: {loss}
What's wrong with the prompt?"
)
new_prompt = LLM( # "fix"
f"Old prompt: {prompt}
Gradient: {gradient}
Improved prompt:"
)No dead end
Keep moving through the map.
Continue in sequence, switch to a related guide, or return to the seven-track learning map.