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
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.
Source-complete notesWhen to buy LangChain
When 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
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.
Source-complete notesLCEL Syntax
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) #asyncLangChain is often criticized for its abstractions that hide what's going on under the hood - and this is what causes most problems in production.
Source-complete notesCommon problems
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
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.
Source-complete notesWhen to take LangGraph instead of LangChain
When 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
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.
Source-complete notesExample State
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]}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.
Source-complete notesBranching example
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})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).
Source-complete notesHuman-in-the-loop pattern
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
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.
Source-complete notesWhen LlamaIndex beats LangChain
When 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
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.
Source-complete notesTypes of Indexes
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 questionsDocument processing pipeline before indexing: transformations, splitters, metadata, deduplication. With caching - re-indexing skips already processed documents.
Source-complete notesExample
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
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.
Source-complete notesWhen CrewAI
When CrewAI
- business processes with clear roles
- quick start of a multi-agent prototype
- complex graph → LangGraph more precise
- hidden prompts inside the framework
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.
Source-complete notesKey primitives
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 launchesAgno 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.
Source-complete notesMinimal example · Applicable
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
The choice between frameworks is determined not by “which is more powerful”, but by “which abstractions coincide with your task.”
Source-complete notestable
| Criterion | CrewAI | AutoGen | LangGraph | Agno |
|---|---|---|---|---|
| Abstraction | Roles / tasks | Agent chat | Graph / state | Agent + tools |
| Control | Medium | Medium | High | High |
| Learning curve | Low | Medium | High | Low |
| Loops | No | Partial | Yes | No |
| Production readiness | Use carefully | Use carefully | Yes | Yes, with caveats |
Eval Frameworks — RAGAS · ARES · DeepEval · Braintrust
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.
Source-complete notesApplicable
Applicable
- RAG assessment without GT dataset
- diagnostics: retrieval vs generation problem
- expensive for LLM-judge on large datasets
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.
Source-complete notesSyntax
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),
]
)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.
Source-complete notesExperiment concept
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"
)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.
Source-complete notesPrinciple · Applicable
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
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.
Source-complete notesStrengths
Strengths
- 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
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.
Source-complete notesUnique features vs Langfuse
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)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.
Source-complete notesConnection in 1 line · Applicable
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
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.
Source-complete notesStandard attributes OTEL LLM span
textgen_ai.system = "anthropic"
gen_ai.request.model = "claude-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
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.
Source-complete notesWhen DSPy beats manual prompting
When 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
DSPy modules are typed LLM call patterns. Each module is independently optimized and can be combined into programs.
Source-complete notesMain modules
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 bestTextGrad (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.
Source-complete notesTextGrad idea
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.